/
Author: Хортон А.
Tags: языки программирования компьютерные технологии программирование программирование в visual c++
ISBN: 978-5-8459-1016-5
Year: 2007
Text
I *
“Используя эту книгу вместе с Visual C++ 2005, вы
сможете понять фундаментальные основы языка C++
и научиться программировать для Microsoft Windows.
Сначала вы изучите принципы, положенные в основу
C++, и освоите стандарты ISO/ANSI C++ и C++/CLI.
Затем, уже обладая базовыми знаниями, вы
посмотрите, как создавать Windows-приложения
с использованием библиотеки Microsoft Foundation
Classes из традиционных программ на C++, и как
добавлять к ним возможности Windows Forms
и C++/CLI в среде .NET.
Вы ознакомитесь с основами технологий доступа
к базам данных из традиционных приложений
C++, а также из приложений для .NET
Это задача непростая, но вы получите немало
удовольствия, решая ее. Совершите свой
первый шаг на пути в мир реального
программирования. ”
Обновления, исходные коды, поддержка от издательства Wrox
BAOWMT™
WWi ЧмЯЖк
www.dialektika.com
Ivor Horton's
Beginning
Visual C++*’ 2005
Ivor Horton
WILEY
Wiley Publishing, Inc.
Visual C++ 2005
Базовый курс
Айвор Хортон
“Диалектика”
Москва • Санкт-Петербург • Киев
2007
ББК 32.973.26-018.2.75
Х82
УДК 004.438
Компьютерное издательство “Диалектика”
Зав. редакцией С.Н. Тригуб
Перевод с английского Ю.И. Корниенко, Н.А. Мухина
Под редакцией Ю.Н, Артеменко
По общим вопросам обращайтесь в издательство “Диалектика” по адресу:
info@dialektika.com, http://www.dialektika.com
115419, Москва, а/я 783; 03150, Киев, а/я 152
Хортон, Айвор.
Х82 Visual C++ 2005: базовый курс. : Пер. с англ. — М. : ООО “И.Д. Вильямс”, 2007. —
1152 с. : ил. — Парал. тит. англ.
ISBN 978-5-8459-1016-5 (рус.)
Книга опытного специалиста в области разработки приложений в среде Visual C++
2005 предлагает исключительно полное введение как в сам язык программирования
C++, так и в особенности его реализации в Visual C++ 2005. Книгу отличает простой
и доступный стиль изложения, изобилие примеров и множество рекомендаций по на-
писанию высококачественных программ. Подробно рассматриваются такие вопросы,
как основные элементы языка C++, фундаментальные типы и управляющие структуры,
модульная организация кода, объектно-ориентированное программирование, особен-
ности написания кода на C++/CLI для .NET, технологии отладки приложений и про-
граммирование для Windows и многое другое. Немало внимания уделяется разработке
визуальных интерфейсов Windows-приложений, использованию библиотеки MFC , вза-
имодействию с базами данных и программированию с применением Windows Forms.
Книга рассчитана на начинающих программистов, а также будет полезной для сту-
дентов и преподавателей дисциплин, связанных с программированием и разработкой
в среде Visual C++.
ББК 32.973.26-018.2.75
Все названия программных продуктов являются зарегистрированными торговыми марками соответствующих
фирм.
Никакая часть настоящего издания ни в каких целях не может быть воспроизведена в какой бы то ни было
форме и какими бы то ни было средствами, будь то электронные или механические, включая фотокопирование и
запись на магнитный носитель, если на это нет письменного разрешения издательства JOHN WILEY&Sons, Inc.
Copyright © 2007 by Dialektika Computer Publishing.
Original English language edition Copyright © 2006 by Ivor Horton
All rights reserved including the right of reproduction in whole or in part in any form. This translation is published by
arrangement with Wiley Publishing, Inc.
No part of this publication may be reproduced, stored in a retrieval system or transmitted in any form or by any means,
electronic, mechanical, photocopying, recording, scanning or otherwise, without either the prior written permission of the
Publisher.
Wiley, the Wiley logo, Wrox, the Wrox logo, Programmer to Programmer, and related trade dress are trademarks or
registered trademarks of John Wiley & Sons, Inc. and/or its affiliates, in the United States and other countries, and may
not be used without written permission. Visual C++ is a registered trademark of Microsoft Corporation in the United States
and/or other countries. All other trademarks are the property of their respective owners. Wiley Publishing, Inc., is not ass-
ociated with any product or vendor mentioned in this book. Wiley also publishes its books in a variety of electronic formats.
Some content that appears in print may not be available in electronic books.
ISBN 978-5-8459-1016-5 (pyc.)
ISBN 0-7645-7197-4 (англ.)
© Компьютерное изд-во “Диалектика”, 2007,
перевод, оформление, макетирование
© Copyright © by Ivor Horton, 2006
Оглавление
Введение 22
Глава 1. Программирование в Visual C++ 2005 27
Глава 2. Данные, переменные и вычисления 63
Глава 3. Решения и циклы 139
Глава 4. Массивы, строки и указатели 179
Глава 5. Структурная организация программ 249
Глава 6. Дополнительные сведения о структурах программ 287
Глава 7. Определение собственных типов данных 337
Глава 8. Дополнительные сведения о классах 411
Глава 9. Наследование классов и виртуальные функции 481
Глава 10. Технологии отладки 567
Глава 11. Концепции программирования для Windows 615
Глава 12. Программирование для Windows с использованием MFC 651
Глава 13. Работа с меню и панелями инструментов 679
Глава 14. Рисование в окне 709
Глава 15. Создание документа и усовершенствование представления 759
Глава 16. Работа с диалогами и элементами управления 815
Глава 17. Сохранение и печать документов 865
Глава 18. Написание собственных DLL-библиотек 897
Глава 19. Подключение к источникам данных 919
Глава 20. Обновление источников данных 975
Глава 21. Приложения, использующие средства Windows Forms 1027
Глава 22. Доступ к источникам данных в приложении Windows Forms 1087
Приложение А. Ключевые слова C++ 1129
Приложение Б. Коды ASCII 1131
Предметный указатель 1135
Содержание
Об авторе 21
Благодарности 21
Введение 22
Для кого предназначена эта книга 22
О чем эта книга 23
Как построена эта книга 23
Что необходимо для работы с этой книгой 25
Соглашения 25
Исходные коды 26
От издательства 26
Глава 1. Программирование в Visual C++ 2005 27
Среда .NET Framework 28
Общеязыковая исполняющая среда (CLR) 28
Написание приложений на C++ 29
Изучение программирования для Windows 31
Изучение C++ 31
Стандарты C++ 32
Консольные приложения 32
Концепции программирования для Windows 33
Что такое интегрированная среда разработки? 34
Компоненты системы 35
Использование IDE 37
Опции панели инструментов 38
Стыкуемые панели инструментов 39
Документация 39
Проекты и решения 40
Настройка опций Visual C++ 2005 54
Создание и выполнение Windows-приложений 55
Создание приложения Windows Forms 58
Резюме 62
Глава 2. Данные, переменные и вычисления 63
Структура программы C++ 64
Функция main () 72
Операторы программы 72
Пробелы 74
Блоки операторов 75
Автоматически сгенерированные консольные программы 75
Определение переменных 76
Именование переменных 77
Содержание
Объявление переменных 78
Начальные значения переменных 79
Фундаментальные типы данных 79
Целочисленные переменные 80
Символьные типы данных 81
Модификаторы целочисленных типов 82
Булевский тип 83
Типы с плавающей точкой 83
Литералы 84
Определение синонимов для типов данных 85
Переменные с определенным набором значений 86
Спецификация типа для перечислимых констант 87
Базовые операции ввода-вывода 88
Ввод с клавиатуры 88
Вывод в командную строку 88
Форматирование вывода 89
Управляющие последовательности 90
Вычисления в C++ 92
Операторы присваивания 93
Арифметические операции 93
Вычисление остатка 98
Модификация переменной 99
Операции инкремента и декремента 100
Последовательность вычислений 102
Типы переменных и приведения 103
Правила приведения операндов 103
Приведения в операторах присваивания 105
Явные приведения 105
Приведения в старом стиле 106
Битовые операции 106
Время хранения и область видимости 112
Автоматические переменные 113
Размещение объявлений переменных 115
Глобальные переменные 115
Статические переменные 119
Пространства имен 119
Объявление пространства имен 121
Множественные пространства имен 122
Программирование на C++/CLI 123
Специфика C++/CLI: фундаментальные типы данных 124
Вывод командной строки C++/CLI 128
Специфика C++/CLI — форматирование вывода 128
Клавиатурный ввод в C++/СЫ 131
Применение safe_cast 132
Перечисления C++/CLI 132
Резюме 135
Упражнения 137
8 Содержание
Глава 3. Решения и циклы 139
Сравнение значений 139
Оператор if 141
Вложенные операторы if 142
Расширенный оператор if 144
Вложенные операторы if-else 146
Логические операции и выражения 147
Условная операция 150
Оператор switch 152
Безусловное ветвление 155
Повторение блока операторов 155
Что такое цикл? 155
Вариации цикла for 157
Цикл while 166
Цикл do-while 167
Вложенные циклы 168
Программирование на C++/CLI 171
Цикл for each 175
Резюме 177
Упражнения 178
Глава 4. Массивы, строки и указатели 179
Обработка множества однотипных элементов данных 180
Массивы 180
Объявление массивов 181
Инициализация массивов 184
Символьные массивы и обработка строк 186
Многомерные массивы 189
Косвенный доступ к данным 192
Что такое указатель? 193
Объявление указателей 193
Использование указателей 194
Инициализация указателей 196
Операция sizeof 201
Константные указатели и указатели на константы 203
Указатели и массивы 204
Динамическое выделение памяти 211
Свободное хранилище, псевдоним “куча” 211
Операции new и delete 212
Динамическое распределение памяти для массивов 212
Динамическое распределение многомерных массивов 215
Использование ссылок 216
Что такое ссылка? 216
Объявление и инициализация ссылок 216
Программирование на C++/CLI 217
Отслеживаемые дескрипторы 217
Массивы CLR 219
Содержание
Строки 233
Отслеживающие ссылки 242
Внутренние указатели 242
Резюме 245
Упражнения 246
Глава 5. Структурная организация программ
249
Что такое функции 250
Зачем нужны функции? 251
Структура функции 251
Использование функций 253
Передача аргументов в функцию 257
Механизм передачи по значению 257
Указатели как аргументы функций 259
Передача массивов в функцию 260
Ссылки как аргументы функции 264
Использование модификатора const 266
Аргументы main () 267
Прием функцией переменного количества аргументов 269
Возврат значений функциями 271
Возврат указателя 271
Возврат ссылки 274
Статические переменные в функциях 276
Рекурсивные вызовы функции 278
Использование рекурсии 280
Программирование на C++/CLI 281
Функции, принимающие переменное количество аргументов 282
Аргументы ma i п () 283
Резюме 284
Упражнения 284
Глава 6. Дополнительные сведения о структурах программ 287
Указатели на функции 287
Объявление указателей на функции 288
Указатель на функцию в качестве аргумента 290
Массивы указателей на функции 292
Инициализация параметров функций 293
Исключения 294
Возбуждение исключений
Перехват исключений
Обработка исключений в MFC
Обработка ошибок выделения памяти
Перегрузка функций
Что такое перегрузка функций?
Когда нужно перегружать функции
Шаблоны функций
Использование шаблона функции
296
297
298
299
300
301
303
303
304
10 Содержание
Пример использования функций
Реализация калькулятора
Удаление пробелов из строки
Вычисление выражения
Получение значения элемента
Анализ числа
Собираем программу вместе
Рас:
прение программы
Извлечение подстроки
Запуск модифицированной программы
Программирование C++/CLI
Что такое обобщенные функции
Программа калькулятора для CLR
Резюме
306
306
309
310
312
314
316
318
319
321
322
323
328
334
Упражнения
Глава 7. Определение собственных типов данных
337
Структуры в C++
Что такое структура?
Определение структуры
Инициализация структуры
Доступ к членам структуры
Поддержка средства Intellisense при работе со структурами
Структура RECT
Использование указателей со структурами
Типы данных, объекты, классы и экземпляры
Первый класс
Операции с классами
Терминология
Что такое класс?
338
338
338
339
339
343
344
344
346
348
348
349
349
Определение класса 350
Определение объектов класса 350
Доступ к данным-членам класса 351
Функции-члены класса 353
Расположение определения функции-члена 355
Встроенные функции 355
Конструкторы классов 356
Что такое конструктор? 356
Конструктор по умолчанию 358
Присваивание параметрам в классе значений по умолчанию 360
Использование списка инициализации в конструкторе 363
Приватные члены класса 363
Доступ к приватным членам класса 366
Дружественные функции класса 367
Конструктор копирования по умолчанию 369
Указатель this 370
const-объекты класса 373
Содержание 11
const-функция-член класса 373
Определения функций-членов вне класса 374
Массивы объектов класса 375
Статические члены класса 377
Статические данные-члены класса 377
Статические функции-члены класса 379
Указатели и ссылки на объекты классов 380
Указатели на объекты класса 380
Ссылки на объекты класса 383
Программирование на C++/CLI 384
Определение типов классов значений 385
Определение типов ссылочных классов 390
Свойства классов 392
Поля initonly 404
Статические конструкторы 406
Резюме 407
Упражнения 408
Глава 8. Дополнительные сведения о классах
411
Деструкторы классов 411
Что такое деструктор? 412
Деструктор по умолчанию 412
Деструкторы и динамическое распределение памяти 414
Реализация конструктора копирования 417
Разделение памяти между переменными 419
Определение объединений 420
Объединения в классах и структурах 421
Перегрузка операций 422
Реализация перегруженной операции 422
Реализация полной поддержки операции 425
Перегрузка операции присваивания 429
Перегрузка операции сложения 433
Перегрузка операций инкремента и декремента 437
Шаблоны классов 438
Определение шаблона класса
Создание объектов из шаблона класса
Шаблоны классов с множественными параметрами
Использование классов
Понятие интерфейса класса 446
Определение проблемы 446
Реализация класса СВох 447
Определение класса СВох 455
Использование класса СВох 464
Организация кода программы 467
Именование программных файлов 469
Программирование на C++/CLI 470
Перегрузка операций в классах значений 470
12 Содержание
Перегрузка операций инкремента и декремента 475
Перегрузка операций в ссылочных классах 476
Резюме 478
Упражнения 479
Глава 9. Наследование классов и виртуальные функции 481
Базовые идеи объектно-ориентированного программирования 481
Наследование в классах 483
Что такое базовый класс? 483
Наследование классов от базового класса 484
Управление доступом при наследовании 487
Работа конструктора в производном классе 490
Объявление членов класса как protected 493
Уровень доступа унаследованных членов класса 496
Конструктор копирования в производном классе 497
Члены класса как друзья 501
Дружественные классы 503
Ограничения отношения дружественности классов 503
Виртуальные функции 503
Что такое виртуальная функция? 505
Использование указателей на объекты классов 507
Использование ссылок с виртуальными функциями 509
Чистые виртуальные функции 511
Абстрактные классы 512
Непрямые базовые классы 514
Виртуальные деструкторы 517
Приведение между типами классов 521
Вложенные классы 522
Программирование на C++/CLI 525
Наследование классов в C++/СЫ 525
Интерфейсные классы 530
Определение интерфейсных классов 531
Классы и сборки 535
Функции, специфицированные как new 539
Делегаты и события 540
Деструкторы и финализаторы в ссылочных классах 550
Обобщенные классы 552
Резюме 562
Упражнения 563
Глава 10. Технологии отладки 567
Что такое отладка? 567
Ошибки в программах 569
Распространенные ошибки 570
Базовые операции отладки 571
Установка точек прерывания 573
Установка точек трассировки 575
Содержание 13
Запуск отладки 576
Изменение значения переменной 580
Добавление отладочного кода 581
Использование утверждений 581
Добавление собственного отладочного кода 583
Отладка программы 588
Стек вызовов 588
Переход к ошибке 590
Тестирование расширенного класса 593
Поиск следующей ошибки 595
Отладка динамической памяти 596
Функции проверки свободного хранилища
Управление отладочными операциями свободного хранилища
Отладочный вывод свободного хранилища
Отладка программ C++/CLI
Использование классов Debug и Trace
Резюме
596
598
599
604
605
613
Глава 11. Концепции программирования для Windows 615
Основы программирования для Windows 616
Элементы окна 617
Windows-программы и операционная система 619
Программирование, управляемое событиями 619
Сообщения Windows 619
Windows API 620
Типы данных Windows 621
Нотация программ Windows 621
Структура Windows-программы 623
Функция WinMain () 624
Функции обработки сообщений 635
Простая Windows-программа 639
Организация Windows-программ 640
Библиотека Microsoft Foundation Classes 642
Нотация MFC 642
Как структурирована программа MFC 643
Использование Windows Forms 646
Резюме
649
Глава 12. Программирование для Windows с использованием MFC 651
Концепция “документ-представление” в MFC 652
Что такое документ? 652
Документные интерфейсы 652
Что такое представление? 653
Связь документа с его представлениями 654
Ваше приложение и MFC 655
Создание приложений MFC 656
Создание SDI-приложения 658
14 Содержание
Вывод мастера MFC Application Wizard 663
Создание MDI-приложения 67 3
Резюме 676
Упражнения 676
Глава 13. Работа с меню и панелями инструментов
679
Взаимодействие с Windows 679
Что такое карты сообщений? 680
Категории сообщений 683
Обработка сообщений в ваших программах 684
Расширение программы S ke t che г 685
Элементы меню 686
Создание и редактирование ресурсов меню 686
Добавление обработчиков сообщений меню 691
Выбор класса для обработки сообщений меню 692
Создание функций сообщений меню 692
Кодирование функций сообщений меню 694
Добавление обработчиков сообщений для обновления пользовательского
интерфейса 699
Добавление кнопок панели инструментов 702
Редактирование свойств кнопки панели инструментов 703
Испытание кнопок панели инструментов 704
Добавление всплывающих подсказок 705
Резюме 706
Упражнения 7 07
Глава 14. Рисование в окне
Основы рисования в окне
Клиентская область окна
Интерфейс графических устройств Windows
Механизм рисования в Visual C++
Класс представления в вашем приложении
Класс С DC
Практическое рисование графики
Программирование для мыши
Сообщения от мыши
Обработчики сообщений мыши
Рисование с помощью мыши
Испытание программы S ke t che г
Запуск примера
Захват сообщений мыши
Резюме
Упражнения
709
709
710
711
713
714
715
723
726
726
728
730
754
754
755
756
757
Глава 15. Создание документа и усовершенствование представления 759
Что такое классы коллекций? 760
Типы коллекций 760
Содержание 15
Безопасные к типам классы коллекций
Коллекции объектов
Типизированные коллекции указателей
Использование шаблонного класса CList
Рисование кривой
Определение класса CCurve
Реализация класса CCurve
Испытание класса CCurve
Создание документа
Использование шаблона CTypedPtrList
Усовершенствование представления
Обновление множественных представлений
Прокрутка представлений
Использование режима отображения MM_LOENGLISH
Удаление и перемещение фигур
Реализация контекстного меню
Ассоциирование меню с классом
Выбор контекстного меню
Подсветка элементов
Обработка сообщений меню
Работа с маскированными элементами
Резюме
Упражнения
761
761
771
773
773
774
776
777
778
778
783
783
785
790
792
792
794
795
800
805
812
813
814
Глава 16. Работа с диалогами и элементами управления 815
Понятие диалогов 815
Что такое элементы управления? 816
Общие элементы управления 818
Создание ресурса диалога 818
Добавление элементов управления в диалоговое окно 819
Программирование для диалога 821
Добавление класса диалога 821
Модальные и немодальные диалоги 822
Отображение диалога 823
Поддержка диалоговых элементов управления 826
Инициализация элементов управления 826
Обработка сообщений переключателей 828
Завершение операций диалога 829
Добавление ширины пера к документу 829
Добавление ширины пера к элементам 830
Создание элементов в представлении 831
Испытание диалога 831
Использование кнопки счетчика 832
Добавление пункта меню и кнопки панели инструментов для функции
масштабирования 833
Создание кнопки счетчика 833
Генерация класса диалога масштабирования 836
16 Содержание
Отображение кнопки счетчика 839
Использование показателя масштаба 839
Масштабируемые режимы отображения 840
Установка размера документа 841
Установка режима отображения 841
Реализация прокрутки с масштабированием 843
Работа с панелями состояния 845
Добавление панели состояния в обрамляющее окно 846
Использование окна списка 849
Удаление диалога масштаба 850
Создание элемента управления — окна списка 850
Использование элемента управления — поля редактирования 853
Создание ресурса поля редактирования 853
Создание класса диалога 855
Добавление пункта меню Text 857
Определение текстового элемента 858
Реализация класса С Text 858
Создание текстового элемента 860
Резюме 861
Упражнения 863
Глава 17. Сохранение и печать документов 865
Что такое сериализация? 865
Сериализация документа 866
Сериализация в определении класса документа 866
Сериализация в реализации класса документа 867
Функциональность классов, базирующихся на СОЬ j ect 870
Как работает сериализация 871
Как реализовать сериализацию класса 872
Применение сериализации 872
Запись изменений в документе 872
Сериализация документа 874
Сериализация классов элементов 875
Испытание сериализации 878
Перемещение текста 879
Печать документа 881
Процесс печати 882
Реализация многостраничной печати 885
Получение полного размера документа 886
Сохранение данных печати 886
Подготовка к печати 887
Очистка после печати 889
Подготовка контекста устройства 889
Печать документа 890
Получение печатного вывода документа 894
Резюме 895
Упражнения 896
Содержание 17
Глава 18. Написание собственных DLL-библиотек 897
Что такое DLL-библиотека? 897
Как работают DLL-библиотеки 899
Содержимое DLL-библиотеки 902
Вариации DLL-библиотек 903
Что помещать в DLL-библиотеку 904
Написание DLL-библиотек 904
Написание и использование DLL расширения 905
Экспорт переменных и функций из DLL-библиотеки 912
Импорт символов в программу 913
Реализация экспорта символов из DLL-библиотеки 914
Резюме 916
Упражнения 917
Глава 19. Подключение к источникам данных 919
Основы баз данных 919
Немного об SQL 922
Извлечение данных с использованием SQL 923
Соединение таблиц с помощью SQL 924
Сортировка записей 927
Поддержка баз данных в MFC 927
Классы MFC для поддержки ODBC 928
Создание приложения базы данных 929
Регистрация базы данных ODBC 929
Генерация программы MFC ODBC 932
Структура программы 935
Тестирование примера 945
Сортировка набора записей 947
Модификация заголовка окна 947
Использование второго объекта набора записей 948
Добавление класса набора записей 949
Добавление класса представления для набора записей 952
Настройка набора записей 955
Доступ к многотабличным представлениям 959
Просмотр заказов для продукта 964
Просмотр подробностей о заказчике 965
Добавление набора записей заказчиков 965
Создание ресурса для диалога заказчика 966
Создание класса представления заказчиков 966
Добавление фильтра 968
Реализация параметра фильтра 970
Связывание диалога заказов с диалогом информации о заказчике 971
Испытание программы просмотра базы данных 973
Резюме 974
Упражнения 974
18 Содержание
Глава 20. Обновление источников данных 975
Операции обновления 975
Операции обновления CRecordset 976
Проверка допустимости операций 978
Блокировка записей 978
Транзакции 979
Операции транзакций класса CDatabase 979
Простой пример обновления 981
Настройка приложения 982
Управление процессом обновления 985
Реализация режима обновления 987
Активизация и отключение полей редактирования 987
Изменение надписи кнопки 989
Управление видимостью кнопки Cancel 990
Отключение меню Record 991
Фактическое выполнение обновления 993
Реализация операции отмены 994
Добавление строк в таблицу 995
Процесс ввода заказа 997
Создание ресурсов 997
Создание наборов записей 998
Создание представлений наборов записей 999
Добавление элементов управления в ресурсы диалогов 1003
Реализация переключения диалоговых окон 1007
Создание идентификатора заказа 1011
Выбор продуктов для включения в заказ 1018
Резюме 1025
Упражнения 1026
Глава 21. Приложения, использующие средства Windows Forms 1027
Общее представление о Windows Forms 1027
Общее представление о приложениях Windows Forms 1028
Изменение свойств формы 1030
Запуск приложения 1031
Индивидуальная настройка графического интерфейса пользователя 1032
Добавление элементов управления в форму 1033
Добавление элемента управления с вкладками 1036
Использование элементов управления GroupBox 1039
Использование элементов управления Button 1041
Использование элемента управления WebBrowser 1043
Работа приложения Winning Application 1045
Добавление контекстного меню 1045
Создание обработчиков событий 1046
Обработка событий меню Limits 1052
Создание диалогового окна 1053
Использование диалогового окна 1059
Добавление второго диалогового окна 1065
Содержание 19
Реализация элемента меню Help*=> About 1074
Обработка щелчка на кнопке 1074
Реакция на щелчок в контекстном меню 1077
Резюме 1085
Упражнения 1085
Глава 22. Доступ к источникам данных в приложении Windows Forms 1087
Работа с источниками данных 1088
Доступ и отображение данных 1089
Использование элемента управления DataGridView 1090
Использование элемента управления DataGridView в несвязанном режиме 1091
Персональная настройка элемента управления DataGridView 1098
Настройка ячеек заголовков 1099
Настройка ячеек, не являющихся заголовками 1099
Динамическое определение стилей ячеек 1107
Использование связанного режима 1112
Компонент Bindingsource 1113
Использование элемента управления BindingNavigator 1118
Привязка к отдельным элементам управления 1121
Работа с множеством таблиц 1126
Резюме 1127
Упражнения 1128
Приложение А. Ключевые слова C++ 1129
Ключевые слова ISO /AN SI C++ 1129
Ключевые слова C++/CLI 1130
Приложение Б. Коды ASCII 1131
Предметный указатель 1135
Эта книга посвящается Александру Гилби (Alexander Gilbey).
Надеюсь получить его комментарии, хотя вероятно, ждать придется долго.
Об авторе
Айвор Хортон (Ivor Horton) получил математическое образование и затем увлек-
ся информационными технологиями, поскольку они обещали значительную отдачу от
достаточно небольших усилий. Реальность оказалась таковой, что, как выяснилось, в
этой области требуется приложить весьма значительные усилия, чтобы добиться от-
носительно средних результатов, однако это не помешало ему продолжать работу с
компьютерами вплоть до настоящего времени. В разное время ему доводилось зани-
маться программированием, проектированием систем, консультациями и управлени-
ем реализацией проектов высокой сложности.
Хортон обладает многолетним опытом проектирования и реализации компьютер-
ных систем применительно к инженерному проектированию и выполнению опера-
ций во многих отраслях промышленности. Ему приходилось разрабатывать множе-
ство полезных приложений на широком разнообразии языков программирования, а
также обучать этому ученых и инженеров. За более чем 10 лет он написал ряд книг
по программированию, среди которых руководства по С, C++ и Java. В настоящее вре-
мя Хортон, когда не пишет очередную книгу и не рецензирует чужие публикации, он
тратит свое время на рыбалку, путешествия и изучение французского.
Благодарности
Я хотел бы выразить признательность издательству John Wiley & Sons, а также
редакторам и команде из Wrox Press за усилия, предпринятые ими для появления на
свет этой книги, а особенно — ведущему редактору Кевину Кенту (Kevin Kent), кото-
рый с самого начала стоял за моей спиной и оставался рядом до самого конца. Также
я хотел бы поблагодарить технического редактора Джона Мюллера (John Mueller) за
тщательный просмотр текста и нахождение (надеюсь) большинства ошибок, за про-
верку всех примеров и за множество конструктивных комментариев, которые позво-
лили сделать эту книгу наилучшим руководством.
И, наконец, я хотел бы поблагодарить свою жену Еву за терпение, доброжелатель-
ность и поддержку на всем протяжении работы над этой книгой. Как я уже не раз
говорил по многим другим поводам, без нее я бы не справился с этой работой.
Введение
Поздравляю с удачным приобретением! Благодаря этой книге вы сможете стать
удачливым программистом на C++. Самая последняя версия системы разработки от
корпорации Microsoft, Visual Studio 2005, поддерживает два различных, но тесно свя-
занных представления языка C++; в ней полностью реализован исходный стандарт
ISO/ANSI C++ и, кроме того вы получаете поддержку новой версии C++, называе-
мой C++/CLI, которая была разработана Microsoft и теперь утверждена в стандар-
те ЕСМА. Эти две версии C++ взаимно дополняют друг друга и играют совершенно
разные роли. ISO/ANSI C++ предназначен для разработки высокопроизводительных
приложений, которые выполняются вашим компьютером как “родные”, в то время
как C++/CLI разработан специально для .NET Framework. Настоящая книга даст вам
все самое необходимое для программирования приложений на обеих версиях C++.
Вы получите существенную помощь от автоматически сгенерированного кода при
написании программ на ISO/ANSI C++, но все же вам придется также писать на C++
самостоятельно. Вам понадобится основательное понимание приемов объектно-ори-
ентированного программирования вообще и программирования под Windows — в
частности. Хотя C++/CLI ориентирован на .NET Framework, он также является от-
личным инструментом программирования приложений Windows Forms, которые вы
можете разрабатывать с минимальным написанием кода вручную, или даже вообще
обходясь без этого. Конечно, когда вам нужно будет добавлять код в приложение
Windows Forms, пусть даже в незначительных объемах по отношению к общему коду
приложения, вам понадобятся достаточно глубокие знания языка C++/CLI.
ISO/ANSI C++ остается выбором многих профессионалов, однако скорость разра-
ботки, которую обеспечивает C++/CLI для приложений Windows Forms, привлекает
и новичков. По этой причине в настоящей книге было решено раскрыть основы обе-
их воплощений языка C++.
Для кого предназначена эта книга
Целью этой книги является научить вас написанию приложений C++ для операци-
онной системы Microsoft Windows с применением Visual C++ 2005 или любой версии
Visual Studio 2005. Она не исходит из каких-либо предположений относительно на-
чального уровня знаний в любом конкретном языке программирования. Это руковод-
ство — для вас, если справедливы следующие утверждения.
□ Вы обладаете минимальным опытом программирования на каком-то другом
языке, например, на BASIC или Pascal, и хотите изучить C++ для приобретения
навыков программирования под Microsoft Windows.
□ У вас имеется некоторый опыт работы на С или C++, но не в контексте Microsoft
Windows, и вы хотите расширить свои знания для программирования в среде
Windows с использованием наиболее современных инструментов и технологий.
□ Вы — новичок в программировании и готовы к тому, чтобы с головой погру-
зиться в C++. Для достижения успеха вам понадобится хотя бы приблизитель-
ное представление о том, как работает компьютер, включая устройство его па-
мяти и способы хранения данных и команд.
Введение 23
О чем эта книга
Моей целью при написании этой книги было научить вас сути программирова-
ния на C++ с применением обеих технологий, поддерживаемых в Visual C++ 2005.
В этой книге представлено детальное руководство по двум воплощениям C++, а имен-
но: по разработке приложений Windows на “родном” ISO/ANSI C++ с применением
библиотеки Microsoft Foundation Classes (MFC), а также по разработке Windows-при-
ложений на C++/CLI с использованием Windows Forms. Из-за важности и широкого
распространения в наши дни технологий баз данных книга также включает представ-
ление приемов доступа к данным, как в приложениях MFC, так и в Windows Forms.
Приложения MFC требуют относительно большего объема кодирования по сравне-
нию с приложениями Windows Forms. Это объясняется тем, что вы создаете послед-
ние с использованием развитых возможностей визуального программирования Visual
C++ 2005, которые позволяют строить графический интерфейс пользователя в инте-
рактивном визуальном режиме, а весь необходимый код при этом генерируется авто-
матически. По этой причине в нашей книге больше страниц посвящено программи-
рованию MFC, чем программированию Windows Forms.
Как построена эта книга
Содержимое настоящей книги структурировано следующим образом.
□ Глава 1 предоставляет базовые концепции, которые необходимы для понима-
ния программирования приложений на “родном” C++ и приложений, выпол-
няющихся под управлением .NET Framework — наряду с основными идеями,
положенными в основу среды разработки Visual C++ 2005. В ней описано ис-
пользование возможностей Visual C++ 2005 для создания различных видов при-
ложений C++, которые затем подробно рассматриваются в остальных главах
книги.
□ Главы 2—10 посвящены исследованию обеих версий языка C++, а также базо-
вым идеям и приемам отладки. Содержимое глав 2—10 структурировано сход-
ным образом; начальная часть каждой главы содержит темы, касающиеся ISO/
ANSI C++, а завершающая — C++/CLI.
□ В главе 11 обсуждается, как структурированы приложения Microsoft Windows,
а также описаны и продемонстрированы наиболее важные элементы, присут-
ствующие в каждом приложении Windows. Эта глава предлагает элементарные
примеры Windows-приложений с использованием ISO/ANSI C++, Windows API
и MFC, а также базовый пример приложения Windows Forms на C++/CLI.
□ В главах 12-17 подробно описаны возможности, предоставляемые MFC для по-
строения GUI. Вы изучите, как создавать и использовать элементы управления
общего назначения для построения графического пользовательского интер-
фейса ваших приложений и как обрабатывать события, возникающие в резуль-
тате взаимодействия пользователя с вашей программой. В процессе чтения вы
создадите реальное работающее приложение. В дополнение к изучению при-
емов построения графического интерфейса, разработанное вами приложение
также продемонстрирует применение MFC для печати документов и сохране-
нию их на диске.
24 Введение
□ Глава 18 научит вас всему, что понадобится для создания собственных библио-
тек, использующих MFC. Вы узнаете о различных видах библиотек и напишете
работающие примеры таких библиотек, которые затем будете развивать и со-
вершенствовать на протяжении целых шести глав.
□ В главах 19 и 20 рассматривается доступ к источникам данных из приложений
MFC. Вы приобретете опыт доступа к базам данных в режиме только для чте-
ния, а затем изучите фундаментальные приемы программирования, необхо-
димые для обновления базы данных с использованием MFC. Эти примеры ис-
пользуют базу данных Nortwind, которую можно загрузить из Internet, но с тем
же успехом вы сможете применить описанные приемы для обращения к своим
собственным источникам данных.
□ В главе 21 вы будете иметь дело с Windows Forms и C++/CLI при построении
примера, который научит вас создавать, настраивать и использовать элементы
управления Windows Forms в приложении. Вы получите практический опыт по-
следовательного построения приложений на протяжении всей этой главы.
□ В главе 22 на основе знаний, приобретенных вами в главе 21, будет показано, как
работают элементы управления, предназначенные для доступа к данным, и как
они настраиваются. Вы также узнаете, как можно создать приложение для досту-
па к базе данных, почти полностью избегая непосредственного кодирования.
Все главы включают многочисленные работающие примеры, которые демонстри-
руют обсуждаемые приемы программирования. Каждая глава завершается подведени-
ем итогов, где перечисляются ключевые моменты, и большинство глав предлагает в
конце набор упражнений, которые вы можете выполнить для закрепления получен-
ных знаний. Решения этих упражнений вместе со всеми кодами примеров доступны
для загрузки на Web-сайте издательства.
В руководстве по языку C++ используются примеры — консольные программы с
простым вводом-выводом в командной строке. Такой подход позволяет вам изучить
различные возможности C++, не затемняя их сложностями программирования гра-
фических интерфейсов Windows. Программирование для Windows стоит начинать
только после того, как вы достигнете определенного уровня понимания языка про-
граммирования вообще.
Если вы хотите, чтобы все было настолько просто, насколько возможно, можете
поначалу просто изучить программирование на ISO/ANSI C++. В каждой из глав, по-
священных языку C++ (со 2-й по 10-ю) сначала обсуждаются определенные аспекты
ISO/ANSI C++, за которыми следуют новые средства C++/CLI, в заранее определен-
ном контексте. Причина такой организации материала состоит в том, что C++/CLI
определен как расширение стандартного языка ISO/ANSI C++, поэтому понимание
C++/CLI требует предварительных знаний ISO/ANSI C++. В результате вы можете
просто читать разделы о ISO/ANSI C++ в главах 2 — 20 и игнорировать следующие
за ними разделы, посвященные C++/CLI. Затем вы можете обратиться к программи-
рованию Windows-приложений на ISO/ANSI C++, без необходимости удержания в па-
мяти особенностей двух версий языка. Вы можете обратиться к C++/CLI тогда, когда
почувствуете себя уверенно с ISO/ANSI C++. Конечно, при желании можете просто
читать все подряд, совершенствуя свои знания обеих версий C++ параллельно.
Введение 25
Что необходимо для работы с этой книгой
Чтобы использовать эту книгу, вам понадобится либо Visual Studio 2005 Standard
Edition, либо Visual Studio 2005 Professional Edition, либо Visual Studio 2005 Team
System. Обратите внимание, что Visual C++ Express 2005 не подойдет, поскольку в эту
версию не включена библиотека MFC. Для Visual Studio 2005 необходима операци-
онная система Windows ХР Service Pack 2 или Windows 2000 Service Pack 4. Чтобы ин-
сталлировать любую из перечисленных редакций Visual Studio 2005, вам понадобится
процессор с тактовой частотой 1 ГГц, минимум 256 Мбайт памяти, минимум 1 Гбайт
свободного пространства на системном жестком диске, а также 2 Гбайт доступного
пространства на диске, куда будет вестись инсталляция. Чтобы установить полную
документацию MSDN, поставляемую с продуктом, вам понадобится дополнительно
1,8 Гбайт на диске инсталляции.
Примеры, рассматриваемые в настоящей книге, которые связаны с базами
данных, используют базу Northwind Traders. Вы можете найти эту базу и загру-
зить на свой компьютер, запустив поиск по “Northwind Traders” на сайте http: //
msdn .microsof t. com. Конечно, при желании вы можете адаптировать примеры для
работы с базой данных по собственному выбору.
Но самое главное, что вам понадобится для того, чтобы извлечь максимальную
пользу от этой книги — это желание учиться и мотивация к овладению наиболее эф-
фективными инструментами разработки Windows-приложений из числа доступных в
настоящее время на рынке. Вам придется потратить время на тщательную проработку
и испытание всех примеров упражнений, приведенных в книге. Это звучит несколь-
ко “страшнее”, чем есть на самом деле, и я думаю, вы удивитесь, насколько многого
можно достичь за сравнительно небольшое время. Имейте в виду, что каждый, кто
изучает программирование, время от времени чувствует себя “увязшим” в большом
объеме информации, но со временем все проясняется и становится понятным. Наша
книга поможет вам сразу приступить к экспериментам и, следовательно, быстрее
стать успешным программистом на C++.
Соглашения
Чтобы помочь вам получить от текста большую отдачу и следить за тем, что проис-
ходит, в этой книге принят ряд соглашений.
Практическое занятие | Здесь ПОмеЩаеТСЯ уПраЖНвНИе, КОТОрОв
вы должны выполнить, следуя инструкциям,
приведенным в книге.
Описание полученных результатов
После нескольких разделов “Практическое занятие” в этом разделе рассмотрен-
ный ранее код объясняется во всех подробностях.
Врезки вроде этой содержат важную информацию, которую не следует упускать из виду, и
которая непосредственно касается текста, где размещена.
26 Введение
Советы, подсказки, трюки и сноски выделены курсивом.
Ниже описаны стили текста книги.
□ Новые термины и важные слова, которые встречаются впервые, выделены по-
лужирным.
□ Клавишные комбинации представлены как <Ctrl+A>.
□ Имена файлов, URL-адреса и код внутри текста выделен моноширинным шриф-
том: persistence.properties.
□ Код представляется двумя разными способами:
В примерах полужирным выделяются новые и важные части кода.
Выделение не используется для кода, который менее важен в текущем контексте
либо уже был представлен ранее.
Исходные коды
Исходные коды примеров и упражнений вместе с решениями доступны для загруз-
ки на Web-сайте издательства.
От издательства
Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим
ваше мнение и хотим знать, что было сделано нами правильно, что можно было сде-
лать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать
и любые другие замечания, которые вам хотелось бы высказать в наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумаж-
ное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои
замечания там. Одним словом, любым удобным для вас способом дайте нам знать,
нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать
наши книги более интересными для вас.
Посылая письмо или сообщение, не забудьте указать название книги и ее авторов,
а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обя-
зательно учтем его при отборе и подготовке к изданию последующих книг.
Наши координаты:
E-mail: inf o@dialektika. com
WWW: http: / /www. dialektika. com
Информация для писем из:
России: 115419, Москва, а/я 783
Украины: 03150, Киев, а/я 152
Программирование
в Visual C++ 2005
Программировать под Windows вовсе не трудно. Фактически Microsoft Visual C++
2005 делает этот процесс изумительно легким, и вы убедитесь в этом, прочитав эту
книгу. Есть лишь одно препятствие на этом пути: прежде, чем обратиться к специ-
фике программирования Windows, вы должны быть хорошо знакомы со средствами
языка программирования C++, в частности, с его объектно-ориентированными аспек-
тами. Техника объектно-ориентированного программирования является ключом к эф-
фективности всех инструментов среды Visual C++ 2005, используемых при создании
программ для Windows, поэтому очень важно, чтобы вы добились хорошего ее пони-
мания. Именно на это и нацелена настоящая книга.
Данная глава предлагает обзор важнейших концепций, касающихся программиро-
вания приложений на языке C++. Мы проведем для вас краткий экскурс в интегриро-
ванную среду разработки (Integrated Development Environment — IDE), поставляемую
вместе с Visual C++ 2005. Эта среда проста и интуитивно понятна во всех своих опера-
циях, поэтому, начав ею пользоваться, вы сможете очень быстро овладеть большин-
ством ее возможностей. Лучший подход к изучению среды разработки — пройти весь
процесс создания, компиляции и исполнения простой программы. Прочитав эту гла-
ву, вы изучите следующие вопросы.
□ Из каких основных компонентов состоит Visual C++ 2005?
□ Из чего состоит среда .NET Framework, и каковы ее преимущества?
□ Что такое решение и проект, и как их создавать?
□ Что собой представляют консольные программы?
□ Как создавать и редактировать программы?
□ Как компилировать, компоновать и исполнить консольную программу на C++?
□ Как создать и выполнить базовую программу Windows?
28 Глава 1
Итак, включайте свой ПК, запускайте Windows, загружайте могучий Visual C++
2005 и начинайте путешествие.
Среда .NET Framework
.NET Framework — центральная часть Visual C++ 2005, как и всех прочих средств
разработки .NET компании Microsoft. Среда .NET Framework состоит из двух эле-
ментов: общеязыковой исполняющей среды (Common Language Runtime — CLR),
в которой выполняются ваши программы, и набора библиотек, называемых библи-
отеками классов .NET Framework. Библиотека классов .NET Framework обеспечивает
функциональную поддержку, которая необходима вашему коду при выполнении под
управлением CLR, независимо от применяемого языка программирования, поэтому
программы .NET, написанные на С++,С# или любом другом языке, поддерживающем
.NET Framework, используют одни и те же библиотеки .NET.
Существуют два принципиально отличающихся вида приложений C++, которые
можно разрабатывать в Visual C++ 2005. Вы можете писать приложения, которые вы-
полняются на вашем компьютере как “родные” (native). Эти программы будем назы-
вать родными программами C++. Такие программы пишутся на версии языка C++,
определенной стандартом ISO/ANSI. Вы также можете разрабатывать программы,
выполняющиеся под управлением CLR и реализованные с помощью расширенной
версии C++, которая носит название C++/CLI. Эти программы мы будем называть
программами CLR, или программами C++/CLI.
.NET Framework не является частью Visual C++ 2005, а скорее компонентом опера-
ционной системы Windows, который облегчает построение приложений и Web-служб.
Каркас .NET Framework представляет ощутимые преимущества в отношении надеж-
ности кода и безопасности, а также возможностей интегрирования вашего кода C++
с кодом, написанным на более чем 20 других языках программирования, ориентиро-
ванных на .NET Framework. Некоторым недостатком ориентации на .NET Framework
является незначительное снижение производительности, которое в большинстве слу-
чаев вообще незаметно.
Общеязыковая исполняющая среда (CLR)
CLR — это стандартизованная среда выполнения программ, написанных на ши-
роком диапазоне высокоуровневых языков, включая Visual Basic, C# и, разумеется,
C++. Спецификации CLR в настоящее время встроены в стандарт ЕСМА (European
Association for Standardizing Information and Computer Systems — Европейская ассоци-
ация по стандартизации информационных и вычислительных систем) инфраструк-
туры общего языка (Common Language Infrastructure — CLI) — ECMA-335, а также в
аналогичный стандарт ISO — ISO/IEC 23271, поэтому CLR представляет собой реа-
лизацию этого стандарта. Вы видите, почему C++ для CLR называется C++/CLI — это
C++ для CLI, поэтому весьма вероятно, что со временем вы будете иметь дело с ком-
пиляторами C++/CLI для других операционных систем, которые реализуют CLI.
Обратите внимание, что информация о стандартах ЕСМА доступна на сайте http://
www, ecma-in terna tional. org, а стандарт ECMA-335 в настоящее время доступен для
бесплатной загрузки.
CLI — это, по сути, спецификация среды виртуальной машины, которая позволя-
ет приложениям, написанным на разнообразных высокоуровневых языках програм-
Программирование в Visual C++ 2005
29
мирования, выполняться в различных системах без изменения и перекомпиляции
оригинального исходного кода. CLI специфицирует стандарт промежуточного языка
виртуальной машины, в который компилируется исходный код высокоуровневого
языка программирования. В .NET Framework этот промежуточный язык называется
Microsoft Intermediate Language (MSIL). Код этого промежуточного языка в конеч-
ном итоге при выполнении программы отображается на машинный код с помощью
оперативного компилятора (just-in-time— JIT). Конечно, код на промежуточном язы-
ке CLI может функционировать только в среде, для которой существует реализация
CLI.
CLI также определяет общий набор типов данных, называемый общей системой
типов (Common Type System — CTS), который должен использоваться программами,
написанными на любом языке, ориентированными на реализацию CLI. CTS специ-
фицирует то, как применяются типы данных внутри CLR, и включает в себя набор
предопределенных типов. Вы можете также определять собственные типы данных,
но их определение должно подчиняться ряду правил, чтобы они были согласован-
ными с CLR. Наличие стандартизованной системы типов для представления данных
позволяет компонентам, написанным на разных языках, обрабатывать данные уни-
фицированным способом и обеспечивает возможность интеграции компонентов, на-
писанных на разных языках, в одно приложение.
Безопасность данных и надежность программ в значительной степени расширены
CLR, отчасти благодаря тому, что динамическое выделение и освобождение памяти
полностью автоматизированы, а отчасти потому, что код программ MSIL полностью
верифицируется перед выполнением программы. CLR — это только одна из реализа-
ций спецификации CLI, которая функционирует под управлением Microsoft Windows
на ПК. Несомненно, появятся и другие реализации CLI для сред других операцион-
ных систем и аппаратных платформ. Вы наверняка заметите, что иногда термины
CLI и CLR используются взаимозаменяемо, хотя должно быть очевидным, что это не
одно и то же. CLI — спецификация стандарта, a CLR — реализация CLI от Microsoft.
Написание приложений на C++
Visual C++ 2005 обеспечивает чрезвычайную гибкость в части разработки разно-
образных типов приложений и программных компонентов. Как уже упоминалось в
настоящей главе, существуют два основных варианта приложений Windows: вы може-
те писать код, который выполняется под CLR, а можете писать код, компилируемый
непосредственно в “родной” машинный код операционной системы. Для приложе-
ний Windows, ориентированных на CLR, в качестве базы построения GUI (graphical
user interface — графический интерфейс пользователя) используется каркас Windows
Forms, представленный библиотекой базовых классов .NET Framework. Применение
Windows Forms обеспечивает быструю разработку GUI, поскольку вы собираете его
графически из стандартных компонентов и получаете полностью автоматически сге-
нерированный код. Затем нужно будет лишь провести небольшую настройку сгенери-
рованного кода, чтобы добиться необходимой функциональности.
Для получения “родного” исполняемого кода также имеется несколько способов.
Один из них — использование библиотеки классов Microsoft Foundation Classes (MFC)
для программирования графического интерфейса пользователя Windows-приложе-
ния. MFC инкапсулирует программный интерфейс операционной системы Windows
(Windows API) в части создания и управления GUI, и значительно облегчает процесс
разработки программ. Windows API появился задолго до того, как на сцену вышел
30 Глава 1
язык C++, поэтому он не имел никаких объектно-ориентированных характеристик,
которых следовало бы ожидать, если бы он разрабатывался в наши дни. Однако вы
не обязаны применять MFC. Если вам нужен выигрыш в производительности, то вы
можете из своего кода C++ обращаться к Windows API непосредственно.
Код C++, выполняемый под управлением CLR, называется управляемым C++, пото-
му что данные и код находятся под контролем CLR. В программах CLR освобождение
памяти, динамически выделенной для размещения данных, осуществляется автомати-
чески, что позволяет исключить главный источник ошибок “родных” приложений C++.
Код C++, который выполняется вне CLR, иногда называется в документации Microsoft
неуправляемым C++, поскольку CLR в его выполнении не участвует. В неуправляемом
C++ вы должны самостоятельно заботиться о выделении и очистке памяти во время
выполнения программы, и вам придется самостоятельно обеспечивать безопасность,
которая встроена в CLR. Мы также будем называть неуправляемый C++ родным C++,
потому что он компилируется непосредственно в родной машинный код.
На рис. 1.1 показаны основные варианты выбора, которые у вас есть при разра-
ботке приложений C++.
Управляемый C++
Родной" C++
“Родной” C++
Каркасные классы
MFC
Общеязыковая исполняющая среда
Операционная система
Аппаратное обеспечение
Рис. 1.1. Основные варианты выбора, доступные при разработке приложений на C++
На рис. 1.1 представлена не полная картина. Приложение может состоять частич-
но из управляемого C++ и частично — из родного кода, то есть вы не привязаны к
одной или другой среде. Конечно, смешивая разнородный код, вы кое-что теряете,
поэтому поступать так нужно только при необходимости, например, когда вы со-
бираетесь перенести существующие родное приложение C++ под управление CLR.
Понятно, что вы не сможете получить выгоды, присущие управляемому C++ в родном
коде C++, к тому же взаимодействие между управляемыми и неуправляемыми компо-
нентами программы влечет за собой определенные накладные расходы. Однако воз-
можность смешения управляемого и неуправляемого кода может оказаться чрезвычай-
но полезной, когда нужно разработать или расширить существующий неуправляемый
код, но при этом в некоторых местах использовать преимущества CLR. Конечно, при
Программирование в Visual C++ 2005
31
разработке новых приложений нужно в самом начале решить, должен быть его код
управляемым или нет.
Изучение программирования для Windows
С интерактивными приложениями, выполняемыми под Windows, всегда связаны
два аспекта: необходим код для создания графического интерфейса (GUI), с которым
взаимодействует пользователь, и необходим код для обработки этого взаимодействия
и реализации полезной функциональности приложения. Visual C++ 2005 предоставля-
ет вам великолепную поддержку в разработке обоих аспектов приложений Windows.
Как вы увидите далее в настоящей главе, можно создать работающую программу
Windows с GUI, не написав самостоятельно ни строчки кода. Весь базовый код по соз-
данию GUI может быть сгенерирован автоматически Visual C++ 2005; однако важно
понимать, как работает этот автоматически генерируемый код, поскольку вам потре-
буется расширять и модифицировать его, чтобы заставить выполнять нужную вам ра-
боту, а для этого понадобится всестороннее понимание C++.
По этой причине в этой книге вы сначала изучите сам C++ — как родной C++, так
и версию C++/CLI — не погружаясь в рассмотрения программирования под Windows.
Только после того, как почувствуете себя уверенно в C++, вы научитесь разрабатывать
полноценные приложения Windows с помощью родного C++ и C++/CLI. Это значит,
что, изучая C++, вы будете работать с программами, которые используют ввод и вы-
вод командной строки. За счет ограничения этими простейшими средствами ввода и
вывода, вы сможете сосредоточиться на специфике самого языка C++, избегая неиз-
бежного усложнения, которое привносит построение и управление GUI. После того,
как вы освоитесь с C++, вы будете в состоянии просто и естественно перейти к при-
менению этого языка в разработке полноценных Windows-приложений.
Изучение C++
Visual C++ 2005 в полной мере поддерживает две версии C++, определенные двумя
разными стандартами.
□ Стандарт C++ ISO/ANSI для реализации “родных” приложений — неуправляе-
мый C++. Эта версия C++ поддерживается большинством компьютерных плат-
форм.
□ Стандарт C++/CLI, разработанный специально для написания программ, ори-
ентированных на CLR, и являющийся расширением ISO/ANSI C++.
Главы от 2 до 10 научат вас языку C++. Поскольку C++/CLI является расширением
ISO/ANSI C++, первая часть каждой главы представляет элементы языка ISO/ANSI
C++, а вторая часть — дополнительные средства C++/CLL
Написание программ на C++/CLI позволит в полной мере воспользоваться пре-
имуществами .NET Framework, которые зачастую недоступны программам, написан-
ным на ISO/ANSI C++. Хотя C++/CLI— расширение ISO/ANSI C++, для того, чтобы
программа полностью выполнялась под CLR, она должна отвечать определенным
требованиям CLR. Это означает, что некоторые средства ISO /ANSI C++ нельзя при-
менять в программах CLR. Как вы могли догадаться из сказанного выше, одним из
примеров таких ограничений может быть запрет на использование средств ISO/ANSI
C++ распределения и освобождения памяти. Они не совместимы с C++/CLI, и вместо
них вы должны будете пользоваться механизмом управления памятью CLR, а это зна-
чит, что вы должны работать с классами C++/CLI, а не родными классами C++.
32 Глава 1
Стандарты C++
Стандарт ISO/ANSI определен в документе ISO/IEC 14882, опубликованном Аме-
риканским национальным институтом стандартизации (ANSI). Стандарт ISO/ANSI
C++- описывает устойчивую версию C++, которая существует с 1998 года и поддержи-
вается компиляторами большинства аппаратных компьютерных платформ и операци-
онных систем. Программы, написанные на ISO/ANSI C++, относительно легко могут
быть перенесены с одной платформы на другую, хотя используемые ими библиотеч-
ные функции, в частности, связанные с построением графического интерфейса, явля-
ются главным фактором, определяющим, насколько легко или трудно такой перенос
осуществить. Стандарт C++ ISO/ANSI — это главный инструмент, который выбирают
профессиональные разработчики программ, поскольку он широко поддерживается и
потому, что на сегодняшний день он является одним из наиболее мощных доступных
языков программирования.
Стандарт C++ ISO/ANSIможно приобрести на http: //www. iso. org.
C++/CLI — версия C++, расширяющая стандарт C++ ISO/ANSI в целях лучшей под-
держки общей инфраструктуры языка (CLI), определенной в стандарте ЕСМА-355.
Первый набросок этого стандарта появился в 2003 году и был разработан на основе
технических спецификаций, представленных Microsoft для поддержки программ' C++
в среде .NET Framework. То есть, как CLI вообще, так и C++/CLI в частности, роди-
лись в Microsoft и предназначены для поддержки .NET Framework. Конечно, стандар-
тизация CLI и C++/CLI значительно повысила вероятность появления реализаций
в средах, отличных от Windows. Важно оценить то, что хотя C++/CLI— это расши-
рение ISO/ANSI C++, существуют такие средства ISO/ANSI C++, которые не следует
использовать в программах, предназначенных полностью для выполнения под управ-
лением CLR. Далее в книге вы узнаете об этом подробнее.
CLR представляет существенные преимущества перед “родным” окружением.
Ориентируя ваши программы C++ на CLR, вы обеспечиваете их повышенную безо-
пасность и снижаете уязвимость по отношению к потенциальным ошибкам, которые
возможны при использовании всех средств ISO/ANSI C++. CLR также исключает не-
совместимость между различными высокоуровневыми языками программирования
за счет стандартизации целевой среды, для которой выполняется компиляция, что
позволяет комбинировать модули, написанные на C++, с модулями, написанными на
других языках, таких как C# или Visual Basic.
Консольные приложения
Наряду с разработкой приложений для Windows, Visual C++ 2005 также позволя-
ет вам писать, компилировать и тестировать программы C++, которые не тащат за
собой всего багажа, необходимого программам Windows — то есть символьно-ориен-
тированные программы командной строки. Эти программы называются в Visual C++
2005 консольными приложениями, потому что вы взаимодействуете с ними через
клавиатуру и экран, работающий в символьном режиме.
Написание консольных приложений может показаться вам отклонением от основ-
ной цели программирования Windows, но когда речь идет об изучении C++ (что совер-
шенно необходимо перед тем, как погрузиться в программирование Windows) — это
наилучший способ. Даже в самой простой Windows-программе присутствует слишком
много кода, и очень важно, чтобы сложности, связанные с программированием под
Windows не затмили для вас азы C++. Поэтому в начальных главах нашей книги, со-
Программирование в Visual C++ 2005
33
средоточенных на основах работы C++, мы потратим некоторое время на рассмотре-
ние нескольких “легковесных” консольных приложений, прежде чем перейдем к “тя-
желовесному” коду мира Windows.
Таким образом, изучая C++, вы сможете сосредоточить внимание на языковых
средствах, не заботясь о среде, в которой они работают. В консольных приложениях,
которые вы разработаете в процессе изучения, вы будете иметь дело только с тек-
стовым интерфейсом, но этого вполне достаточно для понимания C++, поскольку в
определении языка отсутствует описание каких-либо средств графики. Естественно,
я представлю всю необходимую информацию о программировании графического
пользовательского интерфейса, когда очередь дойдет до написания программ под
Windows с применением MFC на “родном” C++ и с применением Windows Forms — на
C++/CLI.
Существует два вида консольных приложений, и вы будете иметь дело с обоими.
Консольные приложения Win32 компилируются в родной код, и их мы будем рас-
сматривать при изучении средств ISO/ANSI C++. Консольные приложения CLR
ориентированы на CLR, поэтому их мы будем использовать для работы со средствами
C++/CLI.
Концепции программирования для Windows
Наш подход к Windows-программированию будет основан на полном использо-
вании инструментов, представленных Visual C++ 2005. Средства создания проек-
та, встроенные в Visual C++ 2005, позволяют автоматически сгенерировать скелет
кода для широкого диапазона различных прикладных программ, включая базовые
Windows-программы. Создание проекта — это начальная точка при разработке всех
приложений и компонентов в Visual C++ 2005, и для того, чтобы получить представ-
ление о том, как это делается, в настоящей главе вы ознакомитесь с процессом созда-
ния некоторых примеров, включая набросок программы для Windows.
Программы Windows имеют структуру, отличающуюся от обычных консольных
программ, которые запускаются из командной строки, к тому же они значительно
сложнее. В консольной программе вы можете получать ввод с клавиатуры и отправ-
лять вывод непосредственно в командную строку, в то время как Windows-програм-
ма может работать со средствами ввода и вывода компьютера только через функции,
предоставленные операционной системой Windows; никакого прямого доступа к ап-
паратным ресурсам не допускается. Поскольку в Windows могут быть активными од-
новременно несколько программ, Windows определяет, какая из них должна получить
ввод — щелчок мыши или нажатие клавиши — и соответственно оповещает нужную
программу. То есть общее управлением всем взаимодействием с пользователем осу-
ществляется операционной системой Windows.
К тому же природа интерфейса между пользователем и приложением Windows та-
кова, что в любой момент времени возможно самое разнообразное воздействие на
программу. Пользователь может выбрать любой из пунктов меню, щелкнуть на кноп-
ке панели инструментов или же щелкнуть кнопкой мыши в любом месте окна при-
ложения. Хорошо спроектированное приложение Windows должно быть готовым об-
рабатывать ввод любого рода в любой момент времени, поскольку заранее не может
быть известно, какое именно воздействие произойдет. Все эти действия пользователя
принимаются в первой инстанции операционной системой, и рассматриваются ею
как события (events). Событие, воспринятое пользовательским интерфейсом вашего
приложения, обычно приводит к выполнению определенного кусочка кода програм-
мы. Таким образом, что именно делает программа — определяется последовательное-
34 Глава 1
тью действий пользователя. Программы, работающие таким образом, называются
программами, управляемыми событиями (event-driven programs), и отличаются от
традиционных процедурных программ, которые имеют единственную последователь-
ность выполнения. Ввод данных в процедурной программе управляется кодом самой
программы и может выполняться только тогда, когда программа принимает его; в
противоположность этому Windows-программа состоит, прежде всего, из фрагмен-
тов кода, реагирующих на события, вызванные действиями пользователя либо самой
Windows. Структура такого рода программ показана на рис. 1.2.
Каждый квадратный блок на рис. 1.2 представляет порцию кода, написанную спе-
циально для обработки определенного события. Программа может выглядеть силь-
но фрагментированной из-за множества не связанных между собой блоков кода, но
основной фактор, связывающий ее в единое целое — это сама операционная система
Windows. Вы можете считать свою программу индивидуальной подгонкой Windows
для выполнения определенного набора действий.
Конечно, все модули, обслуживающие различные внешние события, такие как вы-
бор пункта меню или щелчок мыши, имеют доступ к общему набору специфичных
для приложения данных в конкретной программе. Эти данные приложения содержат
информацию, имеющую отношение к тому, для чего программа предназначена, на-
пример, блоки текста в редакторе или таблица рекордов игрока в программе, пред-
назначенной для отслеживания результатов игры бейсбольной команды, а также
информацию о некоторых событиях, которые произошли во время выполнения про-
граммы. Эта общая коллекция данных позволяет разным частям программы, которые
выглядят независимыми, взаимодействовать согласованным образом. Позднее в этой
книге мы рассмотрим это более детально.
Даже самая элементарная Windows-программа включает несколько строк кода, а
когда речь идет о программах Windows, сгенерированных автоматическими мастера-
ми (wizards) из Visual C++ 2005, то “несколько” превращается в “много”. Чтобы упро-
стить процесс понимания работы C++, вам нужен контекст, упрощенный, насколько
это возможно. К счастью, среда Visual C++ 2005 обеспечивает такую возможность.
Что такое интегрированная
среда разработки?
Интегрированная среда разработки (Integrated Development Environment — IDE),
которая поставляется вместе с Visual C++ 2005 — это полностью самодостаточная
среда, предназначенная для создания, компиляции, компоновки и тестирования про-
грамм на C++. Она также представляет собой великолепное учебное пособие по языку
C++ (особенно в сочетании с хорошей книгой).
Visual C++ 2005 включает в себя множество полностью интегрированных инстру-
ментов, предназначенных для облегчения написания программ на C++. С некоторыми
из них вы познакомитесь в этой главе, но вместо скучного абстрактного перечисле-
ния средств и опций, сначала будет дано общее представление о работе IDE, а затем
постепенно будут раскрываться детали, в контексте изучаемого материала.
Программирование в Visual C++ 2005 35
События:
Рис. 1.2. Структура типичной Window&npoepaMMu
Компоненты системы
Список фундаментальных составляющих Visual C++ 2005, поставляемых как ча-
сти IDE, включает в себя редактор, компилятор, компоновщик и библиотеки. Это —
основные инструменты, необходимые для написания и исполнения программ C++.
Их назначение описано ниже.
Редактор
Редактор представляет собой интерактивную среду, в которой вы можете созда-
вать и редактировать исходный код C++. Наряду с обычными средствами вроде вы-
резания и вставки фрагментов, с которыми вы наверняка знакомы, редактор также
обеспечивает цветовое выделение различных элементов языка. Редактор автоматиче-
ски распознает ключевые конструкции языка C++ и окрашивает их в соответствии с
их значением. Это не только помогает сделать код более читабельным, но также ясно
указывает на ошибки при вводе некоторых слов.
36 Глава 1
Компилятор
Компилятор преобразует ваш исходный код в объектный код, обнаруживает и из-
вещает об ошибках в процессе компиляции. Компилятор может обнаружить широ-
кий диапазон ошибок, связанных с некорректным или нераспознаваемым программ-
ным кодом, а также структурные ошибки, как, например, части программы, которые
никогда не будут выполнены. Выходной объектный код, созданный компилятором,
помещается в так называемые объектные файлы. Существуют два типа объектного
кода, производимого компилятором. Файлы с объектным кодом обычно имеют имена
с расширением . оЬ j.
Компоновщик
Компоновщик комбинирует вместе различные модули, сгенерированные компиля-
тором из файлов исходного кода, добавляет необходимые модули из библиотек, по-
ставляемых в составе C++, и сшивает все это в одно исполняемое целое. Компоновщик
также может обнаруживать ошибки и сообщать о них — например, если какая-то часть
вашей программы пропущена, либо обнаружена ссылка на несуществующий библио-
течный компонент.
Библиотеки
Библиотека — это просто коллекция предварительно написанных процедур, кото-
рые поддерживают и расширяют язык C++, предоставляя в ваше распоряжение стан-
дартные, профессионально разработанные единицы кода, которые вы можете вклю-
чать в свои программы для выполнения стандартных часто встречающихся операций.
Операции, реализованные процедурами из различных библиотек Visual C++ 2005,
значительно повышают вашу производительность за счет экономии усилий, которые
потребовалось бы приложить для их самостоятельной разработки и тестирования. Я
уже упоминал библиотеку .NET Framework, но кроме нее существует множество дру-
гих — слишком много, что бы перечислить здесь все, но некоторые наиболее важные
я все же упомяну.
Стандартная библиотека C++ определяет базовый набор процедур, общий для
всех компиляторов ISO/ANSI C++. Он содержит широкий диапазон подпрограмм,
включая числовые функции, такие как вычисление квадратного корня, тригонометри-
ческие функции, процедуры обработки символов и строк наподобие классификации
символов и сравнения символьных строк, а также многие другие. Вы познакомитесь
со значительной их частью в процессе изучения ISO/ANSI C++. Есть также библиоте-
ки, поддерживающие расширение C++/CLI стандартного ISO/ANSI C++.
“Родные” оконные приложения поддерживаются библиотекой, называемой
Microsoft Foundation Classes (MFC). MFC позволяет значительно сократить усилия,
необходимые для построения графического пользовательского интерфейса приложе-
ний. Вы узнаете об MFC больше, когда мы покончим с нюансами языка C++. Другая
библиотека, содержащая набор средств построения графического интерфейса, носит
название Windows Forms. Она приблизительно эквивалентна MFC, но служит для по-
строения оконных приложений, выполняемых в среде .NET Framework. В свое время
вы узнаете, как ее использовать при разработке приложений.
Программирование в Visual C++ 2005
37
Использование IDE
Вся разработка и выполнение программ, описанных в этой книге, будет осущест-
вляться внутри IDE. При запуске Visual C++ 2005 вы увидите окно приложения, подоб-
ное тому, что показано на рис. 1.3.
Рис. 13. Окно Visual C++ 2005
Часть окна слева на рис. 1.3 называется окном проводника решений (Solution
Explorer), правое верхнее окно, содержащее стартовую страницу (Start page) — это
окно редактора (Editor), а окно в нижней части называется окном вывода (Output).
Окно проводника решений позволяет осуществлять навигацию по программным фай-
лам, отображать их содержимое в окне редактора, а также добавлять новые файлы к
вашей программе. Окно проводника решений содержит три дополнительных вклад-
ки (на рис. 1.3 показаны только две из них), которые отображают Class View (пред-
ставление классов), Resource View (представление ресурсов) и Property Manager
(диспетчер свойств). Вы можете указать, какие именно представления отображаются,
через меню View (Вид). Окно редактора — это место, где вы вводите и модифициру-
ете исходный ко,
сообщения, полученные при компиляции и компоновке вашей программы.
и другие компоненты своей программы. Окно вывода отображает
38 Глава 1
Опции панели инструментов
Выбрать, какие панели инструментов (toolbars) должны отображаться, можно, вы-
полнив щелчок правой кнопкой мыши на области панелей инструментов. При этом
появляется всплывающее меню со списком доступных панелей (рис. 1.4), в котором
отображаемые в данный момент панели помечены галочками.
Рис. 1.4. Всплывающее меню со списком доступных панелей инструментов
Здесъ вы принимаете решение относительно того, какие панели инструментов
должны быть видимы в любой момент времени. Вы можете выбрать такой же на-
бор инструментов, как показан на рис. 1.3, пометив пункты меню Build (Сборка),
Class Designer (Конструктор классов), Debug (Отладка), Standard (Стандартная)
и View Designer (Конструктор представлений). Щелчок в серой области слева от
списка панелей инструментов помечает их либо снимает метку, если она уже стоит.
Помеченные панели отображаются, не помеченные — скрываются.
Вам не нужно загромождать окно IDE панелями инструментов, которые, как вы ду-
маете, могут понадобиться когда-то в будущем. Некоторые из них появляются автома-
тически, когда возникает в них необходимость, поэтому, скорее всего, вы согласитесь
с тем, что выбор панелей по умолчанию является наиболее удобным. Во время разра-
ботки приложения время от времени вам понадобится доступ к панелям, которые не
отображаются в данное время. В любой момент вы можете изменить набор видимых
Программирование в Visual C++ 2005
39
инструментальных панелей, просто выполнив правый щелчок мыши в поле панелей
и выбрав требуемые панели в контекстном меню.
Подобно многим другим Windows-приложениям, панели инструментов Visual C++ 2005 вклю-
чают всплывающие подсказки. Для того чтобы увидеть их, просто наведите курсор мыши
на соответствующую кнопку, подождите секунду или две и вы увидите белую метку с крат-
ким описанием функции этой кнопки.
Стыкуемые панели инструментов
Стыкуемая (dockable) панель инструментов — это такая панель, которую с по-
мощью мыши вы можете перетаскивать в любое удобное место внутри окна. Когда
она размещается у одной из четырех границ окна приложения, говорят, что она при-
стывкована (docked). При этом она выглядит точно так же как панели инструментов в
верхней части окна IDE. Панель в верхней части, содержащая пиктограмму с изобра-
жением дискеты, и текстовое поле справа от кнопки с биноклем — это стандартная
панель инструментов. Вы можете установить курсор мыши на ее поверхность, нажать
левую кнопку, перетащить в нужное место и оставить там, отпустив кнопку мыши.
Затем она будет выглядеть как отдельно окно, которое можно разместить где угодно.
Если вы перетащите любую стыкуемую панель прочь от границы окна приложения,
она будет выглядеть как стандартная панель, показанная на рис. 1.5 ~ в виде отдель-
ного окна с собственным заголовком. В этом состоянии она называется плавающей
(floating) панелью. Все панели инструментов, которые вы видите на рис. 1.3, являют-
ся стыкуемыми, поэтому вы можете поэкспериментировать с перетаскиванием любой
из них. Вы можете поместить их затем в их нормальное положение либо присоеди-
нить к любой границе окна.
Рис. 1.5. Стандартная панель
Документация
В процессе работы вы неоднократно столкнетесь с ситуацией, когда вам пона-
добится дополнительная информация о Visual C++ 2005. Библиотека разработчика
Microsoft Development Network (MSDN) содержит исчерпывающие справочные ма-
териалы обо всех возможностях Visual C++ 2005, а также всю сопутствующую инфор-
мацию. Когда вы инсталлируете Visual C++ 2005, вам предоставляется возможность
установить часть или всю документацию MSDN. Если у вас есть достаточно дискового
пространства, я настоятельно рекомендую инсталлировать библиотеку MSDN.
Для просмотра библиотеки MSDN нажмите функциональную клавишу <F1>. Меню
Help (Справка) также предлагает различные возможности доступа к документации.
Наряду с предоставлением справочной информации, библиотека MSDN — удобный
инструмент для анализа и исправления ошибок в коде, в чем вы убедитесь позднее,
читая настоящую главу.
40 Глава 1
Проекты и решения
Проект — это контейнер для всех составляющих его программ определенного
рода. Это может быть консольная программа, оконная программа либо программа
некоторого другого типа, обычно состоящая из одного или более исходных файлов,
содержащих ваш код плюс ряд вспомогательных файлов. Все файлы проекта сохраня-
ются в папке проекта, а детальная информация о проекте — в XML-файле с расшире-
нием . vcproj, который находится в этой же папке. Папка проекта также содержит
другие папки, используемые для сохранения выходных файлов компиляции и компо-
новки вашего проекта.
Идея решения (solution) выражена в его названии. Решение предоставляет меха-
низм для объединения всех программ и других ресурсов, которые представляют реше-
ние определенной проблемы, связанной с обработкой данных. Например, распреде-
ленная система ввода заказов для некоторой бизнес-операции может быть составлена
из нескольких различных программ, каждая из которых может быть представлена в
виде проекта внутри единого решения; таким образом, решение — это папка, в кото-
рой собрана вся информация об одном или более проектах, причем папки проектов
вложены в папку решения. Информация о проектах решения сохраняется в двух фай-
лах с расширениями . sin и . suo. Когда вы создаете проект, новое решение создается
автоматически, если только вы не добавляли проект к существующему решению.
Когда вы создаете проект вместе с решением, то позднее вы можете добавить к
тому же решению дополнительные проекты. Вы можете добавить проекты любого
рода к существующему решению, но обычно это будут проекты, которые каким-то об-
разом связаны с уже существующими проектами того же решения. Обычно, если нет
веской причины поступать иначе, каждый ваш проект должен относиться к отдельно-
му решению. И все примеры, включенные в эту книгу, представляют собой проекты,
содержащиеся внутри их собственных решений.
Определение проекта
Первый шаг при написании программы в вреде Visual C++ 2005 состоит в созда-
нии проекта. Это делается путем выбора пункта File^New1^Project (Файл^Создать1^
Проект) в главном меню или же нажатием комбинации клавиш <Ctrl+Shift+N>.
Наряду с перечнем файлов, определяющих код и все прочие данные, из которых со-
стоит ваша программа, XML-файл проекта в папке проекта также сохраняет использу-
емые вами опции Visual C++ 2005. Хотя у вас нет нужды напрямую работать с файла-
ми проектов — они целиком поддерживаются самой IDE — вы можете просматривать
их, если хотите увидеть их содержимое, но будьте осторожны, чтобы случайно их не
модифицировать.
На этом мы заканчиваем знакомство с вводной информацией. Пора закатать рукава.
Практическое занятие
Создание проекта консольного приложения
Win32
Сейчас мы попробуем создать проект консольного приложения. Сначала выбери-
те в меню пункт File^New1^Project (Файл^Создать^Проект), чтобы появилось диа-
логовое окно New Project (Новый проект), показанное на рис. 1.6.
В левой панели этого диалогового окна отображены типы проектов, которые мож-
но создавать. В данном случае щелкните на пиктограмме Win32.
Программирование в Visual C++ 2005
41
. 1.6. Диалоговое окно New Project (Новый проект)
Это также идентифицирует мастер создания приложений, который наполнит про-
ект начальным содержимым. Правая панель отображает список шаблонов, доступных
для выбранного слева типа проектов. Выбранный шаблон используется мастером
приложения при создании файлов, составляющих проект. В следующем диалоговом
окне вам представится возможность настроить файлы, созданные в результате щелч-
ка на кнопке ОК данного диалогового окна. Для большинства опций типа/шаблона
базовый набор исходных модулей программы создается автоматически.
После этого вы можете ввести соответствующее имя проекта в редактируемом
поле Name: (Имя:). Например, вы можете назвать его Extl_01 либо выбрать другое
имя проекта по своему усмотрению. Visual C++ 2005 поддерживает длинные имена
файлов, поэтому ваш выбор ничем не ограничен. Имя папки решения появляется в
нижнем редактируемом поле и по умолчанию совпадает с именем проекта. При жела-
нии можете его изменить. Диалоговое окно также позволяет модифицировать место-
положение решения, содержащего ваш проект
емое поле Location: (Расположение:). Если вы просто введете имя проекта, то папка
решения будет автоматически установлена в папку с этим именем, в пути, указанном в
поле Location:. По умолчанию папка решения создается, если она не существует. Если
вы хотите указать другой путь для папки решения, просто введите его в поле Location:.
Альтернативно вы можете использовать кнопку Browse (Обзор) для выбора другого
пути размещения вашего решения. Щелчок на кнопке ОК вызовет диалоговое окно ма-
стера Application Wizard (Мастер создания приложений), показанное на рис. 1.7.
Диалоговое окно отобразит текущие активные установки. Если вы щелкнете на
кнопке Finish (Iotobo) , мастер создаст файлы проекта на их основе.
вы можете щелкнуть на ссылке Application Settings (Настройки приложения) в левой
части, чтобы отобразить страницу настроек приложения, показанную на рис. 1.8.
ддя этого предназначено редактиру-
данном случае
42 Глава 1
Рис. 1.7. Мастер создания приложений
Рис» 1.8. Страница Application Settings (Настройки приложения) масте-
ра Application Wizard
Программирование в Visual C++ 2005
43
Страница Application Settings (Настройки приложения) позволяет выбрать опции,
которые нужно применить к проекту В большинстве случаев в процессе изучения
C++ при создании проектов вы будете помечать флажок Empty project (Пустой про-
ект), но пока оставьте все как есть и щелкните на кнопке Finish (Готово). После этого
мастер приложений создаст проект со всеми файлами по умолчанию.
Папка проекта получит имя, которое вы укажете в качестве имени проекта, и бу-
дет содержать все файлы, составляющие определение проекта. Если вы не измените
этого, то папка решения получит то же имя, что и папка проекта, и будет содержать
папку проекта плюс файлы, описывающие решение. Если вы применяете проводник
Windows Explorer для просмотра содержимого папки решения, то увидите там три
файла.
Файл с расширением .sin, содержащий информацию о проектах, входящих в
решение.
□ Файл с расширением . suo, содержащий опции, выбранные пользователем для
решения.
Файл с расширением .neb, содержащий данные Intellisense для решения.
Intellisense
вод конструкции исходного кода в окне редактора.
это средство, обеспечивающее автоматическое завершение и вы-
Если вы используете Windows Explorer, чтобы просмотреть папку проекта, из-
начально вы увидите там шесть файлов, включая ReadMe .txt с описанием содер-
жимого файлов проекта. Единственный файл, который, может быть, не упомянут
в ReadMe. txt — это файл с составным именем Exl_01. vcproj .ИмяКомпьютера.
ИмяПользователя.изег, в котором хранятся опции проекта.
Созданный вами проект будет автоматически открыт в Visual C++ 2005 и левая па-
нель примет вид, показанный на рис. 1.9. Я увеличил ширину панели, чтобы можно
было видеть внизу полные имена вкладок.
S Header Files
ih“| stdafx.h
-j Resource Files
U — Source Files
Exl_01.cpp
C"] stdafx.cpp
Ц] ReadMe.txt
4^' Solution Explorer .2? Class View ^Property Managerial Resource View
Рис. 1.9. Левая панель проводника решений
Вкладка Solution Explorer (Проводник решений) предоставляет обзор всех про-
ектов текущего решения и файлов, которые они содержат; в данном случае, конеч-
но, проект только один. Вы можете отобразить содержимое любого файла в отдель-
44 Глава 1
ной вкладке окна редактора, просто дважды щелкнув на его имени в поле Solution
Explorer. В поле редактора вы можете переключаться между несколькими загружен-
ными файлами, просто щелкая на соответствующей вкладке.
Вкладка Class View (Представление классов) отображает классы, определенные в
вашем проекте, а также содержимое каждого из классов. Пока в вашем приложении
нет ни одного класса, поэтому поначалу здесь пусто. Когда мы будем говорить о клас-
сах, вы увидите, что вкладку Class View можно использовать для простого и удобного
передвижения по коду, представленному в виде определения и реализации классов.
Вкладка Property Manager (Диспетчер свойств) показывает свойства, установ-
ленные для отладочной (Debug) и рабочей (Release) версий вашего проекта. Разницу
между этими версиями я объясню чуть позже в настоящей главе. Вы можете изме-
нить любое из показанных свойств, щелкнув на нем правой кнопкой мыши и выбрав
Properties (Свойства) из контекстного меню; отобразится диалоговое окно, в котором
можно будет настроить свойства проекта. Можно также нажать комбинацию клавиш
<Alt+F7>, чтобы в любой момент отобразить диалоговое окно свойств. Мы поговорим
об этом подробнее, когда будем рассматривать версии программы Debug и Release.
Вкладка Resource View (Представление ресурсов) отображает диалоговые окна,
пиктограммы, панели меню и другие ресурсы, используемые программой. Поскольку
это консольная программа, в ней не используется никаких ресурсов; однако, когда
вы начнете писать Windows-приложения, то увидите здесь множество разных вещей.
Через эту вкладку вы можете редактировать или добавлять доступные ресурсы к про-
екту.
Подобно большинству элементов IDE-среды Visual C++ 2005, Solution Explorer и
другие вкладки представляют контекстно-зависимые меню, вызываемые щелчком пра-
вой кнопкой мыши на элементах, отображаемых на вкладках, а иногда и на пустом
поле этих вкладок. Если вы обнаружите, что Solution Explorer мешает вам при напи-
сании кода, можете скрыть его, щелкнув на пиктограмме Autohide (Автосокрытие).
Затем, чтобы отобразить его, щелкните на имени вкладки в левой части окна IDE.
Модификация исходного кода
Мастер Application Wizard генерирует полное консольное приложение Win32, ко-
торое можно тут же скомпилировать и запустить. К сожалению, поначалу7 сгенериро-
ванная программа не делает ничего, поэтому для того, чтобы сделать ее несколько
более интересной, в нее потребуется внести изменения. Если файл Ех1_01. срр еще
не отображен в панели редактора, выполните двойной щелчок на его имени в панели
Solution Explorer. Этот файл — главный исходный файл программы, который сгенери-
рован мастером Application Wizard, и выглядит он так, как показано на рис. 1.10.
Если в вашей системе не отображаются номера строк, выберите в главном меню
пункт Tools’^ Options (Сервис1^Параметры), чтобы отобразить диалоговое окно
Options (Параметры). Если вы развернете опцию C/C++ в правой панели и выберете
General (Общие) в расширенном дереве, то сможете выбрать Line Numbers (Номера
строк) в правой панели диалогового окна. Сначала я дам вам общее представление о
том, что делает код на рис. 1.10, а подробности вы узнаете позже.
Первые две строки — просто комментарии. Все, что следует в строке за “/ / ”, ком-
пилятором игнорируется. Когда вы хотите добавить описательные комментарии в
строке, предварите текст символами “//”.
В строке 4 находится директива #include, добавляющая содержимое файла
stdafх.h в то место данного файла, где она расположена. Это стандартный способ до-
бавления содержимого исходных файлов . h в исходные файлы . срр программ на C++.
Программирование в Visual C++ 2005 45
Строка 7 — первая строка исполняемого кода в данном файле и начало функции
_tmain (). Эта функция — просто именованный элемент исполняемого кода програм-
мы на C++; каждая программа C++ состоит, по крайней мере, из одной, а чаще — из
множества функций.
Строки 8 и 10 содержат левую и правую фигурные скобки соответственно, кото-
рые ограничивают исполняемый код функции _tmain (). Таким образом, исполняе-
мый код состоит из единственной строки 9, и все, что он делает — это завершает про-
грамму.
Теперь вы можете добавить следующие две строки кода в окне редактора.
II Ех1_01.срр : Определяет точку входа в консольное приложение.
//
#include "stdafx.h"
#include <xostreaxn>
int _tmain(int argc, TCHAR* argv[])
std: :cout « "Hello world!\n";
return 0;
}
Невыделенные строки — те, что сгенерированы для вас автоматически. Новые
строки, которые вы должны добавить, выделены полужирным. Чтобы вставить каж-
дую новую строку, поместите курсор в конец текста на предыдущей строке и нажмите
<Enter>, чтобы создать пустую строку, в которой вы сможете напечатать новый код.
Убедитесь, что он выглядит точно так, как в предыдущем примере; в противном слу-
чае программа может не скомпилироваться.
Первая новая строка — директива #include, которая добавляет содержимое
одной из стандартных библиотек ISO/ANSI C++ в исходный файл. В библиотеке
<iostream> определены средства для базовых операций ввода-вывода, и одна из них
используется во второй добавленной строке, чтобы вывести сообщение в командной
строке, std: :cout — наименование стандартного потока вывода, и здесь вы пишете
строку "Hello World! \п" в стандартное устройство вывода std: :cout. Все, что на-
ходится между парой двойных кавычек, выводится программой в командной строке.
46 Глава 1
Сборка решения
Чтобы построить решение, нажмите клавишу <F7> или выберите пункт меню
Build*=>Build Solution (СборкамСобрать решение). Альтернативно вы можете щелкнуть
на кнопке в панели инструментов, соответствующей этому пункту меню. Кнопки пане-
ли для меню Build могут не отображаться, но вы легко исправите это, щелкнув правой
кнопкой мыши в области панелей инструментов и выбрав панель Build в выпадающем
меню. После этого программа должна успешно скомпилироваться. Если будут какие-то
ошибки, проверьте внимательно две строки, которые вы добавили в исходный код.
Файлы, создаваемые при сборке консольного приложения
После того, как пример собран без ошибок, загляните в папку проекта, используя
Windows Explorer, чтобы увидеть новую вложенную папку по имени Debug. Эта папка
содержит вывод только что выполненной вами сборки проекта. Обратите внимание,
что в ней находится несколько файлов.
Помимо файла .ехе, который представляет вашу программу в готовом к выпол-
нению виде, вам не нужно ничего знать о том, что собой представляют эти файлы.
Если же вы любопытны, то в табл. 1.1 дано краткое описание наиболее интересных
из них.
Таблица 1.1. Файлы, создаваемые в результате сборки консольного приложения
Расширение файла
.obj
Описание
Исполняемый файл программы. Вы получите этот файл только в том случае,
если и компиляция, и компоновка завершились успешно.
Компилятор генерирует эти объектные файлы, содержащие машинный код ис-
ходных файлов вашей программы. Они используются компоновщиком, вместе
с библиотечными файлами, чтобы создать файл .ехе.
• ilk Этот файл используется компоновщиком, когда вы перестраиваете ваш про-
ект. Он позволяет компоновщику инкрементно связывать объектные файлы,
сгенерированные компилятором из модифицированного исходного кода в
существующий файл . ехе. Благодаря этому нет необходимости заново вы-
полнять всю компоновку при каждом изменении программы.
•pch Это предварительно скомпилированный файл заголовка. Благодаря таким
файлам значительная часть кода, не являющегося субъектом модификации
(в частности, код библиотек C++), может быть обработана один раз и поме-
щена в файл . pch. Применение этих файлов существенно снижает затраты
времени, необходимого для повторного построения ваших программ.
• pdb Этот файл включает отладочную информацию, используемую при выполнении
программы в режиме отладки. В этом режиме вы можете динамически ин-
спектировать информацию, которую генерирует исполняющаяся программа.
Содержит информацию, необходимую для перестройки всего решения.
Отладочная (Debug) и рабочая (Release) версии программы
Вы можете установить широкий диапазон разнообразных опций проекта, выбрав
пункт меню ProjectsЕх1 01 Properties (Проект^Свойства Ех1_01). Эти опции опре-
деляют, как обрабатывается ваш исходный код на стадиях компиляции и компонов-
ки. Набор опций, который порождает конкретную исполняемую версию вашей про-
граммы, называется конфигурацией. Когда вы создаете новое рабочее пространство
Программирование в Visual C++ 2005
47
проекта, Visual C++ 2005 автоматически создает конфигурации для построения двух
версий вашего приложения. Одна из них, называемая отладочной (Debug), включает
информацию, помогающую в отладке программы. Если что-то идет не так, то, запуская
эту версию, вы можете выполнять код программы построчно, проверяя значение дан-
ных, с которыми работает программа. Другая версия, называемая рабочей (Release),
не имеет в себе никакой отладочной информации и содержит оптимизированный
компилятором машинный код, что позволяет обеспечить максимальную эффектив-
ность исполняемого модуля. Этих двух конфигураций будет достаточно для всех при-
меров, которые мы рассмотрим в книге, но когда вам понадобится настроить другую
конфигурацию приложения, вы можете сделать это через меню Builds Configuration
Manager (Сборка*^Диспетчер конфигурации). Обратите внимание, что этот элемент
меню не появляется, если в данный момент в IDE-среду не загружен какой-то проект.
Очевидно, что в таком поведении нет никакой проблемы, но может быть, это смутит
вас, когда вы будете просматривать все доступные опции меню.
Вы можете выбирать текущую конфигурацию программы, с которой собираетесь
работать, указывая ее в выпадающем списке Active solution configuration (Активная
конфигурация решения) в диалоговом окне Configuration Manager (Диспетчер кон-
фигурации), как показано на рис. 1.11.
Рис. 1.11. Выбор текущей конфигурации программы
Выберите в списке конфигурацию, с которой собираетесь работать, и затем щел-
кните на кнопке Close (Закрыть). В процессе разработки приложения вы будете ра-
ботать с отладочной конфигурацией. После того как приложение тщательно проте-
стировано в отладочной конфигурации, и вы убедились, что оно работает правильно,
обычно вы пересобираете его в рабочей конфигурации; это порождает оптимизиро-
ванный код без возможностей отладки и трассировки, в результате чего программа
работает быстрее и требует меньше памяти.
Выполнение программы
После успешной компиляции и компоновки решения вы можете выполнить про-
грамму, нажав <Ctrl+F5>. При этом вы должны увидеть окно, показанное на рис. 1.12.
48
Глава 1
з> C:\WINNT\system32\cmd.exe _ □ х
Рис, 1,12, Выполнение консольной программы
Как видите, в командной строке выводится текст, заключенный в двойные кавыч-
ки в исходном коде. Фрагмент “\п”, который указан в конце текста — это специаль-
ная последовательность, которая называется управляющей последовательностью и
означает символ новой строки. Управляющие последовательности служат для пред-
ставления символов в текстовой строке, которые невозможно непосредственно вве-
сти с клавиатуры.
Практическое занятие
Создание пустого консольного проекта
Предыдущий проект содержит в себе некоторый излишний багаж, который не ну-
жен при работе с простыми примерами на C++. Опция предварительной компиляции
заголовков, выбранная по умолчанию, приводит к созданию в проекте заголовочного
файла stdafx.h. Это механизм, повышающий эффективность процесса компиляции,
когда программа состоит из большого количества файлов, но не являющийся необхо-
димым для большинства наших примеров. В их случае вы будете начинать с пустого
проекта, к которому станете добавлять свои собственные исходные файлы. Теперь
вы увидите, как это делается, создав новый проект и новое решение консольной про-
граммы Win32 по имени Extl_02. После того, как вы введете имя проекта и щелкнете
на кнопке ОК, выполните щелчок на Application Settings (Настройки приложения) в
правой части следующего диалогового окна. Затем вы можете выбрать Empty project
(Пустой проект) в дополнительных опциях, как показано на рис. 1.13.
Когда вы щелкнете на кнопке Finish (Готово), как и ранее, будет создан проект, но
на этот раз — без каких-либо файлов.
После этого добавьте в проект новый исходный файл. Щелкните правой кнопкой
мыши на панели Solution Explorer и выберите в контекстном меню пункт Add^New
Item (Добавить1^ Новый элемент). Появится диалоговое окно; щелкните на элементе
Code (Код) в правой панели и на C++ File (.срр) (Файл C++ (.срр)) — в левой. Введите
имя Extl_02, как показано на рис. 1.14.
Когда вы щелкнете на кнопке Add (Добавить), новый файл добавится в проект и
отобразится в окне редактора. Конечно, файл будет пустым, и в нем ничего не ото-
бразится; в окне редактора введите приведенный ниже код.
// Ех1_02. срр — пример консольной программы
#include <iostream> // Базовая библиотека ввода-вывода
int main()
{
std: :cout « "This is a simple program that outputs some text." « std: :endl;
std: :cout « "You can output more lines of text" « std: :endl;
std: :cout « "just by repeating the output statement like this." « std: :endl;
return 0; // Вернуться в операционную систему
Программирование в Visual C++ 2005
49
Win32 Application Wizard - Ex1_02
Application Settings
Overview
Application Settings
Application type:
Windows application
* Console application
DLL
Static library
Additional options:
H smp.b.prpjectj
Add common header files for:
J
< Previous
Рис» 1.13. Настройка приложения Ext l_02
Рис. 1.14. Добавление к проекту нового файла Extl_02
50 Глава 1
ритель-
кция теперь называется main; ранее она называлась
std::endl;
This is a simple program that outputs some text.
и каждое посылает то, что за ним
Обратите внимание на автоматический отступ, выполненный при вводе вашего
кода. C++ использует отступы, чтобы увеличить читабельность программ, и редактор
автоматически выполняет отступ для каждой вводимой вами строки кода, основыва-
ясь на содержимом предыдущих строк. В процессе ввода исходного кода вы также
увидите в действии автоматическое выделение цветом синтаксиса. Некоторые эле-
менты программы будут показаны в разных цветах, потому что редактор автоматиче-
ски раскрашивает элементы языка в зависимости от их значения.
Приведенный выше код составляет завершенную программу. Возможно, вы от-
метите ряд отличий по сравнению с кодом, сгенерированным мастером Application
Wizard для предыдущего примера. Нет директивы #include для файла stdafx.h.
Этот файл не входит в проект, поскольку вы не используете средство пре
ной компиляции заголовков.
_tmain. Фактически все программы на ISO/ANSI C++ начинают выполнение с функ-
ции по имени main (). Microsoft также называет эту функцию wmain, когда применя-
ется кодировка символов Unicode, и имя __tmain определено либо как main, либо
как wmain, в зависимости того, используется ли кодировка Unicode. В предыдущем
примере имя _tmain определено “за кулисами” как main. Во всех примерах ISO/ANSI
C++ вы будете применять имя main.
Операторы вывода также несколько отличается. Первый оператор в main () вы-
глядит так:
std::cout
Здесь вы видите два появления операции
следует, в std: :cout — стандартный поток вывода. Сначала в поток отправляется
строка между двойными кавычками, затем — std: :endl, где std: :endl определено в
стандартной библиотеке как символ новой строки. Ранее вы уже использовали управ-
ляющую последовательность \п для обозначения символа новой строки внутри стро-
ки в двойных кавычках. То есть, последний оператор можно было бы записать и так:
std: :cout « "This is a simple program that outputs some text.\n";
Я должен пояснить, почему эта строка затенена, в то время как предыдущая — нет.
Когда я повторяю строку кода без изменений, то оставляю ее не затененной. Данная
последняя строка кода — новая, и ранее в примерах она не появлялась, поэтому я вы-
делил ее полужирным.
Теперь вы можете построить проект точно так же, как в предыдущем случае.
Обратите внимание, что все открытые в панели редактора исходные файлы сохраня-
ются автоматически, если вы не сделали этого раньше. Когда программа будет успеш-
но скомпилирована, нажмите <Ctrl+F5>, чтобы ее выполнить. Вы увидите окно, по-
казанное на рис. 1.15.
si C:\WINNT\system32\cmd.exe
X
Thisis a simple program that outputs sone text.
You can output more lines of text
Just by repeating the output statement like this.
Press any key to continue . . . _
Рис. 1.15. Выполнение программы Ext 1_02
Программирование в Visual C++ 2005 51
Обращение с ошибками
Конечно, если вы неправильно введете текст программы, то получите сообщения
об ошибках. Чтобы увидеть, как это работает, вы можете внести ошибку преднамерен-
но. Если вы уже получили какие-то сообщения об ошибках, то можете использовать
их для этого упражнения. Вернитесь в панель редактора и удалите точку с запятой в
конце предпоследней строки между фигурными скобками (в строке 8), а затем пере-
компилируйте исходный файл. В выходной панели (Output рапе) в нижней части
окна приложения IDE вы увидите сообщение об ошибке:
С2143: syntax error : missing ’;’ before ’return’
C2143: синтаксическая ошибка : отсутствует перед 'return'
Каждое сообщение об ошибке времени компиляции сопровождается номером, по
которому вы можете найти ее описание в документации. В данном случае проблема
очевидна, но в более сложных случаях документация может помочь вам найти при-
чину ошибки. Чтобы обратиться к документации об ошибке, щелкните на строке в
панели вывода, которая содержит ее номер, затем нажмите <F1>. Отобразится новое
окно, содержащее более подробное описание этой ошибки. Если хотите, можете по-
пробовать проделать это с данной простой ошибкой.
После того, как вы исправите ошибку, попробуйте заново собрать проект. Процесс
сборки проекта работает эффективно, потому что он автоматически отслеживает со-
стояние составляющих проект файлов. При нормальной сборке Visual C++ 2005 пере-
компилирует только те файлы, которые были изменены с момента предыдущей ком-
пиляции. Это значит, что если ваш проект состоит из нескольких исходных файлов,
и вы отредактировали только один из них после того, как проект был собран в по-
следний раз, то перед компоновкой и построением нового исполняемого файла будет
перекомпилирован только один этот файл.
В процессе изучения вам также придется создавать консольные программы CLR,
поэтому в следующем разделе будет показано, как выглядит консольный проект CLR.
Практическое занятие | СОЗДЗНИв КОНСОЛЬНОГО ПрОбКТЭ CLR
Нажмите <Ctrl+Shift+N> для отображения диалогового окна New Project (Новый
проект); затем выберите тип проекта CLR и шаблон CLR Console Application (Консоль-
ное приложение CLR), как показано на рис. 1.16.
Введите имя Ех1_03. Когда вы щелкнете на кнопке ОК, файлы проекта будут созда-
ны. Для консольного проекта CLR не существует различных опций, поэтому, работая
с этим шаблоном, вы всегда будете начинать с одного и того же набора файлов. Если
вам нужен пустой проект — что в этой книге не понадобится — то для этого предусмо-
трен отдельный шаблон.
Если вы посмотрите на панель Solution Explorer, изображенную на рис. 1.17, то
увидите несколько дополнительных файлов по сравнению с консольным проектом
Win32.
Есть пара файлов в виртуальной папке Resource Files. Файл . ico содержит пик-
тограмму приложения, которая отображается при минимизации программы; файл
. гс содержит ресурсы приложения — в данном случае только ссылку на пиктограмму.
Имеется также файл по имени Assemblyinfo. срр. Каждая программа CLR со-
стоит из одной или более сборок (assembly), каждая из которых представляет собой
коллекцию кода и ресурсов, формирующую функциональную единицу. Сборка также
содержит обширные данные для CLR; там есть спецификации используемых типов
52 Глава 1
данных, информация о версии кода, а также информация, указывающая, должно ли
содержимое данной сборки быть доступно другим сборкам. Короче говоря, сборка —
это фундаментальный строительный блок программ CLR.
Рис. 1.16. Создание нового приложения CLR
Solution Bjorer - Ех1_03 ▼ X
ЕЭ Solution 'Exl_03' (1 project)
Й Header Files
|И resource.h
IA stdatkh
Resource Files
lltf app.ico
app.rc
J Source Files
Assemblylnfo.cpp
c2j Exl_03.cpp
c~] stdafkcpp
= ReadMe.txt
»e
Рис. 1.17. Файлы консольного проекта CLR
Программирование в Visual C++ 2005 53
Если исходный код файла Ех1_03. срр еще не отображается в окне редактора, вы-
полните двойной щелчок на имени файла в панели Solution Explorer. Окно редактора
должно выглядеть, как показано на рис. 1.18.
Как видите, здесь есть те же директивы #include, что и в файле “родной” кон-
сольной программы C++, потому что программы CLR также используют предвари-
тельно компилированные заголовки для эффективности. Но следующая строка отли-
чается:
using namespace System;
Все средства библиотеки .NET определены внутри пространства имен (namespace),
и почти все стандартные функции, которые вы будете использовать, скорее всего, бу-
дут находиться в пространстве имен System. Этот оператор говорит о том, что после-
дующий код программы использует пространство System, но что же это такое — про-
странство имен?
Пространство имен — очень простая концепция. В коде вашей программы, а также
в коде, формирующем библиотеки .NET, имена присваиваются многим вещам — ти-
пам данных, переменным, блокам кода, называемым функциями, и тому подобно-
му. Проблема состоит в том, что если вы используете имя, которое уже встречает-
ся в библиотеке, существует потенциальная вероятность возникновения коллизии.
Пространство имен предоставляет возможность обойти эту проблему. Все имена в
коде библиотек, определенные в пространстве имен System, неявно снабжены пре-
фиксом — именем пространства имен. Поэтому имя вроде String в библиотеке — это
на самом деле System: : String. Это значит, что если вы нечаянно используете имя
String для чего-либо в своем коде, то для доступа к String из библиотеки .NET смо-
жете применить имя System: : String.
Два двоеточия посредине — это операция, называемая операцией разрешения
контекста (scope resolution operator). Здесь эта операция отделяет имя пространства
имен System от имени типа String. Вы уже видели примеры таких составных имен в
примерах C++, представленных выше — std: :cout и std: :endl. Здесь та же история:
std — наименование пространства имен родных библиотек C++, a cout и endl — име-
на, определенные внутри пространства имен std для представления стандартного
выходного потока и символа новой строки соответственно.
Фактически оператор using namespace в данном примере позволяет использо-
вать любое имя из пространства System, не указывая имени пространства имен в ка-
честве префикса. Если у вас случится конфликт между определенным вами именем и
именем, определенным в библиотеке, вы можете решить эту проблему исключением
54 Глава 1
оператора using namespace из текста программы и указанием полного имени би-
блиотечного компонента вместе с префиксом — наименованием пространства имен.
Подробнее о пространствах имен вы узнаете в главе 2.
Вы можете скомпилировать и выполнить программу, нажав <Ctrl+F5>. Результат
показан на рис. 1.19.
C:\WINNT\system32\cmd.exe
ello World
ress any k
Рис. 1.19. Выполнение программы Extl_03
Вывод этой программы точно такой же, как в первом примере. Он генерируется
следующей строкой программы:
Console::WriteLine(L"Hello World");
В этой строке вызывается функция из библиотеки .NET для вывода в командную
строку информации, заключенной в двойные кавычки, то есть это — CLR-эквивалент
оператора “родного” C++, которое вы добавили в код Ех1_01:
std::cout « "Hello world!\n";
наиболее наглядный пример того, как оператор CLR делает то же самое,
что и оператор C++.
Настройка опций Visual C++ 2005
Существует два набора опций, которые вы можете настраивать. Вы можете устано-
вить опции, применяемые к инструментам, включенным в Visual C++ 2005, которые
касаются каждого проекта. Кроме того, вы можете настроить опции, относящиеся
только к конкретному проекту и определяющие, как код проекта будет обрабатывать-
ся при компиляции и компоновке. Опции устанавливаются в диалоговом окне Options
(Параметры), которое отображается после выбора пункта Tools'^Options (Сервис^
Параметры) в главном меню. Диалоговое окно Options показано на рис. 1.20.
Щелчок на символе (+) у любого элемента в левой панели отображает список его
подразделов. На рис. 1.20 показаны опции подраздела General (Общие), относящегося
к разделу Projects and Solutions (Проекты и решения). Правая панель отображает оп-
ции, которые вы можете настроить для раздела, выбранного в левой панели. Пока вам
достаточно рассмотреть лишь несколько из них, но возможно, вы сочтете полезным
потратить некоторое время на просмотр всех опций, доступ к которым обеспечивает
это диалоговое окно. Щелчок на кнопке получения справки (с символом ‘?’) в правом
верхнем углу диалогового окна отобразит пояснение к текущим выбранным опциям.
Возможно, при создании нового проекта вы решите использовать путь по умолча-
нию, и вы можете сделать это через первую опцию, показанную на рис. 1.20. Просто
укажите путь к папке файловой системы, в которой вы хотите размещать свои реше-
ния и проекты.
Вы можете установить опции, прилагаемые к каждому проекту C++, выбрав раздел
Projects and Solutionsc>VC++ Project Settings (Проекты и решения^Настройки про-
екта VC++) в левой панели. Также вы можете установить опции, специфичные для
Программирование в Visual C++ 2005
55
данного проекта через пункт Projects Properties (Проект^Свойства) главного меню.
Этот пункт меню будет дополнен именем текущего проекта.
Рис. 1.20. Диалоговое окно Options (Параметры)
Создание и выполнение Windows-приложений
Только для того, чтобы показать, насколько это просто, создадим два работающих
Windows-приложения. Сначала будет построено родное приложение C++ с исполь-
зованием библиотеки MFC, а затем — приложение Windows Forms, работающее под
CLR. Обсуждение этих программ откладывается до тех пор, пока у вас не появится
достаточного запаса знаний, чтобы понять их во всех подробностях. Пока просто де-
монстрируется тот факт, что создавать Windows-приложения достаточно просто.
Создание приложения MFC
Для начала, если у вас открыт существующий проект (на что указывает имя про-
екта в заголовке главного окна Visual C++ 2005), вы можете выбрать пункт меню
File^ Close Solution (Файл^Закрыть решение). В качестве альтернативы вы можете
просто создать новый проект, что автоматически закроет текущее решение.
Чтобы создать Windows-программу, выберите пункт меню File*^ New^ Project
(Файл1^ Создать^ Проект) или нажмите комбинацию клавиш <Ctrl+Shift+N>, затем вы-
берите тип проекта MFC, а в качестве шаблона проекта — MFC Application (Приложе-
ние MFC). После этого можете ввести имя проекта Ех1_04, как показано на рис. 1.21.
Когда вы щелкнете на кнопке ОК, появится диалоговое окно мастера создания
приложений MFC (MFC Application Wizard). В нем вы найдете множество опций, ко-
торые позволяют указать, какие средства необходимо включить в новое приложение.
Они идентифицируются элементами списка в правой части диалогового окна, как по-
казано на рис. 1.22. Многие из них будут использоваться в дальнейших примерах.
Пока что вы можете проигнорировать все эти опции, и просто принять настрой-
ки по умолчанию, поэтому щелкните на кнопке Finish (Готово) для создания проекта с
настройками по умолчанию. Панель Solution Explorer в окне IDE будет выглядеть так,
как показано на рис. 1.23.
56 Глава 1
New Project
project types.
a Visual C++
ATL
CLR
General
MFC
Smart Device
Test
Win32
• Other Languages
D strlbuted Syste m Solutions
+• other Project Types
• Test Projects
Templates.
Visual Stud ю nstalled templates
-
?.l MFC ActiveX Control
Й MFC DLL
My Templates
Search Online Templates...
’MFC Application
A project for creating an application that uses the Microsoft Foundation Class Library
Name:
EXt_O4]
Location:
So lution:
D:\Beglnnlng Visual C++ 20D5\Examples
Browse..
Create new Solution
3 Create directory for solution
Solution Name:
Ёх1 Г4
OK
Cancel
Puc. 1.21. Создание нового проекта приложения MFC
MFC Application Wizard - Ex1_04
Welcome to the MFC Application Wiza rd
Overview
Application T ype
Compound Document Support
Document Template Strings
Database Support
User Interface Features
Advanced Features
Generated Classes
These are the current project settings:
• Multiple document interface
• No database support
• No compound document support
Click Finish from any window to accept the current settings.
After you create the project, see the project's readme.bet file for information
about the project features and files that are generated.
< Pre
Finish
Cancel
Puc. 1.22. Мастер создания приложений MFC
Программирование в Visual C++ 2005 57
Solution Е>- lorer - Exlj04 ▼ V- X
LJ Solution 'Exl_04' (1 project)
a exijm .
3 - Header Files
n] ChildFrm.h
|h] Exl_04.h
I it] Ex1_04Dqc.Ii
Exl_04View.h
। h°| MainFrm.li
I h*| Resource.h
|h) stdafx.h
— Resource Files
<4| Exl_04.ico
Exl_04.rc
Exl_04.rc2
_J Exl_04Doc.ico
IbI Toolbar.bmp
=) — Source Files
913 ChildFrm.cpp
913 Exl_04.cpp
913 Exl_04Doc.cpp
913 Exl_04View.cpp
913 MainFrm.cpp
913 stdafk-cpp
|b| Read Me. txt
-jSolution Explorer Class View|(^lResource View
Puc. 1.23. Панель Solution Explorer для проекта Exl__04
Обратите внимание, что я скрыл вкладку Property Manager (Диспетчер свойств),
щелкнув на ней правой кнопкой мыши и выбрав в контекстном меню пункт Hide
(Скрыть), поэтому она не появляется на рис. 1.23. Список содержит множество ав-
томатически созданных файлов. Вам потребуется достаточно места на жестком дис-
ке, если вы собираетесь писать программы для Windows! Файлы с расширением . срр
содержат исполняемый исходный код C++, а файлы . h хранят код C++, состоящий
из определений, используемых исполняемым кодом. В файлах . ico находятся пикто-
граммы. Как видите, все файлы сгруппированы в нескольких вложенных папках, что-
бы облегчить к ним доступ. Однако, это не настоящие папки, и они не появляются в
папке проекта на вашем диске.
Если теперь вы заглянете в папку решения Ех1_04 с помощью Windows Explorer
либо любого другого средства, которым вы пользуетесь для работы с файловой систе-
мой на жестком диске, то вы обнаружите там 24 сгенерированных файла. Три из них
находятся в папке решения, еще 17 — в папке проекта и 4 — в папке res, вложенной
в папку проекта. Файлы в этой папке содержат ресурсы, используемые программой,
такие как меню и пиктограммы. Все это вы получаете после простого ввода имени но-
вого проекта. С учетом такого большого числа файлов, создаваемых автоматически,
вам должно стать ясно, что хранение каждого проекта в отдельном каталоге — более
чем просто хорошая идея.
Один из файлов в каталоге проекта Ех1_04 называется ReadMe. txt, и в нем со-
держатся пояснения назначения каждого из файлов, сгенерированных мастером MFC
Application Wizard. Если хотите, загляните в него с помощью Notepad, WordPad или
даже редактора Visual C++ 2005. Чтобы увидеть его в окне редактора IDE, дважды
щелкните на нем в панели Solution Explorer.
Сборка и запуск приложения MFC
Прежде чем вы сможете запустить программу, вам необходимо собрать проект,
то есть скомпилировать исходный код и скомпоновать программные модули. Это
58 Глава 1
делается точно так же, как вы уже делали при сборке консольных приложений. Для
экономии времени, чтобы собрать проект и запустить готовую программу, нажмите
<Ctrl+F5>.
После того, как проект будет успешно собран, в окне Output не появится никаких
сообщений об ошибках, и программа будет запущена. Окно сгенерированной вами
программы будет выглядеть, как показано на рис. 1.24.
<5iEx1_04 - Ех1_041
File Edit View Window Help
Ready
Puc. 1.24. Выполнение программы Extl 04
Как видите, главное окно программы снабжено меню и панелью инструментов.
Хотя у этой программы нет никакой специфической функциональности — то есть
того, что вам нужно добавить, чтобы это была ваша программа — все же ее меню рабо-
тает. Можете попробовать и убедиться в этом. Вы можете даже создавать в ней допол-
нительные вложенные окна, выбирая пункт New (Создать) из ее меню File (Файл).
Думаю, вы согласитесь с тем, что создание Windows-программы с помощью мастера
MFC Application Wizard не требует больших усилий. Однако вам придется напрячься
немного больше, если вы попытаетесь преобразовать эту базовую программу, сгенери-
рованную автоматически, в программу, которая делает нечто более интересное, хотя
это и не так трудно. Конечно, многим людям, которые пишут серьезные Windows-про-
граммы старым способом, не пользуясь поддержкой Visual C++ 2005, потребуется, по
крайней мере, пару месяцев посидеть на рыбной диете, чтобы получить подобный
результат. Вот почему многие программисты едят суши. Но теперь, благодаря Visual
C++ 2005, эти проблемы в прошлом. Однако, пользуясь новыми инструментами, вы
никогда не узнаете того, что лежит в основе технологии программирования. Поэтому
если вы любите суши, то лучше продолжить их есть, дабы пребывать в безопасности.
Создание приложения Windows Forms
Это работа для другого мастера. Создайте еще один новый проект, но на этот раз
в левой панели диалогового окна New Project (Новый проект) выберите тип CLR, а в
правой панели — шаблон Windows Forms Application (Приложение Windows Forms).
Затем можете ввести имя проекта Ех1_05, как показано на рис. 1.25.
Программирование в Visual C++ 2005
Рис. 1.25. Создание нового проекта приложения Windows Forms
В этом случае больше нет никаких опций для выбора, поэтому щелкните на кноп-
ке ОК для создания проекта.
Панель Solution Explorer на рис. 1.26 отображает файлы, сгенерированные для но-
вого проекта.
1=
Solution 'Exl_05' (1 project)
1 Exl JB5
□ — Header Files
h
3? Forml.resX
resource.h
app.ico
. Source Files
Assemblylnfo.cpp
stdafkcpp
exsolution Explorer Class View iqsjiResource View
Puc. 1.26. Панель Solution Explorer для проекта
05
60
Глава 1
Как видите, в этом проекте файлов сравнительно меньше — если вы заглянете в
каталог, то обнаружите, что в решение включено всего 15 файлов. Одна из причин
этого состоит в том, что начальный графический интерфейс пользователя (GUI)
намного проще, чем у родного приложения C++, использующего MFC. Приложение
Windows Forms не имеет меню или панелей инструментов и состоит из единственного
окна. Конечно, вы можете все это добавить относительно легко, но мастер Windows
Forms не предполагает, что это вам понадобится сразу.
Окно редактора в данном случае будет выглядеть несколько иначе (рис. 1.27).
Рис. 1.27. Окно редактора для приложения Windows Forms
Теперь окно редактора показывает образ окна приложения вместо его кода. При-
чина в том, что разработка GUI для Windows Forms в основном ориентирована на
подход графического проектирования, а не подход кодирования. Вы добавляете ком-
поненты GUI в окно приложения, перетаскивая или размещая их интерактивно, в
графическом виде, a Visual C++ 2005 автоматически генерирует весь код, необходи-
мый для их отображения. Если вы нажмете <Ctrl+Alt+X> либо выберете пункт меню
View^Toolbox (Вид^Панель инструментов), то увидите дополнительное окно Toolbox
(Панель инструментов), показывающее список компонентов GUI (рис. 1.28).
Окно Toolbox предлагает список стандартных компонентов, которые вы можете
добавить в приложение Windows Forms. Можете добавить несколько кнопок в окно
Ех1_05. Для этого щелкните на элементе Button (Кнопка) в списке окна Toolbox, а за-
тем щелкните в клиентской области окна приложения Ех 10 5, которое отображено в
окне редактора, в том месте, куда хотите поместить кнопку. Затем можете подкоррек-
тировать размер кнопки, передвигая ее границы, а также переместить ее в любое ме-
сто окна приложения. Вы также можете изменить метку кнопки, просто набрав ее на
клавиатуре; попробуйте набрать Start (Пуск) и нажать клавишу <Enter>. Метка кноп-
ки изменится, и наряду с этим появится другое окно, в котором бу
свойства этой кнопки. Пока я не буду погружаться в это, только отмечу, что свойства
кнопки — это спецификация ее внешнего вида, который вы можете изменить, как
того требует приложение. Попробуйте добавить еще одну кнопку, например, с меткой
Stop (Стоп). Окно редактора будет выглядеть, как показано на рис. 1.29.
отображены
Программирование в Visual C++ 2005
61
Toolbox
й All Windows Forms
- Common Controls
К Pointer
® Button
0 CheckBox
CheckedListBox
Ff ComboBox
° DateTimePicker
A Label
A LinkLabel
= ' ListBox
Listview
1
УС-»
MaskedTextBox
it] MonthCalendar
j Notifylcon
13 NumericUpDown
i
Picture Box
(ПП ProgressBar
1 RadioButton
RichTextBox
abii TextBox
ToolTip
TreeView
WebBrowser
- Containers
Pointer
FlowLayoutPanel
GroupBox
—1 Panel
Splitcontainer
TahCnntml
Puc. 1.28. Панель инструментов для построения
графического интерфейса пользователя
Рис. 1.29. Окно редактора для приложения
Windows Forms после добавления двух кнопок
62 Глава 1
Вы можете графически редактировать любые компоненты GUI в любое время, и
код будет соответствующим образом уточнен. Попробуйте таким же образом добавить
несколько других компонентов, а затем скомпилируйте и запустите программу, нажав
<Ctrl+F5>. Окно приложения отобразится во всей красе. Проще некуда, не правда ли?
Резюме
В настоящей главе вы ознакомились со всей механикой применения Visual C++
2005 для создания различного рода приложений. Вы сформировали и запустили кон-
сольные программы, как “родную”, так и CLR. Применяя автоматизированные масте-
ра создания приложений, вы получили Windows-программу на базе MFC и программу
Windows Forms, выполняемую под CLR.
Прочитав эту главу, вы должны усвоить следующие моменты.
□ Общеязыковая исполняющая среда (Common Language Runtime — CLR) — это
реализация Microsoft стандарта инфраструктуры общего языка (Common
Language Infrastructure — CLI).
□ Среда .NET Framework включает в себя CLR плюс библиотеки .NET, поддержи-
вающие приложения, ориентированные на CLR.
□ Родные приложения C++ пишутся на языке ISO/ANSI C++.
□ Программы, написанные на языке C++/CLI, выполняются под управлением
CLR.
□ Решение — это контейнер для одного или более проектов, формирующих реше-
ние определенной проблемы обработки информации.
□ Проект — это контейнер элементов кода и ресурсов, составляющих функцио-
нальную единицу программы.
□ Сборка — фундаментальная единица программ CLR. Все программы CLR состо-
ят из одной или более сборок.
Начиная со следующей главы и на протяжении первой половины книги, вы будете
интенсивно использовать консольные приложения. Все эти примеры иллюстрируют
применение элементов языка C++ в консольных приложениях как Win32, так и CLR.
Вы вернетесь к мастеру создания приложений для MFC-ориентированных програм-
мы, как только познаете секреты C++.
Данные, переменные
и вычисления
В этой главе мы обратимся к основам программирования на C++. Прочитав ее до
конца, вы сможете писать простые программы на C++ в традиционной форме: ввод-
обработка-вывод. Как я уже говорил в предыдущей главе, сначала я познакомлю вас
со средствами языка ANSI/ISO C++, а затем раскрою отличающиеся и дополнитель-
ные аспекты языка C++/CLI.
Изучая аспекты языка на примере работающих программ, вы получите возмож-
ность попрактиковаться в применении среды разработки Visual C++. Для каждого
из примеров вам придется построить проект, прежде чем собрать и запустить его.
Помните, что все проекты, которые рассматриваются в этой книге, начиная с этой
главы и заканчивая главой 10, представляют собой консольные приложения.
Прочитав эту главу, вы изучите следующие вопросы.
□ Структура программы C++.
□ Пространства имен.
□ Переменные C++.
□ Определение переменных и констант.
□ Базовый ввод с клавиатуры и вывод на экран.
□ Выполнение арифметических вычислений.
□ Приведение операндов.
□ Область видимости переменных.
64 Глава 2
Структура программы C++
Программы, запускаемые как консольные приложения под Visual C++ 2005, чита-
ют данные из командной строки и туда же выводят результаты. Чтобы избежать не-
обходимости погружения в сложности, связанные с созданием и управлением окнами
приложений, пока у вас нет достаточного объема знаний, чтобы понять их работу,
все примеры, которые вы напишете в процессе первоначального изучения языка
C++, будут консольными программами — как консольными программами Win32, так
и консольными программами .NET. Это позволит вам полностью сосредоточиться на
языке C++. Только когда вы освоите его в достаточной мере, тогда будете готовы к
тому, чтобы создавать и управлять оконными приложениями. Для начала вы узнаете,
каким образом структурированы консольные программы.
Программа на C++ состоит из одной или более функций. В главе 1 вы видели при-
мер консольной программы Win32, состоящей из единственной функции main (), где
main — имя функции. Каждая программа C++ стандарта ANSI/ISO содержит функ-
цию main (), и все программы C++ любого размера состоят из нескольких функций —
функции main (), с которой начинается выполнение программы и некоторого коли-
чества других функций. Функция — просто самодостаточный блок кода с уникальным
именем, которое используется для запуска его на выполнение. Как было показано в
главе 1, консольная программа Win32, сгенерированная мастером создания приложе-
ний (Application Wizard), имеет главную функцию по имени tmain. По действующе-
му соглашению в Visual C++ главная функция должна называться main или wmain — в
зависимости от того, использует программа символы Unicode или нет. Имена wmain
и _tmain специфичны для Microsoft. Имя главной функции, отвечающей стандар-
ту ISO/ANSI языка C++ — main. Я буду использовать имя main во всех примерах
ISO/ANSI C++.
Типичная программа командной строки может быть структурирована, как показа-
но на рис. 2.1.
На рис. 2.1 видно, что выполнение программы начинается с начала функции
main (). Из main () управление передается функции input_names (), которая воз-
вращает его в позицию, следующую непосредственно за той точкой, из которой она
была вызвана в main (). Затем из main () вызывается функция sort_names () и, по-
сле возврата управления в main (), вызывается финальная функция output_names ().
В конечном итоге, после завершения вывода, управление опять возвращается в
main () и на этом программа завершается.
Конечно, разные программы могут иметь совершенно различную функциональную
структуру, однако все они начинают выполнение с начала main (). Принципиальная
выгода от разделения программы на функции состоит в том, что вы можете писать и
отлаживать их по отдельности. Есть и дополнительная выгода, которая заключается в
том, что функции, написанные для выполнения определенных задач, могут быть по-
вторно использованы в других программах. Библиотеки, поставляемые с C++, предо-
ставляют множество стандартных функций, которые вы можете применять в своих
программах. Они могут избавить вас от огромного объема рутинной работы.
Подробнее о создании и использовании функций вы узнаете в главе 5.
Данные, переменные и вычисления 65
Когда вызывается функция,
Рис. 2.1. Структура типичной программы командной строки
Практическое занятие) ПрОСТЭЯ ПрОГрЭММЭ
Простой пример поможет вам лучше понять элементы программы. Начните
с создания нового проекта — вы можете воспользоваться комбинацией клавиш
<Ctrl+Shift+N> для ускорения этой операции. Когда появится диалоговое окно New
Project (Новый проект), показанное на рис. 2.2, выберите Win32 в качестве типа про-
екта и Win32 Console Application (Консольное приложение Win32) — в качестве ша-
блона. Назовите проект Ех2_01.
Если вы щелкнете на кнопке ОК, то увидите новое диалоговое окно, которое по-
казывает обзор того, что сгенерирует мастер Application Wizard (рис. 2.3).
Если теперь вы щелкнете на ссылке Application Settings (Настройки приложения)
в левой части диалогового окна, то увидите дополнительные опции приложения
Win32, как показано на рис. 2.4.
66 Глава 2
Рис. 2.2. Создание нового проекта консольного приложения Win32
Рис. 2.3. Мастер создания приложений
данные, переменные и вычисления
67
Рис. 2.4. Дополнительные опции консольного приложения Win32
Настройкой по умолчанию будет Console application (Консольное приложение),
что включает в себя файл, содержащий версию функции main () по умолчанию, но
вы должны начать с самой базовой структуры проекта, поэтому выберите Empty
project (Пустой проект) в наборе дополнительных опций и щелкните на кнопке Finish
(Iotobo). Теперь ваш проект создан, но не содержит никаких файлов. Вы можете ви-
деть, что входит в проект на панели Solution Explorer в левой части главного окна
Visual C++ 2005, как показано на рис. 2.5.
Solution - Е u _0i
Solution 'Ех2_0Г (1 project)
.j Header Files
Resource Files
Source Files
Solution Explorer |E| Class View |jg|l Resource View
Puc. 2.5. Состав проекта Ex2_01
68 Глава 2
Начните с добавления к проекту нового исходного файла, для чего щелкните
правой кнопкой мыши на Source Files (Исходные файлы) в панели Solution Explorer
и выберите в контекстном меню пункт Ad d^ New Item (Добавить1^Новый элемент).
При этом появится диалоговое окно Add New Item (Добавить новый элемент), подоб-
ное тому, что показано на рис. 2.6.
Add New Item - Ех2 01
Categories:
e Visual C++
U1
Code
Data
- Resource
Web
Utility
Props rty Sheets
Jem plates;
Visual Studio nvtabed templates
^]Midl File (Jdl)
component class
Му Templates
; j Search Online Templates
Header File (.h)
^Module-Definition File ( def)
Installer Class
Creates a file contaoning C++ source code
Name:
Location:
d:\Beginning Visual C++ 2005\Examples\Ex2_01\Ex2_01
в rowse...
d----------
Cancel
Рас. 2.6. Диалоговое окно Add New Item (Добавить новый элемент)
Убедитесь, что выделен шаблон C++ File (.срр) (Файл C++ (.срр)), щелкнув на нем,
и введите имя файла, как показано на рис. 2.6. Файл автоматически получит расшире-
ние . срр, поэтому набирать его не нужно. Не будет никаких проблем, если имя файла
совпадет с именем проекта. Файл проекта имеет расширение . vcpro j, поэтому его
полное имя будет отличаться от имени исходного файла.
Щелкните на кнопке Add (Добавить) для создания файла. Затем вы можете на-
брать следующий код в панели редактора окна IDE:
/ / Ех2_01. срр
/ / Простой пример программы
linclude <iostream>
using std::cout;
using std::endl;
int main()
int apples, oranges;
int fruit;
apples = 5; oranges = 6;
fruit = apples + oranges;
cout « endl;
/ / Объявление двух целочисленных переменных
// ... и еще одной
// Присваивание начальных значений
// Получить сумму
// Начать вывод с новой строки
cout « "Апельсины - не единственные фрукты... " « endl
« и всего у нас ” « fruit « ” фруктов.”;
cout « endl; // Вывести символ новой строки
return 0; // Выход из программы
Данные, переменные и вычисления
69
Цель этого примера — проиллюстрировать некоторые способы, применяемые для написания
операторов C++, не являющиеся, однако, демонстрацией хорошего стиля программирования.
Как только на основании расширения файл идентифицирован как файл исходного
кода C++, ключевые слова, распознаваемые редактором, окрашиваются соответствую-
щим образом. Вы сразу увидите ошибку, если введете Int там, где должны ввести int,
потому что слово Int не будет окрашено в тот же цвет, который применяется для вы-
деления ключевых слов языка в исходном коде.
Если вы посмотрите на панель Solution Explorer, где открыт ваш новый проект,
то увидите вновь созданный исходный файл. Solution Explorer всегда показывает все
файлы проекта. Если щелкнуть на вкладке Class View (Представление классов) в ниж-
ней части панели Solution Explorer, будет отображено представление Class View. Эта
панель состоит из двух частей: верхней, показывающей глобальные функции и ма-
кросы, определенные в проекте (и еще классы, если это проект, включающий в себя
классы), и изначально пустой нижней. Функция main () появится в нижней панели,
если вы выберете элемент Global Functions and Variables (Глобальные функции и пе-
ременные) в верхней панели Class View, как показано на рис. 2.7. Чуть позже я объяс-
ню подробно, что все это значит, а пока отмечу, что по сути глобальными являются
функции и/или переменные, доступные в любом месте программы.
Рис. 2.7. Представление Class View
Существуют три способа компиляции и компоновки программы; вы можете вы-
брать пункт Build Ех2_01 (Собрать Ех2_01) в меню Build (Сборка) либо нажать функ-
циональную клавишу <F7>, или же выбрать соответствующую кнопку на линейке
инструментов — найти ее можно, перемещая курсор над кнопками с небольшой за-
держкой. Если предположить, что операция сборки программы завершилась успеш-
но, вы можете запустить программу, нажав комбинацию клавиш <Ctrl+F5> или вы-
брав пункт Start Without Debugging (Запустить без отладки) в меню Debug (Отладка).
В результате вы должны получить следующий вывод в окне командной строки:
70 Глава 2
Апельсины - не единственные фрукты...
- и всего у нас 11 фруктов.
Press any key to continue . . .
Для продолжения нажмите любую клавишу . . .
Первые две строки выдала программа, а последняя указывает, что вы можете за-
вершить выполнение и закрыть окно командной строки.
Комментарии к программе
Первых две строки программы — это комментарии. Комментарии рассматривают-
ся как важная часть программы, однако они не являются исполняемым кодом — это
просто подсказки для читателей-людей. Все комментарии компилятором игнориру-
ются. В любой строке кода два последовательных слеша / /, если они не содержатся
внутри текстовой строки (что такое текстовые строки, вы увидите позже), говорят о
том, что остальная часть строки справа от них представляет собой комментарий.
Как видите, несколько строк в программе содержат комментарии вместе с опера-
торами самой программы. Вы также можете применять альтернативную форму ком-
ментариев, ограничивая их комбинациями символов /* и */. Например, первая стро-
ка программы могла бы выглядеть следующим образом:
/* Ех2_01.срр */
Комментарий, начинающийся с //, включает только часть строки справа от него,
в то время как форма /*. . . */ определяет, что все, находящееся между /* и */, явля-
ется комментарием, и может распространяться на несколько строк. Например, впол-
не можно написать так:
Ех2_01.срр
Простой пример программы
Здесь все четыре строки являются комментариями. Если вы хотите выделить не-
которые строки комментариев, то всегда можете украсить их рамкой:
* Ех2_01.срр *
* Простой пример программы
Как правило, всегда нужно стараться снабжать программы исчерпывающими ком-
ментариями. Комментарии должны быть достаточными, чтобы другой программист
или вы сами через какое-то время, смогли понять назначение конкретной части кода
и то, как он работает.
Директива #include — файлы заголовков
Вслед за начальными комментариями в программе находится директива #include:
#include <iostream>
Она называется директивой, поскольку указывает компилятору сделать что-то — в
данном случае вставить содержимое файла <iostream> в программу перед ее компи-
ляцией. Файл <iostream> называется файлом заголовков, потому что обычно он по-
является в начале программного файла. В действительности точнее было бы назвать
<iostream> заголовком, так как согласно стандарту ANSI C++ заголовок не обязатель-
но должен содержаться в файле. Заголовочный файл <iostream> включает определе-
Данные, переменные и вычисления 71
ния, которые понадобятся вам для того, чтобы можно было использовать операторы
ввода и вывода C++. Если вы не включите в программу содержимое <iostream>, то
она не скомпилируется, поскольку вы используете в ней операторы, зависящие от не-
которых определений, находящихся в этом файле. Существует множество заголовоч-
ных файлов, поставляемых с Visual C++, которые охватывают широкий круг возмож-
ностей. Вы познакомитесь со многими из них в процессе изучения средств языка.
Оператор #include — это одна из директив препроцессора. Редактор Visual C++
распознает их и окрашивает в голубой цвет в окне редактирования. Директивы пре-
процессора — это команды, выполняемые на фазе предварительной обработки компи-
лятора, которая выполняется перед тем, как ваш исходный код будет скомпилирован
в объектный код, и директивы препроцессора обычно некоторым образом воздей-
ствуют на ваш исходный код перед тем, как он будет скомпилирован. По мере необхо-
димости я представлю вам и другие директивы препроцессора.
Пространства имен и объявление using
Как было показано в главе 1, стандартная библиотека — это широкий набор проце-
дур, предназначенных для решения многих часто встречающихся задач: например, об-
работка ввода-вывода и выполнение базовых математических вычислений. Поскольку
существует огромное количество таких процедур, а наряду с ними — и других име-
нованных сущностей, есть вероятность, что вы можете нечаянно использовать для
собственных целей имя, совпадающее с одним из имен, определенных в стандартной
библиотеке. Пространство имен (namespace) — это механизм C++, предназначенный
для предотвращения проблем, связанных с дублированием имен, применяемых в про-
грамме для разных целей. Это делается за счет того, что определенное множество
имен вроде имен стандартной библиотеки ассоциируется с общим именем семейства,
которое и представляет собой пространство имен.
Каждое имя, определенное в коде, появляющемся внутри пространства имен,
включает в себя имя этого пространства имен. Все средства стандартной библиотеки
ISO/ANSI C++ определены внутри пространства имен по имени std, поэтому каждый
элемент стандартной библиотеки, к которому вы можете обратиться в своей програм-
ме, имеет свое собственное имя плюс наименование пространства имен — std — в ка-
честве квалификатора. Имена cout и endl определены в стандартной библиотеке,
поэтому их полные имена выглядят как std: : cout и std: : endl, и вы видели это в
действии в главе 1. Два двоеточия, отделяющие имя пространства имен от имени эле-
мента, образуют операцию, называемую операцией разрешения контекста. Позднее
в этой книге я расскажу о других случаях ее использования. Применение полных
имен в программе делает код несколько громоздким, поэтому было бы неплохо ис-
пользовать их простые имена без квалификатора — имени пространства имен std.
Две строки программы, которые следуют за директивой #include <iostream>, обе-
спечивают упомянутую возможность:
using std::cout;
using std::endl;
Это — объявления using, сообщающие компилятору о вашем намерении ис-
пользовать имена cout и endl из пространства имен std без указания квалификато-
ра — наименования пространства имен. После этого компилятор, встречая в тексте
программы имя cout, в соответствии с первым объявлением будет предполагать, что
вы имеете в виду cout, определенный в стандартной библиотеке. Имя cout представ-
ляет стандартный выходной поток, который по умолчанию соответствует командной
строке, а имя endl — символ новой строки.
72 Глава 2
Чуть позднее в этой главе вы узнаете больше об этом, включая и то, как опреде-
лять свои собственные пространства имен.
Функция main ()
Функция main () в последнем примере состоит из заголовка main () и всего осталь-
ного, начиная с открывающей фигурной скобки ({) и до соответствующей закрываю-
щей фигурной скобки (}). Фигурные скобки заключают в себе исполняемые операто-
ры функции, которые все вместе называются телом функции.
Как вы убедитесь, любая функция состоит из заголовка, определяющего (помимо
всего прочего) ее имя, за которым следует тело функции, включающее множество
операторов, заключенных в пару скобок. Тело функции может не содержать в себе во-
обще никаких операторов — в таком случае функция ничего не делает.
Функция, которая ничего не делает, может показаться излишней, но когда вы пи-
шете крупную программу, то можете изначально отобразить полную структуру про-
граммы на функции, однако опустить код многих из них, оставляя их тела пустыми
либо с минимальным содержимым. Поступая так, вы обеспечиваете возможность
компиляции и выполнения всей программы, со всеми ее функциями в любой момент
времени, с последовательным инкрементным добавлением кода этих функций в про-
цессе разработки.
Операторы программы
Каждый из операторов программы, образующих тело функции main () , завер-
шается точкой с запятой. Этот символ помечает конец оператора, а не конец строки.
Следовательно, один оператор может распространяться на несколько строк, если это
помогает понять код, либо несколько операторов могут находиться в одной строке.
Оператор программы — базовый элемент, определяющий то, что делает программа.
Он в некоторой степени похож на предложение в абзаце текста, где каждое из них
выражает действие или идею, но при этом комбинируется с другими предложениями
абзаца для определения более общей идеи. Оператор — самодостаточное определение
действия, которое может выполнять компьютер, но которое может комбинироваться с
другими операторами с целью определения более сложного действия или вычисления.
Действие функции всегда выражается набором операторов, каждый из которых
завершается точкой с запятой. Взгляните на операторы в последнем примере, просто
чтобы почувствовать, как они работают. Каждый из этих операторов позднее в этой
главе будет описан более подробно.
Вот первый оператор в теле функции main ():
int apples, oranges; // Объявление двух целочисленных переменных
Этот оператор объявляет две переменные — apples и oranges. Переменная — это
просто именованный фрагмент памяти компьютера, который вы можете использо-
вать для сохранения данных, а оператор, представляющий имена одной или более
переменных, называется объявлением переменной. Ключевое слово int в предыду-
щем примере означает, что переменные с именами apples и oranges предназначе-
ны для хранения целочисленных значений. Везде, где в программе объявляется имя
новой переменной, всегда должен указываться вид данных, которые она может сохра-
нять, и это называется типом переменной.
Следующий оператор объявляет другую целочисленную переменную — fruit:
int fruit;
// .. .и еще одной
Данные, переменные и вычисления 73
Хотя и можно объявлять несколько переменных в одном операторе, как это сдела-
но в предыдущей строке с apples и oranges, обычно лучше объявлять каждую пере-
менную в отдельном операторе и в отдельной строке, чтобы можно было индивиду-
ально ее прокомментировать, описывая ее назначение.
Следующая строка в примере выглядит так:
apples =5; oranges =6;
// Присваивание начальных значений
Эта строка содержит два оператора, причем каждый завершается точкой с запя-
той. Я поместил их здесь просто для демонстрации, что вы можете размещать более
одного оператора в строке. Хотя это и не обязательно, но хорошим тоном в програм-
мировании считается размещение только одного оператора в строке, поскольку это
облегчает понимание кода. Хороший тон программирования предполагает такое ко-
дирование, которое упрощает понимание кода и снижает вероятность ошибок.
Два оператора в предыдущей строке сохраняют значения 5 и 6 в переменных
apples и oranges соответственно. Эти операторы называются операторами присва-
ивания, потому что они присваивают новые значения переменным, а = — это опера-
ция присваивания.
Далее идет такой оператор:
fruit = apples + oranges;
// Получить сумму
Это также оператор присваивания, но несколько отличающийся, поскольку справа
от операции присваивания стоит арифметическое выражение. Этот оператор скла-
дывает вместе значения, хранящиеся в переменных apples и oranges, и сохраняет
результат в переменной fruit.
Следующие три оператора:
cout « endl; // Начать вывод с новой строки
cout « "Апельсины - не единственные фрукты.. . " « endl
« "- и всего у нас " « fruit « " фруктов.";
cout « endl; // Вывести символ новой строки
Это операторы вывода. Первый из них посылает символ новой строки, отмечен-
ный словом endl, в командную строку на экране. В C++ источник ввода и место назна-
чения вывода называется потоком (stream). Имя cout специфицирует “стандартный”
выходной поток, а операция << указывает, что все, что находится справа от него,
должно быть направлено в выходной поток cout. Операция « “задает” направление
потока данных — от переменной или строки, находящейся справа, в направлении вы-
ходного места назначения, расположенного слева. Таким образом, в первом операто-
ре значение, представленное именем endl, которое означает символ новой строки,
пересылается в поток, идентифицированный словом cout. В итоге данные, передан-
ные в cout, выводятся в командной строке.
Значение имени cout и операции « определены в заголовочном файле стандарт-
ной библиотеки <iostream>, который вы добавили в код программы с помощью ди-
рективы #include в самом ее начале, cout — имя из стандартной библиотеки, а по-
тому относится к пространству имен std. Как я уже говорил, без директивы using
это имя не могло быть распознано компилятором, если только вы не указали бы его в
полной квалифицированной форме — std:: cout. Поскольку cout предназначено для
того, чтобы представлять стандартный выходной поток, вы не должны использовать
это имя для других целей, а потому не можете применять его, например, в качестве
имени переменной. Очевидно, что использование одного и того же имени для раз-
личных вещей может стать причиной путаницы.
74 Глава 2
Второй из трех операторов вывода распространяется на две строки:
cout « "Апельсины - не единственные фрукты. . . " « endl
« и всего у нас " « fruit « " фруктов.";
Как упоминалось ранее, вы можете размещать один оператор программы в столь-
ких строках, в скольких хотите, если это сделает код более ясным. Конец оператора
всегда отмечается точкой с запятой, а не концом строки. Последовательные строки
читаются и комбинируются компилятором в один оператор, пока он не обнаружит
точку с запятой, означающую его конец. Конечно, это значит, что если вы забудете
поставить точку с запятой в конце оператора, компилятор будет считать следующую
строку частью того же оператора и соединит их вместе. Обычно в результате получа-
ется нечто такое, что компилятор не может понять, поэтому вы получите сообщение
об ошибке.
Приведенный выше оператор посылает в командную строку текст “Апель сины -
не единственные фрукты. . . ”, за которым следует еще один символ новой строки
(endl), потом — еще одну часть текста и всего у нас ”, затем значение переменной
fruit и завершающий текст “ фруктов. ”. В такой последовательности сущностей, от-
правляемой в выходной поток, не кроется никаких проблем. Оператор выполняется
слева направо, при этом каждый элемент отправляется на cout по очереди. Обратите
внимание, что каждому отправленному на вывод элементу при этом предшествует
собственная операция «.
Третий и последний оператор вывода просто посылает на экран еще один символ
новой строки. Так эти три оператора формируют вывод программы, который вы и
наблюдаете.
Последний оператор программы выглядит следующим образом:
return 0; // Выход из программы
Этот оператор прекращает выполнение функции main () и завершает программу.
Управление возвращается операционной системе. Об этом операторе будет рассказа-
но позже.
Операторы программы выполняются в том порядке, в котором они записаны,
если только специальные управляющие операторы не изменяют естественный поря-
док их выполнения. В главе 3 вы ознакомитесь с операторами, изменяющими после-
довательность выполнения.
Пробелы
Пробел (whitespace) — термин, используемый в C++ для обозначения символов про-
бела, табуляции, новой строки, новой страницы и комментариев. Пробелы служат раз-
делителями одной части оператора от другой и позволяют компилятору идентифици-
ровать, где заканчивается один элемент оператора, такой как int, и начинается другой.
В остальных случаях пробелы игнорируются и никакого влияния не оказывают.
Посмотрим, к примеру, на следующий оператор:
int fruit; // ...и еще одной
Должен быть, по крайней мере, один пробельный символ (обычно — пробел) меж-
ду int и fruit, чтобы компилятор мог различить их, но если пробелов будет больше,
они игнорируются.
С другой стороны, взгляните на такой оператор:
fruit = apples + oranges; // Получить сумму
Данные, переменные и вычисления 75
Ни один пробельный символ не является необходимым между fruit и =, или меж-
ду = и apples, хотя при желании вы можете их включить сюда. Дело в том, что = —
это не буква и не цифра, поэтому компилятор может отделить его от окружения.
Аналогично никакие пробельные символы не нужны вокруг знака +, но вы можете
включить их, если хотите сделать код более читабельным.
Как я сказал, помимо применения в качестве разделителей между элементами опе-
ратора, которые в противном случае могли бы быть спутаны, пробелы компилятором
игнорируются (конечно, если только они не являются частью строки между двумя ка-
вычками). Поэтому вы можете включить в свою программу сколько угодно пробелов,
если считаете, что это повысит ее читабельность, как это и было сделано в приведен-
ном выше примере, где один оператор распространялся на несколько строк програм-
мы. Помните, что в C++ концом оператора является точка с запятой.
Блоки операторов
Вы можете заключить несколько операторов в фигурные скобки. В этом случае
они образуют блок, или составной оператор. Примером блока может служить тело
функции. Такой составной оператор можно воспринимать как единственный опера-
тор (в чем вы убедитесь, изучая управляющие операторы C++ в главе 3). Фактически,
всякий раз, когда вы помещаете отдельный оператор в C++, вы можете вместо него
использовать блок операторов в фигурных скобках. Как следствие, блоки могут быть
включены в другие блоки. Фактически блоки могут быть вложенными один в другой
на любую глубину.
Блок операторов также оказывает важное влияние на переменные, но я отложу дискуссию на
эту тему до того момента, когда позднее в этой главе обращусь к теме видимости переменных.
Автоматически сгенерированные консольные программы
В последнем примере вы настроили проект как пустой (Empty project), без исхо-
дных файлов, а затем последовательно добавили их. Если же вы позволите мастеру
Application Wizard сгенерировать проект, как это делалось в главе 1, то проект изна-
чально будет содержать несколько файлов, и вам стоит немного глубже разобраться
в их содержимом. Создайте новый проект консольной программы Win32 по имени
Ех2_01А, на этот раз позволив мастеру Application Wizard сделать свою работу, не
устанавливая никаких опций в диалоге Application Settings (Настройки приложения).
Проект будет включать в себя три файла с кодом: Ех2_01А. срр и stdafх. срр, со-
держащие исходный код, и заголовочный файл stdafx.h. Они предназначены для
обеспечения базовых средств, необходимых работающей консольной программе, ко-
торая не делает ничего. Если у вас есть открытый проект, вы можете закрыть его, вы-
брав пункт меню Fite^Close Solution (Файл*=>Закрыть решение). Вы можете создать
новый проект, не закрывая старый — при этом он будет закрыт автоматически, если
только вы не предпочтете добавить новый к существующему решению.
Прежде всего, содержимое Ех2__01А.срр будет таким:
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
return 0;
76 Глава 2
Это совершенно отличается от предыдущего примера. Присутствует директива
#include для заголовочного файла stdafx.h, которой не было в предыдущей вер-
сии, и функция, с которой начинается выполнение программы, называется _tmain (),
а не main ().
Мастер Application Wizard сгенерировал заголовочный файл stdafx.h как часть
проекта, и если вы заглянете в его код, то увидите там еще две директивы #include
для заголовочных файлов стандартной библиотеки stdio. h и tchar. h. stdio. h — за-
головок старого стиля для ввода-вывода, который использовался до появления те-
кущего стандарта ISO/ANSI C++. Он обеспечивает ту же функциональность, что и
<iostream>. Второй файл, tchar . h, является специальным заголовочным файлом
Microsoft, определяющим некоторые текстовые функции. Идея в том, что stdafx.h
должен определять набор стандартных системных включаемых файлов для вашего
проекта, в который директивой #include вы можете включать любые другие систем-
ные заголовочные файлы, которые понадобятся. При изучении ISO/ANSI C++ вам
не придется использовать ни один из включенных в stdafx.h заголовочных файлов,
что является одной из причин того, чтобы не применять средств генерации файлов
по умолчанию мастером Application Wizard.
Как я ранее объяснял, Visual C++ 2005 поддерживает wmain () как альтернативу
main () при написании программ, использующих символы Unicode, wmain () — специ-
фичное для Microsoft имя, которое не является частью ISO/ANSI C++. Для поддерж-
ки этого заголовок tchar .h определяет имя _tmain таким образом, что оно обычно
заменяется main, но при определении символа ^UNICODE заменяется wmain. То есть
для того, чтобы идентифицировать программу, использующую UNICODE, вы должны
добавить следующий оператор в начало заголовочного файла stdafx.h:
#define -UNICODE
Разобравшись во всем этом, мы можем вернуться к использованию простой старой
функции main () в последующих примерах для ISO/ANSI C++.
Определение переменных
Фундаментальное назначение всех компьютерных программ — манипулировать
некоторыми данными и получать некоторые ответы. Существенным условием этого
процесса является наличие в вашем распоряжении фрагмента памяти, к которому
можно обращаться по некоторому осмысленному имени, и где можно хранить эле-
мент данных. Каждый фрагмент памяти, специфицированный таким образом, назы-
вается переменной.
Как вы уже знаете, каждая переменная сохраняет данные определенного вида, и
тип сохраняемых данных фиксируется при объявлении переменной в программе.
Одна переменная может хранить только целые числа, и вы не можете использовать
ее для хранения дробных значений. Конкретное значение, хранимое в переменной в
каждый отдельный момент времени, определяется операторами вашей программы, и
конечно, как правило, ее значение будет меняться много раз в процессе вычислений,
выполняемых программой.
В следующем разделе мы поговорим о правилах именования переменных при
объявлении их в программах.
77
4
данные, переменные и вычисления
Именование переменных
Имя, которое вы присваиваете переменной, называется идентификатором, или
проще — именем переменной. Имена переменных могут включать буквы A-z (в
верхнем или нижнем регистре), цифры 0-9 и знаки подчеркивания. Никакие дру-
гие символы не допускаются, и если вы нечаянно укажете какой-то другой символ,
то получите сообщение об ошибке при попытке скомпилировать программу. Имена
переменных должны начинаться либо с буквы, либо с подчеркивания. Обычно выбор
имен переменных обусловлен видом хранимой в них информации.
Поскольку имена переменных в Visual C++ 2005 могут иметь длину до 2048 сим-
волов, вам предоставляется достаточно свободы в именовании ваших переменных.
Фактически, помимо переменных есть несколько других вещей в C++, которые име-
ют собственные имена, и они тоже могут содержать до 2048 символов, подчиняясь
тем же правилам, что и имена переменных. Применение имен максимальной длины
может затруднить чтение ваших программ, и если только у вас нет изумительных на-
выков работы на клавиатуре, их очень трудно набирать. Но более серьезное обстоя-
тельство, которое следует принять во внимание при выборе имен — это то, что не все
компиляторы поддерживают такие длинные имена. Если вы намереваетесь компили-
ровать свой код в других средах, то желательно ограничиться именами не длиннее 31
символа. Обычно этого достаточно, чтобы выбирать осмысленные значащие имена,
и позволяет избежать проблем, связанных с ограничениями на длину имен, присущи-
ми другим компиляторам.
Хотя разрешается использовать имена переменных, начинающиеся со знака под-
черкивания, например, _this и _that, все же этого лучше избегать, потому что это
чревато потенциальными конфликтами со стандартными системными переменными,
имеющими такую же форму. По той же причине следует избегать применения имен
переменных, начинающихся с двух знаков подчеркивания.
Ниже представлены примеры хороших имен переменных.
price
□ discount
pShape
value_
COUNT
Имена вроде 8_Ball, 7Up и 6__pack не допускаются, равно как и Hash! или
Mary-Ann. Последний пример демонстрирует распространенную ошибку, хотя имя
Магу_Апп со знаком подчеркивания вместо тире вполне приемлемо. Конечно, Магу
Ann — тоже неправильно, поскольку пробелы в именах переменных не разрешены.
Обратите внимание, что имена переменных republican и Republican трактуются
как различные, потому что имена переменных зависят от регистра, то есть символы
верхнего и нижнего регистра различаются. Конечно, пробельные символы вообще
не могут появляться в именах, и если вы нечаянно включите пробельные символы,
то получите два или более имен вместо одного, что обычно заставляет компилятор
выдавать соответствующее сообщение.
Обычное соглашение, принятое в C++, заключается в том, что имена, начинающи-
еся с заглавных букв, резервируются для именования классов, а начинающиеся с про-
писных — для именования переменных. О классах речь пойдет в главе 8.
78 Глава 2
Ключевые слова в C++
В C++ присутствуют зарезервированные слова, которые называются ключевыми
словами и имеют специальное значение внутри языка. Редактор Visual C++ 2005 под-
свечивает их определенным цветом, когда вы вводите текст программы — в моей систе-
ме по умолчанию они окрашены в синий цвет. Если ключевое слово, которое вы ввели,
не окрашивается соответствующим образом — значит, вы ввели его неправильно.
Помните, что ключевые слова, как весь язык C++, зависят от регистра. Например,
программа, которую вы набирали ранее, содержит ключевые слова int и return.
Если бы вы написали Int или Return, то это не считалось бы ключевыми словами,
и компилятор не распознал бы их как таковые. В процессе дальнейшего изучения вы
познакомитесь и со многими другими ключевыми словами C++. Необходимо обеспе-
чивать, чтобы имена, которые вы выбираете для сущностей в своих программах, не
совпадали ни с одним ключевым словом C++. Полный список ключевых слов Visual
C++ 2005 приведен в приложении А.
Объявление переменных
Как вы уже видели, объявление переменной — это оператор программы, который
специфицирует имя переменной данного типа, например:
int value;
Это объявляет переменную по имени value для хранения целых чисел. Тип дан-
ных, который может быть сохранен в переменной value, указан ключевым словом
int, поэтому value вы можете применять только для хранения данных типа int.
Поскольку int — ключевое слово, его нельзя использовать в качестве имени ни одной
из ваших переменных.
Обратите внимание, что объявление переменной всегда завершается точкой с запятой.
В одном объявлении можно указать имена нескольких переменных, но, как я уже
говорил, обычно лучше их объявлять в отдельных операторах — по одному в строке.
В этой книге я иногда отклоняюсь от этого правила, но лишь для того, чтобы не рас-
тягивать код на слишком много страниц.
Чтобы хранить данные (например, значение целого числа), недостаточно про-
сто определить имя переменной. С этим именем также необходимо ассоциировать
некоторый участок компьютерной памяти. Этот процесс называется определением
переменной. В C++ объявление переменной также является ее определением (за ис-
ключением некоторых специальных случаев, с которыми вы позже познакомитесь в
этой книге). В отдельном операторе вы указываете имя переменной, и это связывает
его с участком памяти определенного размера.
Таким образом, оператор:
int value;
одновременно является и объявлением, и определением. Здесь использовано имя пе-
ременной value, которое объявляется, для доступа к участку компьютерной памяти,
который здесь же определяется, и который может хранить отдельное значение типа
int.
Термин объявление вы будете применять для представления имени вашей программе, вместе
с информацией о том, для чего это имя используется. Термин определение описывает вы-
деление компьютерной памяти, связанной с этим именем. В случае переменных вы можете
Данные, переменные и вычисления 79
совместить объявление и определение в одном операторе, как показано в предыдущей строке
кода. Причина столь педантичного различия между объявлением и определением состоит
в том, что позднее вы столкнетесь с операторами, которые являются объявлениями, но не
определениями.
Вы должны объявлять переменную в некоторой точке, находящейся между нача-
лом вашей программы и тем местом, где она будет впервые задействована. В C++ хо-
рошим тоном считается объявлять переменную поближе к тому месту, где она первый
раз используется.
Начальные значения переменных
Объявляя переменную, вы тут же можете присвоить ей начальное значение.
Объявление переменной, которое присваивает ей начальное значение, называется
инициализацией. Чтобы инициализировать переменную, когда вы объявляете ее
вам просто нужно написать знак равенства, за которым следует инициализирующее
значение. Вы можете написать следующие операторы, чтобы присвоить каждой пере-
менной начальное значение:
int value = 0;
int count = 10;
int number =5;
В данном случае value получает значение 0, count — значение 10, a number — зна-
чение 5.
Существует и другой способ указания начальных значений переменных C++, ко-
торый называется функциональной нотацие
Вместо знака равенства и значения
вы просто указываете значение в скобках, следующих за именем переменной. То есть
предыдущие объявления можно переписать следующим образом:
int value (0);
int count(10);
int number (5);
Если вы не присваиваете переменной начального значения, то, как правило, она
обычно содержит произвольный мусор, который находился в том участке памяти, ко-
торый для нее выделен (есть исключение из этого правила, и позднее в этой главе вы
о нем узнаете). Поэтому, где только возможно, вы должны инициализировать пере-
менные при их объявлении. Если переменные изначально содержат известные значе-
ния, то гораздо проще разобраться, когда что-то идет не так. А уж в чем можно быть
абсолютно уверенным, так это в том, что что-нибудь обязательно пойдет не так.
Фундаментальные типы данных
Разновидность информации, которую может содержать переменная, называется
ее типом данных. Все данные и переменные в вашей программе должны относиться
к определенному типу данных. Стандарт C++ ISO /ANSI предоставляет набор фунда-
ментальных типов данных, специфицированных определенными ключевыми слова-
ми. Фундаментальные типы данных называются так потому, что они хранят значения
типов, представляющих фундаментальные данные на вашем компьютере — по сути,
числовые значения, в которые входят и символы, потому что они представлены чис-
ловыми кодами. Вы уже видели ключевое слово int, применяемое для определения
целочисленных переменных. C++/CLI также определяет фундаментальные типы дан-
80 Глава 2
ных, которые не являются частью ISO/ANSI C++, и позднее в этой главе я расскажу
о них.
Как часть объектно-ориентированных аспектов языка, предусмотрена возмож-
ность определения ваших собственных типов данных, и конечно, разнообразные би-
блиотеки, поставляемые с Visual C++ 2005 также определяют свои типы данных. Но
пока мы ограничимся элементарными числовыми типами данных, представленными
в ISO/ANSI C++. Фундаментальные типы подразделяются на три категории — типы,
содержащие целые числа, типы, содержащие не целочисленные значения, и тип
void, указывающий пустое множество значений или отсутствие типа.
Целочисленные переменные
Как уже говорилось, целочисленные переменные — это переменные, которые со-
держат только значения целых чисел. Количество игроков футбольной команды —
целое число, по крайней мере, в начале игры. Вы уже знаете, что целочисленные
переменные можно объявлять с ключевым словом int. Переменные типа int зани-
мают 4 байта памяти, и могут сохранять как положительные, так и отрицательные
целые значения. Верхний и нижний пределы значений переменных типа int соот-
ветствуют максимальному и минимальному двоичному значению со знаком, которое
может быть представлено 32 битами. Верхний предел переменной типа int — 231 - 1,
что равно 2 147 483 647, а нижний предел-(231), что соответствует -2 147 483 648.
Ниже показан пример определения переменной типа int.
int toeCount = 10;
В Visual C++ 2005 ключевое слово short также определяет целые переменные, но
занимающие 2 байта в памяти. Ключевое слово short эквивалентно short int, и вы
можете определить две переменных типа short с помощью следующих операторов:
short feetPerPerson =2;
short int feetPerYard = 3;
Обе переменных относятся к одному и тому же типу, потому что short означает
то же самое, что short int. Здесь я использовал обе формы названия типа, чтобы
продемонстрировать их применение, но при написании программ лучше ограничи-
ваться одним вариантом, и short встречается более часто.
В C++ также предусмотрен другой целочисленный тип — long, который также
можно записывать как long int. Вот как объявляются переменные типа long:
long bigNumber = 1000000L;
long largeValue = 0L;
Эти операторы объявляют переменные bigNumber и largeValue с начальными
значениями 1000000 и 0 соответственно. Буква L, добавленная в конец литералов,
указывает на то, что это — целые типа long. Для той же цели можно также применять
и прописную букву 1, но ее недостаток в том, что ее легко спутать с цифрой 1. Целые
литералы без добавленной буквы L имеют тип int.
При написании больших чисел в программе вы не должны вставлять в них запятые или про-
белы в качестве разделителей групп, В тексте можно написать 12,245,678либо 12 245 678,
но в коде программы следует писать 12345678,
Целочисленные переменные, объявленные как long, в Visual C++ 2005 занимают
4 байта, и могут принимать значения от -2 147 483 648 до 2 147 483 647. Тот же диа-
пазон могут принимать переменные типа int.
81
4
данные, переменные и вычисления
В других компиляторах C++ переменные типа long (то же самое, что и тип long
int) могут отличаться от типа int, поэтому, если вы собираетесь компилировать
свои программы в других средах, не полагайтесь на то, что long и int эквивалентны.
При написании действительно переносимого кода вы даже не должны рассчитывать
на то, что int занимает 4 байта (например, в старых 16-разрядных версиях Visual C++
переменные int были двухбайтными).
Символьные типы данных
Тип данных char служит двум целям. Он специфицирует однобайтную пере-
менную, в которой можно сохранять целые числа в пределах определенного диапа-
зона значений, или же код отдельного символа ASCII (American Standard Code for
Information Interchange — американский стандартный код обмена информацией).
Набор кодов символов ASCII приведен в приложении Б. Вы можете объявить пере-
менную char с помощью следующего оператора:
char letter = 'А';
Этот код объявляет переменную по имени letter, инициализированную констан-
той 1 А*. Обратите внимание, что значение указывается как отдельный символ в оди-
ночных кавычках, а не в двойных, которые вы использовали ранее для определения
строк символов с целью отображения на экране. Строка символов — это последова-
тельность значений типа char, которая сгруппирована в единое целое, называемое
массивом. Массивы и обработка строк в C++ рассматриваются в главе 4.
Поскольку символ ’А’ в кодировке ASCII представлен десятичным значением 65,
вы могли бы написать этот оператор следующим образом:
char letter =65; // Эквивалент символа А
Это дало бы тот же результат, что и предыдущий оператор. Диапазон целых, которые
могут быть сохранены в переменных типа char, в Visual C++ составляет от -128 до 127.
Обратите внимание, что стандарт ISO/ANSI C++ не требует, чтобы тип char представ-
лял однобайтные целые со знаком. Это выбор реализации компилятора: представлять char
как целые со знаком в диапазоне от -128 до 127либо как целые без знака в диапазоне от 0 до
255. Следует иметь это в виду, если вы собираетесь переносить свой код C++ в другую среду.
Тип wchar_t назван так, поскольку это расширенный символьный тип (wide char
type), и переменные этого типа сохраняют 2-байтные символьные коды со значения-
ми от 0 до 65 535. Ниже показан пример определения переменной типа wchar t.
wchar_t letter = L’Z'; // Переменная хранит 16-битный код символа
Этот оператор определяет переменную letter, инициализированную 16-битным
кодом буквы Z. Символ L, предшествующий символьной константе 1 Z ’, сообщает
компилятору, что это 16-битное значение кода символа.
Для инициализации переменных char (как и других целых типов) вы также мо-
жете использовать шестнадцатеричные константы, что во многих случаях удобнее.
Шестнадцатеричное число записывается с применением стандартного представления
шестнадцатеричных цифр: от 0 до 9 и от А до F (или от а до f), означающие в деся-
тичном эквиваленте цифры от 0 до 15. Кроме того, они предваряются сочетанием Ох
(или ОХ) для того, чтобы отличать их от десятичных значений. Таким образом, чтобы
получить тот же результат, вы могли бы переписать оператор, приведенный выше,
следующим образом:
char letter = 0x41;
// Эквивалент А
82 Глава 2
Не записывайте десятичные целочисленные значения с ведущим нулем. Компилятор интер-
претирует их как восьмеричные, поэтому значение, записанное как 065, равно 53 в десятич-
ной нотации.
Обратите также внимание, что в Windows ХР имеется утилита Character Мар (Таб-
лица символов), которая позволяет находить символы в любых шрифтах, доступных
в Windows. Она показывает коды символов в шестнадцатеричном виде и сообщает,
на какие клавиши нужно нажать для их ввода. Утилиту Character Мар можно найти,
если щелкнуть на кнопке Start (Пуск) и заглянуть в папку System Tools (Системные),
находящуюся внутри папки Accessories (Стандартные).
Модификаторы целочисленных типов
Переменные целочисленных типов char, int, short или long сохраняют по умол-
чанию целые значения со знаком (signed), поэтому вы можете применять их как для
хранения положительных, так и отрицательных значений. Это объясняется тем, что
для этих типов по умолчанию принят модификатор типа signed. Поэтому когда вы
пишете int или long, то это означает signed int или signed long соответственно.
Вы также можете использовать ключевое слово signed отдельно для специфика-
ции типа переменной. В этом случае оно означает signed int, например:
signed value = -5; // Эквивалент signed int
Но такое написание не очень распространено, и я предпочитаю применять int,
что выглядит более очевидно.
Диапазон значений, которые могут быть сохранены в переменной типа char, на-
ходится в пределах от -128 до +127, что совпадает с диапазоном допустимых значе-
ний переменных типа short char. Но как это ни прискорбно, тип char и тип signed
char трактуются как разные типы, поэтому вы не должны допускать ошибку, думая,
что это одно и то же.
Если вы уверены, что вам не нужно хранить в переменной отрицательные зна-
чения (например, если собираетесь записывать в нее количество миль, которые вы
проезжаете за рулем в неделю), то вы можете специфицировать переменную как
unsigned:
unsigned long mileage = OUL;
Минимальное значение, которое можно записать в переменную mileage, равно
нулю, а максимальное — 4 294 967 295 (то есть 232 - 1). Сравните это с диапазоном
от -2 147 483 648 до 2 147 483 647 для signed long. Один бит, который в перемен-
ных signed служит для указания знака, в unsigned является частью числового значе-
ния. Как следствие, переменные unsigned имеют больший диапазон положительных
значений, но не могут представлять отрицательных. Обратите внимание на букву U
(или и), добавленную к константе unsigned. В предыдущем примере я также добавил
к ней L, чтобы указать, что константа — long. Вы можете использовать эти симво-
лы-модификаторы констант в любом регистре и любой последовательности. Однако
при указании таких значений желательно придерживаться какого-то согласованного
стиля.
Вы также можете применять в качестве спецификатора типа unsigned отдельно,
что подразумевает unsigned int.
Помните, что и signed, и unsigned— ключевые слова, поэтому их нельзя применять в
качестве имен переменных.
4
анные, переменные и вычисления
83
Булевский тип
Булевские переменные — это такие переменные, которые могут хранить только
два значения: true и false. Тип таких логических переменных называется bool, по
имени Джорджа Буля (Gerorge Boole), который разработал булеву алгебру. Тип bool
трактуется как целый. Булевские переменные также называют логическими перемен-
ными. Переменные типа bool используются для сохранения результата проверок, ко-
торые могут принимать значения либо “истина” (true), либо “ложь” (false), напри-
мер, как в случае проверки равенства одного значения другому.
Имя переменной типа bool объявляется с помощью следующего оператора:
bool testResult;
Конечно, вы можете сразу инициализировать переменные типа bool при их объ-
явлении:
bool colorlsRed = true;
Вы обнаружите, что достаточно часто в коде можно встретить значения TRUE и FALSE
как значения числовых типов — в частности, int. Это наследие тех времен, когда в C++ еще
не был реализован тип bool, и для представления логических значений обычно применялись
переменные типа int. При этом нулевые значения трактовались как “ложь ”, а ненулевые—
как “истина ”. Символы TRUE и FALSE все еще применяются в MFC, где они представляют
ненулевые целые и 0, соответственно. Обратите внимание, что TRUE и FALSE, записанные
заглавными буквами, не являются ключевыми словами в C++; это просто символы, опреде-
ленные внутри MFC. Помните также, что TRUE и FALSE не являются допустимыми значе-
ниями типа bool, а потому не путайте TRUE и true.
Типы с плавающей точкой
Числовые переменные, не относящиеся к целым, хранятся как числа с плавающей
точкой. Число с плавающей точкой может быть выражено в виде десятичного значе-
ния наподобие 112,5 либо в экспоненциальном виде, таком как 1Д25Е2, где десятич-
ная часть умножается на 10 в степени, указанной после Е (экспонента). Таким обра-
зом, последнее число— это 1,125х102, что равно 112,5.
Константы с плавающей точкой должны включать десятичную точку, либо экспоненту, либо
и то, и другое. Если вы записываете числовое значение без них, то получаете целое.
Вы можете специфицировать переменную с плавающей точкой, используя ключе-
вое слово double, как в следующем операторе:
double in_to_mm = 25.4;
Переменная типа double занимает 8 байт памяти и сохраняет значения, точность
которых определяется примерно 15 десятичными знаками. Диапазон их значений на-
много шире, чем можно выразить 15-ю знаками — начиная от 1,7x10-308 и заканчивая
1,7x1g308, положительные и отрицательные.
Если вам не требуется 15-значная точность и не нужны очень большие диапазоны
значений, которые обеспечивает тип double, можете воспользоваться ключевым сло-
вом float для объявления переменных с плавающей точкой, занимающих 4 байта.
84 Глава 2
Например:
float pi = 3.14159f;
Этот оператор определяет переменную pi с начальным значением 3,14159. Символ
f в конце константы указывает, что она имеет тип float. Без f константа имела бы
тип double. Переменные, объявленные с типом float, имеют точность примерно в
7 десятичных знаков и допускают значения от 3,4x10"38 до 3,4х1038, положительные и
отри цател ьные.
Стандарт C++ ISO/ANSI также определяет тип с плавающей точкой long double,
который в Visual C++ 2005 реализован с тем же диапазоном и точностью, что и
double.
Фундаментальные типы ISO/ANSI С++
В табл. 2.1 представлен список всех фундаментальных типов ISO/ANSI C++, а так-
же диапазоны их допустимых значений в Visual C++ 2005.
Таблица 2.1. Фундаментальные типы ISO/ANSI C++
Тип Размер в байтах Диапазон значений
Во°1 1 true или false
Char 1 По умолчанию — ТО же, что и signed char: от -128 ДО
+127. Необязательно вы можете задать для char тот же
диапазон, что и у типа unsigned char.
signed char 1 от-128 ДО+127
unsigned char 1 от 0 ДО 255
wchart 2 от 0 до 65535
short 2 от -32768 to +32767
unsigned short 2 от 0 ДО 65535
int 4 от-2147483648 до 2147483647
unsigned int
long 4
unsigned long 4
float 4
double 8
long double 8
от О до 4294967295
от -2147483648 до 2147483647
от 0 до 4294967295
± 3,4x10±38, примерно с 7-значной точностью
± IJxlO*308, примерно с 15-значной точностью
± 1,7x10±308, примерно с 15-значной точностью
Литералы
Я уже использовал в этой книге множество явных значений для инициализации
переменных. В C++ фиксированные значения любого рода называются литералами.
Литерал — это значение определенного типа, поэтому 23, 3.14159, 9.5f и true —
примеры литералов типа int, типа double, типа float и типа bool, соответственно.
Литерал "Samuel Beckett" — это пример строкового литерала, но обсуждение этого
типа мы отложим до главы 4. В табл. 2.2 представлены примеры записи литералов
различных типов.
Данные, переменные и вычисления 85
Таблица 2.2. Примеры литералов различных типов
Тип
Примеры литерало
char, signed char или unsigned char
wchar_t
a
unsigned int
long
unsigned long
float
double
long double
bool
'A', 'Z', '8',
L’A’, L’Z’, L'8’, L'*'
-77, 65, 12345, 0x9FE
10U, 64000U
-77L, 65L, 12345L
5UL, 999999999UL
3.14f, 34.506f
1.414,2.71828
1.414L, 2.71828L
true, false
Вы можете специфицировать литерал как относящийся к типу short или
unsigned short, но компилятор примет начальные значения, являющиеся литера-
лами типа int для переменных этого типа, если значение литерала находится в до-
пустимом диапазоне для типа переменной.
Часто вам нужно будет использовать литералы в процессе вычислений внутри про-
граммы, например, преобразуя значения вроде 12 футов в дюймы, или 25,4 дюймов
в миллиметры, или же для спецификации строк сообщений об ошибках. Однако вы
должны избегать явного применения литералов в программах, когда их смысл не оче-
виден. Вовсе не всем и не всегда ясно, что когда вы указываете значение 2,54, то это
означает число сантиметров в дюйме. Лучше объявить переменную с фиксированным
значением, равным литералу. Например, вы можете назвать ее inchesToCentimeters.
Тогда всякий раз, когда в коде встретится переменная inchesToCentimeters, ее
смысл будет достаточно очевиден. Чуть позже в этой главе вы увидите, как можно за-
фиксировать значение переменной.
Определение синонимов для типов данных
Ключевое слово typedef позволяет определить ваше собственное имя для существу-
ющего типа данных. Используя typedef, вы можете определить имя типа BigOnes как
эквивалент стандартного типа long int, применив следующее объявление:
typedef long int BigOnes; // Определение BigOnes как имени типа
Это определяет BigOnes как альтернативное имя для long int, поэтому вы сможе-
те объявить переменную mynum типа long int таким объявлением:
BigOnes mynum = OL; // определение переменной типа long int
При этом не будет никакой разницы между таким объявлением и тем, что исполь-
зует встроенное имя типа. То есть следующий оператор эквивалентен предыдущему:
Long int mynum = OL; // определение переменной типа long int
Фактически, если вы определяете ваше собственное имя типа, такое как BigOnes,
то можете применять оба спецификатора типа в одной и той же программе для объяв-
ления различных переменных, которые в итоге будут восприняты компилятором как
однотипные.
86 Глава 2
Поскольку typedef лишь определяет синоним для существующего типа, это может
показаться излишним, однако это вовсе не так. Позднее вы увидите, что это средство
играет очень важную роль в упрощении сложных объявлений за счет определения
одного имени, представляющего сложную спецификацию типа, что может сделать
ваш код более читабельным:
Переменные с определенным набором значений
Иногда у вас будет возникать потребность в переменных, которые могут прини-
мать значения лишь из ограниченного набора допустимых значений, ссылаться на ко-
торые удобно было бы по каким-то меткам — дни недели, например, или месяцы года.
В C++ предусмотрено специальное средство для таких ситуаций, называемое перечис-
лением (enumeration). Возьмем пример, который я упомянул — переменная, которая
должна принимать значения, соответствующие дням недели. Вы можете определить
ее следующим образом:
enum Week{Mon, Tues, Wed, Thurs, Fri, Sat, Sun} thisWeek;
Этот оператор объявляет тип перечисления по имени Week и переменную thisWeek,
являющуюся экземпляром перечислимого типа Week, которая может принимать толь-
ко константные значения, указанные между фигурными скобками. Если вы попытае-
тесь присвоить thisWeek что-то такое, что не входит в указанный набор, то это вызо-
вет ошибку. Эти символические имена, перечисленные между скобками, называются
перечислителями (enumerators). Фактически каждое из названий дней автоматиче-
ски представляется компилятором как фиксированное целое значение. Первое имя в
списке — Моп — получает значение 0, Tues будет 1 и так далее.
Вы можете присвоить одну из перечисленных констант в качестве значения пере-
менной thisWeek:
thisWeek = Thurs;
Обратите внимание, что вам не нужно квалифицировать перечислимые констан-
ты именем перечисления. Значение thisWeek будет равно 3, потому что определен-
ным в перечислении символическим константам присваиваются значения типа int
по умолчанию, начиная с 0.
По умолчанию каждый последующий перечислитель на единицу больше значения
предыдущего, но если вы предпочитаете явно указать начальное значение, отличное
от 0, вы можете просто написать так:
enum Week {Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun} thisWeek;
Теперь константы перечисления будут равны от 1 до 7. Перечислители даже не
обязаны иметь уникальные значения. Вы можете определить, что и Моп, и Tues долж-
ны, например, иметь значение 1, с помощью следующего оператора:
enum Week {Mon = 1, Tues = 1, Wed, Thurs, Fri, Sat, Sun} thisWeek;
Поскольку, по сути, типом переменной thisWeek является int, она займет 4 бай-
та, как и все переменные перечислимого типа.
Имея определение перечисления, вы можете определить другую переменную того
же типа:
enum Week nextWeek;
Это определяет переменную nextWeek как перечисление, которое может прини-
мать значения, специфицированные ранее. Вы даже можете пропустить ключевое
Данные, переменные и вычисления 87
слово enum в объявлении переменной, поэтому предыдущий оператор может быть
переписан так:
Week nextWeek;
При желании вы можете присвоить определенные значения всем перечислите-
лям. Например, можно определить следующее перечисление:
enum Punctuation {Comma = Exclamation = 1 ! ’, Question = '?'} things;
Здесь вы определяете возможные значения переменной things как числовые эк-
виваленты соответствующих символов. Если вы заглянете в таблицу ASCII в приложе-
нии Б, то увидите, что коды этих символов в десятичном виде равны, соответствен-
но, 44, 33 и 63. Как видите, значения перечислителей не обязательно должны идти в
возрастающем порядке. Если вы не специфицируете все значения явно, то каждому
перечислителю, значение которого не задано, будет присвоено значение на единицу
больше предыдущего, как в нашем втором примере Week.
Если нет необходимости объявлять позднее другие переменные перечислимого
типа, вы можете пропустить имя этого типа, например:
enum {Mon, Tues, Wed, Thurs, Fri, Sat, Sun} thisWeek, nextWeek, lastWeek;
Здесь объявлены три переменных, которые могут принимать значения от Моп
до Sun. Поскольку тип перечислителя не указан, вы не можете позднее сослаться не
него. Обратите внимание, что вообще вы не можете объявлять другие переменные
типа этого перечисления, потому что повторять это определение также не разреша-
ется. Попытка сделать это означала бы, что вы собираетесь переопределить значения
от Моп до Sun, а это недопустимо.
Спецификация типа для перечислимых констант
Перечислимые константы по умолчанию имеют тип int, но вы также можете ука-
зать их тип явно, добавив двоеточие и имя типа для констант вслед за именем типа
перечисления в его объявлении. Тип перечислимых констант может быть знаковым
либо беззнаковым целочисленным типом: short, int, long, char или же bool.
Таким образом, вы можете определить перечисление, представляющее дни неде-
ли, следующим образом:
enum Week: char {Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
Здесь константы перечисления будут относиться к типу char, причем первая из
них будет равна 0. Однако, имея константы типа char, вы, вероятно, предпочтете
инициализировать их явно:
enum Week : char{ Monday=*M’, Tuesday=’T’, Wednesday=*W’,
Thursday=’T*, Friday=’F’, Saturday=*S’, Sunday=’S’};
Теперь значения констант немного лучше отображают то, что они представляют,
хотя не делают различия между Thursday и Tuesday или между Saturday и Sunday.
Нет никаких проблем в том, что разные константы имеют одно и то же значение, но
имена, конечно, должны быть уникальными.
А вот пример перечисления с константами типа bool:
enum State : bool { On = true, Off};
Поскольку On имеет начальное значение true, то Off будет false. Если бы были
еще указаны последующие константы перечисления, они по умолчанию получили бы
чередующиеся значения.
88 Глава 2
Базовые операции ввода-вывода
Здесь вы только поверхностно ознакомитесь с вводом и выводом в “родном” C++ —
настолько, чтобы использовать его в примерах в процессе дальнейшего изучения
языка. Это совсем не трудно, даже наоборот. А при программировании для Windows
оно и вовсе не понадобится. Ввод и вывод C++ вращается вокруг понятия потоков
данных; данные можно вставлять в выходной поток и принимать из входного. Вы
уже видели, что стандартный выходной поток ISO/ANSI C++ в командную строку на
экране называется cout. Дополняющий его входной поток с клавиатуры известен под
именем с in.
Ввод с клавиатуры
Вы получаете ввод с клавиатуры через поток с in, используя для этого операцию
извлечения из потока ». Чтобы прочитать два целых значения с клавиатуры в пере-
менные numl и num2, можно написать следующий оператор:
cin » numl » num2;
Операция извлечения » “указывает” в направлении, куда передаются данные — в
данном случае, из cin в каждую из двух переменных по очереди. Любые ведущие про-
белы пропускаются, и первое целое значение, введенное с клавиатуры, поступает в
переменную numl. Так происходит потому, что оператор ввода выполняется слева на-
право. Любые пробелы, следующие за numl, игнорируются, и второе введенное целое
значение читается в num2. Между следующими друг за другом значениями должны
быть какие-нибудь пробельные символы, чтобы их можно было разделить. Операция
потокового ввода завершается, когда вы нажимаете клавишу <Enter>, и выполнение
программы продолжается со следующего оператора. Конечно, могут возникать ошиб-
ки, если вы введете некорректные данные, но я предполагаю, что вы всегда все дела-
ете правильно!
Значения с плавающей точкой читаются из клавиатурного ввода точно таким же
образом, как целые числа, и, конечно, вы можете смешивать их. Потоковый ввод и
его операции автоматически распознают переменные и данные любого фундамен-
тального типа. Например, в следующих операторах:
int numl = 0, num2 = 0;
double factor = 0.0;
cin » numl » factor » num2;
последняя строка читает целое значение в numl, затем значение с плавающей точкой
в factor и, наконец, еще одно целое в num2.
Вывод в командную строку
Вы уже видели в рассмотренных выше примерах вывод в командную строку, но я
все равно хочу вернуться к этой теме. Вывод информации на дисплей осуществляют-
ся способом, дополняющим ввод. Как вы уже видели, поток вывода называется cout,
и для передачи данных в него используется операция вставки «. Эта операция также
“указывает” в направлении движения данных. Вы уже применяли ее для вывода тек-
стовой строки, заключенной в кавычки. Я могу продемонстрировать процесс вывода
значения переменной на примере простой программы.
Данные, переменные н вычисления 89
Практическое занятие I
н ____________I Вывод в командную строку
Предположим, что вы уже создали новый пустой проект, добавили к нему новый
исходный файл и построили исполняемую программу. Ниже показан код, который
необходимо поместить в исходный файл после создания проекта Ех2_02.
// Ех2_02.срр
// Упражнение по выводу
#include <iostream>
using std::cout;
using std::endl;
int main()
{
int numl = 1234, num2 = 5678;
}
cout « cout « : endl; : numl < < num2; // Начать новую строку // Вывести две переменные
cout « : endl; // Завершить строку
return 0; // Завершить программу
Описание полученных результатов
Первый оператор в теле main () объявляет и инициализирует две целочисленных
переменных — numl и num2. За ним следуют два оператора вывода, первый из кото-
рых перемещает позицию экранного курсора на новую строку. Поскольку операторы
вывода выполняются слева направо, второй оператор отображает значение перемен-
ной numl, за которым следует значение num2.
Когда вы скомпилируете и запустите приведенный выше код, то получите на экра-
не следующий вывод:
12345678
Вывод правильный, но не слишком полезный. На самом деле вам нужно увидеть
два отдельных значения, разделенных хотя бы одним пробелом. По умолчанию вы-
ходной поток просто отображает десятичные цифры, представляющие значение, что
не предполагает какого-либо разделения выводимых значений пробелами. А потому у
вас нет никакой возможности сказать, где заканчивается одно значение и начинается
другое.
Форматирование вывода
Проблему, связанную с отсутствием пробелов между значениями, можно испра-
вить довольно просто — вставив пробел в поток вывода между двумя значениями. Это
можно сделать, заменив следующий оператор вашей оригинальной программы:
cout « numl « num2; // Вывести два значения
Просто подставьте вместо него оператор:
cout « numl « ' ' « num2; // Вывести два значения
Конечно, если у вас несколько строк вывода, и вы хотите выровнять колонки, то
вам понадобятся какие-то дополнительные возможности, поскольку вы не знаете,
сколько знаков будет в каждом значении. С этой ситуацией можно справиться, ис-
пользуя то, что называется манипуляторами. Манипулятор модифицирует способ
управления выводом данных в поток (или вводом из потока).
90 Глава 2
Манипуляторы определены в заголовочном файле <iomanip>, поэтому вам пона-
добится для него добавить директиву # include. Манипулятором, который вам не-
обходим, будет setw (п). Он выводит значение, следующее за ним, выравнивая его в
поле пробелов шириной п, то есть setw (6) представит следующее за ним значение
в поле шириной 6 пробелов. Посмотрим на это в действии.
Практическое занятие | ПрИМвНвНИв МаНИПуЛЯТОрОВ
Чтобы получить вывод более похожий на тот, что вам нужен, вы можете изменить
программу следующим образом:
// Ех2_03.срр
// Упражнения по выводу
#include <iostream>
#include <iomanip>
using std::cout;
using std::endl;
using std::setw;
int main ()
{
int numl = 1234, num2 = 5678;
cout « endl; // Начать новую строку
cout « setw (6) « numl « setw (6) « num2; // Вывести два значения
cout « endl; // Завершить строку
return 0; // Завершить программу
}
Описание полученных результатов
Среди изменений, внесенных в последнем примере — добавление директивы
#include для заголовка <iomanip>, добавление объявления using для имени setw из
пространства имен std и вставка манипулятора setw () в выходной поток перед вы-
водом значений каждой переменной, так что их значения выводятся в поля шириной
в шесть символов. В результате вы получите симпатичный четкий вывод, в котором
два значения разделены:
1234 5678
Обратите внимание, что манипулятор setw () работает только с единственным
выходным значением, которое следует непосредственно за его вставкой в поток. Вы
должны вставлять манипуляторы непосредственно перед каждым значением, которое
хотите выровнять в пределах поля определенной ширины. Если вы вставите только
один setw (), он воздействует лишь на первое значение, отправленное в выходной
поток вслед за ним. Любые последующие значения будут выведены в обычной мане-
ре. Можете убедиться в этом, исключив второй setw (6) и его операцию вставки из
последнего примера.
Управляющие последовательности
Когда вы пишете символьную строку, заключенную в двойные кавычки, то мо-
жете включить в нее специальные символы, называемые управляющими последо-
вательностями (escape sequences). Они так называются потому, что позволяют по-
местить в строку символы, которые не могут быть представлены иным образом, за
Данные, переменные и вычисления 91
счет того, что они избегают (escaping) обычного процесса интерпретации символов.
Управляющая последовательность начинается с символа обратного слеша \, который
заставляет компилятор интерпретировать следующий за ним символ особым обра-
зом. Например, символ табуляции записывается, как \t, так что t понимается компи-
лятором как табуляция в строке, а не буква ‘t’. Взгляните на следующие два оператора
вывода:
cout « endl « "Это вывод.";
cout « endl « "\ЪЭто вывод после табуляции.";
Они выведут на экран следующие строки:
Это вывод.
Это вывод после табуляции.
Комбинация \t во втором операторе вывода сдвигает следующий за ней текст в
первую позицию табуляции.
Фактически, вместо использования endl вы можете применять управляющую по-
следовательность символа новой строки \п в каждой строке, поэтому предыдущие
операторы можно переписать так:
cout « "\пЭто вывод.";
cout « "\n\t3TO вывод после табуляции.";
В табл. 2.3 даны некоторые управляющие последовательности, которые могут вам
пригодиться.
Таблица 2.3. Некоторые полезные управляющие последовательности
Управляющая последовательность
\п
\\
\Ь
\t
Что делает
Выдает звуковой сигнал
Символ новой строки
Одиночная кавычка
Обратный слеш
Забой
Символ табуляции
Двойная кавычка
Знак вопроса
Очевидно, что если вы хотите включить обратный слеш или двойную кавычку в
строку, вы должны использовать соответствующую управляющую последовательность,
чтобы представить их. В противном случае обратный слеш будет интерпретирован
как начало другой управляющей последовательности, а двойная кавычка — как конец
символьной строки.
Вы также можете применять символы, специфицированные управляющими после-
довательностями, в инициализации переменных типа char, например:
char Tab = *\t’; // Инициализировать символом табуляции
Поскольку символьный литерал ограничивается символами одиночной кавычки,
вы должны использовать управляющую последовательность, чтобы специфицировать
символьный литерал, представляющий саму одиночную кавычку, то есть ' \ .
92 Глава 2
Практическое занятие | ИСПОЛЬЗОВЭНИе уПрЭВЛЯЮЩИХ
последовательностей
Ниже приведен текст программы, использующей некоторые управляющие после-
довательности из предыдущей таблицы:
// Ех2_04.срр
// Применение управляющих последовательностей
tfinclude <iostream>
tfinclude <iomanip>
using std::cout;
int main()
{
char newline = '\n'; // Управляющая последовательность - символ новой строки
cout « newline; // Начать новую строку
cout « "\"We\'ll make our escapes in sequence\”, he said.’’;
cout « "\n\tThe program\’s over, it\'s time take make a beep beep.\a\a";
cout << newline; // Начать новую строку
return 0; // Выход из программы
}
Если вы скомпилируете и запустите эту программу, то увидите следующий вывод:
"We’ll make our escapes in sequence", he said.
The program’s over, it's time take make a beep beep.
Описание полученных результатов
Первая строка в main () определяет переменную newline и инициализирует ее
символом новой строки, представленным в виде управляющей последовательности.
Затем вы можете применять newline вместо endl из стандартной библиотеки.
После отправки newline на cout выводится строка, которая применяет управля-
ющие последовательности для представления символов двойной (\ ”) и одиночной
(\ •) кавычек. Вы не обязаны использовать управляющую последовательность для оди-
ночной кавычки, потому что строка ограничена двойными кавычками, и компилятор
воспринимает одиночную кавычку внутри нее так, как она есть, а не в качестве разде-
лителя. Однако внутри этой строки для представления двойной кавычки применять
управляющую последовательность необходимо. Строка начинается с управляющей
последовательности — символа новой строки, за которым идет управляющая последо-
вательность символа табуляции, поэтому выходная строка сдвигается на расстояние
табуляции вправо. Строка также заканчивается двумя экземплярами управляющих
последовательностей, выдающих звуковой сигнал, поэтому вы можете услышать два
подряд звуковых сигнала из динамика вашего компьютера.
Вычисления в C++
Здесь вы действительно начнете делать что-то с введенными данными. Теперь вы
знаете, как организовать простой ввод и вывод; теперь обратимся к тому, что посре-
дине — той части программы C++, которая занята обработкой данных. Все аспекты
C++, связанные с вычислениями, достаточно интуитивно понятны, так что изучение
этой темы пойдет у вас, как по маслу.
Данные, переменные н вычисления 93
1
Операторы присваивания
Вы уже видели примеры присваивающих операторов. Типичный такой оператор
выглядит следующим образом:
whole = parti + part2 + part3;
Оператор присваивания позволяет вычислить значение выражения, стоящего
справа от знака равенства — в данном случае сумму parti, part2 и part3 — и сохра-
нить результат в переменной, указанной слева от знака равенства — в данном случае,
переменной whole. В показанном операторе whole — просто сумма частей, и ничего
более.
Обратите внимание, что оператор, как всегда, завершается точкой с запятой.
Вы можете также писать повторяющиеся присваивания, как здесь:
а = Ь = 2;
Это эквивалентно присваиванию значения 2 переменной Ь с последующим при-
сваиванием значения Ь переменной а. В результате обе переменных получают значе-
ние 2.
Понятия lvalue и rvalue
lvalue (left value — левое значение) — это нечто такое, что ссылается на адрес памя-
ти и называется так потому, что любое выражение, дающее в результате lvalue, может
быть поставлено слева от знака равенства в операторе присваивания. Большинство
переменных являются lvalue, потому что они специфицируют место в памяти.
Однако, как вы вскоре убедитесь, существуют переменные, которые не являются lval-
ue, и не могут появляться слева в операторах присваивания, потому что их значения
определены как константные.
Переменные а и Ь, которые вы видели в предыдущем разделе, являются lvalue, в
то время как результат вычисления выражения а+Ь не может быть таковым, посколь-
ку для этого результата не определено место в памяти, где он должен быть сохранен.
Результат выражения, не являющегося lvalue, называют rvalue (right value — правое
значение).
Термин lvalue еще не раз появится в этой книге — иногда там, где вы менее всего будете его
ожидать, поэтому запомните его определение.
Арифметические операции
Базовые арифметические операции, которые предоставлены в ваше распоряже-
ние — сложение, вычитание, умножение и деление — обозначаются символами +, -, *
и / соответственно. В основном они работают так, как и можно было ожидать, за ис-
ключением деления, в поведении которого, как вы увидите, существуют некоторые
отклонения, когда оно применяется к целым или константам. Вы можете писать опе-
раторы вроде следующего:
netPay = hours * rate - deductions;
Здесь будет вычислено произведение hours и rate, затем из полученного ре-
зультата вычтено deductions. Операции умножения и деления выполняются перед
операциями сложения и вычитания, чего и следовало ожидать. Позднее в этой гла-
94 Глава 2
ве я расскажу подробнее о порядке выполнения различных операций в выражениях.
Общий результат вычисления выражения hours * rate — deductions помещается в
переменную netPay.
Знак минус, использованный в последнем операторе, работает с двумя операнда-
ми — он вычитает значение правого операнда из значения левого операнда. Он на-
зывается бинарной операцией, потому что тут вовлечены два значения. Знак минус
также может быть использован с одним операндом — при этом он меняет его знак на
противоположный. В этом случае его называют унарной операцией. Вы можете напи-
сать следующий код:
int а = 0;
int b = -5;
а = -Ь; // минус изменяет знак операнда
Здесь а будет присвоено значение +5, поскольку унарный минус изменил знак опе-
ранда Ь.
Обратите внимание, что присваивание не эквивалентно равенству, как принято в
алгебре. Оно специфицирует действие, которое должно быть выполнено, а не кон-
статацию факта. Выражение справа от оператора присваивания вычисляется, и ре-
зультат сохраняется в lvalue — обычно переменной, которая стоит слева.
Взгляните на приведенный ниже оператор:
number = number +1;
Это значит “прибавить единицу к текущему значению number и затем сохранить
результат обратно в number”. Если рассматривать это как алгебраическое выражени-
е, оно не имеет смысла.
Практическое занятие | УпраЖНвНИЯ В бЭЗОВОЙ арИфМвТИКв
Вы можете поупражняться в арифметике на C++, вычисляя, сколько стандартных
рулонов обоев понадобится, чтобы обклеить комнату. Следующий пример делает это.
// Ех2_05.срр
// Вычисление количества рулонов обоев, необходимых для того,
// чтобы обклеить комнату
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
int main()
{
double height = 0.0, width = 0.0, length =0.0; // Размеры комнаты
double perimeter = 0.0; // Периметр комнаты
const double rollwidth = 21.0; // Ширина стандартного рулона
const double rolllength = 12.0*33.0; // Длина стандартного рулона (33 фута)
int strips_per_roll =0; // Количество полос в рулоне
int strips_reqd =0; // Необходимое количество полос
int nrolls =0; // Общее число рулонов
cout « endl // Начать новую строку
« "Введите высоту комнаты в дюймах: ";
cin » height;
cout « endl / / Начать новую строку
« "Введите длину и ширину в дюймах: ";
cin » length » width;
95
данные, переменные и вычисления
strips_per_roll = rolllength / height; // Получить количество полос в рулоне
perimeter = 2.0*(length + width); // Вычислить периметр комнаты
strips_reqd = perimeter / roll width; // Получить необходимое количество полос
nrolls « strips_reqd / strips_per_roll; // Вычислить количество рулонов
cout « endl
« ’’Для оклейки вашей комнаты понадобится ” « nrolls « ” рулонов обоев. ”
« endl;
return 0;
Если только вы не более тренированы в печати на клавиатуре, чем я, то весьма
вероятно, что при первой попытке компиляции будут обнаружены некоторые опечат-
ки. Когда вы исправите их, программа должна компилироваться и работать успешно.
Возможно, вы получите пару предупреждающих сообщений от компилятора. Не бес-
покойтесь о них — компилятор просто дает вам возможность убедиться в том, что вы
понимаете то, что делаете. Причины сообщений об ошибках я объясню чуть позже.
Описание полученных результатов
Одну вещь хочу подчеркнуть с самого начала — я не несу ответственности за ваш
расход обоев, если при расчете вы воспользуетесь этой программой! Как вы увидите,
ошибки в расчете необходимого количества рулонов, которое выдаст эта программы,
происходят по причине того, как работает C++, и недостаток рулонов для оклейки
реальной комнаты может достичь более 50%!
Я разберу по косточкам все операторы этого примера, отмечая интересные, ори-
гинальные и даже захватывающие моменты. Операторы, следующие за началом тела
функции main (), для вас уже знакомая территория, и я буду исходить из этого.
Стоит отметить пару важных моментов, связанных с композицией программы. Во-
первых, операторы в теле main () выровнены так, чтобы их легче было прочесть, и,
во-вторых, различные группы операторов разделены пустыми строками, чтобы выде-
лить функциональные группы. Смещение вправо (indenting) операторов — это фунда-
ментальная техника организации программного кода C++. Вы увидите, что это приме-
няется повсеместно для обеспечения визуального выделения различных логических
блоков программы.
Модификатор const
В самом начале тела main () находится блок объявлений переменных, используе-
мых программой. Эти операторы также уже вам знакомы, но среди них есть два, ко-
торые включают в себя нечто новое:
const double rollwidth = 21.0; // Стандартная ширина рулона
const double rolllength = 12.0*33.0; // Стандартная длина рулона ( 33 фута)
Оба они начинаются с нового ключевого слова — const. Это модификатор типа,
указывающий, что переменная не только имеет тип double, но также является кон-
стантной. Поскольку вы однозначно сообщаете компилятору, что эти две перемен-
ные — константы, он может проверить любой оператор, который попытается из-
менить значения этих переменных, и если обнаружит такое, выдаст сообщение об
ошибке. Переменная, объявленная как const, не является lvalue, а потому не может
помещаться в левой части оператора присваивания.
Вы можете убедиться в этом, добавив в текст программы где-нибудь после объявле-
ния rollwidth оператор вроде такого:
rollwidth = 0;
96 Глава 2
После этого программа перестанет компилироваться, а будет выдана ошибка
’error С2166: 1-value specifies const object’ (ошибка C2166: lvalue специфи-
цирует константный объект).
Иногда бывает очень удобно определять константы, используемые в программе,
снабжая модификатором const типы переменных, в частности, когда вы используе-
те некоторые константы в программе несколько раз. Во-первых, это гораздо лучше,
чем разбрасывать по всей программе литералы, назначение которых не очевидно.
Например, значение 4 2 в программе может означать все что угодно, но если вы ис-
пользуете константную переменную по имени myAge, имеющую значение 42, то ее
назначение сразу становится совершенно очевидным. Кроме того, если вам понадо-
бится изменить значение применяемой константной переменной, то вам придется
сделать это один-единственный раз, в одном исходном файле, дабы гарантировать,
что это изменение автоматически появится везде, где упомянутая переменная исполь-
зуется. Эту технику вы будете использовать очень часто.
Константные выражения
Константная переменная roll length также инициализируется арифметическим
выражением (12.0*33.0). Возможность использования константных выражений для
инициализации переменных избавляет вас от необходимости вычислять их вручную
при написании программы. К тому же такие выражения могут оказаться более ин-
формативными, потому что, например, 33 фута по 12 дюймов каждый более ясно
указывает на смысл величины, чем если просто написать 396. Обычно компилятор
точно вычисляет константные выражения, в то время как если вы станете делать это
вручную, то в зависимости от сложности выражения и ваших способностей к вычис-
лениям, появляется вероятность того, что оно будет вычислено неверно.
Вы можете использовать любое выражение, которое в результате вычисления даст
константное значение во время компиляции, включая константные объекты, кото-
рые уже определены выше. Поэтому, например, если это пригодится в программе, вы
можете объявить площадь стандартного рулона обоев так:
const double rollarea = rollwidth*rolllength;
Этот оператор должен быть помещен после объявления двух переменных const,
использованных при инициализации rollarea, потому что все переменные, которые
появляются в константном выражении, должны быть известны компилятору в той
точке исходного файла, где появляется константное выражение.
Ввод программы
После объявления некоторых целочисленных переменных следующие четыре опе-
ратора программы обрабатывают ввод с клавиатуры:
cout « endl
Введите
// Начать новую строку
высоту комнаты в дюймах:
cin » height;
cout « endl
"Введите
length
// Начать новую строку
длину и ширину в дюймах:
width;
Здесь выводится текст в cout, приглашающий пользователя ввести необходимую
информацию с клавиатуры, используя cin, который представляет собой стандартный
входной поток. Сначала вы получаете значение height, а затем последовательно чи-
таете length и width. В реальной программе вам понадобилось бы проверить введен-
97
данные, переменные и вычисления
ные данные на предмет возможных ошибок и убедиться, что прочитанные значения
имеют смысл, но пока у вас недостаточно знаний для этого!
Вычисление результата
В рассматриваемой программе присутствует четыре оператора, участвующих в вы-
числении количества стандартных рулонов обоев, необходимых для оклейки комнаты:
strips_per__roll = rolllength / height;
perimeter = 2.0*(length + width);
// Получить количество полос в рулоне
// Вычислить периметр комнаты
strips_reqd = perimeter / rollwidth; // Получить необходимое количество полос
nrolls = strips_reqd / strips_per_roll; // Вычислить количество рулонов
Первый оператор вычисляет количество полос обоев длиной, равной высоте ком-
наты, которые получаются при разрезке одного стандартного рулона, разделив длину
рулона на высоту комнаты. То есть, если комната имеет высоту 8 футов, то вы делите
96 на 396, что должно дать результат с плавающей точкой, равный 4,125. Но здесь име-
ется одна тонкость. Переменная, куда вы помещаете результат — strips_per_roll —
была объявлена как int, поэтому она может хранить только целые значения. Как
следствие, попытка сохранить любое значение с плавающей точкой как целое приво-
дит к округлению к ближайшему меньшему целому — в данном случае, к 4 — и это зна-
чение сохраняется. В общем, это тот результат, который вам нужен, поскольку хотя
они и могут подойти для оклейки стены под окном или над дверью, дробные части
полос обоев лучше проигнорировать при расчете потребности в рулонах.
Преобразование значения одного типа к другому называется приведением
(casting). Этот конкретный случай является примером неявного приведения (implicit
cast), потому что в коде явно не указано, что приведение необходимо, и компилятор
должен делать это самостоятельно. Два предупреждения, которые вы получите во
время компиляции, касаются именно неявного приведения, которое может привести
к утере части информации из-за преобразования одного типа в другой, менее точ-
ный.
Вы должны быть очень осторожны, применяя неявные приведения. Компиляторы
не всегда выдают предупреждения о выполнении таких приведений, и если вы при-
сваиваете значение одного типа переменной, имеющей тип с меньшим диапазоном
допустимых значений, то в таких случаях всегда существует риск потери информа-
ции. Если в вашей программе присутствуют неявные приведения, которые вы вклю-
чили непреднамеренно, они могут стать причиной ошибок, которые трудно обнару-
жить.
Но поскольку такие присваивания неизбежны, вы можете специфицировать при-
ведение явно, чтобы продемонстрировать компилятору, что здесь нет ничего случай-
ного, и это именно то, что вы намеревались сделать. Делается это посредством явно-
го приведения значения в правой части присваивания к типу int, то есть оператор
становится таким:
strips_per_roll = static_cats<int>(rolllength / height);
// Получить количество полос в рулоне
Добавка static_cats<int> со скобками вокруг выражения в правой части явно
сообщает компилятору, что вы хотите преобразовать значение выражения в тип int.
Хотя это значит, что вы по-прежнему теряете дробную часть значения, компилятор
предполагает, что вы знаете, что делаете, и на этот раз не будет выдавать предупреж-
дений. Позднее в этой главе вы узнаете больше о static_cats<int> () и других ти-
пах явного приведения типов.
98 Глава 2
Обратите внимание на то, как вычисляется периметр комнаты в следующем опе-
раторе. Чтобы умножить сумму length и width на два, выражение сложения заклю-
чается в скобки. Это гарантирует, что сложение будет выполнено первым, а результат
будет умножен на 2.0, чтобы получить правильное значение периметра. С помощью
скобок вы можете гарантировать, что вычисление будет выполнено именно в том по-
рядке, в каком нужно, потому что выражения в скобках всегда выполняются первыми.
Если есть несколько вложенных друг в друга выражений со скобками, то эти выраже-
ния вычисляются последовательно — от внутренних скобок к внешним.
Третий оператор, вычисляющий количество полос обоев, необходимых для оклей-
ки комнаты, использует тот же эффект, что вы видели в первом операторе. Результат
округляется в меныпую сторону до ближайшего целого, поскольку он должен быть
присвоен целочисленной переменной strips_reqd. Но это не то, что вам нужно на
самом деле. Было бы лучше округлить в сторону большего, но пока у вас нет достаточ-
ных знаний о C++, чтобы сделать это. Прочитав следующую главу, вы сможете вер-
нуться и внести соответствующие исправления.
Последнее арифметическое выражение вычисляет количество необходимых руло-
нов, разделив количество полос (как целое) на количество полос в рулоне (тоже как
целое). Поскольку вы делите одно целое на другое целое, результат также будет це-
лым числом, и любой остаток игнорируется. То же самое случится, даже если nrolls
будет переменной с плавающей точкой. Целое значение, полученное от выражения
деления, будет преобразовано в значение с плавающей точкой и сохранено в nrolls.
Полученный результат, по сути, будет тем же, как если бы вы получили значение с
плавающей точкой и округлили его в сторону ближайшего меньшего целого. Опять-
таки, это не то, что вам нужно, поэтому если вы хотите использовать эту программу,
ее придется откорректировать.
Отображение результата
Результат вычисления отображается с помощью следующего оператора:
cout « endl
« "Для оклейки вашей комнаты понадобится ” « nrolls « " рулонов обоев.”
« endl;
Это один оператор вывода, разнесенный на три строки кода. Сначала оно вы-
водит символ новой строки, затем текстовую строку "Для оклейки вашей комнаты
понадобится ". После этого следует значение переменной nrolls, за которым — еще
одна текстовая строка " рулонов обоев. ". Как видите, операторы вывода на C++ пи-
сать очень легко.
Программа завершается следующим оператором:
return 0;
Здесь 0 — это возвращаемое значение, которое в данном случае передается опера-
ционной системе. Подробнее о возвращаемых значениях вы узнаете в главе 5.
Вычисление остатка
Вы видели в последнем примере, что деление одного целого значения на другое
дает целое с игнорирированием остатка, поэтому если 11 разделить на 4, то в резуль-
тате получится 2. Поскольку остаток от деления может представлять значительный
интерес, например, когда вы делите печенье между детьми, C++ предусматривает для
этого специальную операцию — %. То есть, проблему деления печений можно решить,
написав следующие операторы:
99
данные, переменные и вычисления
int residue = 0, cookies = 19, children = 5;
residue = cookies % children;
Переменная residue получит значение 4 — число, оставшееся после деления 19 на 5.
Чтобы вычислить, сколько печенья получит каждый ребенок, вы просто используете
деление:
each = cookies / children;
Модификация переменной
Часто возникает необходимость модифицировать существующее значение пере-
менной, например, увеличивая или удваивая его. Увеличит значение переменной по
имени count можно с помощью следующего оператора:
count = count +5;
Здесь просто к текущему значению count прибавляется 5 и полученный результат
помещается обратно в count, поэтому, если сначала count было равно 10, то после
этого оно будет равно 15.
В C++ предусмотрен альтернативный сокращенный синтаксис для записи того же
выражения:
count += 5;
Это значит “взять значение count, добавить к нему 5 и сохранить полученный ре-
зультат в count”. Можно также использовать и другие операторы в подобной нота-
ции. Например:
count *= 5;
дает эффект, выражающийся в умножении текущего значения count на 5 и сохране-
нии результата в count. В общем случае, вы можете писать операторы в форме:
Ihs ор= rhs;
где ор — любая из следующих операций:
Вы уже встречались с первыми пятью из этих операций, а с остальными (опера-
циями сдвига и логическими) вы ознакомитесь далее в этой главе. Ihs представляет
любое корректное выражение, которое можно поместить в левой части присваива-
ния, и обычно (но не обязательно) это имя переменной, rhs — это любое корректное
выражение, которое можно поместить в правой части оператора.
Общая форма этого оператора эквивалентна следующей:
Ihs = Ihs op (rhs);
Скобки вокруг rhs указывают на то, что это выражение вычисляется первым, а за-
тем результат становится правым операндом операции ор.
Это значит, что вы можете писать такие операторы, как:
а /= b + с;
и это даст тот же результат, что и:
а = а/ (Ь + с);
Таким образом, значение а будет разделено на сумму b и с, а результат присвоен а.
100 Глава 2
Операции инкремента и декремента
Теперь я представлю несколько необычные арифметические операции, называе-
мые операциями инкремента и декремента, и уверен, что вы сочтете их весьма по-
лезными по мере серьезного применения C++. Это унарные операции, которые слу-
жат для увеличения или уменьшения значения, хранимого в переменной целого типа.
Например, если предположить, что переменная count имеет тип int, то следующие
три оператора дадут один и тот же эффект:
count = count + 1; count += 1; ++count;
Каждый из них увеличивает переменную count на 1. Последняя форма, использу-
ющая операцию инкремента — вне всяких сомнений, самая краткая.
Операция инкремента не только изменяет значение переменной, к которой он
применен, но также сама возвращает значение. Таким образом, используя операцию
инкремента для увеличения значения переменной, ее также можно сделать частью
более сложного выражения. Если увеличивать переменную, используя операцию ++,
как в ++count, в составе другого выражения, то действие операции будет заключать-
ся в том, что сначала увеличится значение переменной, а затем это новое значение бу-
дет использовано в выражении. Например, предположим, что count имеет значение
5, и вы определили переменную total типа int. Предположим, что вы записываете
следующий оператор:
total = ++count +6;
В результате это увеличит count до 6, и к этому значению будет прибавлено 6, по-
этому total получит значение 12.
До сих пор мы помещали операцию инкремента ++ перед переменной, к которой
она применялся. Это называется префиксной формой операции инкремента. Однако
эта операция также имеет постфиксную форму, когда операция ставится после пере-
менной, к которой она применяется. В результате получается несколько другой эффект.
Переменная, к которой применена операция, увеличивается только после того, как ее
значение будет использовано в объемлющем контексте. Например, сбросьте значение
count снова до 5 и перепишите предыдущий оператор следующим образом:
total = count++ + 6;
В результате total получит значение 11, потому что начальное значение count
используется для вычисления всего выражения до того, как count увеличится на 1
операцией инкремента. Этот оператор эквивалентен следующим двум:
total = count +6;
++count;
Но когда у вас есть такое выражение, как а++ +Ь, или даже а+++Ь, то не совсем
очевидно, что это должно означать, и что будет делать компилятор. На самом деле
это одно и то же, но во втором случае вы на самом деле могли иметь в виду а + ++Ь,
что не одно и то же.
Точно те же правила, о которых я рассказал относительно операции инкремента,
применимы к операции декремента —. Например, если count имеет начальное зна-
чение 5, то оператор:
total = —count +6;
в результате присвоит total значение 10, в то время как:
total = 6 + count—;
Данные, переменные и вычисления 101
установит в total значение 11. Обе операции обычно применяются к целым, в част-
ности, в контексте циклов, как будет показано в главе 3. Позже в других главах вы
узнаете, что они также могут применяться и к другим типам данных C++, например, к
переменным, хранящим адреса памяти.
Практическое занятие | 0ПерЭЦИЯ ЗЭПЯТОЙ
Операция запятой (,) позволяет специфицировать несколько выражений там, где
обычно может присутствовать только одно. Лучше всего это пояснить на примере,
который демонстрирует, как это работает.
// Ех2_06.срр
// Упражнение с операцией запятой
#include <iostream>
using std::cout;
using std::endl;
int main ()
{
long numl = 0, num2 = 0, num3 = 0, num4 = 0;
num4 = (numl = 10, num2 = 20, num3 = 30);
cout << endl
« "Значением серии выражений "
« "является значение самого правого из них: "
« num4;
cout « endl;
return 0;
}
Описание полученных результатов
Если вы скомпилируете и запустите эту программу, то получите следующий вы-
вод:
Значением серии выражений является значение самого правого из них: 30
Это не требует пояснений. Переменная num4 принимает значение последнего из
трех присваиваний, а присваиваемым значением является значение, которое полу-
чает его левая часть. Скобки в присваивании num4 важны. Вы можете попробовать
выполнить этот пример без них, чтобы увидеть, что получится. Без скобок первое
выражение, отделенное запятыми, станет таким:
num4 = numl = 10
То есть, num4 будет присвоено значение 10.
Конечно, выражения, разделенные операцией запятой, не должны быть присваи-
ваниями. Вы могли бы с таким же успехом записать следующие операторы:
long numl = 1, num2 = 10, num3 = 100, num4 = 0;
num4 = (++numl, ++num2, ++num3);
Эффект от этого оператора присваивания будет выражен в увеличении значений
переменных numl, num2 и num3 на 1, а также в присваивании num4 значения послед-
него выражения, которое в данном случае будет равно 101. Этот пример нацелен на
то, чтобы проиллюстрировать эффект от операции запятой, но не является приме-
ром того, как следует писать хороший код.
102 Глава 2
Последовательность вычислений
До сих пор я не говорил о том, как повлиять на последовательность вычислений,
участвующих в выражении. В основном это согласуется с тем, что вы изучали в шко-
ле, когда знакомились с основными арифметическими операциями, но в C++ присут-
ствует множество других операций. Чтобы понять, что произойдет с ними, нужно
рассмотреть механизм, используемый C++ для определения этой последовательности.
Это то, что называется приоритетом операции.
Порядок выполнения операций
Порядок выполнения операций упорядочивает операции в порядке приоритетов.
В любом выражении операции с более высоким приоритетом всегда выполняются
первыми, за ними выполняются операции со следующим по возрастанию приорите-
том и так далее, вплоть до тех, чей приоритет самый низкий. Порядок выполнения
операций C++ представлен в табл. 2.4.
Таблица 2.4. Порядок выполнения операций C++
Операции
Ассоциативность
Левая
Левая
! - +(унарная) -(унарная) ++ — &(унарная) *(унарная)
(приведение типа) static cast const_cast dynamic_cast
reinterpret_cast sizeof new delete...typeid
. *(унарная) ->*
Правая
Левая
Левая
Левая
Левая
Левая
Левая
Левая
Левая
Левая
& &
Левая
Левая
?: (условная операция)
Правая
Правая
Левая
Здесь приведено множество операций, с которыми вы пока не знакомы, но до кон-
ца книги вы узнаете их все. Вместо того чтобы разбрасывать их по главам, я собрал
все операторы C++ в одну таблицу порядка выполнения, чтобы вы всегда могли об-
ратиться к ней, когда не уверены в том, как соотносится приоритет одной операции
с приоритетом другой.
Данные, переменные и вычисления 103
Операции с наивысшим приоритетом находятся в верхней части таблицы. Все
операции, которые указаны в одной ячейке таблицы, имеют одинаковый приори-
тет. Если в выражении нет скобок, операции с равным приоритетом выполняются
в последовательности, определяемой их ассоциативностью. То есть, если ассоци-
ативность “левая”, то самая левая операция в выражении выполняется первой, за-
тем последовательно выполняются операции всего выражения — слева направо. Это
значит, что выражение вроде а + b + с + d выполняется, как записано, то есть как
(((а + Ь) + с) +d), потому что бинарная операция + имеет левую ассоциативность.
Обратите внимание, что когда операция имеет и унарную (работающую с одним
операндом), и бинарную (с двумя операндами) формы, то унарная форма всегда име-
ет более высокий приоритет, а потому выполняется первой.
Вы всегда можете изменить приоритеты операций в выражении с помощью скобок. Пос*
кольку в C++ очень много операций, иногда бывает затруднительно понять, каким должен
порядок вычисления сложного выражения. Поэтому хорошей идеей будет применять скоб*
ки, чтобы обрести уверенность. Дополнительная выгода от них проявляется в том, что это
часто облегчает чтение кода.
Типы переменных и приведения
Вычисления в C++ могут выполняться только между однотипными значениями.
Когда вы пишете выражение, включающее переменные или константы разных типов,
то для каждой выполняемой операции компилятор должен выполнить преобразова-
ние типа одного операнда к типу другого. Процесс преобразования типов называется
приведением. Например, если вы хотите прибавить значение типа double к значе-
нию целочисленного типа, то целочисленное значение сначала преобразовывается в
double, после чего выполняется сложение. Конечно, сама переменная, содержащая
исходное значение, не изменяется. Компилятор сохраняет преобразованное значе-
ние во временной памяти, которая теряется по завершении вычислений.
Выбор операнда, подлежащего преобразованию в любой операции, регулируется
строгими правилами. Любое выражение, которое должно быть вычислено, разбива-
ется на серии операций с двумя операндами. Например, выражение 2 * 3 - 4 + 5
состоит из следующих серий: 2 * 3 в результате дает 6, затем 6-4, которая в ре-
зультате дает 2 и, наконец, 2 + 5, результат которого — 7. Таким образом, правила
приведения операндов необходимы только для принятия решений относительно пар
операндов. Поэтому для любой пары операндов разного типа проверяются описан-
ные ниже правила, в том порядке, как они записаны. Если правило применимо к кон-
кретной паре, оно используется.
Правила приведения операндов
1. Если любой из операндов имеет тип long double, то второй преобразуется в
тип long double.
2. Если любой из операндов имеет тип double, то второй преобразуется в тип
double.
3. Если любой из операндов имеет тип float, то второй преобразуется в тип
float.
104 Глава 2
4. Любой операнд типа char, signed char, unsigned char, short или unsigned
short преобразуется в тип int.
5. Перечислимый тип преобразуется сначала в int, unsigned int, long или
unsigned long, в зависимости от того, какого из них достаточно, чтобы вме-
стить диапазон перечислителей.
6. Если один из операндов типа unsigned long, то другой преобразуется в
unsigned long.
7. Если один из операндов типа long, а другой — типа unsigned int, то оба опе-
ранда преобразуются к типу unsigned long.
8. Если любой из операндов типа long, то второй преобразуется в тип long.
Это выглядит неимоверно сложным, но базовый принцип прост: всегда преобразу-
ется значение того типа, которые имеет более ограниченный диапазон, к типу второ-
го значения. Это увеличивает вероятность получения правильного результата. Чтобы
увидеть, как работают эти правила, вы можете попробовать их на примере гипоте-
тического выражения. Предположим, что у вас есть следующая последовательность
объявлений переменных:
double value = 31.0;
int count = 16;
float many = 2.Of;
char num =4;
Также предположим, что у вас есть следующее, достаточно произвольное арифме-
тическое выражение:
value = (value - count)*(count - num)/many + num/many;
Теперь вы можете попробовать предположить, какие приведения выполнит ком-
пилятор при выполнении этого оператора.
Первая операция, которую нужно вычислить — это (value — count). Правило 1
здесь не применимо, зато подходит правило 2, поэтому значение count преобразует-
ся в double, и в результате получается значение 15.0 типа double.
Следующим должно выполниться (count — num), и здесь первое подходящее пра-
вило из последовательности — это правило 4, поэтому num преобразуется из char в
int, и получается результат 12 типа int.
Следующий шаг вычисления — перемножение двух первых результатов — double,
равное 15.0, и int, равное 12. Здесь применимо правило 2, и 12 преобразуется в
12.0 с типом double, в результате получается значение double, равное 180.0.
Полученный результат теперь должен быть разделен на many, поэтому опять при-
меняется правило 2, и значение many преобразуется в double перед генерацией
double результата 90.0.
Следующим вычисляется выражение num/many, и здесь применяется правило 3,
чтобы получить значение float, равное 2 . Of, после преобразования типа num из
char в float.
В конце к double-значению 90.0 прибавляется float-значение 2 . Of, для чего
применяется правило 2, которое требует преобразования 2 . Of в double-значение
2.0, и окончательный результат 92.0 присваивается value.
Хотя чтение этой последовательности несколько напоминает “песню аукционис-
та”, основную идею вы поняли.
Данные, переменные и вычисления 105
Приведения в операторах присваивания
Как вы видели ранее в этой главе на примере Ех2_05. срр, вы можете вызвать не-
явное приведение, записав справа от оператора присваивания выражение, тип кото-
рого отличается от типа переменной, находящейся слева от него. Это может изме-
нить значение, и информация будет утеряна. Например, если вы присвоите float
или double переменной типа int или long, то дробная часть float или double будет
потеряна, а сохранена только целая часть. (Вы можете потерять даже больше инфор-
мации, если ваша переменная с плавающей точкой содержит значение, выходящее за
диапазон допустимых значений целочисленных типов).
Например, после выполнения следующего фрагмента кода:
int number =0;
float decimal = 2.5f;
number = decimal;
значение number будет равно 2. Обратите внимание на f в конце константы 2.5f.
Это указывает компилятору, что это константа с плавающей точкой одинарной точ-
ности. Без f по умолчанию она бы имела тип double. Любая константа, содержащая
десятичную точку, является значением с плавающей точкой. Если вам не нужно, что-
бы она имела двойную точность, добавляйте к ней f. Заглавная F тоже подходит.
Явные приведения
Когда смешанные выражения включают базовые типы, то компилятор при необхо-
димости выполняет нужные приведения, но вы также можете принудительно указать
приведение одного типа к другому, используя явные приведения. Чтобы привести
значение выражения к определенному тип); необходимо написать так:
static_cast<тип_к_которому_привести> (выражение)
Ключевое слово static_cast отражает тот факт, что приведение выполняется
статически — то есть, когда программа компилируется. Никаких дальнейших прове-
рок безопасности приведения во время выполнения программы не осуществляется.
Позднее, когда вы будете иметь дело с классами, вы познакомитесь с dynamic_cast —
когда преобразование проверяется динамически, то есть, во время выполнения
программы. Есть еще два других вида приведений: const_cast — для исключения
константности выражения и reinterpret_cast, которое означает безусловное при-
ведение, но о них я пока не буду говорить.
Эффект операции static_cast заключается в преобразовании значения-ре-
зультата вычисления выражения к типу, который указан между угловыми скобками.
Выражение может любым — от отдельной переменной, до сложнейшего составного,
содержащего множество вложенных скобок.
Вот специфический пример применения static_cast<> ():
double valuel = 10.5;
double value2 = 15.5;
int whole_number = static_cast<int>(valuel) + static_cast<int>(value2);
Инициализирующим выражением для whole_number является сумма целых частей
valuel и value2, в которых остается 10.5 и 15.5 соответственно. Значения 10 и 15,
порожденные приведением, сохраняются лишь временно, для использования в вы-
числении суммы, а затем теряются. Хотя оба приведения приводят при вычислении
к потере информации, компилятор предполагает, что вы знаете, что делаете, приме-
няя явное приведение.
106 Глава 2
Кроме того, как я писал в Ех2_05. срр относительно присваивания с участием раз-
ных типов, вы всегда можете прояснить код, сделав приведение явным:
strips_per_roll = static_cast<int>(rolllength / height);
// Получить количество полос в рулоне
Вы можете применять явные приведения с числовыми значениями любых типов,
но должны осознавать возможность потери информации. Если вы приводите значе-
ние типа float или double к типу long, например, то при преобразовании теряете
дробную часть, поэтому если значение было меньше 1.0, результатом будет 0. Если
же вы приводите значение типа double к типу float, то теряете точность, потому
что float имеет лишь 7-значную точность, в то время как double — 15-значную. Даже
приведение между целочисленными значениями может привести к потенциальной
потере информации, в зависимости от конкретных значений, участвующих в нем.
Например, значение целого типа long может выйти за пределы допустимых для типа
short, поэтому приведение значения long к типу short может привести к потере
информации.
Вообще лучше избегать приведений типов, насколько это возможно. Если вы об-
наруживаете, что ваша программа нуждается в большом количестве приведений, ве-
роятно, это следствие неудачного общего дизайна, и его лучше пересмотреть. Вам
нужно проверить структуру программы и применяемые типы данных, чтобы найти
способ исключить или хотя бы сократить случаи применения приведений.
Приведения в старом стиле
До того, как в C++ появилась операция приведения static catso () (а также
другие операции приведения: const_cast<> (), dynamic_cast<> () и reinterpret_
casto (),o которых мы поговорим позднее), явное приведение результата выраже-
ния к другому типу записывалось следующим образом:
(тип_к_которому_привести) выражение
Здесь результат выражения приводится к типу, указанному в скобках. Например,
оператор вычисления strips_per_roll из предыдущего примера мог быть записан
так:
strips_per_roll - (int)(rolllength / height);
// Получить количество полос в рулоне
По сути, существуют четыре разных вида приведений, и синтаксис приведений
старого стиля покрывал все. По этой причине код, использующий приведения в ста-
ром стиле, более подвержен ошибкам — компилятору не всегда были ясны ваши наме-
рения, и вы не получали ожидаемый результат. Хотя вы встретите в унаследованном
коде интенсивное использование приведений старого стиля (это все еще часть язы-
ка, и их много в коде MFC — по причинам исторического характера), я настоятельно
рекомендую в новом коде применять только новые приведения.
Битовые операции
Битовые операции трактуют свои операнды как последовательности индивидуаль-
ных битов, а не числовые значения. Они работают только с целочисленными пере-
менными или целыми константами в качестве операндов, поэтому с ними могут ис-
пользоваться только типы данных short, int, long, signed char и char, а также их
беззнаковые варианты. Битовые операции удобны для программирования аппаратных
Данные, переменные и вычисления 107
устройств, где состояние устройства часто представляется серией индивидуальных
флагов (то есть, каждый бит может описывать состояние определенного аспекта дан-
ного устройства), или же их можно применять в ситуациях, когда желательно упако-
вать набор флагов “включено/выключено” в единую переменную. Вы увидите их в
действии, когда будете подробно изучать средства ввода-вывода, где отдельные биты
служат для управления различными опциями управления потоками данных.
Существует шесть битовых операций.
& битовое И | битовое ИЛИ л битовое исключающее ИЛИ
~ битовое НЕ » сдвиг вправо « сдвиг влево
В следующем разделе мы посмотрим, как работает каждая из них.
Битовое И
Битовое И (&) — это бинарная операция, которая комбинирует соответствующие
биты своих операндов определенным образом. Если в обоих операндах соответству-
ющие биты равны 1, то и в результате будет 1, если же оба или один из битов операн-
дов равен 0, то и в результате соответствующий бит будет равен 0.
1 Эффект применения конкретной битовой операции часто показывают с помощью
таблицы истинности. В ней показаны все возможные комбинации битов операндов
и соответствующий результирующий бит, полученный в результате применения опе-
ратора. Таблица истинности для & представлена в табл. 2.5.
Таблица 2.5. Таблица истинности для операции битового И
Битовое И 0
о
Для каждой комбинации строки и столбца результат применения & находится на
их пересечении. Как это работает, можете увидеть на примере.
char letterl = 'A', letter2 = ' Z', result = 0;
result = letterl & letter2;
Для того чтобы понять, что произойдет, нужно взглянуть на битовые шаблоны.
Буквы ‘А’ и ‘Z* соответствуют шестнадцатеричным значениями 0x41 и 0х5А, соответ-
ственно (см. в приложении Б таблицу кодов ASCII). Работа операции битового И с
этими двумя значениями показана на рис. 2.8.
letterl: 0x41
Ietter2:0х5А
result: 0x40
Puc. 2.8. Работа операции битового И
108 Глава 2
Вы можете проверить это, пользуясь таблицей истинности операции &, которая
была приведена выше. После присваивания result будет иметь значение 0x40, что
соответствует символу ‘@’.
Поскольку & дает 0, если любой из соответствующих битов операндов равен 0, вы
можете применять его в тех случаях, когда в переменной определенные биты должны
были установлены в 0, независимо от их исходного значения. Это достигается соз-
данием так называемой “маски” и комбинацией ее с исходной переменной с помо-
щью &. Вы создаете маску, специфицируя значение 1 в тех битах, в которых старое
значение нужно сохранить, и 0 — в тех битах, значение которых нужно сбросить в 0.
Результатом применения И к маске и другому целому будет бит 0 в тех позициях, где
стоит 0 в маске, и неизменное значение бита исходной переменной там, где в маске
стоит 1. Предположим, что имеется переменная letter типа char, где в целях демон-
страции вы хотите сбросить в 0 старшие 4 бита. Это очень просто достигается уста-
новкой значения маски 0x0F и комбинацией ее со значением letter посредством &:
letter = letter & OxOF;
или, более коротко:
letter &= OxOF;
Если изначально значение letter равно 0x41, то в результате любого из этих двух
операторов оно станет равным 0x01. Эта операция проиллюстрирована на рис. 2.9.
letter: 0x41 0 1 0 0 0 0 0 1
&&&& & & & &
mask: OxOF 0 0 0 0 1 1 1 1
result: 0x01 0000 0001
Рис. 2.9. Применение маски к значению переменной
Нулевые биты маски сбрасывают в ноль соответствующие биты letter, а биты
маски, равные 1, оставляют соответствующие биты letter неизмененными.
Аналогично вы можете использовать маску 0xF0, чтобы сохранить 4 старших бита
и сбросить в ноль 4 младших.
Таким образом, следующий оператор:
letter &= 0xF0;
даст в результате изменение значения letter с 0x41 на 0x40.
Битовое ИЛИ
Битовое ИЛИ (|), иногда называемое включающим ИЛИ, комбинирует соответ-
ствующие биты операндов так, что результат равен 1, если хотя бы один из битов
операндов в данной позиции равен 1, и 0, если оба бита равны 0. Таблица истинности
для битового ИЛИ показана в табл. 2.6.
Данные, переменные и вычисления 109
Таблица 2.6. Таблица истинности для операции битового ИЛИ
Битовое ИЛИ
Вы можете проверить это на примере установки индивидуальных флагов, упакован-
ных в переменную типа int. Предположим, что имеется переменная по имени style
типа short, которая содержит 16 индивидуальных 1-битных флагов. Предположим
далее, что вам нужно установить отдельные флаги в переменной style. Одним из
способов сделать это является определение значений, которые можно скомбиниро-
вать операцией ИЛИ для установки определенных битов в 1. Чтобы установить край-
ний правый бит, вы можете определить переменную:
short vredraw = 0x01;
Чтобы установить второй справа бит, вы должны определить вторую переменную
hredraw:
short hredraw = 0x02;
Тогда для установки двух правых крайних битов переменной style в 1 можно вос-
пользоваться следующим оператором:
style = hredraw | vredraw;
Эффект от этого оператора проиллюстрирован на рис. 2.10.
vredraw: 0x01
style: 0x03
hredraw: 0x02
OR OR OR OR
0 0 0 0
0 0 0 0
OR OR OR OR
0 0 0 0
0 0 0 0
0 0 0 0
OR OR OR OR
0 0 0 0
0 0 0 0
0 0 10
OR OR OR OR
0 0 0 1
0 0 11
Puc. 2.10. Работа операции битового ИЛИ
Поскольку операция ИЛИ дает в результате 1, когда любой из двух битов операн-
дов равен 1, то объединение с ее помощью двух переменных дает в результате уста-
новку обоих бит.
Очень часто в программах предъявляется требование иметь возможность устано-
вить набор определенных флагов, не трогая остальных, которые могут устанавливать-
ся где-то в другом месте. Вы можете легко сделать это с помощью такого оператора:
style |= hredraw > vredraw;
Этот оператор установит два крайних правых бита переменной style в 1, остав-
ляя нетронутыми все остальные.
110 Глава 2
Битовое исключающее ИЛИ
Битовое исключающее ИЛИ (А), так называется потому, что работает подобно
обычному ИЛИ, но когда оба бита операндов установлены в 1, возвращает 0. Таким
образом, таблица истинности для него выглядит так, как показано в табл. 2.7.
Таблица 2.7. Таблица истинности для операции битового исключающего ИЛИ
Битовое исключающее ИЛИ 0 1
0 0 1
1 1 0
Применяя те же значения переменных, что вы использовали с обычной операци-
ей ИЛИ, можете посмотреть на результат такого оператора:
result = letterl л letter2;
Эта операция может быть представлена следующим образом:
letterl 0100 0001
letter2 0101 1010
Объединение их исключающим ИЛИ даст:
result 0001 1011
Переменная result получает значение 0x1В, или 27 в десятичной нотации.
Операция А имеет одно довольно неожиданное свойство. Предположим, что у вас
есть две переменных типа char — first со значением ‘А’ и last со значением ‘Z’, что
в двоичном виде представляется как 0100 0001 и 0101 1010. Если вы запишете такие
операторы:
first л= last; // в результате first равно 0001 1011
last л= first; // в результате last равно 0100 0001
first л= last; // в результате first равно 0101 1010
то в результате first и last обменяются значениями без использования какой-либо
памяти для промежуточного результата. Это работает с любыми целыми числами.
Битовое НЕ
Битовое НЕ (-) принимает единственный операнд, в котором инвертирует все
биты: 1 становится 0, а 0 становится 1. Таким образом, если выполнить оператор
result = -letterl;
то если letterl равно 0100 0001, переменная result получит значение 1011 1110, то
есть ОхВЕ, или 190 в десятичной нотации.
Битовые операции сдвига
Эти операции сдвигают значение целочисленных переменных на указанное коли-
чество битов вправо или влево. Операция » предназначена для сдвига вправо, а опе-
рация « — для сдвига влево. Биты, которые при этом “выпадают” за край значения,
теряются. На рис. 2.11 показан эффект от сдвига значения 2-байтной переменной
влево и вправо.
Вы объявляете и инициализируете переменную по имени number с помощью сле-
дующего оператора:
unsigned int number = 16387U;
Данные, переменные и вычисления 111
Десятичное 16 387 в двоичном виде:
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 1
Нули
010000000000001 1
задвигаются справа
Сдвиг влево на 2:
Эти два бита
сдвигаются за
край и теряются
0000000000001 1 00
=12
Сдвиг вправо на 2:
0001 000000 0 0 0000
= 4096
Эти два бита
сдвигаются за
край и теряются
Рис, 2.11. Сдвиг 2-байтной переменной влево и вправо
Как было показано ранее в этой главе, беззнаковые целочисленные литералы пи-
шутся с добавлением к числу буквы U или и. Вы можете сдвинуть содержимое этой
переменной влево следующим образом:
number «= 2; // сдвинуть влево на две битовых позиции
Левый операнд операции сдвига — это значение, которое нужно сдвинуть, а
количество бит, на которое необходимо сдвинуть, задается правым операндом.
Иллюстрация на рис. 2.11 демонстрирует эффект от операции сдвига. Как видите,
сдвиг значения 16 387 на две позиции влево дает в результате 12. Такое значительное
изменение значения объясняется потерей крайних бит, который уходят “за край” зна-
чения.
Вы также можете сдвинуть значение вправо. Вернем переменной number началь-
ное значение 16 387. Затем вы можете написать так:
number »= 2; // сдвинуть вправо на две битовых позиции
Это сдвигает значение 16 387 на две позиции вправо, что дает в результате значе-
ние 4096. Сдвиг вправо на две позиции равнозначен делению на 4 (без остатка). Это
также показано на рис. 2.11.
До тех пор, пока крайние биты не теряются, сдвиг на п бит влево эквивалентен
умножению значения на 2 п раз.
Другими словами, это эквивалентно умножению на 2”. Но будьте осторожны: как
вы видели на примере сдвига влево переменной number, если значащие биты теряют-
ся, то результат будет совсем не таким, какой вы ожидаете. Однако это не отличается
от операции умножения. Если вы умножите 2-байтное число на 4, то получите тот же
результат, поэтому сдвиг влево и умножение все-таки эквивалентны. Проблема точно-
сти возникает, когда результат умножения выходит за пределы допустимых значений
2-байтного целого.
Вы можете подумать, что между операциями сдвига и операциями, используемы-
ми для ввода и вывода, возникает конфликт. До тех пор, пока это рассматривает ком-
пилятор, значение операции в каждом конкретном случае всегда ясно из контекста.
112 Глава 2
Если же нет, то компилятор выдаст сообщение, но все же вы должны быть осторож-
ны. Например, если вы хотите вывести результат сдвига переменной number на 2
бита влево, то должны написать следующий оператор:
cout « (number « 2);
Здесь скобки очень важны. Без них операция сдвига интерпретировалась бы ком-
пилятором как операция потока, поэтому вы не получили бы должного результата; на
вывод было бы отправлено значение number, а за ним 2.
Операция сдвига вправо в основном подобна операции сдвига влево. Например,
предположим, что переменная number имеет значение 24, и выполняется следующий
оператор:
number »= 2;
В результате этого number получит значение 6, разделив исходное значение на 4.
Однако сдвиг вправо работает специальным образом со знаковыми целочисленны-
ми типами, которые содержат отрицательные значения (то есть, у которых бит зна-
ка — крайний левый — равен 1). В этом случае бит знака распространяется вправо.
Например, можно объявить и инициализировать переменную number типа char с
десятичным значением -104:
char number = -104; // двоичное представление — 1001 1000
Теперь вы можете сдвинуть его вправо на 2 бита с помощью следующего оператора:
number »= 2; // результат 1110 ОНО
Десятичное значение результата будет равно -26, поскольку бит знака повторяет-
ся. При операциях с беззнаковыми целочисленными типами, конечно, бит знака не
повторяется, поэтому в первой позиции появляется ноль.
Время хранения и область видимости
Все переменные имеют ограниченное время жизни при выполнении программы.
Они появляются в точке, где вы их объявили, а затем в некоторой точке они исчеза-
ют — не позднее момента завершения программы. Насколько долго существует кон-
кретная переменная, определяется свойством, называемым временем хранения (stor-
age duration). Существуют три разных вида времени хранения переменных.
□ автоматическое;
□ статическое;
□ динамическое.
Какое из них будет иметь переменная — зависит от того, как вы ее создаете. Пока
я отложу дискуссию о динамическом времени хранения до главы 4, но характеристи-
ки двух других будут раскрыты в этой главе.
Другое свойство, присущее переменным — это область видимости (scope).
Область видимости переменной — это просто часть программы, на протяжении ко-
торой имя данной переменной определено. Вне этой области видимости вы не мо-
жете ссылаться на ее имя — любая попытка сделать это вызовет ошибку компиляции.
Обратите внимание, что переменная все еще может существовать вне этой области
видимости, даже несмотря на то, что вы не можете обратиться к ней по имени. Чуть
позднее будут даны примеры подобной ситуации.
Данные, переменные и вычисления 113
Все переменные, которые вы до сих пор объявляли в примерах, имели автома-
тическое время хранения, а потому назывались автоматическими переменными.
Давайте рассмотрим их первыми.
Автоматические переменные
Все переменные, которые вы объявляли до сих пор, объявлялись внутри блока —
то есть, внутри пары фигурных скобок. Такие переменные называются автоматиче-
скими, и о них говорят, что у них локальная область видимости, или область види-
мости блока. Автоматическая переменная “видима”, начиная с точки, в которой она
объявлена, и до конца блока, содержащего ее объявление. Пространство, которое за-
нимает автоматическая переменная, выделяется автоматически в области памяти, на-
зываемой стеком, которая специально предназначена для этой цели. По умолчанию
размер стека составляет 1 Мбайт, чего достаточно для большинства случаев, хотя
если его не хватает, вы можете увеличить размер стека, установив опцию проекта
/STACK в необходимое значение по своему выбору.
Автоматическая переменная “рождается” в момент ее определения, и для нее
выделяется пространство в стеке, а прекращает свое существование в конце блока,
содержащего ее определение. Это происходит в точке, где находится закрывающая
фигурная скобка, которая соответствует первой открывающей фигурной скобке,
предшествующей объявлению этой переменной. Каждый раз, когда выполняется блок
операторов, содержащий объявление автоматической переменной, переменная созда-
ется вновь, и если вы специфицируете начальное значение автоматической перемен-
ной, она повторно инициализируется при каждом создании. Когда автоматическая
переменная, исчезает, занятая ею память в стеке освобождается для использования
другими автоматическими переменными.
Существует ключевое слово auto, которое вы можете применять для специфика-
ции автоматических переменных, но оно редко используется, поскольку предполага-
ется по умолчанию. Ниже приведен пример, иллюстрирующий то, что я до сих пор
рассказал об области видимости.
Практическое занятие АВТОМЭТИЧвСКИе ПвреМвННЫв
Эффект области видимости автоматических переменных демонстрируется на сле-
дующем примере.
// Ех2_07.срр
// Демонстрация области видимости
#include <iostream>
using std::cout;
using std::endl;
int main()
{ // Область видимости функции начинается здесь
int countl = 10;
int count3 = 50;
cout « endl
« "Значение внешней countl = " « countl
« endl;
{ // Здесь начинается новая область видимости...
int countl = 20; // Это скрывает внешнюю countl
int count2 = 30;
114 Глава 2
cout « "Значение внутренней countl = " « countl
« endl;
countl += 3; // Это изменяет внутреннюю countl
count3 += count2;
} // ...а здесь она заканчивается
cout « "Значение внешней countl = " « countl
« endl
« "Значение внешней count3 = " « counts
« endl;
// cout « count2 « endl; // уберете комментарий - получите ошибку
return 0;
} / / Область видимости функции здесь заканчивается
Вывод этого примера:
Значение внешней countl =10
Значение внутренней countl =20
Значение внешней countl =10
Значение внешней count3 = 80
Описание полученных результатов
Первые два оператора объявляют и определяют две целочисленных переменных,
countl и counts, с начальными значениями 10 и 50 соответственно. Обе эти пере-
менные существуют, начиная с этой точки и до закрывающей скобки в конце про-
граммы. Область видимости этих переменных также распространяется до закрываю-
щей скобки в конце main ().
Помните, что время жизни и область видимости переменной — разные вещи. Важно не пу-
тать эти два понятия. Время жизни — это период во время выполнения программы, нами-
ная с момента первого объявления переменной до момента ее уничтожения и освобождения
занятой ею памяти для другого использования. Область видимости переменной — это часть
программного кода, в которой переменная доступна.
Вслед за определением переменной значение countl выводится на экран, что дает
первую из строк, показанных выше. Далее идет новая открывающая фигурная скобка,
с которой начинается новый блок. Две переменных, countl и count2, определены в
этом блоке, со значениями 20 и 30 соответственно. Переменная countl, объявленная
здесь, отличается от первой переменной countl. Первая countl все еще существу-
ет, но ее имя замаскировано второй переменной countl. Любое упоминание имени
countl после объявления внутри вложенного блока ссылается на countl, объявлен-
ную в этом блоке.
Я использовал дублированное имя переменной countl здесь только для того, чтобы проиллю-
стрировать, что произойдет. Хотя этот код совершенно легален, он не является примером
правильного подхода к программированию. При разработке реальных программ это приве-
дет к путанице, и если вы используете дублированные имена, то очень легко нечаянно скрыть
переменные, определенные во внешней области видимости.
Значение во второй строке вывода демонстрирует, что внутри вложенного блока
обращение к имени countl означает обращение к переменной, имеющей область ви-
димости этого блока, то есть объявленной во внутренних фигурных скобках:
cout « "Значение внутренней countl = " « countl
« endl;
Данные, переменные и вычисления 115
Если бы здесь использовалась внешняя переменная count 1, то было бы выведено
ее значение — 10. Далее значение count 1 увеличивается следующим оператором:
countl += 3; // Это изменяет внутреннюю countl
Инкремент касается переменной, имеющей внутреннюю область видимости блока,
поскольку внешняя все еще сокрыта. Однако count 3, которая была определена во внеш-
ней области видимости, увеличивается в следующем операторе без всяких проблем:
count3 += count2;
Это доказывает, что переменные, объявленные в начале внешней области види-
мости, доступны также и во вложенной области. (Обратите внимание, что если бы
count3 была объявлена после закрывающей скобки вложенного блока, она бы также
существовала во внешней области видимости, но в этом случае она бы еще не суще-
ствовала во вложенном блоке.)
После закрывающей скобки вложенного блока count2 и внутренняя переменная
countl прекращают существовать. Переменные countl и count3 по-прежнему нахо-
дятся во внешней области видимости, и отображаемые значения доказывают, что зна-
чение count3 было увеличено во вложенном блоке.
Если вы снимите комментарий со следующей строки:
// cout « count2 « endl; // уберете комментарий - получите ошибку
то программа не станет корректно компилироваться, потому что попытается об-
ратиться к несуществующей переменной. В этом случае вы получите сообщение об
ошибке вроде следующего:
c:\microsoftvisual studio\myprojects\Ex2_07\Ex2_07.срр(29) :
error С2065: 'count2' : undeclared identifier
c:\microsoft visual studio\myprojects\Ex2_07\Ex2_07.cpp (29) :
ошибка C2065: 'count2' : необъявленный идентификатор
Это потому, что в этой точке count2 вышла из своей области видимости.
Размещение объявлений переменных
Вам предоставляется значительная свобода в том, где помещать объявления ваших
переменных. Наиболее важный аспект, который следует при этом учитывать — это ка-
кова должна быть область видимости этой переменной. Помимо этого, обычно сле-
дует размещать объявление переменной поближе к тому месту, где она будет впервые
использована в программе. Вы должны писать свои программы так, чтобы максималь-
но облегчить их понимание другими программистами, и объявление переменной не-
посредственно перед первым ее использованием весьма поможет в достижении этой
цели.
Можно поместить объявление переменной вне любой функции, входящей в про-
грамму. В следующем разделе будет рассказано о последствиях таких объявлений.
Глобальные переменные
Переменные, которые объявлены вне всех блоков и классов (о классах речь пой-
дет позже), называются глобальными, и они имеют глобальную область видимости
(которая также называется областью видимости глобального пространства имен
или областью видимости файла). Это значит, что они доступны всем функциям в
файле, начиная с точки, где они были объявлены. Если вы объявляете их в самом на-
чале ваше программы, то они будут доступны в любом месте файла.
116 Глава 2
Глобальные переменные также по умолчанию имеют статическое время жизни.
Глобальные переменные со статическим временем жизни существуют с момента нача-
ла выполнения программы и до момента ее завершения. Если вы не специфицируете
начальное значение глобальной переменной, то по умолчанию она инициализируется
нулем. Инициализация глобальных переменных происходит перед началом выполне-
ния функции main (), поэтому они всегда доступны в любой части кода, находящейся
внутри области видимости переменной.
На рис. 2.12 показано содержимое исходного файла Expample. срр, и стрелками
указана область видимости каждой переменной.
Переменная valuel, которая появляется в начале файла, объявлена с глобаль-
ной областью видимости, как и value4, которая появляется после функции main ().
Область видимости каждой глобальной переменной простирается от точки ее опре-
деления до конца файла. Даже несмотря на то, что value4 существует в момент на-
чала выполнения программы, к ней нельзя обратиться из тела main (), потому что
main () не находится в ее области видимости. Для того чтобы main () могла обратить-
ся к переменной value4, ее объявление следует переместить в начало файла. Обе
переменные — и valuel, и value4 — по умолчанию будут инициализированы нулем,
что отличает их от автоматических переменных. Обратите внимание, что локальная
переменная по имени valuel в function () скрывает глобальную переменную с тем
же именем.
// Example, срр
long valuel;
valuel
value3
valuel
value2
value5
value4
Puc. 2.12. Области видимости переменных
Данные, переменные и вычисления 117
Поскольку глобальные переменные продолжают существовать столько, сколько
работает программа, это может вызвать вопрос: “Почему бы не сделать все перемен-
ные глобальными и избежать неприятностей, связанных с исчезновением локальных
переменных?”. На первый взгляд это кажется привлекательным, но, как и в случае с
Сиренами из мифологии, у такого решения были бы серьезные побочные эффекты,
которые полностью перевешивают любые возможные преимущества.
Реальные программы обычно состоят из множества операторов, существенного
количества функций и огромного числа переменных. Объявление всех переменные
глобальными значительно увеличивает риск непреднамеренной их модификации, а
также существенно затрудняет работу по их именованию. К тому же они будут зани-
мать память в течение всего времени выполнения программы. Сохраняя переменные
локальными по отношению к функции или блоку, вы можете быть уверенными, что
они почти полностью защищены от внешних эффектов, потому что они существуют
и занимают память, только начиная с точки их объявления и до завершения включа-
ющего блока. При этом становится легче управлять всем процессом разработки.
Если в 8взглянете на панель Class View (Представление классов) в правой части
окна IDE для любого примера из тех, что были рассмотрены до сих пор, и развернете
дерево классов проекта, щелкнув на знаке +, то увидите там элемент по имени Global
Functions and Variables (Глобальные функции и переменные). Если щелкнуть на нем,
можно увидеть список всего того в вашей программе, что имеет глобальную область
видимости. Это включает все глобальные функции, а также все объявленные глобаль-
ные переменные.
[Практическое занятие | Операция ра3решеНИЯ КОНТвКСТЗ
Как вы уже видели, глобальная переменная может быть скрыта локальной пере-
менной с тем же именем. Однако все же остается возможность получить доступ к
такой глобальной переменной, используя операцию разрешения контекста (: :),
которую вы уже видели в главе 1, когда речь шла о пространствах имен. Я продемон-
стрирую, как она работает, на измененной версии последнего примера:
// Ех2__08.срр
// Демонстрация области видимости переменных
#include <iostream>
using std: : conf-
using std::endl;
int countl = 100; // Глобальная версия countl
int main ()
{ // Область видимости функции начинается здесь
int countl = 10;
int count3 = 50;
cout « endl
« "Значение внешней countl = " « countl
« endl;
cout « "Значение глобальной countl = " « : : countl //из внешнего блока
« endl;
{ // Здесь начинается новая область видимости.. .
int countl = 20; //Это скрывает внешнкю countl
int count2 = 30;
cout « "Значение внутренней countl = " « countl
« endl;
cout « "Значение глобальной countl = " « :: countl //из внутреннего блока
« endl;
118 Глава 2
countl += 3; // Это изменяет внутреннюю countl
count3 += count2;
} // ...а здесь она заканчивается.
cout « "Значение внешней countl = ” « countl
« endl
« "Значение внешней counts = ” « counts
« endl;
//cout « counts « endl; // уберете комментарий - получите ошибку
return 0;
} // Область видимости функции здесь заканчивается
Если вы скомпилируете и запустите этот пример, то получите следующий вывод:
Значение внешней countl =10
Значение глобальной countl = 100
Значение внутренней countl = SO
Значение глобальной countl = 100
Значение внешней countl =10
Значение внешней counts =80
Описание полученных результатов
Выделенные полужирным строки кода указывают изменения, которые я внес в
предыдущий пример. Поговорим только о них. Объявление переменной countl,
предшествующее определению функции main () — глобальное, поэтому в принципе
она доступна на всем протяжении функции main (). Эта глобальная переменная ини-
циализируется значением 100:
int countl =100; // Глобальная версия countl
Однако у вас есть две других переменных countl, определенные внутри main (),
поэтому по всей программе глобальная переменная countl скрыта локальными
countl. Первый новый оператор вывода:
cout « "Значение глобальной countl = ” « :: countl // из внешнего блока
« endl;
Здесь используется операция разрешения контекста (: :), поясняющая компиля-
тору, что вы хотите обратиться к глобальной переменной countl, а не к локальной.
Посмотрев на вывод программы, можно убедиться, что это работает.
Во вложенном блоке глобальная переменная countl скрыта за двумя переменны-
ми по имени countl: внутренней и внешней. Мы можем видеть, что глобальная опе-
рация разрешения контекста выполняет свою работу и во внутреннем блоке, как до-
казывает вывод, генерируемый еще одним добавленным оператором:
cout « "Значение глобальной countl = ” « ::countl // из внутреннего блока
« endl;
Оно отображает значение 100, как и ранее — “длинная рука” операции разрешения
контекста, использованной в такой манере, всегда достанет глобальную переменную.
Ранее вы уже видели, что можно сослаться на имя в пространстве имен std, дополнив его
именем этого пространства имен— как, например, в случае std: :cout и std: :endl.
Компилятор ищет указанное имя в пространстве имен, имя которого совпадает с левым
операндом операции разрешения контекста. В предыдущем примере вы использовали опера-
цию разрешения контекста для поиска в глобальном пространстве переменной countl. Тем,
что перед оператором не было указано имя пространства имен, вы сообщили компилятору,
что для поиска имени он должен обратиться к глобальному пространству имен.
Данные, переменные и вычисления 119
Вы узнаете еще больше об этой операции, когда в главе 9 пойдет речь об объектно-
ориентированном программировании, где она применяется очень широко.
Статические переменные
Вероятно, рано или поздно вам понадобится иметь переменную, которая опреде-
лена и доступна локально, но продолжает существовать после выхода из блока, в ко-
тором она объявлена. Другими словами, необходимо иметь возможность объявить
переменную, имеющую область видимости в пределах блока, но обеспечить ей ста-
тическое время хранения. Спецификатор static обеспечивает такую возможность,
а потребность в ней станет более очевидной, когда мы будем говорить о функциях в
главе 5.
На самом деле статическая переменная существует на протяжении всего времени
жизни программы, даже если она объявлена внутри блока и доступна только в нем
(или в его подблоках). Она также имеет область видимости блока, но при этом имеет
и статическое время хранения. Чтобы объявить статическую целочисленную пере-
менную count, вы должны написать так:
static int count;
Если вы не предоставляете статической переменной начальное значение при ее
объявлении, она будет инициализирована значением по умолчанию, а именно — ну-
лем. Значение инициализации по умолчанию для статических переменных всегда рав-
но 0, преобразованному к типу данной переменной. Напомним, что автоматических
переменных это правило не касается.
Если вы не инициализируете свои автоматические переменные, они будут содержать мусор—значе-
ния, которые остались в выделенной для них памяти от предыдущих запусков других программ.
Пространства имен
Я уже несколько раз упоминал о пространствах имен, так что теперь наступило
время дать более точное представление об этом понятии. Пространства имен не при-
меняются в библиотеках, поддерживающих MFC, но библиотеки, поддерживающие
CLR и Windows Forms, используют их интенсивно, и стандартная библиотека ANSI
C++, конечно, тоже.
Вы уже знаете, что все имена, используемые в стандартной библиотеке ISO/ANSI
C++, определены в пространстве имен std. Это значит, что все имена, встречающие-
ся в стандартной библиотеке, имеют дополнительное квалифицирующее имя — std,
поэтому cout, например — это на самом деле std:: cout. Вы можете видеть примене-
ние полных квалифицированных имен в следующем тривиальном примере.
// Ех2_09.срр
// Демонстрация пространства имен
#include <iostream>
int value =0;
int main ()
std::cout « "Введите целое число:
std:.’Gin » value;
std::cout « "\пВы ввели " « value
« std:: endl;
return 0;
120 Глава 2
Объявление переменной value находится вне определения main (). Об этом
объявлении также говорят, как об области видимости глобального пространства имен,
потому что объявление переменной находится вне какого-либо пространства имен.
Переменная доступна в любом месте main (), равно как и из определения любой дру-
гой функции, которая может встретиться в том же исходном файле. Я поместил объ-
явление value вне main (), просто чтобы продемонстрировать в следующем разделе,
как его можно поместить в пространство имен.
Обратите внимание на отсутствие объявления using для cout и endl. В данном
случае оно не нужно, поскольку имена из пространства имен std квалифицированы
полностью. Может быть, поступать так и неразумно, но вы можете использовать cout
в качестве имени своей переменной, и это не приведет ни к какой путанице, потому
что cout будет отличаться от std:: cout. Таким образом, пространства имен предо-
ставляют возможность отделить имена, используемые в одной части программы, от
имен, применяемых в другой. Это чрезвычайно ценно, когда речь идет о крупном
проекте, в работе над которым задействовано несколько групп программистов, зани-
мающихся разными частями программы. Каждая команда может иметь собственное
пространство имен, и в этом случае можно не беспокоиться о том, что они непредна-
меренно используют одно и то же имя для различных функций.
Взгляните на следующую строку кода:
using name space std;
Этот оператор называется директивой using.
Эффект от его применения состоит в том, что все имена из пространства std
импортируются в ваш исходный файл, так что вы можете из своей программы ссы-
латься на все, что определено в этом пространстве имен, без квалифицированного
имени. То есть, вы можете писать имя cout вместо std: :cout и endl вместо std: :
endl. Недостатком такого применения директивы using является то, что сводится
на нет основная причина применения пространств имен — то есть, предотвращение
непреднамеренных конфликтов. Самый безопасный способ доступа к именам из про-
странства имен — это квалифицировать каждое имя явно вместе с наименованием
пространства имен; к сожалению, это делает код очень многословным и снижает его
читабельность. Другая возможность — представить объявлением using только те име-
на, которые вы используете в своем коде, например:
using std::cout;
using std::endl;
// Позволяет использовать cout без квалификации
// Позволяет использовать endl без квалификации
Эти операторы называются объявлениями using. Каждый оператор представляет
одно имя из указанного пространства и позволяет применять его без квалификации
внутри последующего программного кода. Это обеспечивает более удобный способ
импортирования из пространства имен, поскольку импортируются лишь те имена, ко-
торые действительно нужны в программе. Поскольку Microsoft установила прецедент
импорта всего пространства имен System для кода C++/CLI, я буду придерживаться
этого в примерах C++/CLI. Но вообще я рекомендую использовать в своем коде объ-
явления using вместо директив using, когда вы пишете программы сколько-нибудь
значительного размера.
Конечно, вы можете определить свое собственное пространство имен по своему
выбору. В следующем разделе показано, как это сделать.
Данные, переменные и вычисления 121
Объявление пространства имен
Для объявления пространства имен применяется ключевое слово name space:
namespace myStuff
// код, который нужно поместить в пространство имен myStuff...
Это определяет пространство имен по имени myStuff. Все объявления имен
в коде между фигурными скобками будут определены внутри пространства имен
myStuff, поэтому для доступа к любому из этих имен из точки, находящейся вне дан-
ного пространства, имя должно быть квалифицированным, то есть, снабженным пре-
фиксом myStuff::, либо должно присутствовать объявление using, идентифицирую-
щее, что данное имя относится к пространству myStuff.
Вы не можете объявить пространство имен внутри функции. Оно предназначе-
но для другого применения: вы используете в вашей программе пространства имен
как контейнер для функций, глобальных переменных и других именованных сущно-
стей наподобие классов. Функция main (), где начинается выполнение программы,
всегда должна быть в глобальном пространстве имен, иначе компилятор не распозна-
ет ее.
Вы можете поместить переменную value из предыдущего примера в пространство
имен:
// Ех2_10.срр
// Объявление пространства имен
#include <iostream>
namespace myStuff
{
int value =0;
}
int main()
std::cout « "введите целое число:
std: :cin » myStuff::value;
std: :cout « ”\пВы ввели " « myStuff::value
« std:: endl;
return 0;
}
Пространство имен myStuff определяет область видимости, и все, что находится
внутри него, квалифицировано именем этого пространства имен. Чтобы сослаться
извне на имя, объявленное внутри пространства имен, вы должны квалифицировать
его именем пространства имен. Внутри же области видимости пространства имен на
любое имя, объявленное в нем, можно ссылаться без квалификации — здесь все иден-
тификаторы принадлежат одному семейству. Но в функции main () вы теперь должны
квалифицировать имя value префиксом myStuff, иначе программа не скомпилиру-
ется. Функция main () теперь ссылается на имена из двух разных пространств, и в
общем случае вы можете иметь столько пространств имен в вашей программе, сколь-
ко вам нужно. Вы можете исключить необходимость квалификации имени value, до-
бавив директиву using:
// Ех2_11.срр
// Использование директивы using
#include <iostream>
122 Глава 2
namespace myStuff
int value = 0;
using namespace myStuff; // Сделать доступными все имена в myStuff
int main
О
{
std: :cout « "Введите целое число:
std: :cin » value;
std: :cout « "\пВы ввели " « value
« std:: endl;
return 0;
}
Вы также можете вставить директиву using и для пространства имен std, так что
вам не придется квалифицировать имена из стандартной библиотеки, однако это от-
меняет саму идею пространств имен. Вообще говоря, если вы применяете простран-
ства имен в своей программе, то вы не должны добавлять директивы using по всей
программе, также вы можете не беспокоиться о них с самого начала. Но, отметив
это, мы все же будем добавлять директиву using для std во всех примерах, чтобы код
был менее громоздким и легче читался. Когда вы начинаете изучать новый язык про-
граммирования, то должны стараться обходиться без усложнения кода, независимо
от того, насколько это применяется на практике.
Множественные пространства имен
Реальные программы, с которыми вам придется столкнуться, скорее всего, будут
включать в себя множество пространств имен. Вы можете иметь множество объявле-
ний пространств с одним и тем же именем, при этом содержимое всех одноименных
блоков пространств имен попадает в одно и то же пространство. Например, вы може-
те иметь программный файл с двумя пространствами имен:
namespace sortstuff
// Все, что здесь есть, относится к пространству имен sortstuff
namespace calculateStuff
// Все, что здесь есть, относится к пространству имен calculateStuff.
// Чтобы обращаться к именам из sortstuff, они должны быть квалифицированными
namespace sortstuff
// Это продолжение пространства имен sortstuff,
// поэтому отсюда можно обращаться ко всему, что объявлено
// в первом пространстве sortstuff, без квалификации
Второе объявление пространства имен с тем же именем — это просто продолже-
ние первого, поэтому вы можете обращаться к именам первого блока пространства
имен из второго без необходимости квалифицировать их. Все они находятся в одном
пространстве имен. Конечно, обычно вы не будете намеренно организовывать ис-
ходный файл подобным образом, но такое может случиться естественным образом с
заголовочными файлами, которые вы включаете в свою программу. Например, у вас
может быть нечто вроде:
Данные, переменные и вычисления 123
#include <iostream>
#include "myheader.h”
#include <string>
//и так далее...
// Содержимое -
// Содержимое -
// Содержимое -
в пространстве имен std
в пространстве имен myStuff
в пространстве имен std
Здесь iostream и string — заголовочные файлы стандартной библиотеки ISO/
ANSI C++, a myheader. h — заголовочный файл, содержащий ваш программный код.
То есть вы имеете ситуацию с пространствами имен, которая в точности повторяет
предыдущий пример.
Все это должно дать вам представление о том, как работают пространства имен.
Можно еще очень много сказать о пространствах имен, но того, что я показал здесь,
достаточно, чтобы вы ухватили суть и были готовы без проблем узнать больше при
необходимости.
Обратите внимание, что две формы директивы #include в предыдущем фрагменте кода
заставляют компилятор искать включаемые файлы разными способами. Когда вы специ-
фицируете включаемый файл между угловыми скобками, то тем самым сообщаете компиля-
тору, что он должен искать его в пути, указанном опцией компилятора /I, а не найдя там,
обратиться к переменной окружения INCLUDE. Эти пути указывают на библиотечные фай-
лы C++, поэтому такая форма зарезервирована для библиотечных заголовков. Переменная
окружения INCLUDE указывает на папку, содержащую библиотечные заголовки, а опция
/1 позволяет специфицировать дополнительные каталоги, содержащие библиотечные за-
головки. Когда же имя файла указано между двойных кавычек, компилятор ищет папку, где
находится файл, в котором встретилась данная директива #include. Если там файл не
найден, поиск продолжается в библиотечных каталогах.
Программирование на C++/CLI
C++/CLI предлагает множество расширений и дополнительных возможностей
вдобавок к тем, которые рассматривались до сих пор в этой главе. Прежде чем по-
грузиться в детали, я представлю краткий перечень этих дополнительных возможно-
стей. Итак, ниже перечислены дополнительные возможности C++/CLI.
□ Все фундаментальные типы данных ISO/ANSI, о которых я говорил ранее, мо-
гут быть использованы в программах C++/CLI, но здесь они имеют некоторые
дополнительные свойства в определенных контекстах, которые будут раскры-
ты позже.
□ C++/CLI предоставляет собственный механизм клавиатурного ввода и вывода
в командную строку консольных программ.
□ C++/CLI предоставляет операцию saf e_cast, которая гарантирует, что опера-
ция приведения приведет к генерации проверяющего кода.
□ C++/CLI предоставляет альтернативную возможность перечисления на основе
классов и обеспечивает большую гибкость, чем объявление enum из ISO/ANSI
C++, которое вы видели ранее.
Вы узнаете больше о ссылочных типах классов CLR в начале главы 4, но поскольку
я уже представил глобальные переменные родного C++, то здесь отмечу, что перемен-
ные ссылочных типов классов CLR не могут быть глобальными.
Я бы хотел начать с обзора фундаментальных типов данных C++/CLI.
124 Глава 2
Специфика C++/CLI: фундаментальные типы данных
Вы можете и должны использовать имена фундаментальных типов ISO /ANSI C++
в своих программах C++/CLI, и с арифметическими операциями они работают точно
так же, как вы видели в родном C++. Кроме того, в C++/CLI определены два дополни-
тельных целочисленных типа, которые описаны в табл. 2.8.
Таблица 2.8. Дополнительные целочисленные типы C++/CLI
Тип Размер в байтах Диапазон значений
long long 8 от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807
unsigned long long 8 от 0 до 18 446 744 073 709 551 615
Чтобы специфицировать литералы типа long long, необходимо добавлять LL или
11 к целому значению. Например:
long long big = 123456789LL;
Литерал типа unsigned long long указывается добавлением ULL или ull к целому
значению:
unsigned long long huge = 999999999999999ULL;
Хотя все операции с фундаментальными типами, что вы видели ранее, работают
аналогичным образом с C++/CLI, имена фундаментальных типов в программах C++/
CLI имеют другой смысл и предоставляют дополнительные возможности в некото-
рых ситуациях. Фундаментальный тип в программе C++/CLI — это класс типа значе-
ния, и может вести себя как обычное значение или как объект, если обстоятельства
того требуют.
Внутри языка C++/CLI каждый фундаментальный тип ISO/ANSI отображается
на класс типа значения, определенный в пространстве имен System. То есть в про-
грамме на C++/CLI имена фундаментальных типов являются сокращениями для ас-
социированных с ними классовых типов. Это позволяет трактовать значение фунда-
ментального типа как просто значение либо, при необходимости, как автоматически
преобразованный объект ассоциированного типа класса. Фундаментальные типы, объ-
ем занимаемой ими памяти и соответствующие им типы классов показаны в табл. 2.9.
По умолчанию тип char эквивалентен signed char, поэтому ассоциированный с
ними класс типа значения — System: : SByte. Обратите внимание, что вы можете из-
менить умолчание char на unsigned char, установив опцию компилятора / J, и в этом
случае ассоциированным типом класса будет System: :Byte. System — это корневое
пространство имен, в котором определены классы типа значения C++/CLI. Существует
множество других типов, определенных в пространстве имен System — такие как
String — для представления строк, с которым вы познакомитесь в главе 4. C++/CLI
также определяет в пространстве имен System класс типа значения System: : Decimal.
Переменные этого типа могут содержать десятичные числа с точностью до 28 знаков.
Как я уже говорил, то, что классы типа значений ассоциированы с каждым фун-
даментальным типом, добавляет новые возможности для переменных этих типов в
C++/СЫ. Когда необходимо, компилятор выполняет автоматические преобразования
от исходного значения в ассоциированный тип класса и обратно; этот процесс назы-
вается упаковкой (boxing) и распаковкой (unboxing) соответственно. Это позволя-
ет переменной любого из этих типов вести себя как простое значение либо как объ-
ект — в зависимости от обстоятельств. Подробнее о том, когда и как это происходит,
вы узнаете в главе 6.
Данные, переменные и вычисления 125
Таблица 2.9. Фундаментальные типы C++/CLI
Фундаментальный тип
Размер (в байтах)
Класс значений CLI
bool
char
signed char
unsigned char
short
unsigned short
int
unsigned int
long
unsigned long
long long
unsigned long long
float
double
long double
wchar_t
1
1
1
1
2
2
4
4
4
4
8
8
4
8
8
2
System::Boolean
System::Sbyte
System::Sbyte
System::Byte
System::Intl6
System::Uintl6
System::Int32
System::Uint32
System::Int32
System::Uint32
System::Int64
System::UInt64
System::Single
System::Double
System::Double
System::Char
Поскольку имена фундаментальных типов ISO/ANSI C++ служат псевдонимами
для имен классов в программе C++/CLI, в принципе вы можете использовать в коде
C++/CLI и те, и другие. Например, вы уже знаете, как записывать операторы, создаю-
щие целые переменные и переменные с плавающей точкой:
int count = 10;
double value » 2.5;
Вы можете использовать имена классов, соответствующие фундаментальным ти-
пам, и компилировать программу без каких-либо проблем:
System::Int32 count = 10;
System::Double value = 2.5;
Хотя это совершенно законно, все же в своем коде вы должны использовать име-
на фундаментальных типов, такие как int и double, вместо имен классов System: :
Int32 и System: : Double. Причина в том, что отображение имен фундаментальных
типов на имена классов и обратно — это функциональность компилятора Visual C++.
Другие компиляторы не обязательно реализуют такие преобразования. Имена фунда-
ментальных типов фиксированы в стандарте языка C++/CLI, но отображение боль-
шинства их на имена классов зависит от реализации. Тип long в Visual C++ 2005 ото-
бражается на тип Int32, но вполне возможно, что в какой-то другой реализации он
будет отображаться на тип Int 64.
Возможность представления данных фундаментальных типов в виде объектов
классов типа значений — важное средство C++/CLI. В ISO/ANSI C++ фундаменталь-
ные типы и типы классов совершенно различны, в то время как в C++/CLI все дан-
ные сохраняются в виде объектов классов — как классов типов значений, так и клас-
сов ссылочных типов. О классах ссылочных типов вы узнаете в главе 7.
Теперь попробуем написать консольную программу CLR.
126 Глава 2
Практическое занятие
Консольная программа CLR
Создайте новый проект и выберите тип проекта CLR и шаблон CLR Console
Application (Консольное приложение CLR). Затем вы можете ввести имя проекта
Ех2_12, как показано на рис. 2.13.
New Project
Project types:
В Visual C++
ATL
CLR
General
MFC
Smart Device
Win32
• Other Languages
Distributed System Solutions
+ othe Project Types
+ Test Projects
lem plates.
Visual Studio installed templates
> ASP.NET Web Service
jS CLR Console Application
ЗП SQL server Project
3 Windows Fo ms Control Library
My Templates
J Search Online Templates...
"3ctas5 Library
"1 CLR Em pty Project
^Windows Forms Application
"Windows Service
A project Гог creating a console app lication
Name:
Location:
Solution:
D:\Beginning Visual C++ 2Q05\Examples
Create new Solution
v] Create directory for solution
Solution Namc
Ex2 12
Puc. 2.13. Создание консольного приложения CLR
Когда вы щелкнете на кнопке ОК, мастер Application Wizard сгенерирует проект,
содержащий следующий код:
// Ех2_12.срр : главный файл проекта.
#include ”stdafx.h”
using namespace System;
int main(array<System::String A> Aargs)
Console::WriteLine(L”Hello World”);
return 0;
Уверен, что вы обратили внимание на то, что находится между скобками вслед за
main. Это связано с передачей значений функции main (), когда вы инициируете вы-
полнение программы из командной строки, но об этом вы узнаете больше, когда мы
обратимся к теме функций. Если вы скомпилируете и запустите проект по умолча-
нию, то увидите, что он выдаст в командную строку “Hello World”. А теперь преоб-
разуем эту программу в CLR-версию примера Ех2_02, чтобы посмотреть, насколько
она будет похожа. Чтобы сделать это, модифицируйте код Ех2_12. срр следующим об-
разом:
Данные, переменные и вычисления 127
// Ех2_12.срр : главный файл проекта.
#include "stdafx.h”
using namespace System;
int main(array<System::String A> Aargs)
int apples, oranges;
int fruit;
apples = 5; oranges =6; // Установить начальные значения
fruit = apples + oranges; // Сколько всего фруктов
Console: : WriteLine (Ь’ЛпАпельсины - не единственные фрукты... ");
Console: :Write(L" - и всего у нас ”);
Console::Write (fruit);
Console::Write(L" фруктов.\n”);
return 0;
Новые строки выделены полужирным, и они заменяют две автоматически сгене-
рированные строки в main (). Теперь вы можете скомпилировать и запустить проект.
Программа должна выдать следующий вывод:
Апельсины - не единственные фрукты...
- и всего у нас 11 фруктов.
Описание полученных результатов
Единственное серьезное отличие — способ вывода. Определения переменных и
вычисления — те же самые. Хотя вы применяете те же имена типов, что и в версии
этого примера ISO/ANSI C++, это не то же самое. Переменные apples, oranges и
fruit имеют тип C++/CLI System: : Int32, который специфицирован как int, и
они обладают некоторыми дополнительными возможностями по сравнению с типом
ISO/ANSI. Здесь переменные в некоторых случаях могут вести себя либо как про-
стые значения, либо как объекты. Если вы хотите подтвердить, что в данном случае
Int32 — то же самое, что int, то можете заменить имя типа int на Int32 и переком-
пилировать пример. Он должен работать точно таким же образом.
Очевидно, что следующая строка кода выдает первую строку вывода:
Console: :WriteLine(L”\nAnenbCiaai - не единственные фрукты... ”);
WriteLine () — это функция C++/CLI, которая определена в классе Console про-
странства имен System. Подробнее о классах вы узнаете в главе 6, а пока что скажу,
что класс Console предоставляет стандартные потоки ввода и вывода, соответствую-
щие клавиатуре и командной строке консольного окна. То есть функция WriteLine ()
пишет все, что находится между скобками, следующими за ее именем, в командную
строку, добавляя при этом символ новой строки, чтобы переместить курсор в начало
следующей строки экрана. То есть предыдущий оператор выводит в командную стро-
ку текст "ХпАпельсины — не единственные фрукты. . . ". Буква L, предшествующая
строке, указывает на то, что это строка, состоящая из “широких” символов, где каж-
дый занимает 2 байта.
Функция Write () класса Console — это, по сути, то же самое, что функция
WriteLineO, с единственным отличием — она не добавляет к указанному выводу сим-
вол новой строки. Поэтому вы можете использовать Write (), когда нужно вывести
два или более элемента данных в одну строку несколькими отдельными операторами.
Значение, которые вы помещаете между скобками, следующими за именем функ-
ции, называются аргументами. В зависимости от того, как написана функция, она
128 Глава 2
может принимать при ее вызове ноль, один или более аргументов. Когда вам нужно
передать более одного аргумента, их следует разделять запятыми. Функции вывода
класса Console заслуживают более детального рассмотрения, поэтому я опишу их
подробно.
Вывод командной строки C++/CLI
Вы увидели в последнем примере, как можно использовать методы Console: :
Write () и Console : : WriteLine () для вывода строк или других элементов данных
в командную строку. Между их скобками можно поместить переменную любого из ти-
пов, которые вам встречались в этой книге до сих пор, и ее значение будет выведено
в командную строку. Например, вы можете написать следующие операторы для выво-
да выходной информации о количестве пакетов:
int packageCount = 25; //
Console::Write (Ь’’Имеется ”); //
Console::Write(packageCount); //
Console::WriteLine(L" пакетов.”);
Количество пакетов
Вывести строку без символа новой строки
Вывести значение без символа новой строки
// Вывести строку с символом новой строки
Выполнение этих операторов даст следующий вывод:
Имеется 25 пакетов.
Весь вывод уместился в одной строке, потому что в первых двух операторах ис-
пользуется функция Write (), которая не выводит символ новой строки после дан-
ных. В последнем операторе применяется функция WriteLine (), которая при выводе
присоединяет символ новой строки к переданным данным, поэтому весь последую-
щий вывод окажется в новой строке.
Выглядит довольно утомительным использовать целых три оператора, чтобы по-
лучить одну строку вывода, поэтому не удивительно, что предусмотрен и другой спо-
соб. Это средство связано с форматированием вывода в командной строке программ
.NET Framework, и об этом мы поговорим в следующем разделе.
Специфика C++/CLI — форматирование вывода
Обе функции — и Console: : Write (), и Console : :WriteLine () — имеют сред-
ства управления форматом вывода, и этот механизм работает совершенно одинаково
в обеих функциях. Проще всего понять его на примере. Во-первых, посмотрим, как
можно одним оператором получить вывод, который в предыдущем примере форми-
ровался тремя операторами:
int packageCount = 25;
Console:: WriteLine (Ь’’Имеется {0} пакетов.", packageCount);
Здесь второй оператор даст тот же вывод, что вы видели в предыдущем разделе.
Первый аргумент Console:: WriteLine — строка L"HMeeTcn {0} пакетов. в кото-
рой фрагмент “ {0} ” отмечает место, куда будет помещен второй аргумент. Этот фраг-
мент заключает в себе форматную строку, применяемую для вывода второго аргумен-
та, хотя в данном случае она предельно проста, и состоит из одного нуля. Аргументы,
следующие в функции Console: :WriteLine () за первым, пронумерованы по поряд-
ку, начиная с нуля:
Ссылаться по : 0 1 2 и так далее.
Console::WriteLine("Строка формата", arg2, агдЗ, агд4,... );
данные, переменные и вычисления
129
Таким образом, ноль между фигурными скобками в предыдущем фрагменте кода
указывает на то, что аргумент packageCount должен заменить {0} в строке-аргументе
при выводе ее на консоль.
Если вы захотите вывести еще и вес пакетов вместе с числом, то можете написать
так:
int packageCount =25;
double packageweight = 7.5;
Console::WriteLine(Ь”Имеется {0} пакетов весом {1} фунтов.”,
packageCount, packageweight);
Теперь оператор вывода имеет три аргумента, и ко второму и третьему в формат-
ной строке выполняется обращение по номерам 0 и 1 соответственно. Поэтому это
даст следующий вывод:
Имеется 25 пакетов весом 7.5 фунтов.
Вы также можете написать оператор, который выведет два последних аргумента в
обратном порядке, как здесь:
Console::WriteLine(Ь”Есть {1} пакетов весом {0} фунтов.",
packageweight, packageCount);
Теперь в форматной строке 0 ссылается на переменную packageweight, а 1 — на
packageCount; вывод получится точно таким же, как прежде.
У вас также есть возможность специфицировать, как будут представлены данные в
командной строке. Предположим, что вам нужно вывести значение с плавающей точ-
кой packageweight с двумя десятичными разрядами после точки. Это можно сделать
следующим образом:
Console::WriteLine(Ь"Имеется {0} пакетов весом {1:F2} фунтов.",
packageCount, packageweight);
В подстроке 1: F2 двоеточие отделяет значение индекса — 1, идентифицирующее
аргумент, от следующей за ним спецификации формата — F2. Буква F в спецификации
формата указывает, что вывод должен быть в форме “±ddd.dd...” (где d представляет
десятичную цифру), а 2 — количество разрядов после точки. Вывод этого оператора
будет таким:
Имеется 25 пакетов весом 7.50 фунтов.
Вообще вы можете писать спецификацию формата в виде {n, w: Ахх}, где п — зна-
чение индекса, указывающее номер аргумента, следующего за форматной строкой,
w — необязательная спецификация ширины поля, А — односимвольная спецификация
формата значения, а хх — необязательное одно- или двузначное число, задающее точ-
ность вывода значения. Спецификация ширины поля — целое со знаком. Значение
будет выровнено вправо в поле w, если ширина положительна, и влево — если отрица-
тельна. Если значение занимает меньше знаков, чем указано в w, то вывод дополняет-
ся пробелами; если значение не помещается в ширину w, то спецификация ширины
игнорируется. Вот еще один пример:
Console::WriteLine(Ь"Пакетов:{0,3} Вес: {1,5:F2} фунтов.",
packageCount, packageweight);
Количество пакетов выводится в поле шириной 3 знака, а вес — в поле шириной 5,
поэтому в результате получим:
Пакетов: 25 Вес: 7.50 фунтов.
130 Глава 2
Существуют и другие спецификаторы формата, которые позволяют вам предста-
вить различные типы данных различными способами. Некоторые из наиболее часто
используемых спецификаторов формата перечислены в табл. 2.10.
Таблица 2.10. Часто используемые спецификаторы формата
Спецификатор формата Описание
С или с Выводит значение в денежном формате.
БИЛИ d Выводит целое в десятичном виде. Если указать более высокую точность, количе- ство десятичных знаков в числе, то оно будет дополнено нулями слева.
Е или е Выводит значение с плавающей точкой в научной нотации, то есть с экспонентой. Значение точности указывает количество десятичных разрядов после точки.
РИЛИ f Выводит значение с плавающей точкой как число с фиксированной точкой в форме tddd.dd...
G или g Выводит значение в наиболее компактном виде, в зависимости от его типа и указанной точности. Если точность не указана, принимается значение точности по умолчанию.
N ИЛИ П Выводит десятичное значение с плавающей точкой, используя при необходимости разделитель — запятую между группами по три разряда.
X ИЛИ X Выводит целое в шестнадцатеричном виде. Шестнадцатеричные цифры выводятся в верхнем или нижнем регистре в зависимости от того, указан х или х.
Это даст вам достаточные знания относительно вывода, чтобы продолжить из-
учать примеры C++/CLI. Теперь посмотрим на кое-что из описанного в действии.
Практическое занятие форМЭТИрОВЭННЫЙ ВЫВОД
Ниже приведен демонстрационный пример вывода консольной программы CLR,
вычисляющий цену коврового покрытия.
// Ех2_13.срр : главный файл проекта.
// Вычисление цены коврового покрытия
#include "stdafx.h"
using namespace System;
int main(array<System::String Л> Aargs)
{
double carpetPriceSqYd = 27.95;
double roomwidth = 13.5; // В футах
double roomLength =24.75; // В футах
const int feetPerYard = 3;
double roomWidthYds = roomWidth/feetPerYard;
double roomLengthYds = roomLength/feetPerYard;
double carpetPrice = roomWidthYds*roomLengthYds*carpetPriceSqYd;
Console::WriteLine(Ь"Размер комнаты {0:F2} ярдов на {1:F2} ярдов",
roomLengthYds, roomWidthYds);
Console::WriteLine(Е"Площадь комнаты {0:F2} квадратных ярдов",
roomLengthYds*roomWidthYds);
Console::WriteLine(Е"Цена коврового покрытия ${0:F2}", carpetPrice);
return 0;
}
Вывод этой программы должен быть таким:
Данные, переменные и вычисления 131
Размер комнаты 8.25 ярдов на 4.50 ярдов
Площадь комнаты 37.13 квадратных ярдов
Цена коврового покрытия $1037.64
Press any key to continue . . .
Описание полученных результатов
Размеры комнаты указаны в футах, в то время как цена коврового покрытия
определяется площадью в квадратных ярдах, поэтому вы определяете константу
feetPerYard, чтобы использовать ее при пересчете футов в ярды. В выражении
преобразования каждое измерение получается в результате деления значения типа
double на int. Компилятор вставит код для преобразования типа int в double, преж-
де чем выполнит умножение. После преобразования размеров комнаты в ярды выпол-
няется вычисление цены коврового покрытия перемножением размеров в ярдах, что-
бы получить площадь, и умножением этой площади на цену за квадратный ярд.
Операторы вывода используют спецификатор формата F2, чтобы ограничить вы-
ходные значения двумя десятичными знаками после точки. Без этого в выводе мо-
жет получиться больше знаков, что нежелательно, особенно когда речь идет о цене.
Чтобы увидеть разницу, можете удалить спецификатор формата.
Обратите внимание, что оператор вывода площади использует арифметическое
выражение во втором аргументе функции WriteLine (). Компилятор сначала вычис-
лит это выражение, а затем передаст результат в виде аргумента функции. В общем
случае вы всегда можете использовать выражение в качестве аргумента функции — до
тех пор, пока тип полученного результата согласуется с типом параметра функции.
Клавиатурный ввод в C++/CLI
Возможности ввода с клавиатуры, которые имеются у консольных программ .NET
Framework, несколько ограничены. Вы можете прочесть полную строку ввода как тек-
стовую строку, используя для этого функцию Console: :ReadLine (), или же вы мо-
жете прочесть отдельный символ, применяя для этого функцию Console: :Read ().
Можно также прочесть нажатие клавиши функцией Console:: ReadKey ().
Функция Console: :ReadLine () используется следующим образом:
String* line = Console::ReadLine();
Это читает полную входную строку текста, завершенную нажатием клавиши
<Enter>. Переменная line имеет тип String* и хранит ссылку на строку, которая
получается в результате выполнения функции Console: :ReadLine О . Маленький
символ Л, который следует за именем типа String, указывает, что это — дескриптор
(handle), который ссылается на объект типа String. О типе String и дескрипторах
объектов String вы узнаете в главе 4.
Оператор, читающий один символ с клавиатуры, выглядит следующим образом:
char ch = Console::Read();
С помощью функции Read () вы можете читать входные данные символ за симво-
лом, и затем анализировать прочитанные символы и преобразовывать их в соответ-
ствующие числовые значения.
Функция Console::ReadKey () возвращает нажатую клавишу в виде объекта класса
ConsoleKeylnfо, который представляет собой класс типа значения, определенный в
пространстве имен System. Вот оператор чтения нажатой клавиши:
ConsoleKeylnfo keyPress - Console::ReadKey(true);
132 Глава 2
Аргумент true функции ReadKey () подавляет отображение нажатой клавиши в
командной строке. Аргумент false (или отсутствие аргумента) заставляет функцию
отображать символ нажатой клавиши. Результат выполнения функции сохраняется
в keyPress. Чтобы идентифицировать символ, соответствующий нажатой клавише
(или клавишам), необходимо использовать выражение keyPress .KeyChar. Таким об-
разом, чтобы вывести сообщение, показывающее символ нажатой клавиши, нужно
воспользоваться следующим оператором:
Console::WriteLine(Ь"Нажатая клавиша соответствует символу: {0}",
keyPress.KeyChar);
Нажатая клавиша идентифицируется выражением keyPress .Key. Это выражение
ссылается на значение из перечисления C++/CLI (с которым вы вскоре познакоми-
тесь), идентифицирующее нажатую клавишу. Об объекте ConsoleKeylnfo можно ска-
зать еще много чего. И в этой книге мы еще к нему вернемся.
Хотя то, что в консольных программах C++/CLI нет форматирования ввода, мо-
жет показаться неудобным во время изучения, на практике это ограничение не суще-
ственно. Почти все реальные программы, которые вам придется писать, принимают
ввод через компоненты окна, поэтому обычно нет необходимости читать данные из
командной строки.
Применение safe_cast
Операция safe_cast предназначена для явных приведений типов в среде CLR. В
большинстве случаев вы можете без проблем использовать static_cast для приведе-
ния одного типа к другому в программах C++/CLI, но поскольку есть исключения, ко-
торые приводят к сообщениям об ошибках, лучше все-таки использовать safe_cast.
Приведение safe_cast применяется точно так же, как static_cast. Например:
double valuel = 10.5;
double value2 = 15.5;
int whole_number = safe_cast<int>(valuel) + safe_cast<int>(value2);
Последний оператор приводит каждое из значений типа double к типу int перед
тем, как сложить их и присвоить результат whole_number.
Перечисления C++/CLI
Перечисления в программах C++/CLI существенно отличаются от тех, что приме-
няются в программах ISO/ANSI C++. Прежде всего, они и определяются в C++/CLI
иначе:
enum class Suit{Clubs, Diamonds, Hearts, Spades};
Этот оператор определяет перечислимый тип Suit (масть), и переменным типа
Suit можно присваивать значения только из перечня, заданного в определении —
Hearts, Clubs, Diamonds и Spades. Когда вы обращаетесь к константам из перечис-
ления C++/CLI, вы всегда должны квалифицировать их именем типа перечисления.
Например:
Suit suit = Suit: :Clubs;
Этот оператор присваивает значение Clubs из перечисления Suit переменной по
имени suit. Символ ::, отделяющий имя типа Suit от имени перечислимой констан-
ты Clubs — это операция разрешения контекста, которая указывает на то, что Clubs
существует внутри контекста перечисления Suit.
Данные, переменные и вычисления 133
Обратите внимание на ключевое слово class в определении перечисления, кото-
рое следует за ключевым словом enum. Оно не появлялось в определении перечисле-
ний в ISO/ANSI C++, как вы видели ранее, а идентифицирует только перечисления
C++/CLL Кроме того, оно указывает на еще одно отличие от перечислений ISO/ANSI
C++; здесь константы, определенные в перечислении — Hearts, Clubs и так далее —
являются объектами, а не простыми значениями фундаментального типа, как в версии
ISO/ANSI C++. Фактически, по умолчанию они являются объектами типа Int32, поэ-
тому каждая из них инкапсулирует значение типа int; однако перед тем как пытаться
использовать их как таковые, вы должны привести эти константы к типу int.
Поскольку перечисления C++/CLI являются типом класса, вы не можете объяв-
лять их локально, например, внутри функции, поэтому если вы хотите определить та-
кое перечисление для использования, к примеру, в main (), то должны объявить его в
глобальном контексте.
Это легко проиллюстрировать примером.
Практическое занятие
Определение перечисления C++/CLI
Ниже предлагается очень простой пример использования перечисления:
// Ех2_14.срр : главный файл проекта.
// Определение и использование перечисления C++/CLI.
#include "stdafx.h"'
using namespace System;
// Определение перечисления в глобальном контексте
enum class Suit{Clubs, Diamonds, Hearts, Spades};
int main(array<System::String Л> Aargs)
{
Suit suit = Suit::Clubs;
int value = safe_cast<int> (suit);
Console::WriteLine(Ь"Масть - {0} и значение - {1} ", suit, value);
suit = Suit::Diamonds;
value = safe_cast<int>(suit);
Console::WriteLine(Ь"Масть - {0} и значение - {1} ", suit, value);
suit = Suit::Hearts;
value = safe_cast<int>(suit);
Console::WriteLine(Ь"Масть - {0} и значение - {1} ", suit, value);
suit = Suit::Spades;
value = safe_cast<int>(suit);
Console::WriteLine(Ь"Масть - {0} и значение - {1} ", suit, value);
return 0;
Этот пример выдаст на консоль следующее:
}
Масть - Clubs и значение - О
Масть - Diamonds и значение - 1
Масть - Hearts и значение - 2
Масть - Spades и значение - 3
Press any key to continue . . .
Описание полученных результатов
Поскольку это тип класса, перечисление Suit не может быть определено внутри
функции main (), поэтому его определение помещено перед определением main (), а
134 Глава 2
потому относится к глобальному контексту. В примере объявляется переменная suit
типа Suit и размещает в ней значение Suit::Clubs следующим оператором:
Suit suit = Suit::Clubs;
Квалификация имени константы Clubs именем типа Suit существенна; без нее
компилятор не может распознать Suit.
Если вы посмотрите на вывод, то увидите, что значение переменной suit отобра-
жается как имя соответствующей константы, в первом случае — Clubs. Чтобы полу-
чить константное значение, соответствующее объекту, вы должны явно привести его
к типу int, как в следующем операторе:
value = safe_cast<int>(suit);
Из вывода программы видно, что константам перечисления присвоены значения,
начиная с 0. Фактически вы можете изменить тип, используемый для перечислимых
констант. В следующем разделе мы посмотрим, как это делается.
Спецификация типа перечислимых констант
Константы в перечислениях C++/CLI могут относиться к любому из следующих
типов:
short
unsigned
short
unsigned
long
unsigned
long
long long
unsigned
long long
signed char
unsigned
char
char
bool
Чтобы специфицировать тип констант перечисления, название типа констант за-
писывается после имени типа перечисления и отделяется двоеточием — как и в пере-
числениях “родного” C++. Например, чтобы специфицировать константу перечисле-
ния как char, потребуется написать:
enum class Face : char {Ace, Two, Three, Four, Five, Six, Seven,
Eight, Nine, Ten, Jack, Queen, King};
Константы в перечислении будут иметь тип Char, а лежащий в их основе фунда-
ментальный тип будет char. Первая константа по умолчанию соответствует кодовому
значению 0, а все последующие присваиваются по порядку возрастания. Чтобы полу-
чить лежащее в основе значение, следует явно привести его к требуемому типу.
Спецификация значений для перечислимых констант
Вы не обязаны принимать значения по умолчанию для констант перечисле-
ний. Можно явно присвоить значения любой или всем константам перечисления.
Например:
enum class Face : char {Асе = 1, Two, Three, Four, Five, Six, Seven,
Eight, Nine, Ten, Jack, Queen, King};
В результате этого Асе будет иметь значение 1, Two - 2 и так далее, вплоть до
King, которое примет значение 13. Если же вы хотите, чтобы значения констант ото-
бражали старшинство карт, где самый старший — туз (Асе), то можете написать опре-
деление перечисления так:
enum class Face : char {Асе = 14, Two = 2, Three, Four, Five, Six, Seven,
Eight, Nine, Ten, Jack, Queen, King};
В этом случае Two принимает значение 2, а последующие константы — последую-
щие значения в порядке возрастания, так что King опять будет равен 13. Асе получа-
ет значение 14 — в соответствии с явным присваиванием.
Данные, переменные и вычисления 135
1
Значения, присвоенные перечислимым константам, не обязательно должны быть
уникальными. Это дает возможность использовать значения констант для выражения
некоторого дополнительного свойства. Например:
enum class WeekDays : bool { Mon =true, Tues = true, Wed = true,
Thurs = true, Fri = true, Sat = false, Sun = false };
Здесь определено перечисление WeekDays, константы которого имеют тип bool.
Лежащие в основе значения присвоены явно, символизируя отличие выходных от
будней.
Резюме
В этой главе вы ознакомились с основами вычислений на C++. Вы изучили все
элементарные типы данных, представленные в языке, и все операторы для непосред-
ственного манипулирования этими типами. Ниже кратко передана суть того, о чем
было сказано в этой главе.
□ Программа на C++ состоит как минимум из одной функции по имени main ().
□ Исполняемая часть функции состоит из операторов, заключенных в фигурные
скобки.
□ Операторы в C++ завершаются точкой с запятой.
□ Именованные объекты C++, такие как переменные и функции, могут иметь
имена, состоящие из последовательностей букв и цифр, начинающихся с бук-
вы, причем знак подчеркивания трактуется как буква. Символы верхнего и
нижнего регистра считаются различными.
□ Для именования объектов программы, таких как переменные, нельзя исполь-
зовать ключевые слова C++. Полный список зарезервированных слов C++ при-
веден в приложении А.
□ Все константы и переменные в C++ относятся к определенному типу. Фундамен-
тальные типы ISO/ANSI C++ — char, int, long, float и double. В C++/CLI
также определены типы Inti6, Int32 и Int64.
□ Имя и тип переменной определяются в операторе объявления, завершающем-
ся точкой с запятой. В объявлениях может выполняться инициализация пере-
менных начальными значениями.
□ Можно защищать значения переменных базовых типов с помощью модифика-
тора const. Это предотвращает непосредственные модификации таких пере-
менных в программе. В любом месте программы, где обнаружится попытка из-
менить значение константы, выдается ошибка компиляции.
□ По умолчанию переменная является автоматической; это означает, что она су-
ществует, только начиная с точки ее объявления и до конца блока, в котором
она объявлена, указанного закрывающей фигурной скобкой.
□ Переменная может быть объявлена как static. В этом случае она продолжа-
ет существовать в течение всего времени выполнения программы. Но доступ к
ней открыт только в области видимости (блоке), где она определена.
□ Переменные могут быть объявлены вне любых блоков программы — в этом слу-
чае они имеют глобальную область видимости. Переменные, имеющие глобаль-
ную область видимости, доступны из любой ее точки, за исключением тех мест,
136 Глава 2
где существуют локальные переменные с теми же именами. Но даже там к ним
можно обратиться, используя операцию разрешения контекста.
□ Пространство имен определяет область видимости, объявленные в которой
имена квалифицированы дополнительным именем — именем пространства
имен. Обращение к таким именам извне этого пространства имен требует обя-
зательной квалификации (полного имени).
□ Стандартная библиотека ISO/ANSI C++ содержит функции и операторы, кото-
рые вы можете применять в своих программах. Все они объявлены в простран-
стве имен std. Корневое пространство имен библиотек C++/CLI называется
System. Получить доступ к индивидуальным объектам в пространстве можно
путем указания имени пространства имен в составе квалифицированного име-
ни с применением операции разрешения контекста, либо после указания в
объявлении using имени этого пространства имен.
□ lvalue — это объект, который может стоять в левой части выражения присваи-
вания. Примерами lvalue могут быть неконстантные переменные.
□ Можно смешивать разнотипные переменные и константы в пределах одного
выражения, но при этом они при необходимости автоматически преобразуют-
ся к одному общему типу. Преобразование типа выражения, стоящего справа
от знака равенства в выражениях присваивания, к типу левой части также при
необходимости выполняется автоматически. Это может привести к потере ин-
формации, когда тип левого элемента не может содержать информацию типа
правого выражения: например double, преобразованное в int, или long, пре-
образованное в short.
□ Значение выражения может быть явно приведено к другому типу. Вы всегда
должны выполнять явное приведение, когда существует опасность потери ин-
формации. Бывают ситуации, когда необходимо специфицировать явное при-
ведение, чтобы получить необходимый результат.
□ Ключевое слово typedef позволяет определять синонимы для других типов.
Хотя я привел описание всех фундаментальных типов, не стоит думать, что это
все типы, которые имеются. Как вы вскоре увидите, существуют также более слож-
ные типы, основанные на базовом наборе, а вдобавок к этому со временем вы научи-
тесь создавать и собственные типы.
В этой главе вы узнали о трех стратегиях кодирования, которых можно придержи-
ваться при написании программ C++/CLI.
□ Вы должны использовать имена фундаментальных типов для переменных, но
иметь в виду, что на самом деле они — синонимы имен классов типа значения в
программах C++/CLI. Важность этого обстоятельства станет более очевидной,
когда вы узнаете больше о классах.
□ Вы должны использовать в своем коде C++/CLI safe_cast , а не static_cast.
Разница между ними более важна в контексте объектов классов, но если вы вы-
работаете привычку применять safe_cast, то можно ожидать, что тем самым
избежите проблем.
□ Для объявления перечислимых типов в C++/CLI вы должны использовать enum
class.
Данные, переменные и вычисления 137
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Напишите программу ISO/ANSI C++, которая приглашает пользователя ввести
число, а затем печатает его, используя локальную целочисленную переменную.
2. Напишите программу, которая читает целое значение, вводимое с клавиатуры
в переменную типа int и использует одну из битовых операций (но не %!) для
определения положительного остатка от деления на 8. Например, 29 = (3 х 8) + 5
и -14 = (-2 х 8) + 2 имеют положительные остатки 5 и 2 соответственно.
3. Снабдите скобками следующие выражения, чтобы проиллюстрировать приори-
теты и ассоциативность:
1 + 2 + 3 + 4
16 * 4 / 2 * 3
a>b?a: с > d? е: f
a&b&&c&d
4. Создайте программу, которая вычисляет коэффициент пропорциональности
(aspect ratio) экрана вашего компьютера по заданным значениям ширины и вы-
соты в пикселях, используя следующие операторы:
int width = 1280;
int height = 1024;
double aspect = width / height;
Какой ответ вы получите в результате? Будет ли он удовлетворительным? Если
нет — как следует модифицировать код, не добавляя новых переменных?
5. (Усложненное.) Не запуская на выполнение следующий код, можете ли вы ска-
зать, какой результат он выдаст и почему?
unsigned s = 555;
int i = (s » 4) & ~(~0 « 3);
cout « i;
6. Напишите консольную программу C++/CLI, которая использует перечисле-
ние для идентификации месяцев года значениями номеров месяцев от 1 до 12.
Программа должна выдавать каждую константу перечисления и лежащее в осно-
ве значение.
7. Напишите консольную программу C++/CLI, которая вычислит площади трех
комнат до ближайшего числа полных квадратных футов. Размеры комнат:
Комната 1: 10,5 на 17,6, Комната 2: 12,7 на 18,9, Комната 3: 16,3 на 15,4
Программа также должна вычислить и вывести среднюю площадь этих трех
комнат, а также их общую площадь, причем в каждом случае результат должен
быть ближайшим целым числом квадратных футов.
3
Решения и циклы
В этой главе вы узнаете, как можно добавить в программы C++ средства принятия
решений. Кроме того, вы научитесь тому, как заставить программу повторно выпол-
нять набор действий до наступления определенного условия. Это позволит вам обра-
батывать значительные объемы входной информации, а также выполнять проверку
корректности прочтенных данных. Вы сможете писать программы, которые адапти-
руют свое поведение в зависимости от входных данных, и иметь дело с проблемами,
где принятие решений основано на логике. После изучения этой главы вы узнаете
следующие моменты.
□ Как сравнивать значения данных.
□ Как изменять последовательность выполнения программы на основе некоторо-
го результата.
□ Как применять логические операции и выражения.
□ Как справляться с ситуациями с множеством вариантов выбора.
□ Как писать и использовать циклы в программах.
Начну я с одного из наиболее мощных и фундаментальных инструментов програм-
мирования: возможности сравнения переменных и выражений с другими перемен-
ными и выражениями, чтобы на основе полученного результата выполнять один или
другой набор операторов.
Сравнение значений
Если только вы не хотите принимать решения наобум, то вам понадобится меха-
низм сравнения сущностей. Это вовлекает в дело некоторые новые операции, называ-
емые операциями отношений. Поскольку вся информация в компьютере в конечном
итоге представлена в числовом виде (в предыдущей главе вы видели, как в числовом
виде представляется символьная информация), сравнение числовых значений — суть
140 Глава 3
практически всего механизма принятия решений. Существует шесть фундаменталь-
ных операции для сравнения двух доступных значении:
меньше чем
больше чем
равно
с= меньше или равно
>= больше или равно
! = не равно
Операция сравнения “равно” состоит из двух подряд знаков равенства. Это не то же са-
мое, что операция присваивания, которая состоит лишь из одного знака равенства.
Распространенная ошибка связана с использованием операции присваивания вместо опера-
ции сравнения, поэтому будьте внимательны.
Каждая из этих операций сравнивает значения двух своих операндов и возвраща-
ет одно из двух возможных значений типа bool: true — если сравнение истинно, и
false — если нет. Вы можете увидеть, как это работает, рассмотрев несколько про-
стых примеров сравнений. Предположим, что объявлены целочисленные перемен-
ные i и j со значениями 10 и -5 соответственно. Все представленные ниже выраже-
ния возвращают значение true:
j > -8 i <= j + 15
Далее предположим, что вы определили следующие переменные:
char first = ’А’, last = ’Z’;
Вот несколько примеров сравнений, использующих эти символьные переменные:
first == 65 first < last 'Е* <= first first != last
Все четыре выражения сравнивают значения кодов ASCII. Первое выражение воз-
вращает true, потому что first инициализировано символом ’ А’, что эквивалент-
но десятичному числу 65. Второе выражение проверяет, меньше ли значение first,
которое равно ’ А’, чем значение last, которое равно ’ Z ’. Если заглянуть в табли-
цу кодов символов ASCII в приложении Б, то мы увидим, что заглавные буквы пред-
ставлены последовательными числовыми величинами — от 65 до 90, при чем 65 пред-
ставляет ’ А *, а 90 — ’ Z *, поэтому второе сравнение также вернет true. Третье же
выражение вернет false, потому что ’Е’ больше, чем значение first. Последнее
выражение вернет true, поскольку ’А’ определенно не равно • Z ’.
Теперь рассмотрим несколько более сложные сравнения чисел. Имея переменные,
определенные следующим образом:
int i = -10, j = 20;
double x = 1.5, у = -0.25E-10;
взгляните на такие выражения:
-1 < у j < (10 - i) 2.0*х >= (3 + у)
Как видите, в качестве операндов сравнения можно использовать выражения,
возвращающие числовые значения. Если вы заглянете в таблицу приоритетов, при-
веденную в главе 2, то увидите, что скобки не являются совершенно необходимыми,
однако они помогают сделать выражения яснее. Первое сравнение истинно, поэтому
возвращает bool-значение true. Переменная у содержит очень малое отрицательное
число, — 0,000000000025, а потому оно больше, чем -1. Второе сравнение возвращает
false. Выражение 10 — i равно 20, то есть тому же, что и j. Третье выражение воз-
вращает true, потому что 3 + у чуть меньше, чем 3.
Вы можете использовать операции отношений для сравнения значений любого
фундаментального типа, поэтому все, что вам нужно — какой-то практический способ
использования результатов сравнения для модификации поведения программы.
Оператор if
Базовый оператор i f позволяет программировать выполнение единственно-
го оператора или блока операторов, заключенных в фигурные скобки, если данное
условное выражение оценено как истинное, или же пропустить оператор или блок
операторов, если условие оценено как ложное. Это показано на рис. 3.1.
if( условие)
условие
оценивается как true
условие
оценивается как false
// Операторы
// Еще операторы
Рис. 3.1. Базовый оператор i
Вот простой пример оператора if:
if(letter == ’А’)
cout « "Первая заглавная, в алфавитном порядке.’’;
Проверяемое условие помещается в скобки, следующие за ключевым словом if,
после чего следует оператор, который должен быть выполнен, если условие возвра-
щает true. Обратите внимание на то, где находится точка с запятой. Она идет после
следующего за if со скобками оператора; то есть, не должно быть точки с запятой сра-
зу после проверочного условия в скобках, так как эти две строки, по сути, составляют
единый оператор. Вы также можете видеть, как оператор, следующий за if, сдвинут,
чтобы отметить, что он должен быть выполнен только в том случае, когда условие if
вернет true. Такой отступ не является необходимым, чтобы программа компилиро-
валась и исполнялась, но он помогает вам увидеть отношение между условием i f и
оператором, который от него зависит. Оператор вывода в этом фрагменте кода вы-
полняется только в том случае, если переменная letter содержит значение А’.
Вы можете расширить этот пример, добавив оператор, изменяющий значение
letter, если оно равно ‘А’:
if(letter == ’А’)
cout « "Первая заглавная, в алфавитном порядке.’’;
letter = ’ а ’;
142 Глава 3
Блок операторов, управляемый оператором if, ограничен фигурными скобками.
Здесь операторы блока выполняются, только если условие (letter == ' А') оцени-
вается как true. Без этих скобок лишь первое из них было бы субъектом if, а при-
сваивание ' а' переменной letter выполнялось бы всегда, независимо от условия if.
Обратите внимание, что после каждого оператора в блоке ставится точка с запятой,
но не после закрывающей фигурной скобки. В блоке может быть столько операторов,
сколько вам нужно. Здесь в результате того, что letter содержит ' А', его значение
изменяется на ' а ’ после вывода такого же сообщения, как и раньше. Если же условие
вернет false, ни один из операторов блока не выполняется.
Вложенные операторы if
Оператор, который должен быть выполнен в случае истинности условия if, также
может быть еще одним оператором if. Такая организация называется вложенным
if. Условие вложенного if проверяется только в случае истинности условия внешне-
го if. Вложенный if, в свою очередь, может также содержать еще один вложенный
if. Вы можете вкладывать операторы if друг в друга на любую глубину, если только
знаете, что делаете.
Практическое занятие
Использование вложенных операторов if
Рассмотрим пример вложенных операторов i f в рабочем примере.
// ЕхЗ_01.срр
// Демонстрация вложенных операторов if
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main ()
{
char letter =0; // Для хранения введенного значения
cout « endl
« ’’Введите букву: ’’; // Приглашение ввода
cin » letter; // прочитать символ
if (letter >= ’А’) // Больше или равно ’А’
if (letter <= ’Z’) // Меньше или равно ’ Z’
{
cout « endl
« ”Вы ввели заглавную букву.”
« endl;
return 0;
}
if (letter >= ’a’) // Больше или равно ’а’
if (letter <= ’z’) // Меньше или равно ’z’
{
cout « endl
« ”Вы ввели прописную букву.”
« endl;
return 0;
}
cout « endl « ”Вы ввели не букву." « endl;
return 0;
}
Решения и никлы 143
Описание полученных результатов
Программа начинается с обычных строк комментариев; затем идет оператор
#include для включения заголовочного файла поддержки ввода-вывода и объявле-
ние using для cin, cout и endl из пространства имен std. Первое действие в теле
main () — приглашение пользователю на ввод буквы. Введенная буква сохраняется в
переменной типа char по имени letter.
Оператор if, который следует за вводом, проверяет введенный символ на пред-
мет того, больше или равен он ’ А ’. Поскольку коды ASCII для прописных букв
(от 97 до 122) больше, чем коды заглавных букв (от 65 до 90), ввод прописной бук-
вы заставит программу выполнить блок первого оператора if, потому что условие
(letter >= ’А’) возвращает true для всех букв. В этом случае выполняется вло-
женный оператор if, который проверяет, меньше или равен введенный символ 1Z •.
Если он равен ’ Z ’ или меньше, это значит, что введена заглавная буква, отображается
соответствующее сообщение, и на этом программа завершается оператором return.
Оба оператора заключены в фигурные скобки, поэтому оба они выполняются, если
условие вложенного if возвращает true.
Следующие операторы i f проверяют, является ли введенный символ буквой ниж-
него регистра с помощью, по сути, того же механизма, что и в первой паре if, ото-
бражают сообщения и завершают программу.
Если же введенный символ не является буквой, выполняется оператор вывода,
следующий за последним блоком if. Он выдает сообщение о том, что введенный сим-
вол не был буквой. Затем выполняется return и программа завершается.
Как видите, отношения между вложенными if и оператором вывода гораздо про-
ще отследить, имея отступы в строках кода.
Ниже показан пример выполнения этого примера.
Введите букву: Т
Вы ввели заглавную букву.
Можно легко организовать преобразование символа верхнего регистра в ниж-
ний, добавив только один дополнительный оператор к if, проверяющий верхний
регистр:
if (letter >= 'А’) // Больше или равно ’А’
if (letter <= ’Z’) // Меньше или равно ’Z’
cout « endl
« "Вы ввели заглавную букву."
« endl;
letter += 'а* - 'А’; // Преобразовать в нижний регистр
return 0;
Здесь добавлено один дополнительный оператор. Он преобразует заглавную букву
в прописную, увеличивая значение letter на разность • а• - ’А’. Это работает, по-
тому что ASCII-коды от • А • до • Z • и от ’ а ’ до 1 z • представляют две непрерывных
группы последовательных числовых кодов, поэтому выражение • а • - • А • представ-
ляет значение, которое должно быть добавлено к букве верхнего регистра, чтобы по-
лучить эквивалентную букву нижнего регистра.
С тем же успехом вы могли бы применить здесь эквивалентные коды ASCII, но за
счет использования букв гарантируется, что программа будет работать и на компью-
терах, где применяется кодировка символов, отличная от ASCII — до тех пор, пока
множества прописных и заглавных букв представлены непрерывными последователь-
ностями числовых значений.
144 Глава 3
Существует библиотечная функция ISO/ANSI C++ для преобразования букв в верхний ре-
гистр, поэтому обычно вам не придется программировать эту операцию самостоятельно.
Она называется toupper (), и ее заголовок содержится в стандартном библиотечном фай-
ле <ctype>. Позднее вы узнаете больше о библиотечных средствах, когда мы поговорим о
написании функций.
Расширенный оператор if
Оператор if, который вы использовали до сих пор, выполнял оператор или блок,
если указанное условие возвращало true. Затем выполнение программы продолжа-
лось со следующего оператора по порядку. Но есть и другая версия if, которая позво-
ляет выполнить один оператор, когда условие if возвращает true, и другой — когда
оно возвращает false. После этого выполнение программы продолжается со следу-
ющего оператора по порядку. Как вы уже видели в главе 2, блок операторов всегда
может заменить один оператор, и это также касается расширенной версии if.
Практическое занятие
Расширение оператора if
Ниже показан пример с расширенным оператором if.
// ЕхЗ_02.срр
// Использование расширенного оператора if
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
long number = 0; // Здесь хранится ввод
cout « endl
« "Введите целое число меньше 2 миллиардов: ";
cin » number;
if(number % 2L) // Проверить остаток после деления на 2
cout « endl // Здесь остаток равен 1
« "Ваше число нечетное." « endl;
else
cout « endl // Here if remainder 0
« "Ваше число четное." « endl;
return 0;
}
Типичный вывод этой программы:
Введите целое число меньше 2 миллиардов: 123456
Ваше число четное.
Описание полученных результатов
После чтения входного значения в number оно проверяется на четность путем взя-
тия остатка от деления на 2 (используя для этого операцию %, с которой вы познако-
мились в предыдущей главе) и проверки его в условии оператора if. В данном случае
условие оператора if возвращает целое, а не булевское значение. Оператор if ин-
терпретирует ненулевое значение, возвращенное условием, как true, а нулевое — как
false. Другими словами, условное выражение:
(number % 2L)
145
эквивалентно такому:
(number % 2L != 0)
Если остаток равен 1, условие равно true, и тогда выполняется оператор, следу-
ющий непосредственно за if. Если же остаток равен 0, то условие оценивается как
false, и тогда выполняется оператор, следующий за ключевым словом else.
Условие оператора i f может быть выражением, возвращающим значение любого из фунда-
ментальных типов данных, с которыми вы познакомились в главе 2. Когда условное выраже-
ние возвращает числовое значение вместо bool, то компилятор вставляет автоматическое
приведение результата такого выражения к типу bool. Приведение к bool ненулевого зна-
чения дает true, а нулевого— false.
Поскольку остатком от деления целого на 2 может быть только единица или ноль,
я снабдил код комментарием, отражающим этот факт. После любого исхода оператор
return завершает программу.
Ключевое слово else пишется без точки с запятой— так же, как часть оператора if.
Опять-таки, отступы служат для визуального выделения отношений между операторами.
Благодаря им, вы можете ясно видеть, какой оператор выполняется в случае true или нену-
левого результата, а какое— в случае false или нуля. Вы всегда должны сдвигать операторы
в своей программе, чтобы подчеркнуть ее логическую структуру.
Комбинация if-else предоставляет выбор между двумя возможностями. Общая
логика if-else показана на рис. 3.2.
if( условие)
// И еще операторы
Рис. 3.2. Оператор if~else
146 Глава 3
Стрелочки на диаграмме указывают последовательность выполнения операторов,
в зависимости от того, возвращает условие if значение true или false.
Вложенные операторы if-else
Как вы уже видели, можно вкладывать операторы if в другие операторы if.
Точно так же можно вкладывать операторы i f - е 1 s е внутрь операторов i f, а опера-
торы if — внутрь if-else. Здесь существует некоторая возможность путаницы, поэ-
тому рассмотрим несколько примеров. Ниже представлен пример вложения if-else
внутрь if.
if(coffee == ’ у ’)
if(donuts == ’ у’)
cout « "У нас есть кофе и пончики.";
else
cout « "У нас есть кофе, но нет пончиков.";
Проверка donuts выполняется только в случае, когда проверка coffee возвраща-
ет true, поэтому сообщения корректно отражают ситуацию в каждом случае. Однако
здесь легко запутаться. Если вы напишете то же самое с неправильными отступами,
то, глядя на код, можете прийти к неверным заключениям.
if(coffee == ’у’)
if(donuts == ’у’)
cout « "У нас есть кофе и пончики.";
else // Этот else выровнен неправильно
cout « "У нас есть кофе..."; // Не верно!
Здесь сравнительно легко обнаружить ошибку, но в случае более сложных структур
if вам следует помнить о правиле, определяющем, какому if принадлежит else.
else всегда относится к ближайшему предшествующему if, у которого нет другого else.
В любом случае, когда код выглядит сложным, вы можете применить это правило,
чтобы разобраться. А когда пишете собственную программу, всегда применяйте фигур-
ные скобки, чтобы сделать ситуацию яснее. Без них можно обойтись в таком простом
случае, но, тем не менее, вы могли бы записать последний пример следующим образом:
if(coffee == ’ у’)
if(donuts == 'у')
cout « "У нас есть кофе и пончики.";
else
cout « "У нас есть кофе, но нет пончиков.";
И в этом случае не остается места для неправильного толкования. Теперь, когда
вы знаете правила, понять случай с вложением if внутрь if-else будет нетрудно.
if(coffee == ’ у’)
if(donuts == ’у’)
cout « "У нас есть кофе и пончики.";
else
if (tea == ' у')
cout « "У нас есть кофе, но нет пончиков.";
147
Здесь скобки важны. Если их убрать, то else будет относиться ко второму if, ко-
торый проверяет donuts. В ситуации подобного рода легко забыть о скобках, и тем
самым породить ошибку, которую будет трудно обнаружить. Программы с такими
ошибками компилируются успешно, и даже иногда выдают правильные результаты.
Если в данном примере удалить фигурные скобки, то вы получите корректный ре-
зультат только тогда, когда и coffee, и donuts равны • у *, то есть тогда, когда про-
верка if (tea == ’ у •) не выполняется.
А теперь рассмотрим случай вложения операторов if-else в другие операторы
if-else. Такая конструкция может показаться весьма запутанной, даже при одном
уровне вложенности.
if(coffee ==
if(donuts
cout «
else
cout «
else
if(tea ==
cout «
else
cout «
'У')
== 'у' )
"У нас есть кофе и пончики.";
"У нас есть кофе, но нет пончиков.’’;
’У’)
"У нас нет кофе, но есть чай, и может быть, пончики. .
"Ни чая, ни кофе, но, может быть, хотя бы пончики..
Логика этого примера выглядит не столь очевидной, даже несмотря на правиль-
ные отступы. Необходимости в фигурных скобках нет, потому что правило, упомяну-
тое выше, выполняется, но код будет яснее, если скобки все-таки вставить.
if(coffee == ’ у’)
if(donuts
cout «
else
cout «
else
if(tea ==
cout «
else
— * у *)
"У нас есть кофе и пончики.";
"У нас есть кофе, но нет пончиков.’’;
"У нас
нет кофе, но есть чай, и может быть, пончики..
cout « "Ни чая, ни кофе, но может, хотя бы пончики...’’;
Однако существует гораздо лучший способ программирования логики такого рода.
Если вы собираете достаточно много if в одну конструкцию, то почти наверняка
рано или поздно допустите ошибку. В следующем разделе я покажу, как упростить си-
туацию.
Логические операции и выражения
Как видите, использование i f может выглядеть несколько неуклюже, когда нужно
проверить два или более взаимосвязанных условия. В предыдущем примере мы по-
пробовали применить if для выяснения ситуации с кофе и пончиками, но на прак-
тике может понадобиться проверять намного более сложные условия. Вы можете ис-
кать в файле персонала кого-нибудь старше 21, но младше 35 лет, женского пола, с
дипломом колледжа, не замужем и разговаривающую на хинди или урду. Определение
148 Глава 3
такого проверочного условия может породить монстроподобную конструкцию опе-
раторов if.
Простое и четкое решение обеспечивают логические операции. Применяя ло-
гические операции, вы можете комбинировать серии сравнений в одно логическое
выражение, так что в конечном итоге понадобится всего один оператор if — почти
независимо от сложности набора условий, до тех пор, пока он сводится в конечном
итоге к выбору из двух альтернатив (true или false).
Существуют всего три логических операции.
& & Логическое И
I | Логическое ИЛИ
! Логическое НЕ
Логическое И
Операция логического И (&&) применяется тогда, когда есть два условия, и оба
должны вернуть результат true, чтобы общий результат был равен true. Вы хотите
быть богатым и здоровым. Например, вы можете использовать операцию & & при про-
верке принадлежности символа к буквам верхнего регистра; проверяемое значение
должно быть больше или равно ’ А ’ И меньше или равно ’ Z •. Оба условия должны
вернуть true, чтобы значение относилось к заглавным буквам.
Как и ранее, условия, комбинируемые логическими операциями, могут возвращать числовые
значения. Помните, что ненулевые значения приводятся к true, а нулевые— к false.
Если вернуться к примеру, в котором проверяется значение типа char, сохранен-
ное в переменной letter, то можно было бы заменить проверку с двумя оператора-
ми i f на одну, использующую операцию & &:
if ((letter >= 'А') && (letter <= ' Z'))
cout « "Это заглавная буква.";
Скобки внутри проверочного выражения условия i f гарантируют, что операции
сравнения, вне всяких сомнений, будут выполнены первыми, и это делает весь опера-
тор яснее. Здесь вывод сообщения произойдет только в том случае, если оба условия,
объединенные операцией &&, окажутся истинными.
Как и в случае с бинарными операциями, о которых мы говорили в предыдущей
главе, эффект применения каждой логической операции можно представить табли-
цей истинности. Упомянутая таблица для && представлена в табл. 3.1.
Таблица 3.1. Таблица истинности для операции логического И
&&
false
false
false
false
true false true
Заголовки строк слева и заголовки столбцов сверху представляют значения ло-
гических выражений, комбинируемых операцией &&. Таким образом, чтобы опреде-
лить результат комбинации условия true с условием false, выберите строку с true
слева, и столбец с false сверху, а на пересечении строки и столбца ищите результат
(false). В действительности вам даже не нужна таблица истинности, поскольку все
достаточно просто: операция && возвращает true, только если оба операнда равны
true.
Решения и циклы 149
Логическое ИЛИ
Операция логического ИЛИ (| |) применяется тогда, когда имеются два условия и
нужно получить результат true, если любое из них или оба возвращают true. Напри-
мер, банк может положительно оценить вашу кредитоспособность для получения за-
йма, если ваш доход составляет не менее $100 000 в год или же у вас есть $1 000 000
наличными. Это может быть проверено с помощью следующего оператора if:
if((income >= 100000.00) I I (capital >= 1000000.00))
cout « "Сколько вы хотели бы занять, сэр (кланяясь)?";
Банковский клерк проявит любезность, когда любое из двух условий выполнено
либо оба сразу. (Более подходящим вопросом был бы: “Почему вы хотите занять де-
нег?”. Вообще-то странно, что банки готовы одалживать деньги только тогда, когда
вы в них совершенно не нуждаетесь.)
Таблица истинности для операции | | представлена в табл. 3.2.
Таблица 3.2. Таблица истинности для операции логического ИЛИ
false,
false
false
true
true
true
Здесь также результат может быть выражен очень просто: результат false получа-
ется только тогда, когда оба операнда операции | | равны false.
Логическое НЕ
Третья логическая операция — НЕ (!) — принимает один операнд типа bool и ин-
вертирует его значение. Поэтому если значением переменной test является true,
то ! test получит значение false. Если же test равно false, то !test будет равно
true. В качестве примера простого выражения, если х имеет значение 10, то выра-
жение:
будет false, поскольку х > 5 соответствует true.
Можно применить операцию ! к любимому выражению Чарльза Диккенса:
!(доходы
расходы)
Если это выражение возвращает true, то в результате вы получите нищету, по
крайней мере, как только банк перестанет оплачивать подписанные вами чеки.
И, наконец, вы можете применять операцию ! к другим базовым типам данных.
Предположим, что имеется переменная rate типа float, содержащая значение 3,2.
По некоторой причине вы можете пожелать убедиться, что значение rate отличает-
ся от нуля, в этом случае можно использовать следующее выражение:
!(rate)
Значение 3,2 отличается от нуля, а потому преобразуется в значение true типа
bool, и результат всего выражения будет false.
150 Глава 3
практическое занятие Комбинирование логических операций
Вы можете комбинировать условные выражения и логические операции так, как
вам будет удобно. Например, вы можете сконструировать тест, определяющий, отно-
сится ли символ к буквам, применив единственный оператор if. Давайте рассмотрим
рабочий пример.
// ЕхЗ_ОЗ.срр
// Проверка буквы с использованием логических операций
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
char letter =0; // Здесь сохранить ввод
cout « endl
« "Введите символ: ";
cin » letter;
if(((letter >= ’ A’) && (letter <= ’Z ’)) II
((letter >= ’a’) && (letter <= 'z'))) // Проверка на вхождение в алфавит
cout << endl
« "Вы ввели букву." « endl;
else
cout « endl
« "Вы ввели не букву." « endl;
return 0;
}
Описание полученных результатов
Пример начинается так же, как ЕхЗ_01. срр — с чтения символа, введенного поль-
зователем в ответ на приглашение. Что здесь интересно, так это условие оператора
if — оно состоит из двух логических выражений, скомбинированных операцией | |
(ИЛИ), так что если любое из них равно true, то все условие возвращает true, и на
экран выдается сообщение:
Вы ввели букву.
Если же оба выражения возвращают false, то выполняется оператор else, кото-
рый выдает сообщение:
Вы ввели не букву.
Каждое из логических выражений, объединенных | |, само состоит из комбина-
ций пары выражений, объединенных операцией && (И), поэтому каждое выражение,
участвующее в этих парах, должно возвращать true, чтобы данная пара вернула true.
Первое логическое выражение возвращает true, если введена буква верхнего реги-
стра, а второе — если это буква нижнего регистра.
Условная операция
Условную операцию иногда называют тернарной операцией, потому что она ра-
ботает с тремя операндами. Лучше всего объяснить ее на примере. Предположим,
Решения и циклы 151
что имеется две переменных - а и b - и вы хотите присвоить максимальное из этих
двух значений третьей переменной с. Это можно сделать с помощью следующего опе-
ратора:
с = а>Ь?а : Ь; // Присвоить с большее из двух значений а и b
Первый операнд условной операции должен быть выражением, которое возвра-
щает значение типа bool — true или false, как в случае с а > Ь. Если это выражение
возвращает true, то в качестве результирующего значения возвращается значение
второго операнда — а. Если же первый аргумент равен false, то возвращается зна-
чение третьего операнда — то есть Ь. Таким образом, результатом условного выраже-
ния а > b ? а : b будет а, если а больше Ь, и b — в противном случае. Это значение
записывается в переменную с операцией присваивания. Такое применение условной
операции в операторе присваивания эквивалентно следующей конструкции if:
if(а > Ь)
с = а;
else
с = Ь;
В общем виде условная операция может быть записана так:
условие 2 выражение! : выражение?
Если условие оценивается как true, результатом будет значение выражение!, если
же оно равно false, то результатом будет значение выражение?.
Практическое занятие | ИСППЛЬВОВЯНИЯ уСЛПВНПЙ ОПРрЯЦИИ при ВЫВОДЯ
Часто условная операция применяется для управления выводом, в зависимости от
результата выражения или значения переменной. Вы можете варьировать сообщение,
выбирая одну или другую текстовую строку, в зависимости от указанного условия.
// ЕхЗ_04.срр
// Условная операция для выбора вывода
#include <iostream>
using std::cout;
using std::endl;
int main()
{
int nCakes =1; // Количество пирожных
cout « endl
« "У нас есть " « nCakes « ’’ пирожн" « ((nCakes >1) ? ”ых.” : ”oe.’’)
« endl;
++nCakes;
cout « endl
« ”У нас есть ’’ « nCakes « ’’ пирожн" « ((nCakes >1) ? "ых." : "oe.")
« endl;
return 0;
}
Вывод этой программы:
У нас есть 1 пирожное.
У нас есть 2 пирожных.
152 Глава 3
Описание полученных результатов
Сначала вы инициализируете переменную nCakes значением 1; затем идет выра-
жение, выводящее на экран сообщение о количестве пирожных. Часть, использую-
щая условную операцию, просто проверяет значение переменной, чтобы определить,
сколько есть пирожных — одно или больше:
((nCakes > 1) ? ”ых." : ”ое.’’)
Выражение оценивается как "ых. ”, если nCakes больше 1, или ”ое. ” — в против-
ном случае. Это позволяет использовать один оператор вывода для любого количе-
ства пирожных, получая грамматически корректное сообщение. Это доказывает по-
вторный вывод сообщения после увеличения значения переменной nCakes.
’ Существует много аналогичных ситуаций, в которых вы можете применить этот
механизм; например, выбирая между "является" и "являются".
Оператор switch
Оператор switch дает возможность выбирать из множества вариантов, основыва-
ясь на наборе фиксированных значений заданного выражения. Он действует подобно
вращающемуся переключателю, который можно установить в одно из фиксирован-
ного числа положений. Некоторые стиральные машины снабжены подобным пере-
ключателем, которым выбирается режим стирки. Есть заданное число возможных
позиций, в которые его можно установить — хлопок, шерсть, синтетическое волокно
и тому подобное, и вы можете выбрать любой из них, установив переключатель в со-
ответствующее положение.
В операторе switch выбор определяется значением указанного выражения. Вы
устанавливаете возможные позиции switch одним или более case-значений, одно из
которых выбирается, если значение выражения совпадает с ним. Каждому возможно-
му значению выражения оператора switch соответствует одно case-значение, при-
чем эти case-значения должны отличаться друг от друга.
Если значение выражения switch не соответствует ни одному из case-значений,
то switch автоматически выбирает ветвь default. При желании вы можете специ-
фицировать код ветви default, как это показано ниже. Если этого не сделать, то ког-
да выражение switch не соответствует ни одному из case-значений, по умолчанию
не делается ничего, а управление просто передается оператору, следующему за завер-
шающей фигурной скобкой switch.
[Практическое занятие) ОПбрЭТОР Switch
Увидеть на практике работу оператора switch можно, запустив приведенный
ниже пример.
// ЕхЗ_05.срр
// Использование оператора switch
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
int choice =0; // Здесь сохранить выбранное значение
Решения и циклы 153
cout « endl
« "Электронная книга рецептов к вашим услугам." « endl
« "Вы можете выбрать любое из следующих изысканных блюд: "
« endl
« endl « "1 Вареные яйца"
« endl « "2 Жареные яйца"
« endl « "3 Взбитые яйца"
« endl « "4 Запеченные яйца"
« endl << endl « "Укажите номер выбора: ";
cin » choice;
switch(choice)
{
case 1: cout « endl << "Сварить несколько яиц." « endl;
break;
case 2: cout « endl « "Зажарить несколько яиц." « endl;
break;
case 3: cout « endl « "Взбить несколько яиц." « endl;
break;
case 4: cout « endl « "Запечь несколько яиц." « endl;
break;
default: cout « endl «"Вы ввели неправильный номер. Попробуйте сырые яйца."
« endl;
}
return 0;
}
Описание полученных результатов
После выдачи в выходной поток списка возможных вариантов выбора и прочте-
ния введенного пользователем значения в переменную choice оператор switch ис-
полняется с условием, специфицированным в скобках просто как (choice), которое
следует за ключевым словом switch. Все возможные варианты switch заключены в
фигурные скобки, и каждый снабжен меткой case. Эта метка состоит из ключевого
слова case, за которым идет значение choice, соответствующее данному выбору, и
завершается двоеточием.
Как видите, выполняемые операторы для конкретного case записываются между
двоеточием данной метки case и завершаются оператором break. Слово break пере-
дает управление оператору, следующему за закрывающей фигурной скобкой switch,
break — необязательная часть конструкции, но если вы ее пропустите, то выполне-
ние продолжится с оператора, идущего за следующим case, а это обычно не совсем
то, что вам нужно. Можете попробовать убрать несколько break в последнем приме-
ре и посмотреть, что получится.
Если значение choice не соответствует ни одному значению case, то выполняют-
ся операторы, которым предшествует метка default. Однако эта метка не обязатель-
на. В случае ее отсутствия, если значение проверочного выражения не соответствует
ни одному значению case, управление передается за пределы switch, и программа
выполняет оператор, следующий за закрывающей скобкой switch.
Практическое занятие) РаЗДвЛвНИв СЭЗв
Каждое из выражений, которые вы специфицируете для идентификации вариан-
тов case, должно быть константой, чтобы его значение могло быть определено во
время компиляции, и должно бать уникальным целым числом. Причина в том, что
154 Глава 3
если бы два константы case были одинаковы, то компилятор не мог бы определить,
какой из операторов, следующих за case, необходимо выполнить; однако разные
case не обязаны вести к уникальным действиям. Несколько ветвей case могут раз-
делять одно и то же действия, как показано ниже.
// ЕхЗ_06.срр
// Множество действий case
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main ()
char letter =0;
cout « endl
« "Введите строчную букву:
cin » letter;
switch (letter* (letter >= ’a’ && letter <= 'z'))
case 'a’:
case 'e':
case 'i’:
case 'o':
case 'u' : cout « endl « "Вы ввели гласную.";
break;
case 0: cout « endl « "Это не прописная буква.";
break;
default: cout « endl « "Вы ввели согласную.";
cout « endl;
return 0;
Описание полученных результатов
В данном примере вы имеете более использовано более сложное выражение в
switch. Если введенный символ не является буквой в нижнем регистре, то выражение:
(letter >= 'а' && letter <= ' z')
даст в результате false, иначе — true. Поскольку letter умножается на это выра-
жение, значение логического выражения приводится к целому (0, если выражение
ложно, и 1 — если истинно). Таким образом, выражение switch равно 0, если введен-
ный символ не является буквой нижнего регистра, в противоположном случае оно
равно самому введенному символу. То есть оператор, следующий за case 0, выполня-
ется всякий раз, когда код символа, сохраненного в letter, не является прописной
буквой нижнего регистра.
Если же введена прописная буква, то выражение switch в результате вычисления
дает само значение этой буквы, поэтому для того, чтобы выдать сообщение о том,
что введенная буква — гласная, оператор вывода помещается после серии меток case,
каждая из которых сравнивает значение с одной из гласных букв. Один и тот же опе-
ратор выполняется для любой гласной, потому что когда обнаруживается подходящая
метка case, то после этого выполняются все последующие операторы — до тех пор,
пока не встретится break. Вы можете видеть, что одно и то же действие может соот-
Решения и циклы
155
ветствовать нескольким последовательным меткам case, размещенным перед опера-
тором, выполняющим это действие. Если же будет введена согласная буква, то выпол-
нится оператор, следующий за меткой default.
Безусловное ветвление
Оператор i f предоставляет вам возможность гибко выбирать один или другой на-
бор операторов, в зависимости от указанного условия, то есть последовательность вы-
полнения операторов варьируется в зависимости от данных программы. В отличие от
этого, оператор goto — грубый инструмент. Он позволяет вам перейти к определен-
ному оператору программы безо всяких условий. Оператор, на который выполняет-
ся переход, должен быть идентифицирован меткой — идентификатором, определен-
ным в соответствии с теми же правилами, которым подчиняются имена переменных.
Метка должна быть снабжена двоеточием и помещена перед оператором, к которому
выполняется переход. Ниже представлен пример помеченного оператора.
myLabel: cout « ’’Активизирован переход на myLabel’’ « endl;
Оператор имеет метку myLabel, и безусловный переход к этому оператору в про-
грамме записывается так:
goto myLabel;
Где только возможно, вы должны избегать в своих программах применения goto.
Это порождает запутанный код, который чрезвычайно трудно отследить.
Поскольку теоретически goto не является необходимым в программах — применению goto
всегда существует альтернатива— большая часть программистов считает, что использо-
вать его вообще никогда не следует. Я не подпишусь под этой крайней точкой зрения. В кон-
це концов, это — законный оператор языка, и бывают случаи, когда применить его удобно.
Однако я рекомендую, чтобы он использовался только тогда, когда очевидно его преимуще-
ство перед другими возможными вариантами организации кода. В противном случае вы
рискуете получить запутанный, подверженный ошибкам код, который трудно понять и еще
труднее сопровождать.
Повторение блока операторов
Возможность повторно выполнять группу операторов — фундаментальна для боль-
шинства приложений. Без этой возможности организации пришлось бы модифици-
ровать программу начисления зарплаты всякий раз, когда нанимается новый сотруд-
ник, и вам пришлось бы перезагружать Halo 2 всякий раз, когда вы хотите сыграть в
другую игру. Поэтому давайте разберемся, как работают циклы.
Что такое цикл?
Цикл выполняет последовательность операторов до тех пор, пока истинно (или
ложно) определенное условие. Вы можете написать цикл, используя лишь те операто-
ры C++, с которыми вы познакомились до сих пор. Вам понадобится для этого только
if и “страшный” goto. Взгляните на следующий пример.
// ЕхЗ_07.срр
// Создание цикла средствами if и goto
#include <iostream>
156 Глава 3
using std::cin;
using std::cout;
using std::endl;
int main()
{
int i = 0, sum = 0;
const int max = 10;
i = 1;
loop:
sum += i; // Добавить текущее значение i к sum
if (++i <= max)
goto loop; // Возвращаться к loop до тех пор, пока i меньше 11
cout « endl
« "sum = " « sum
« endl
« "i = " « i
« endl;
return 0;
}
В этом примере осуществляется накопление суммы целых чисел от 1 до 10. При
первом выполнении последовательности операторов i, равное 1, добавляется к пере-
менной sum, которая поначалу равна 0. В операторе if переменная i увеличивается
до 2 и, до тех пор, пока она меньше или равна max, выполняется безусловный переход
к метке loop и значение i — на этот раз 2 — прибавляется к sum. Программа продолжа-
ет работать, увеличивая i и прибавляя его к sum до тех пор, пока i не вырастет до 11,
и поскольку условие if станет ложным, очередной возврат к loop выполнен не будет,
и цикл завершится. Если вы запустите этот пример, то получите следующий вывод:
sum = 55
i = 11
Этот пример достаточно наглядно демонстрирует работу цикла; однако он исполь-
зует goto и вводит метку, чего обычно следует избегать. Вы можете достичь того же
результата, и даже более того, если применяете следующий оператор, специально
предназначенный для организации циклов.
Практическое занятие | ИСПОЛЬЗОВЭНИб ЦИКЛЭ for
Последний пример можно переписать, используя то, что называется циклом for.
// ЕхЗ__08.срр
// Суммирование целых чисел циклом for
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main ()
{
int i = 0, sum = 0;
const int max = 10;
for(i = 1; i <= max; i++) // Спецификация Loop
sum += i; // Оператор цикла
cout « endl
« "sum = " « sum
« endl
Решения и циклы 157
« endl
return 0;
Описание полученных результатов
Если вы скомпилируете и запустите это, то получите тот же вывод, что и в пред-
ыдущем примере, но как видите, здесь код заметно меньше. Условия, определяющие
операцию цикла, появляются в скобках после ключевого слова for. В этих скобках
содержится три выражения, разделенные точкой с запятой.
□ Первое выражение выполняется один раз, в начале, и устанавливает начальное
условие цикла. В данном случае переменной i присваивается значение 1.
□ Второе выражение — логическое — определяет, до каких пор должен выпол-
няться оператор цикла (или блок операторов). Если второе выражение истин-
но, цикл продолжает выполняться; когда оно ложно, он завершается и выпол-
нение продолжается с оператора, расположенного за телом цикла. В данном
случае оператор цикла в строке, следующей за for, выполняется до тех пор,
пока значение i меньше или равно max.
□ Третье выражение выполняется после оператора цикла (или блока операто-
ров), и в данном случае оно увеличивает i на 1 на каждой итерации. После
того, как это выражение оценено, еще раз вычисляется второе выражение,
чтобы проверить, должен ли цикл продолжаться.
На самом деле такой цикл — не совсем то же самое, что представлено в версии
ЕхЗ_07 . срр. Вы можете убедиться в этом, если установите значение max равным 0
в обеих программах и запустите их опять. При этом вы увидите, что значение sum
будет равно 1 в ЕхЗ_07 . срр и 0 — в ЕхЗ_08 . срр. Конечное значение i также будет
различаться. Причина в том, что первая версия программы (с if и goto) всегда вы-
полняет тело цикла, как минимум, один раз, потому что условие не проверяется до
его конца. Цикл for работает иначе, поскольку его условие на самом деле проверяет-
ся в начале.
Обобщенная форма цикла for выглядит следующим образом.
for (вираже ние_инициализации ; вираже ние_про верки ; виражение_инкремента)
опера тор_внутри_цикла;
Конечно же, оператор_внутри_цикла может быть отдельным оператором или
блоком операторов в фигурных скобках. Последовательность событий при работе
цикла for показана на рис. 3.3.
Как уже упоминалось, оператор_внутри_цикла на рис. 3.3 также может быть бло-
ком операторов. Выражения, управляющие циклом for, очень гибки. Вы даже мо-
жете написать два или более выражений, разделенных запятыми, вместо любого из
трех управляющих операторов цикла for. Это дает вам широчайшие возможности в
его применении.
Вариации цикла for
В большинстве случаев выражения в цикле for используются довольно стандарт-
ным способом: первое из них инициализирует один или более счетчиков цикла, вто-
рое проверяет, должен ли цикл продолжаться, а третье увеличивает или уменьшает
значение одного или более счетчиков. Однако вы не обязаны использовать выраже-
ния именно таким образом — на самом деле, возможны несколько вариаций.
158 Глава 3
Рис. 3.3. Оператор цикла for
Выражение инициализации цикла for может также включать объявление пере-
менной цикла. В предыдущем примере вы могли бы написать цикл так, чтобы в пер-
вом его управляющем выражении содержалось объявление переменной i.
for (int i = 1; i <= max; i++) // Спецификация цикла
sum += i; / / Оператор цикла
Естественно, исходное объявление i теперь должна быть исключена из програм-
мы. Если вы внесете это изменение в последний пример, то обнаружите, что про-
грамма не компилируется, потому что переменная цикла i исчезает после заверше-
ния цикла, и вы не можете обращаться к ней в операторе вывода. Цикл имеет область
видимости, распространяющуюся от управляющих выражений for до конца его тела,
которое может быть либо блоком кода в фигурных скобках, либо единственным опе-
ратором. Счетчик i теперь объявлен внутри области видимости цикла, и вы не мо-
жете обращаться к нему в операторе вывода, потому что оно находится за пределами
этой области. По умолчанию компилятор C++ придерживается стандарта ISO/ANSI
Решения и циклы 159
C++, который утверждает, что переменная, определенная внутри условия цикла, не
может быть доступна вне этого цикла.
Вы можете вообще исключить инициализирующее выражение из цикла. Если ини-
циализировать i в отдельном операторе объявления, то цикл можно написать так:
int i = 1;
for(; i <= max; i++) // Спецификация цикла
sum += i; // Оператор цикла
Тут по-прежнему нужна точка с запятой, отделяющая выражение инициализации
от проверочного условия цикла. Фактически обе точки с запятой всегда должны при-
сутствовать, независимо от того, пропущено ли какое-то одно из управляющих выра-
жение, или даже все сразу. Если вы пропустите первую точку с запятой, то компиля-
тор не сможет понять, какое именно из трех управляющих операторов отсутствует,
или даже какой именно точки с запятой не хватает.
Цикл for может быть пустым. Например, вы можете поместить оператор цикла
из предыдущего примера в выражение инкремента. Тогда цикл будет выглядеть так:
for(i = 1; i <= max; sum += i++); // Полный цикл
Точка с запятой после закрывающей скобки необходима, чтобы сообщить компи-
лятору, что оператор цикла теперь пусто. Если вы пропустите ее, то оператор, следу-
ющий непосредственно после этой строки, будет интерпретирован как тело цикла.
Иногда пустой оператор цикла пишется в программах в отдельной строке:
for(i = 1; i <= max; sum += i++)
Практическое занятие | ПрИМбНеНИб МНОЖвСТВвННЫХ СЧ6ТЧИК0В
Вы можете использовать операцию запятой, чтобы включить несколько счетчи-
ков в цикл for. Ниже показан пример.
// ЕхЗ_09.срр
// Применение множественных счетчиков для показа степеней 2
#include <iostream>
#include <iomanip>
using std::cin;
using std::cout;
using std::endl;
using std::setw;
int main()
{
long i = 0, power = 0;
const int max = 10;
for(i = 0, power = 1; i <= max; i++, power += power)
cout « endl
« setw(10) « i « setw(10) « power; // Оператор цикла
cout « endl;
return 0;
Описание полученных результатов
Вы инициализируете две переменных в разделе инициализации цикла for, раз-
деляя их операцией запятой и увеличивая значения каждой в секции инкремента.
160 Глава 3
Понятно, что таким образом в каждый из разделов for можно вставить столько вы-
ражений, сколько понадобится.
Можно даже специфицировать множество условий, разделенных запятыми во втором выра-
жении, представляющем проверочную часть цикла for, которая определяет, должен ли он
продолжаться. Однако при этом лишь самое правое из них будет управлять циклом, опреде-
ляя, когда он должен завершиться.
Обратите внимание, что присваивания, задающие начальные значения i и power,
являются выражениями, а не операторами. Оператор всегда заканчивается точкой с
запятой.
При каждом увеличении i на 1 значение power удваивается за счет сложения с са-
мим собой. Это порождает последовательность чисел, представляющих степени двой-
ки. Результат работы программы выглядит следующим образом.
О 1
1 2
2 4
3 8
4 16
5 32
6 64
7 128
8 256
9 512
10 1024
Манипулятор setw, с которым вы познакомились в предыдущей главе, использует-
ся для симпатичного выравнивания столбцов цифр. Чтобы использовать setw () без
квалифицированного имени, в программу включен заголовочный файл <iomanip> и
добавлено объявление using для имен из пространства std.
Практическое занятие
Бесконечный цикл for
Если вы пропустите второе управляющее выражение, которое специфицирует
проверочное условие цикла for, то предполагается, что это условие всегда будет
true, поэтому цикл будет продолжаться бесконечно, если только вы не предусмотри-
те какой-то другой способ выхода из него. На самом деле при желании вы можете
пропустить все управляющие выражения в скобках после for. Это может показаться
не слишком полезным, однако, в действительности все как раз наоборот. Довольно
часто возникают ситуации, когда необходимо выполнить цикл некоторое число раз,
но вы заранее не знаете, сколько именно итераций понадобится. Рассмотрим следую-
щий пример.
// ЕхЗ_10.срр
// Использование бесконечного цикла для вычисления среднего
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
double value = 0.0; // Введенное значение сохраняется здесь
double sum =0.0; // Сумма значений
Решения и циклы 161
int i « 0;
char indicator = ’n’;
cout « endl
// Счетчик значений
11 Продолжать или нет?
// Бесконечный цикл
« "Введите значение:
cin » value; // Читать значение
++i; // Увеличить счетчик
sum += value; // Добавить текущее к сумме
cout « endl
« "Хотите ввести еще одно значение (для завершения введите п) ?
cin » indicator; // Читать индикатор
if ((indicator == 'n') || (indicator == 'N'))
break; // Выход из цикла
cout « endl
« "Среднее из " « i
« " введенных значений равно
« endl;
return 0;
sum/i
Описание полученных результатов
Данная программа вычисляет среднюю величину произвольного числа значений.
После ввода каждого числа пользователь должен указать, желает ли он ввести еще
одно значение, введя один символ — у или п. Типичный сеанс работы с этой програм-
мой будет выглядеть так:
Введите значение: 10
Хотите ввести еще одно значение (для завершения введите п) ? у
Введите значение: 20
Хотите ввести еще одно значение (для завершения введите п) ? у
Введите значение: 30
Хотите ввести еще одно значение (для завершения введите n) ? п
Среднее из 3 введенных значений равно 20.
После объявления и инициализации переменных, которые вы собираетесь ис-
пользовать, запускается цикл for без управляющих выражений, а, следовательно, и
без гарантий завершения. Непосредственно следующий за этим блок — субъект цикла,
который должен повторяться.
Блок цикла выполняет следующие базовые действия.
□ Читает значение.
□ Добавляет прочитанное из cin значение к sum.
□ Проверяет, желаете ли вы продолжить вводить значения.
Первое действие внутри блока приглашает вас ввести число, и читает введенное
значение в переменную value. Введенное значение добавляется в sum, после чего за-
дается вопрос — желаете ли вы продолжить ввод, и предлагается ввести ’ п ’, если не
желаете. Введенный символ помещается в переменную indicator для последующей
проверки на равенство *п* или ’N* в операторе if. Если равенства нет, цикл про-
должается, в противном случае выполняется break. Эффект от break в цикле подо-
бен тому, что он имеет в контексте оператора switch. В данном случае он приводит
к немедленному выходу из цикла с передачей управления оператору, следующему за
закрывающей фигурной скобкой блока цикла.
162 Глава 3
И, наконец, выполняется вывод количества введенных значений и их средней ве-
личины, которая вычисляется делением sum на i. Конечно, было бы неплохо приве-
сти i к типу double перед вычислением, если вы помните разговор о приведениях в
главе 2.
Использование оператора continue
Имеется еще один оператор помимо break, служащий для управления циклом —
continue. Записывается он очень просто:
continue;
Выполнение continue внутри цикла немедленно начинает следующую итерацию
цикла, пропуская операторы тела цикла, которые оставалось выполнить в текущей
итерации. Я могу продемонстрировать, как это работает, на следующем примере:
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
int i = 0, value = 0, product = 1;
ford = 1; i <= 10; i++)
{
cout « "Введите целое число: ";
cin » value;
if (value == 0) // Если введен 0
continue; // перейти к следующей итерации
product *= value;
cout « "Произведение (игнорируя нули): " « product
« endl;
return 0; // Выход из программы
}
Цикл читает 10 чисел с намерением получить произведение всех введенных зна-
чений. Оператор if проверяет, не был ли введен 0, и если это так, то выполняется
continue, чтобы сразу перейти к следующей итерации. Это делается для того, чтобы
произведение введенных чисел не обратилось в ноль при первом же вводе нулевого
значения. Понятно, что если ноль будет введен на последней итерации, цикл просто
завершится. Безусловно, существуют и другие способы достичь того же результата, но
continue представляет очень удобное средство, в частности, для сложных циклов,
где вам может понадобиться пропускать остаток текущей итерации, начиная с разных
точек его тела.
Влияние операторов break и continue на логику цикла for иллюстрирует
рис. 3.4.
Очевидно, что в реальных ситуациях вы будете использовать операторы break и
continue с некоторой логикой проверки условии для определения того, когда цикл
должен быть завершен, или когда текущая итерация должна быть пропущена. Вы мо-
жете применять операторы break и continue и с циклами других типов, о которых я
расскажу позднее в этой главе. Там они работают точно так же, как и в for.
Решения и циклы 163
Рис. 3.4. Влияние операторов break и continue на логику цикла for
Практическое занятие | ПрИМеНвНИб ДРУГИХ ТИПОВ В ЦИКЛ ЭХ
До сих пор вы применяли только целочисленные переменные (int) в качестве
счетчиков цикла. Но вы никоим образом не ограничены в выборе типа переменной,
используемой в качестве счетчика итераций. Рассмотрим следующий пример.
// ЕхЗ_11.срр
// Отображение кодов ASCII букв алфавита
#include <iostream>
#include <iomanip>
using std::cout;
using std::endl;
using std::hex;
using std::dec;
using std::setw;
164 Глава 3
int main ()
for(char capital = ’A’, small = ’a’; capital <= ’Z’; capital++, small++)
cout « endl
« "\t" « capital // Вывести capital как символ
« hex « setw(10) « static_cast<int>(capital)
// и как шестнадцатеричное число
« dec « setw(10) « static_cast<int> (capital) // и как десятичное
« " " « small // Вывести small как символ
hex « setw(10) « static_cast<int>(small)
//и как шестнадцатеричное число
dec « setw(10) « static_cast<int> (small); //и как десятичное
cout « endl;
return 0;
Описание полученных результатов
Программа начинается с нескольких объявлений using для имен некоторых но-
вых манипуляторов, управляющих представлением вывода.
Цикл управляется переменной capital типа char, которая объявлена вместе с
переменной small в выражении инициализации. Обе переменных подвергаются ин-
крементированию в третьем управляющем выражении цикла, так что capital изме-
няется от ’ А ’ до ' Z', а значение small — соответственно от 'а’ до ’ z1.
Тело цикла содержит единственный оператор вывода, растянутый на семь строк.
Первая из них:
cout « endl
начинает новую строку на экране.
Следующие три строки таковы:
capital
// Вывести capital как символ
« hex « setw(10) « static__cast<int> (capital) //и как шестнадцатеричное число
« dec « setw(10) « static_cast<int> (capital) // и как десятичное
При каждой итерации после вывода символа табуляции значение capital отобра-
жается три раза: как символ, в шестнадцатеричном и в десятичном видах.
Когда вы вставляете манипулятор hex в поток cout, это заставляет следующие зна-
чения данных отображаться в шестнадцатеричном виде вместо обычного принято-
го по умолчанию для целых чисел десятичного, поэтому следующий вывод capital
представляет код символа как шестнадцатеричное число.
Затем в поток вставляется манипулятор dec, что заставляет его отобразить следу-
ющее значение в десятичном виде. По умолчанию переменная типа char интерпрети-
руется потоком как символ, а не как числовая величина. Вы отправляете в поток пе-
ременную capital типа char как числовое значение путем приведения его значения
к типу int операцией static casto (), которая вам знакома по предыдущей главе.
Значение small выводится точно таким же образом в следующих трех строках
оператора вывода:
« ’’ ’’ « small // Вывести small как символ
« hex « setw(10) « static_cast<int>(small) // и как шестнадцатеричное число
« dec « setw(10) « static_cast<int> (small) // и как десятичное
В результате программа генерирует следующий вывод:
41 65 a
42 66 b
43 67 c
44 68 d
45 69 e
46 70 f
47 71 g
48 72 h
49 73 i
4a 74 j
4b 75 к
4c 76 1
4d 77 m
4e 78 n
4f 79 о
50 80 p
51 81 q
52 82 r
53 83 s
54 84 t
55 85 u
56 86 v
57 87 w
58 88 x
59 89 у
5a 90 z
61 97
62 98
63 99
64 100
65 101
66 102
67 103
68 104
69 105
6a 106
6b 107
6c 108
6d 109
6e 110
6f 111
70 112
71 113
72 114
73 115
74 116
75 117
76 118
77 119
78 120
79 121
7a 122
Счетчики с плавающей точкой
Вы можете также использовать в качестве счетчиков цикла значения с плавающей
точкой. Ниже приведен пример цикла for со счетчиком такого типа:
double а = 0.3, b = 2.5;
for (double х = 0.0; х <= 2.0; х += 0.25)
cout « "\n\tx = " « х
« "\ta*x + b = " « а*х + b;
Этот фрагмент кода вычисляет значение а*х4-Ь для значений х от 0.0 до 2.0 с
шагом 0.25; однако вы должны быть осторожны, применяя в циклах счетчик с пла-
вающей точкой. Многие десятичные значения не могут быть представлены точно в
двоичной форме с плавающей точкой, поэтому в аккумулируемых значениях может
накапливаться погрешность. Это значит, что вы не должны кодировать цикл for так,
чтобы его завершение зависело от достижения счетчиком с плавающей точкой точ-
ного значения. Например, следующий неправильно спроектированный цикл никогда
не завершится:
for (double х = 0.0 ; х != 1.0 ; х 4-= 0.2)
cout « х;
Казалось бы, этот цикл должен отобразить значения х, изменяя их от 0.0 до 1.0;
однако шаг 0.2 не может быть представлен точно в формате с плавающей точкой,
поэтому значение х никогда не будет точно равно 1.0. Следовательно, второе управ-
ляющее циклом выражение всегда будет true, а потому цикл будет продолжаться бес-
конечно.
166 Глава 3
Цикл while
Второй тип циклов C++ — цикл while. В то время как цикл for предназначен глав-
ным образом для повторения оператора или блока операторов определенное количе-
ство раз, цикл while служит для выполнения оператора или блока до тех пор, пока
указанное условие остается истинным. Общая форма цикла while показана ниже.
while (условие)
опера тор_внутри_цикла;
Здесь оператор_внутри_цикла выполняется повторно до тех пор, пока выра-
жение условие имеет значение true. После того, как условие становится равным
false, программа выходит из цикла и переходит к оператору, следующему за ним.
Как всегда, единственный опера тор_внутри_цикла может быть заменен блоком опе-
раторов в фигурных скобках.
Логика цикла while представлена на рис. 3.5.
Рис. 3.5. Оператор цикла while
Практическое занятие | ИСПОЛЬЗОВаНИв ЦИКЛЭ while
Вы можете переписать предыдущий пример, вычисляющий среднее группы вве-
денных числовых значений (ЕхЗ_10. срр), используя цикл while.
// ЕхЗ__12.срр
// Использование цикла while для вычисления среднего
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main ()
{
// Введенное значение сохраняется здесь
double value = 0.0;
double sum =0.0; // Сумма значении
int i = 0; // Счетчик значении
char indicator = ’у*; 11 Продолжать или нет?
while (indicator = ’у’) 11 Повторять тело цикла, пока введено 'у'
cout « endl
« "Введите значение: ";
cin » value; // Читать значение
++i; // Увеличить счетчик
sum += value; // Добавить текущее значение к сумме
cout « endl
« "Хотите ввести еще одно значение (для завершения введите п)? ";
cin » indicator; // Читать индикатор
cout « endl
« "Среднее из " « i
« " введенных значений равно " « sum/i « "."
« endl;
return 0;
Описание полученных результатов
При том же вводе, что и ранее, эта программа генерирует такой же вывод. Один
оператор был изменен, другой добавлен — они выделены полужирным в исходном
коде. Оператор цикла for заменен while, и исключена проверка indicator в if, по-
скольку теперь эту функцию выполняет условие цикла while. Вы должны инициали-
зировать indicator значением ’у* вместо ’п1, как было раньше. Иначе цикл while
прервется немедленно. До тех пор, пока условие while возвращает true, цикл будет
продолжаться.
Вы можете поместить в условие цикла while любое выражение, которое в результа-
те дает true или false. Данный пример можно усовершенствовать, если сформулиро-
вать условие цикла так, чтобы для продолжения цикла можно было вводить 1Y1 наряду
с ' у ’. Для этого потребуется модифицировать оператор while следующим образом:
while((indicator = 'у') || (indicator = '¥'))
Можно также создать потенциально бесконечный цикл while, используя условие,
которое всегда true. Это можно написать так:
while(true)
{
• • •
}
Можно также записать управляющее выражение цикла как целочисленное значе-
ние 1, которое будет преобразовано в значение true типа bool. Естественно, здесь
также остается в силе то же требование, что предъявляется к бесконечным циклам
for, а именно: необходимо предусмотреть какой-нибудь способ выхода из цикла вну-
три его тела. Вы увидите в главе 4 другие примеры применения цикла while.
Цикл do-while
Цикл do-while подобен циклу while в том, что он выполняется до тех пор, пока
указанное условие остается истинным. Главное отличие его состоит в том, что здесь
условие проверяется в конце цикла — что отличает его от циклов for и while, где
168 Глава 3
оно проверяется в начале. Как следствие, оператор внутри цикла do-while всегда вы-
полняется, по меньшей мере, один раз. Общая форма цикла do-while следующая:
do
оператор_внутри_цикла;
} wh i 1 е (условие);
Логика этой формы цикла показана на рис. 3.6.
Рис, 3.6. Оператор цикла do-while
Вы можете заменить цикл while в последней версии программы вычисления сред-
него циклом do-while:
« "Введите значение:
> value; // Читать значение
sum += value; // Добавить текущее значение к сумме
cout « "Хотите ввести еще одно значение (для завершения введите п) ? ”;
cin » indicator; // Читать индикатор
} while((indicator = ’у’) | | (indicator = fYr));
Между двумя последними версиями цикла нет особой разницы, за исключени-
ем того, что правильная работа этой версии не зависит от начального значения
indicator. До тех пор, пока вы уверены, что будет введено хотя бы одно осмыслен-
ное значение, которое нужно обработать, применение данной версии цикла более
предпочтительно.
Вложенные циклы
Циклы можно вкладывать друг в друга. Смысл этого станет более очевидным в гла-
ве 4: такая техника обычно применяется для повторения действий на разных уровнях
классификации. Примером может быть вычисление суммы оценок каждого студента в
классе с повторением процесса для каждого класса школы.
Решения и циклы 169
Практическое занятие) ВЛОЖвННЫв ЦИКЛЫ
Эффект от вложения одного цикла в другой можно увидеть на примере вычисле-
ния значений по простой формуле. Факториал целого числа — это произведение всех
целых чисел от 1 и до заданного включительно. Поэтому факториал 3, например —
это 1, умноженное на 2 и на 3, что равно 6. Следующая программа вычисляет факто-
риал целых чисел, которые вводит пользователь:
// Ех3_13.срр
// Демонстрация вложенных циклов для вычисления факториала
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
char indicator = ’n’;
long value = 0,
factorial = 0;
do
{
cout « endl
« "Введите целое число: ";
cin » value;
factorial = 1;
for (int i = 2; i <= value; i++)
factorial *= i;
cout « "Факториал " « value « " равен " « factorial;
cout « endl
« "Хотите ввести еще одно число (у или п) ? ";
cin » indicator;
} while((indicator == ’у’) || (indicator == ’Y’));
return 0;
}
Если вы скомпилируете и выполните этот пример, он будет работать примерно
так:
Введите целое число: 5
Факториал 5 равен 120
Хотите ввести еще одно число (у или п) ? у
Введите целое число: 10
Факториал 10 равен 3628800
Хотите ввести еще одно число (у или п) ? у
Введите целое число: 13
Факториал 13 равен 1932053504
Хотите ввести еще одно число (у или п) ? у
Введите целое число: 22
Факториал 22 равен -522715136
Хотите ввести еще одно число (у или n) ? п
Описание полученных результатов
Значения факториала возрастают очень быстро. Фактически, 12— наибольшее
входное значение, для которого данный пример выдает правильный результат.
Факториал 13 на самом деле равен 6 227 020 800, а не 1 932 053 504, как сообщает про-
170 Глава 3
грамма. Если вы введете еще большие входные значения, это приведет к потере стар-
ших десятичных разрядов в переменной factorial, и можно даже получить отрица-
тельные значения факториала, как это произошло при вводе числа 22.
В этой ситуации не выдается никаких сообщений об ошибке, поэтому чрезвычайно важно
при разработке программы быть уверенным в том, что значения, с которыми будет иметь
дело программа, не выйдут за пределы допустимого диапазона для типа данных используе-
мых переменных. Вы должны также предусмотреть обработку некорректных входных зна-
чений. Ошибки такого рода, происходящие “молча”, чрезвычайно трудно найти.
Внешний из двух циклов, цикл do-while, управляет завершением программы. До
тех пор, пока вы вводите ’ у ’ или ’ Y ’ в ответ на запрос о продолжении, програм-
ма продолжает вычислять значения факториала. Факториал целых чисел вычис-
ляется во вложенном цикле for. Он выполняется, value раз умножая переменную
factorial (чье начальное значение равно 1), на последовательные целые числа — от
2 до value.
Практическое занятие | £ще ОДИН ВЛОЖвННЫЙ ЦИКЛ
Вложенные циклы могут выглядеть немного запутанными, поэтому давайте рас-
смотрим другой пример. Эта программа генерирует таблицу умножения заданного
размера.
// ЕхЗ_14.срр
// Использование вложенных циклов для генерации таблицы умножения
#include <iostream>
#include <iomanip>
using std::cout;
using std::endl;
using std::setw;
int main()
{
const int size = 12; // Размер таблицы
int i = 0, j = 0; // Счетчики циклов
cout « endl / / Заголовок таблицы
« "Таблица умножения " « size « " на "
« size « endl « endl;
cout « endl « " I " ;
for(i = 1; i <= size; i++) // Цикл для вывода заголовков колонок
cout « setw(3) « i « ” ”;
cout « endl; // Начало строки подчеркивания
for(i = 0; i <= size; i++)
cout « ”"; // Подчеркнуть каждый заголовок
for(i = 1; i <= size; i++) // Внешний цикла для строк таблицы
{
cout « endl
« setw(3) « i « ’’ I”;// Вывести метку строки
for(j = 1; j <= size; j++) // Внутренний цикл остальной части строки
cout « setw(3) « i*j « " "; // Конец вложенного цикла
} // Конец внешнего цикла
cout « endl;
return 0;
}
Ниже показан вывод этого примера.
Решения и циклы 171
Таблица умножения 12 на 12
1 1 2 3 4 5 7 8 9 10 11 12
1 1 1 2 3 4 5 6 7 8 9 10 11 12
2 I 2 4 6 8 10 12 14 16 18 20 22 24
3 I 3 6 9 12 15 18 21 24 27 30 33 36
4 1 4 8 16 20 24 28 32 36 40 44 48
5 I 5 10 15 20 25 30 35 40 45 50 55 60
6 I 6 12 18 24 30 36 42 48 54 60 66 72
7 I 7 14 21 28 35 42 49 56 63 70 77 84
8 I 8 16 24 32 40 48 56 64 72 80 88 96
9 I 9 18 27 36 45 54 63 72 81 90 99 108
Ю | 10 20 30 40 50 60 70 80 90 100 110 120
11 1 11 22 33 44 55 66 77 88 99 110 121 132
12 I 24 36 48 60 72 84 96 108 120 144
Описание полученных результатов
Заголовок таблицы выдается первым оператором вывода программы. Следующий
оператор вывода, вставленный в следующий за ним цикл, генерирует заголовки столб-
цов. Каждый столбец имеет ширину в пять символов, поэтому значения заголовков
отображаются в поле шириной три символа, что указано манипулятором setw (3), за
которым идет два пробела. Оператор вывода, предшествующий циклу, выводит четыре
пробела и вертикальную линию перед заголовком первого столбца. Затем ниже идет
серия символов подчеркивания, отделяющих заголовки от содержимого столбцов.
Вложенный цикл генерирует основное содержимое таблицы. Внешний цикл повто-
ряется по разу для каждой строки, поэтому здесь i — номер столбца. Оператор вывода
cout « endl
« setw(3) « i « " I”;
11 Вывести метку строки
переходит на новую строку и затем выводит ее заголовок в виде значения i в поле
шириной три символа, за которым идет пробел и вертикальная линия.
Строка значений генерируется вложенным циклом:
for(j = 1; j <= size; j++) // Внутренний цикл остальной части строки
cout « setw(3) « i*j « ” // Конец вложенного цикла
Этот цикл выводит значения i * j, соответствующие произведению текущего значе-
ния строки i на номер столбца j, изменяя j от 1 до size. Таким образом, при каждой
итерации внешнего цикла внутренний цикл проходит size итераций. Полученные
значения размещаются таким же образом, как заголовки столбцов.
Когда завершается внешний цикл, выполняется return для завершения программы.
Программирование на C++/CLI
Все, о чем я говорил в этой главе, в равной мере относится к программам C++/CLI.
Для того чтобы просто проиллюстрировать это, мы можем рассмотреть некоторые
примеры консольных программ CLR, которые демонстрируют кое-что из того, что
вы изучили до сих пор в этой главе. Ниже представлена программа CLR, представля-
ющая собой вариацию на тему ЕхЗ_01.
172 Глава 3
Практическое занятие | ПрОГрЭММЭ CLR, ИСПОЛЬЗуЮЩЭЯ ВЛОЖвННЫв
операторы if
Создайте консольную программу с кодом по умолчанию и модифицируйте функ-
цию main () следующим образом:
// ЕхЗ_15.срр : главный файл проекта.
#include "stdafx.h"
using namespace System;
int main(array<System::String л> лargs)
{
wchar^t letter; // Соответствует типу C++/CLI Char
Console::Write(L"Введите букву: ") ;
letter = Console: :Read() ;
if (letter >= 'A') // Проверка letter равно или больше 'A'
if (letter <= ' Z') I/ Проверка letter равно или меньше 1 Z ’
{
Console::WriteLine(Ь"Вы ввели заглавную букву.") ;
return 0;
}
if (letter >= 'a') // Проверка letter равно или больше 'a'
if (letter <= ’z’) // Проверка letter равно или меньше ’z'
{
Console: ‘.WriteLine(Ь"Вы ввели прописную букву. ") ;
return 0;
}
Console: :WriteLine(L"Bbi ввели не букву.") ;
return 0;
}
Как всегда, новые строки кода выделены полужирным.
Описание полученных результатов
Логика этой программы в точности повторяет логику примера ЕхЗ_01 (фактиче-
ски все операторы те же самые, за исключением операторов вывода и объявления
letter). Я изменил тип этой переменной на wchar t, поскольку тип Char име-
ет несколько дополнительных возможностей, о которых я еще упомяну. Функция
Console: : Read () читает одиночный символ с клавиатуры. Поскольку для вывода на-
чального приглашения применяется Console: :Write (), символ новой строки не вы-
водится, так что вы можете вводить символ в той же строке, что и приглашение.
Среда .NET Framework предлагает свои собственные функции в классе Char для
преобразования кодов символов в верхний и нижний регистр. Это функции Char: :
ToLower () и Char: : ToUpper (), и вы помещаете преобразуемый символ в качестве
аргумента между скобками при вызове этих функций. Например:
wchar_t uppcaseLetter = Char::ToUpper(letter);
Конечно, результат преобразования можно сохранить обратно в исходной пере-
менной, как в следующем операторе:
letter = Char::ToUpper(letter);
Класс Char также предлагает функции IsUpper () и IsLower (), проверяющие ре-
гистр символа. Вы передаете проверяемый символ этим функциям в виде аргумен-
Решения и циклы 173
та, а они возвращают значение bool в качестве результата. Это можно использовать,
чтобы закодировать main () несколько иначе.
wchar_t letter; // Соответствует типу C++/CLI Char
Console::Write(Ь"Введите букву: ") ;
letter = Console:: Read () ;
wchar_t upper = Char: :ToUpper(letter) ;
if (upper >= 'A' && upper <= 'Z') // Проверка на вхождение в диапазон от 'А' до ’ Z ’
Console: :WriteLine(Ь"Вы ввели {0} букву. ",
Char::IsUppeг(letter) ? "заглавную" : "прописную");
else
Console: :WriteLine(Ь"Вы ввели не букву.") ;
return 0;
Применение этих функций заметно упрощает код. После преобразования letter
в верхний регистр вы проверяете преобразованное значение на вхождение в диапа-
зон от 1А1 до 1 Z1. Если оно входит в этот диапазон, выдается сообщение, зависящее
от выражения условной операции, которое формирует второй аргумент для функции
WriteLine (). Условная операция возвращает "заглавную”, если letter относится
к верхнему регистру, и "прописную", если к нижнему, а результат вставляется в пози-
цию выходной строки, отмеченную форматной строкой {0}.
Теперь посмотрите на другой пример CLR, использующий функцию Console: :
ReadKey() в цикле и раскрывающий немного подробнее класс ConsoleKeylnfo.
Практическое занятие ЧТвНИв НЭХЭТИЙ КЛЗВИШ
Создайте консольную программу CLR и добавьте следующий код в main ():
// ЕхЗ_16.срр : главный файл проекта.
// Проверка нажатой клавиши в цикле.
#include "stdafx.h"
using namespace System;
int main(array<System::String A> Aargs)
{
Console: :WriteLine(L"Нажмите комбинацию клавиш — для выхода нажмите Esc.") ;
ConsoleKeylnfo keyPress;
do
{
keyPress = Console::ReadKey(true);
Console: :Write(L"Bbi нажали") ;
if(safe_cast<int>(keyPress.Modifiers)>0)
Console::Write(L" {0},", keyPress.Modifiers);
Console::WriteLine (L" {0} что дает символ {1} ",
keyPress.Key, keyPress.KeyChar);
}while(keyPress.Key != ConsoleKey::Escape);
return 0;
}
Ниже показан результат работы этой программы.
Нажмите комбинацию клавиш — для выхода нажмите Esc.
Вы нажали Shift, В что дает символ В
Вы нажали Shift, Control, N что дает символ _
Вы нажали Shift, Control, Oeml что дает символ
Вы нажали Oeml что дает символ ;
Вы нажали ОешЗ что дает символ '
174 Глава 3
Вы нажали Shift, ОешЗ что дает символ @
Вы нажали Shift, 0em7 что дает символ ~
Вы нажали Shift, Оешб что дает символ }
Вы нажали D3 что дает символ 3
Вы нажали Shift, D3 что дает символ ?
Вы нажали Shift, D5 что дает символ %
Вы нажали 0еш8 что дает символ
Вы нажали Esc что дает символ
Конечно, некоторые комбинации клавиш не представляют никакого отображаемо-
го символа, поэтому в этих случаях никакие символы и не выводятся. Программа так-
же завершается по нажатию <Ctrl+C>, потому что операционная система распознает
эту комбинацию как команду завершения программы.
Описание полученных результатов
Нажатия клавиш проверяются в цикле do-while, и этот цикл продолжается до
тех пор, пока не будет нажата клавиша <Esc>. Внутри цикла вызывается функция
Console: :ReadKey (), и ее результат помещается в переменную keyPress, имею-
щую тип ConsoleKeylnfo. Класс ConsoleKeylnfo имеет три свойства, обратившись
к которым, можно получить вспомогательную информацию относительно нажатой
клавиши или комбинации клавиш. Свойство Key идентифицирует нажатую клави-
шу, свойство KeyChar представляет код клавиши в кодировке Unicode, а свойство
Modifiers — это битовая комбинация констант ConsoleModifiers, представляющих
состояние клавиш Shift, Alt и Ctrl. ConsoleModifiers
перечисление, опреде-
ленное в библиотеке System, и константы, определенные в нем, имеют имена Alt,
Shift и Control.
Как можно видеть из аргументов функции WriteLine () в последнем операторе
вывода, для доступа к свойству объекта необходимо поместить наименование свой-
ства следом за именем объекта, отделив его точкой. Здесь точка — это операция до-
ступа к члену. Чтобы получить доступ к свойству KeyChar объекта keyPress, вы
должны написать keyPress .KeyChar.
Программа работает очень просто. Внутри цикла вызывается функция ReadKey ()
для чтения нажатия клавиши, и ее результат помещается в переменную keyPress.
Далее с помощью функции Write () начальная часть вывода помещается в командную
строку. Поскольку в этом случае не выводится символ новой строки, последующий вы-
вод продолжается в той же строке. Затем выполняется проверка свойства Modifiers
на предмет того, больше ли оно нуля. Если это так, значит, были нажаты модифици-
рующие клавиши, и об этом выводится сообщение, в противном случае информация
о модифицирующих клавишах пропускается. Возможно, вы помните, что перечисли-
мые константы C++/CLI являются объектами, которые нужно явно привести к цело-
численном типу, прежде чем использовать их как числовые значения — отсюда при-
ведение к int в выражении if.
Интересен вывод Modifiers. Как видно из результирующего вывода программы,
когда нажато более одной модифицирующей клавиши, вы получаете информацию о
них всех в одном операторе вывода. Это потому, что перечисление Modifiers опреде-
лено с атрибутом FlagsAttribute, который указывает на то, что этот перечислимый
тип представляет набор отдельных битовых флагов. Благодаря этому переменные
перечислимого типа могут состоять из нескольких флагов, объединенных вместе ло-
гическим И, а индивидуальные флаги распознаются и выводятся функциями Write ()
и WriteLine ().
Решения и циклы 175
Цикл продолжается до тех пор, пока истинно условное выражение keyPress .Key
!= ConsoleKey: :Escape. Оно возвращает false, когда свойство keyPress.Key
принимает значение ConsoleKey: :Escape, что происходит при нажатии клавиши
<Esc>.
Цикл for each
Все операторы циклов, о которых я говорил до сих пор, применяются как в языке
ISO/ANSI C++, так и в C++/CLI. Но язык C++/CLI предлагает еще один роскошный
тип цикла, называемый for each. Он предназначен специально для итерации по объ-
ектам, принадлежащим к определенному набору, и поскольку вы пока ничего не зна-
ете об этом, я кратко представлю здесь цикл for each, отложив более подробные
пояснения на потом.
Вы уже немного знаете об объектах String, которые представляют набор симво-
лов, поэтому можете применить цикл for each для итерации по символам строки.
Давайте рассмотрим соответствующий пример.
Практическое занятие
Использование цикла for each для доступа к
каждому символу в String
Создайте новый проект консольной программы CLR по имени ЕхЗ_17 и модифи-
цируйте код следующим образом:
// ЕхЗ_17.срр : главный файл проекта.
// Анализ строки с помощью цикла for each
#include "stdafx.h"
using namespace System;
int main(array<System::String Л> *args)
{
int vowels = 0;
int consonants = 0;
String* proverb = L"A nod is as good as a wink to a blind horse.";
for each(wchar_t ch in proverb)
{
if(Char::IsLetter(ch))
{
ch = Char: :ToLower(ch) ; // Преобразовать в нижний регистр
switch(ch)
{
case ’a': case ’e’: case ’i’:
case 'o': case 'u':
++vowels;
break;
default:
++consonants;
break;
}
}
}
Console::WriteLine(proverb);
Console::WriteLine(Ь"Английская поговорка содержит {0} гласных и {1} согласных.",
vowels, consonants);
return 0;
}
176 Глава 3
Запуск этого примера даст следующий вывод:
A nod is as good as a wink to a blind horse.
Английская поговорка содержит 14 гласных и 18 согласных.
Описание полученных результатов
Программа подсчитывает количество гласных и согласных в строке, содержащей-
ся в переменной proverb. Программа делает это путем итерации по каждому симво-
лу строки в цикле for each. Сначала определяется две переменных для накопления
общего количества гласных и согласных:
int vowels = 0;
int consonants = 0;
“За кулисами” они имеют тип C++/CLI Int32, который хранит 32-битные целые
значения.
Далее определяется строка для анализа:
StringA proverb = L"A nod is as good as a wink to a blind horse.";
Переменная proverb имеет тип String74, который описывается как тип “дескрип-
тора String” (handle to String); дескриптор применяется для хранения местополо-
жения объекта в динамически распределяемой “куче” памяти под управлением CLR,
очищаемой сборщиком мусора. Позже вы более подробно узнаете о дескрипторах и
типе String74, когда речь пойдет о типах классов C++/CLI; пока просто примите к
сведению, что этот тип применяется в объявлении переменных C++/CLI, предназна-
ченных для хранения строк.
Цикл for each, проходящий по символам строки, на которую ссылается proverb,
выглядит так:
for each(wchar t ch in proverb)
// обработка текущего символа строки, сохраненного в ch
Символы в строке proverb представлены в кодировке Unicode, поэтому для хране-
ния каждого из них применяется переменная типа wchar_t (эквивалент типа Char).
Цикл последовательно присваивает символы из строки proverb переменной цикла
ch, относящейся к типу C++/CLI Char. Эта переменная локальна по отношению к
циклу (другими словами, она существует только в блоке цикла). При первой итера-
ции ch содержит первый символ строки, при второй итерации — второй символ, при
третьей — третий и так далее, до тех пор, не будут обработаны все символы и цикл
завершен.
Внутри цикла определяется, является ли символ буквой, в следующем выражении
*
if(Char::IsLetter(ch))
Функция Char:: IsLetter () возвращает значение true, если аргумент (в данном
случае — ch) является буквой, и false — в противном случае. Таким образом, следую-
щий за if блок выполняется только в том случае, если ch содержит букву. Это необхо-
димо, поскольку вы не хотите, чтобы знаки препинания обрабатывались так, как если
бы они были буквами.
Установив, что ch — на самом деле буква, с помощью следующего оператора она
преобразуется в нижний регистр:
ch = Char::ToLower(ch);
Решения и циклы 177
Здесь применяется функция Char: : ToLower () из библиотеки .NET Framework,
которая возвращает эквивалент своего аргумента (в данном случае — ch) в нижнем ре-
гистре. Если аргумент уже находится в нижнем регистре, функция просто возвращает
его без изменений. Преобразование символа в нижний регистр позволяет избежать
необходимости последовательно проверять его на вхождение в список гласных верх-
него и нижнего регистра.
Определение того, является ли ch гласной или согласной буквой, выполняется
внутри оператора switch:
switch(ch)
case ’a’: case ’e’: case ’i’:
case 'o’: case 'u’:
++vowels;
break;
default:
++consonants;
break;
В любом из пяти случаев, когда ch — гласная, вы увеличиваете значение счетчика
vowels; в противном случае увеличивается значение счетчика consonants. Оператор
switch выполняется для каждого символа в proverb, так что когда цикл завершается,
vowels содержит количество гласных в строке, a consonants — количество соглас-
ных. Затем результат выводится с помощью следующего оператора:
Console::WriteLine(proverb);
Console::WriteLine(L"Английская поговорка содержит {0} гласных и {1} согласных.",
vowels, consonants);
В последнем операторе значение vowels подставляется вместо {0} в строке, а зна-
чение consonants — вместо {1}. Это потому, что аргументы, следующие за первым
аргументом — строкой формата, нумеруются, начиная с 0.
Резюме
В этой главе вы изучили все важнейшие механизмы принятия решений в програм-
мах C++. Также вы ознакомились со средствами организации повторного выполнения
групп операторов. Суть того, что было изложено, представлено следующими тезисами.
□ Базовые средства принятия решений основаны на наборе операций отноше-
ний, которые позволяют строить выражения проверки и сравнения, возвраща-
ющие в результате значения типа bool (true или false).
□ Решения можно принимать также на основе условий, возвращающих не булев-
ские значения. Любое ненулевое значение в проверочных условиях приводит-
ся к true, а нулевое значение — к false.
□ Основное средство принятия решений в C++ представлено оператором if.
Более высокая степень гибкости обеспечивается оператором switch и услов-
ной операцией.
□ В ISO/ANSI C++ предусмотрено три основных метода для повторного выпол-
нения блока операторов: цикл for, цикл while и цикл do-while. Цикл for по-
зволяет выполнять операторы, указанные в его теле, определенное количество
раз. Цикл while позволяет выполнять свои операторы до тех пор, пока его
178 Глава 3
проверочное условие возвращает true. И, наконец, цикл do-while выполняет
операторы как минимум один раз, а затем позволяет повторять их до тех пор,
пока указанное условие истинно.
□ В C++/CLI, в дополнение к трем перечисленным операторам циклов, пред-
усмотрена еще одна форма цикла — for each.
□ Циклы любого вида могут быть вложены в любые другие циклы.
□ Ключевое слово continue позволяет пропускать остаток операторов текущей
итерации цикла и сразу переходить к следующей итерации.
□ Ключевое слово break обеспечивает немедленный выход из цикла. Оно также
выполняет выход из switch в конце группы операторов case.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Напишите программу, которая читает из cin входные числа и суммирует их,
прекращая процесс после ввода 0. Сконструируйте три версии этой программы,
с использованием циклов while, do-while и for.
2. Напишите программу ISO/ANSI C++ для чтения символов с клавиатуры и под-
счета гласных. Программа должна прекращать работу при вводе Q (или q).
Используйте комбинацию бесконечного цикла для чтения символов с операто-
ром switch для их подсчета.
3. Напишите программу для распечатки таблицы умножения размером от 2 до 12
столбцов.
Предположим, что необходимо установить в программе переменную “режима
открытия файла” на базе двух атрибутов: типа файла, который может быть тек-
стовым или двоичным, и способа открытия файла — для чтения, записи или до-
бавления данных. Используя битовые операции (& и |), а также набор флагов,
предложите метод, позволяющий в одной целочисленной переменной устанав-
ливать любую комбинацию этих двух атрибутов. Напишите программу, которая
установит значение этой переменной, а затем декодирует его, распечатав все
установки флагов для всех возможных комбинаций атрибутов.
5. Воспроизведите пример ЕхЗ_2 в виде программы C++/CLI (можете использо-
вать Console:: ReadKey () для чтения символов с клавиатуры).
6. Напишите консольную программу CLR, которая определяет строку (типа
StringA) и затем анализирует ее символы для подсчета символов верхнего ре-
гистра, символов нижнего регистра, небуквенных символов и общего количе-
ства символов в строке.
4
Массивы, строки
и указатели
Итак, до настоящего момента вы последовательно изучили все фундаментальные
типы данных, и уже обладаете базовыми знаниями о том, как в программе выполня-
ются вычисления и принимаются решения. В этой главе вы сможете расширить при-
ложение основных приемов программирования, которые вы изучили до сих пор — от
применения отдельных элементов данных к работе с целыми их коллекциями. В этой
главе вы узнаете следующие моменты.
□ Что такое массивы и как они применяются.
□ Как объявлять и инициализировать массивы различных типов.
□ Как объявлять и использовать многомерные массивы.
□ Что такое указатели и как их использовать.
□ Как объявлять и инициализировать указатели различных типов.
□ Отношения между указателями и массивами.
□ Ссылки, как они объявляются и некоторые начальные представления об их ис-
пользовании.
□ Как динамически выделять память для переменных в программах на “родном”
C++.
□ Как работает динамическое распределение памяти в программе CLR.
□ Отслеживание дескрипторов и ссылок и зачем они нужны в программах CLR.
□ Как работать со строками и массивами в программах C++/CLI.
□ Что такое внутренние указатели, и как вы можете создавать и применять их.
180 Глава 4
В этой главе вы будете более интенсивно применять объекты, хотя вы пока и не
знаете всех деталей их создания — поэтому не волнуйтесь, если что-то будет не совсем
ясно. Изучение классов и объектов во всех подробностях начнется в главе 7.
Обработка множества однотипных
элементов данных
Вы уже знаете, как объявлять и инициализировать переменные различных типов,
каждая из которых содержит отдельный элемент информации. Я буду называть их
элементами данных. Вы знаете, как создать отдельный символ в переменной типа
char, отдельное целое число в переменной типа short, типа int, типа long, или же
отдельное число с плавающей точкой в переменной типа float или типа double.
Наиболее очевидное развитие этих идей — возможность ссылаться на несколько эле-
ментов данных определенного типа по одному имени переменной. Это позволило бы
вам управлять приложениями в более широком контексте.
Рассмотрим пример того, где это может понадобиться. Предположим, что вам
необходимо написать программу для ведения ведомости зарплат. Применение от-
дельных именованных переменных для каждой индивидуальной выплаты, связан-
ных с ней налоговых отчислений и так далее было бы, мягко говоря, непростой за-
дачей. Гораздо удобнее было бы справиться с этой проблемой, если бы можно было
ссылаться на наемного работника по какому-то обобщенному имени — к примеру,
employeeName — и также иметь другие обобщенные имена для различных данных,
связанных с каждым работником, вроде pay (выплата), tax (налог) и так далее. И, ко-
нечно же, нужен какой-то способ указания на конкретного работника из общей груп-
пы, вместе с данными из обобщенных переменных, которые его касаются. Подобные
требования возникают всегда, когда программа имеет дело с любыми коллекциями
однотипных данных — будь то члены баскетбольной команды или группа боевых ко-
раблей. Естественно, в C++ предусмотрены возможности для решения таких задач.
Массивы
Базовое решение всех этих проблем в ISO/ANSI C++ обеспечивают массивы.
Массив — это просто некоторое множество мест в памяти, называемых элементами
массива, или просто элементами, каждый из которых может хранить единицу дан-
ных определенного типа, и к которым можно обращаться по одному имени перемен-
ной. Имена сотрудников в программе платежной ведомости могут быть помещены
в один массив, суммы выплат — в другой, а суммы налогов каждого сотрудника — в
третий.
Индивидуальные элементы массива специфицируются значением индекса, кото-
рый представляет собой просто целое число — порядковый номер элемента в масси-
ве, причем первый имеет индекс 0, второй — 1 и так далее. Вы можете представлять
индекс массива как смещение от его первого элемента. Первый элемент имеет сме-
щение 0, а потому — индекс 0, а индекс 3 ссылается на четвертый элемент в массиве.
Что касается платежной ведомости, вы можете упорядочить массивы таким образом,
что если имя определенного сотрудника помещено в элемент массива employeeName
с определенным значением индекса, то массивы pay и tax также должны хранить
данные, ассоциированные с этим сотрудником, в элементах с тем же индексом.
Базовая структура массива проиллюстрирована на рис. 4.1.
Массивы, строки и указатели 181
На рис. 4.1 показан массив. Имя height содержит шесть элементов, каждый со-
держит отдельное значение. Это могут быть значения роста членов семьи, измерен-
ные в дюймах. Поскольку имеется шесть элементов, значение индекса находится в
пределах от 0 до 5. Чтобы сослаться на определенный элемент, вы пишете имя мас-
сива, за которым идет значение индекса определенного элемента, заключенное в ква-
дратные скобки. Так, например, чтобы сослаться на третий элемент, нужно написать
height [2]. Если вы представите индекс в виде смещения от первого элемента, то
легко увидеть, что значение индекса четвертого элемента будет равно 3.
Общий объем памяти, необходимый для хранения каждого элемента, определя-
ется его типом, и все элементы массива сохраняются в одном непрерывном блоке
памяти.
Значение индекса
для 2-го элемента
Имя массива
Значение индекса
для 5-го элемента
Имя массива
height[O] height[1] height[2] height[3] height[4] height[5]
73 62 51 42 41 34
Массив height содержит 6 элементов
Рис. 4.1. Массив height, содержащий 6 элементов
Объявление массивов
По сути, вы объявляете массив точно так же, как до сих пор объявляли перемен-
ные. Единственным отличием является то, что рядом с именем массива нужно ука-
зать количество его элементов. Например, вы можете объявить массив целых чисел
height, показанный на рис. 4.1, используя следующий оператор объявления:
long height[6];
Поскольку каждое значение типа long занимает 4 байта памяти, весь массив по-
требует 24 байт. Массивы могут иметь любой размер — количество элементов огра-
ничено лишь объемом доступной памяти компьютера, на котором выполняется ваша
программа.
Вы можете объявить массив любого типа. Например, чтобы объявить массив,
предназначенный для хранения рабочего объема и выходной мощности серии двига-
телей, вы можете написать следующее:
double cubic_inches[10];
double horsepower[10];
/ / Объемы двигателей в кубических дюймах
/ / Выходные мощности
Это позволит хранить рабочие объемы и мощности до 10 двигателей, ссылаясь на
них по индексам от 0 до 9. Как вы уже видели это на примере других переменных,
можно объявлять несколько массивов одного и того же типа в одном операторе, но
на практике почти всегда лучше объявлять эти переменные в отдельных операторах.
182 Глава 4
Практическое занятие | ИСПОЛЬЗОВЭНИе МЭССИВОВ
Для целей базового примера использования массивов предположим, что вы храни-
те записи об объемах бензина, купленного для вашей машины, и показания одометра
в каждом случае. Вы можете написать программу для анализа данных, чтобы увидеть,
как изменяется показатель расхода бензина на милю пробега.
// Ех4_01.срр
// Вычисление расхода бензина
#include <iostream>
#include <iomanip>
using std::cin;
using std::cout;
using std::endl;
using std::setw;
int main()
{
const int MAX = 20; // Максимальное число значений
double gas [ MAX ]; // Объем бензина в галлонах
long miles[ MAX ]; // Показания одометра
int count = 0; // Счетчик цикла
char indicator = ’у’; // Индикатор ввода
while( (indicator == ’у’ | | indicator == *Y’) && count < MAX )
{
cout « endl
« "Введите объем бензина: ";
cin » gas[count]; // Читать объем бензина
cout « "Введите показания одометра: ";
cin » miles[count]; // Читать показания одометра
++count;
cout « "Продолжить ввод (у или п) ? ";
cin » indicator;
}
if(count <= 1) // count = 1 после ввода одной пары значений
{ // ... необходимо минимум 2
cout « endl
« "Извините — необходимо минимум два измерения.";
return 0;
}
// Вывод результатов от 2-го замера до последнего
for (int i = 1; i < count; i++)
cout « endl
« setw (2) « i « "." // Номер вывода
« "Куплено бензина = " « gas[i] « " галлонов" // Вывод бензина
« " пробег составил " // Расстояние в милях на один галлон
« (miles [i] - miles [i - l])/gas[i] « " миль на галлон.";
cout « endl;
return 0;
}
Программа предполагает, что вы наполняете бак каждый раз, так что потреблен-
ный объем соответствует записанному пробегу. Ниже показан пример вывода этой
программы.
Массивы, строки и указатели 183
Введите объем бензина: 12.8
Введите показания одометра: 25832
Продолжить ввод (у или п) ? у
Введите объем бензина: 14.9
Введите показания одометра: 26337
Продолжить ввод (у или п) ? у
Введите объем бензина: 11.8
Введите показания одометра: 26598
Продолжить ввод (у или п) ? п
1. Куплено бензина =
2. Куплено бензина =
14.9 пробег составил
11.8 пробег составил
33.8926 миль на галлон.
22.1186 миль на галлон.
Описание полученных результатов
Поскольку вам нужно брать разницу между двумя показаниями одометра, чтобы
вычислить пробег в милях на купленном объеме бензина, из первой пары значений
используется только показание одометра, а объем купленного бензина игнорируется,
поскольку он покрывает расход на расстояние, пройденное до первого замера.
Как видим, во время второго периода, отображенного программой на выходе, ре-
жим движения был весьма неэкономным — вы либо много стояли в пробках, либо ча-
сто парковались.
Размеры двух массивов gas и miles, использованных для хранения входных дан-
ных, определялись значением константы по имени МАХ. Изменяя значение МАХ, вы
можете адаптировать программу для приема различного количества входных значе-
ний. Эта техника часто используется для обеспечения гибкости в отношении объема
обрабатываемой информации. Конечно, весь код программы должен быть написан с
учетом размеров массивов или любых других параметров, специфицированных пере-
менными const. На практике это предоставляет некоторое неудобство, однако, нет
причин, которые не позволили бы в определенной мере адаптировать такой подход.
Чуть позже вы увидите, как можно выделять память для хранения данных во время
выполнения программы, так что не придется фиксировать заранее определенный
объем памяти, выделенный для данных.
Ввод данных
Значения данных читаются в цикле while. Поскольку переменная-счетчик цикла
count может изменяться от 0 до МАХ - 1, мы не можем позволить пользователю про-
граммы ввести больше значений, чем может вместить массив. Вы инициализировали
переменные count и indicator значениями 0 и * у* соответственно, так что тело цик-
ла while будет выполнено, по крайней мере, один раз. Перед каждым вводом данных
выводится приглашение, и прочитанное значение записывается в соответствующий
элемент массива. Элемент, используемый для сохранения конкретного значения, опре-
деляется переменной count, которая при первом вводе содержит значение 0. Элемент
массива указывается в операторе cin посредством count в качестве индекса, и count
затем увеличивается на единицу, готовя к приему значения следующий элемент.
После ввода каждого значения программа запрашивает подтверждение намерения
продолжать ввод. Введенный символ читается в переменную indicator, затем про-
веряется в условии цикла. Цикл прерывается, если не будет введено ’ у ’ или 1Y ’, или
же count достигнет значения МАХ.
После завершения цикла (по любой причине) count содержит значение на едини-
цу больше индекса последнего заполненного элемента каждого массива. (Вспомните,
что вы инкрементируете его после ввода каждого нового элемента.) Это значение
184 Глава 4
проверяется, чтобы убедиться, что было введено не менее двух пар значений. Если
это не так, программа завершается с соответствующим сообщением, поскольку для
вычисления пробега необходимо два показания одометра.
Выдача результата
Вывод программы генерируется в цикле for. Управляющая переменная i изме-
няется от 0 до count-1, позволяя вычислять пробег как разницу между текущим по-
казанием miles [i] и предыдущим miles [i-1]. Обратите внимание, что значение
индекса может быть любым выражением, вычисление которого дает целочисленный
результат — корректное значение от нуля до величины, на единицу меньшей, чем ко-
личество элементов в массиве.
Если значение индексного выражения окажется вне допустимого диапазона, соот-
ветствующего элементам массива, то вы обратитесь к несуществующим данным, ко-
торые могут содержать значения других переменных, или какой-то мусор, или даже
код самой программы. Если обращение к такому элементу произойдет в выражении,
то вы получите какие-то произвольные данные, которые, участвуя в вычислениях, по-
родят некорректные результаты. Если же вы попытаетесь записать значение в эле-
мент массива, указанный неправильным индексом, то перезапишете любые данные,
которые окажутся в этом месте памяти. Если это будет программный код, результат
окажется катастрофическим. Когда вы используете неправильное значение индекса,
не выдается никаких предупреждений — ни во время компиляции, ни во время выпол-
нения. Единственный способ предохраниться от такой ситуации — соответствующим
образом писать код программы.
Для генерации вывода для всех введенных значений, за исключением первой,
используется единственный оператор cout. Номер каждой выходной строки также
генерируется значением управляющей переменной цикла i. Количество миль про-
бега на один галлон вычисляется непосредственно в операторе вывода. Вы можете
использовать элементы массива в выражениях точно таким же образом, как любые
другие переменные.
Инициализация массивов
Чтобы инициализировать массив в его объявлении, нужно поместить начальные
значения элементов в ограниченный фигурными скобками список, разделенный запя-
тыми, который находится справа от знака равенства, следующего за именем массива.
Ниже показан пример того, как можно объявить и инициализировать массив:
int cubic_inches [5] = { 200, 250, 300, 350, 400 };
Массив называется cubiclinches, содержит пять элементов, каждый из кото-
рых хранит значение типа int. Элементы массива инициализированы списком
значений в фигурных скобках, соответствующим последовательности значений ин-
декса массива, поэтому в данном случае cubic_inches [0] получает значение 200,
cubic_inches [1] — значение 250, cubic_inches [2] — значение 300 и так далее.
Вы не должны специфицировать больше значений, чем объявлено элементов в
массиве, но меньше — указать можно. Если задано меньше значений, то они присва-
иваются последовательно элементам массива, начиная с первого — то есть элемента
с индексом 0. Элементы массива, для которых не указаны начальные значения, ини-
циализируются нулями. Это не то же самое, что происходит, если вообще не указать
списка инициализации. Без списка инициализации элементы массива получают про-
извольные случайные значения. Кроме того, если вы включаете список инициализа-
Массивы, строки и указатели 185
ции, в нем должен присутствовать хотя бы один элемент, иначе компилятор генери-
рует сообщение об ошибке. Я могу проиллюстрировать это на следующем довольно
ограниченном примере.
Практическое занятие | ИНИЦИЭЛИЗЭЦИЯ МЭССИВЭ
// Ех4_02.срр
// Демонстрация инициализации массива
#include <iostream>
#include <iomanip>
using std::cout;
using std::endl;
using std::setw;
int main()
{
int value [5] = { 1, 2, 3 };
int junk [5];
cout « endl;
for (int i = 0; i < 5; i++)
cout << setw(12) « value [i];
cout « endl;
for (int i = 0; i < 5; i++)
cout « setw(12) « junkfi];
cout << endl;
return 0;
}
В этом примере объявляются два массива, первый из которых — value, инициали-
зируется частично, а второй — junk, не инициализируется вообще. Программа выда-
ет две строки вывода, которые на моем компьютере выглядят так:
1 2 3 0 0
-858993460 -858993460 -858993460 -858993460 -858993460
Вторая строка (соответствующая значениям от junk[0] до junk[4]) на вашем
компьютере может отличаться.
Описание полученных результатов
Первые три элемента массива value инициализируются указанными значениями,
а остальные два — по умолчанию нулями. В случае junk все значения случайны, по-
скольку никаких начальных значений не указано вообще. Элементы этого массива со-
держат то, что осталось в данной области памяти от программы, которая использова-
ла ее раньше.
Удобный способ инициализации всего массива нулевыми значениями — это просто
указание единственного начального значения, равного 0, например:
long data[100] = {0}; // Инициализировать все элементы нулями
Этот оператор объявляет массив data размером 100 элементов, инициализирован-
ных нулями. Первый элемент инициализируется значением, указанным в фигурных
скобках, а остальные инициализируются нулями по умолчанию, поскольку значения
для них не указаны явно.
186 Глава 4
Вы можете также опустить размер массива числового типа, если указываете спи-
сок значений инициализации. Количество элементов в массиве определяется количе-
ством указанных инициирующих значений. Например, объявление массива:
int value [] ={2,3,4};
определяет массив из трех элементов с начальными значениями 2, 3 и 4.
Символьные массивы и обработка строк
Массив типа char называется символьным массивом и обычно используется для
хранения символьных строк. Символьная строка — это последовательность символов,
дополненная специальным символом-ограничителем, указывающим конец строки.
Ограничивающий строку символ записывается управляющей последовательностью
’ \0 ’, и иногда его называют нулевым символом, поскольку он представлен байтом, в
котором все биты равны 0. Строки, организованные подобным образом, часто назы-
вают строками в стиле С, поскольку такое определение строк впервые было представ-
лено в языке С, на основе которого Бьерн Страуструп разработал язык C++. Это не
единственное представление строк, которое вы можете использовать; позднее в этой
книге вы познакомитесь и с другими. В частности, программы C++/CLI используют
другое представление строк, а библиотека MFC определяет класс CString для пред-
ставления строк.
Представление строки в стиле С в памяти показано на рис. 4.2.
name [4]
Символ-ограничитель строки —
Каждый символ в строке
занимает один байт
char name[] = "Albert Einstein";
Рис. 4,2. Представление строки в стиле С
На рис. 4.2 можно видеть, как выглядит строка в памяти, и показана форма объяв-
ления строки, которую мы вскоре рассмотрим.
Каждый символ в строке занимает один байт, поэтому вместе с ограничивающим нулевым
символом строке необходимо количество байт, на единицу превышающее количество симво-
лов в ней.
Вы можете объявить символьный массив и инициализировать его строчным лите-
ралом, например:
char movie_star [15] ® "Marilyn Monroe";
Обратите внимание, что ограничивающий символ ’ \ 0 ’ добавляется компилято-
ром автоматически. Если вы включите его в строчный литерал явно, то у вас будет
два нулевых символа. Однако вы должны учитывать место для нулевого символа, ког-
да определяете необходимое количество элементов для символьного массива.
Вы можете позволить компилятору определить длину инициализированного мас-
сива самостоятельно, как вы видели на рис. 4.1. Вот другой пример:
char president!] = "Ulysses Grant";
Массивы, строки и указатели 187
Поскольку размер не указан, компилятор выделяет пространство, достаточное
для размещения инициализирующей строки, плюс ограничивающий нулевой символ.
В данном случае он выделит 14 элементов для массива president. Конечно, если вы
хотите использовать данный массив позднее для хранения другой строки, ее длина
(включая нулевой символ-ограничитель) не должна превышать 14 байт. В общем слу-
чае на вас ложится ответственность обеспечить достаточный размер массива, чтобы в
него поместилась любая строка, которую вы в последствии захотите в нее поместить.
Строковый ввод
Заголовочный файл <iostream> содержит определения множества функций для
чтения символов с клавиатуры. Одну их них мы рассмотрим здесь. Это — функция
getline (), которая читает последовательность вводимых с клавиатуры символов и
помещает ее в символьный массив в виде строки с ограничивающим нулем \ 0. Обычно
вы будете использовать функцию get line () в операторах вроде следующего:
const int MAX = 80; // Максимальная длина строки, включая \0
char name[МАХ]; // Массив для хранения строк
cin. get line (name, MAX, '\n'); // Прочитать введенную последовательность как строку
В этом фрагменте сначала объявляется массив char размером в МАХ элементов,
а затем в него читается строка из cin с помощью функции get line (). Источник
данных — cin — записывается так, как показано, с точкой, отделяющей его от име-
ни функции. Что именно означают аргументы функции get line (), можно видеть на
рис. 4.3.
Максимальное количество
символов для прочтения.
Когда указанное количество
прочитано, ввод прекращается.
Имя массива типа char[],
в который помещаются символы,
прочтенные из cin.
Символ, прекращающий процесс
ввода. Вы можете специфицировать
здесь любой символ, и первое его
появление остановит ввод.
cin.getline( name , MAX, ’\n’);
Рис. 43. Аргументы функции getline ()
Поскольку последний аргумент, переданный функции getline () — это ’ \п* (сим-
вол новой строки), а второй аргумент — МАХ, символы читаются из cin до тех пор,
пока не будут прочитан символ ’ \п ’, или когда будет прочитано МАХ-1 символов — в
зависимости от того, что произойдет раньше. Максимальное количество прочитан-
ных символов равно МАХ-1, чтобы позволить дописать символ-ограничитель • \ 0 ’ к
последовательности символов, сохраняемых в массиве. Символ ’ \п ’ генерируется,
когда вы нажимаете клавишу <Enter> на клавиатуре, а потому это — наиболее удобный
символ для завершения ввода. Однако вы можете указать какой-то другой символ в
качестве признака завершения ввода, изменив последний аргумент. Символ ’ \п ’ не
сохраняется во входном массиве name, но как уже было сказано, в конец введенной
строки, помещенной в массив, добавляется ’ \0 ’.
188 Глава 4
Позднее, когда мы будем говорить о классах, вы узнаете больше об используемом
здесь синтаксисе. А пока примем, его как данность и применим в примере.
Практическое занятие | ПрОГраММИрОВЭНИв СО СТрОКЭМИ
// Ех4_03.срр
// Подсчет символов строки
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
const int MAX =80; // Максимальный размер массива
char buffer[MAX]; // Входной буфер
int count =0; // Счетчик символов
cout << "Введите строку не длиннее 80 символов: \п";
cin.getline(buffer, MAX, ’\n’); // Читать строку до \n
while(buffer[count] != ’\0') // увеличивать count пока
count++; // текущий символ — не нулевой
cout << endl
« "Строка \"" « buffer
« "\" содержит " << count « " символов.";
cout « endl;
return 0;
}
Типичный вывод этой программы должен выглядеть так:
Введите строку не длиннее 80 символов:
Радиация погубит ваши гены
Строка "Радиация погубит ваши гены" содержит 26 символов.
Описание полученных результатов
Эта программа объявляет символьный массив buffer и читает символьную строку
в этот массив с клавиатуры после вывода приглашения на ввод. Чтение с клавиатуры
прекращается, когда пользователь нажимает <Enter>, или после того, как прочитано
МАХ-1 символов.
Цикл while используется для подсчета количества прочитанных символов. Цикл
продолжается до тех пор, пока текущий символ, находящийся в buffer [count], не
равен ’ \ 0 ’. Такая проверка текущего символа при проходе по элементам массива ча-
сто применяется в “родном” C++. Единственным действием, выполняемым в цикле,
является инкремент значения счетчика count для каждого ненулевого символа.
Существует также библиотечная функция strlen (), которая избавляет вас от са-
мостоятельного кодирования при подсчете символов строки. Если вы хотите ее ис-
пользовать, то должны включить в свою программу заголовочный файл <cstring> с
помощью директивы # include:
#include <cstring>
Буква “с” в имени заголовочного файла говорит о том, что этот файл содержит
определения, относящиеся к библиотеке языка С, которая является частью библио-
Массивы, строки и указатели 189
теки C++. Этот заголовок также содержит функцию wcsnlen (), возвращающей длину
строки широких символов.
Используя функцию strlen (), вы можете заменить цикл while следующим опе-
ратором:
count = std: :strlen(buffer);
Аргументом служит имя массива, содержащего строку, а возвращает функция
strlen () длину этой строки в виде целочисленного значения типа size_t. Многие
функции стандартной библиотеки возвращают значение типа size_t, а сам тип
size_t определен в стандартной библиотеке с помощью оператора typedef как эк-
вивалент одного из фундаментальных типов, обычно — unsigned int. Причина при-
менения size_t вместо непосредственного использования фундаментального типа
связана с тем, что это обеспечивает гибкость в том, что действительный тип, скрыва-
ющийся за псевдонимом size t, может определяться конкретной реализацией C++.
Стандарт C++ разрешает варьировать диапазоны допустимых значений фундаменталь-
ных типов для наилучшего соответствия аппаратной архитектуре, и size_t может
быть определено как эквивалент наиболее подходящего типа в текущем аппаратном
окружении.
В конце примера введенная строка и количество ее символов отображаются един-
ственным оператором вывода. Обратите внимание на применение управляющей по-
следовательности 1 \ " ’ для вывода двойной кавычки.
Многомерные массивы
Массивы, которые мы определяли до сих пор, имеют один индекс и называются
одномерными массивами. Но массив может иметь и более одного индексного зна-
чения — в этом случае он называется многомерным массивом. Предположим, что у
вас есть поле, на котором расположена плантация бобов, по 10 растений на грядке, и
это поле содержит 12 таких грядок (то есть всего имеется 120 единиц растений). Вы
можете объявить массив для записи веса бобов, собранных от каждого растения, ис-
пользуя следующий оператор:
double beans[12] [10];
Здесь объявляется двумерный массив beans, где первый индекс — номер грядки,
а второй — номер растения на грядке. Чтобы обратиться к любому конкретному эле-
менту, необходимо указать два индекса. Например, вы можете установить значение
элемента, соответствующего пятому растению в третьей грядке, следующим операто-
ром:
beans[2] [4] = 10.7;
Напомним, что значения индексов начинаются с нуля, поэтому значение индекса
третьей грядки — 2, а индекс пятого растения — 4.
Будучи успешным бобовым фермером, вы можете иметь несколько таких полей,
засеянных бобами по тому же шаблону. Предполагая, что у вас 8 полей, можно опре-
делить трехмерный массив для записи данных об урожае следующим образом:
double beans[8][12][10];
Такая запись позволяет организовать учет каждого растения, растущего на всех
этих полях, причем самый левый индекс ссылается на определенное поле. Если вы
когда-нибудь займетесь выращиванием бобов в международном масштабе, то сможете
использовать четырехмерный массив, где дополнительное измерение будет обозна-
190 Глава
чать страну. Если предположить, что вы — настолько же успешный продавец, как и
фермер, то выращивание такого количества бобов, в конце концов, станет оказывать
влияние на озоновый слой.
Массивы хранятся в памяти так, что самый правый индекс растет быстрее всего.
То есть массив data [ 3 ] [ 4 ] — это трехмерный массив, состоящий из массивов по че-
тыре элемента в каждом. Организация такого массива показана на рис. 4.4.
Элементы массива располагаются в непрерывном блоке памяти, как показано стре-
лочками на рис. 4.4. Первый индекс выбирает определенную строку внутри массива,
а второй индекс выбирает элемент внутри строки.
data[O][O] data[0][1] data[0][2] data[0][3]
data[1][0]
data[1][1] data[1][2] data[1][3]
data[2][0]
data[2][1]
data[2][2]
data[2][3]
Элементы массива располагаются в непрерывной области памяти
Рис. 4.4. Организация массива data[3] [4]
Обратите внимание, что двумерный массив в родном C++ — на самом деле одномер-
ный массив, состоящий из одномерных массивов. Массив с тремя измерениями в род-
ном C++ — это одномерный массив элементов, в котором каждый элемент представля-
ет собой одномерный массив одномерных массивов. Большую часть времени вам не
придется об этом беспокоиться, но как вы увидите позднее, массивы C++/CLI отлича-
ются от массивов C++. Из сказанного следует, что для массива, показанного на рис. 4.4,
выражения data [ 0 ], data [ 1 ] и data [ 2 ] представляют одномерные массивы.
Инициализация многомерных массивов
Для того чтобы инициализировать многомерный массив, используется расширен-
ный метод инициализации одномерных массивов. Например, вы можете инициализи-
ровать двумерный массив data с помощью следующего объявления:
long data[2] [4] = {
То есть, инициализация значений каждой строки массива содержится внутри соб-
ственной пары фигурных скобок. Поскольку в каждой строке четыре элемента, в
каждой группе присутствует по четыре значения инициализации, и поскольку строк
всего две, внутри скобок находится две группы инициализирующих значение, разде-
ленных запятой.
Массивы, строки и указатели 191
Вы можете пропустить инициализацию значений в любой строке, в этом случае
остальные элементы массива инициализируются нулевыми значениями, например:
long data[2][4] = {
{1,2,3 },
{ 7, 11 }
};
Я дополнил пробелами списки инициализации, чтобы показать, где пропущены
значения. Элементы data [ 0 ] [ 3 ], data [ 1 ] [ 2 ] и data [ 1 ] [ 3 ] не получают инициали-
зирующих значений и потому равны нулю.
Если вы хотите инициализировать весь массив нулевыми значениями, вы можете
написать просто:
long data[2] [4] = {0};
Если вы инициализируете массивы с большим числом измерений, помните, что
вы должны указать столько вложенных фигурных скобок для групп инициализирую-
щих значений, сколько в массиве измерений.
Практическое занятие | ХрЭНвНИе МНОЖвСТВЭ СТРОК
Вы можете использовать один двумерный массив для сохранения нескольких
строк в стиле С. Как это работает, можно увидеть на примере.
// Ех4_04.срр
// Сохранение строк в массиве.
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
int main ()
{
char stars[6][80] = {"Robert Redford",
"Hopalong Cassidy",
"Lassie",
"Slim Pickens",
"Boris Karloff",
"Oliver Hardy"
};
int dice = 0;
cout « endl
« " Выберите свою счастливую звезду!"
« " Введите число от 1 до 6: ";
cin » dice;
if (dice >= 1 && dice <= 6) // Проверить корректность ввода
cout « endl // Вывести имя звезды
« "Ваша счастливая звезда " « stars[dice - 1];
else
cout « endl // Неверный ввод
« "Очень жаль, но у вас нет счастливой звезды.";
cout « endl;
return 0;
}
192 Глава 4
Описание полученных результатов
Помимо развлекательного значения, главное, что интересно в этом примере — объ-
явление массива stars. Это двумерный массив элементов типа char, который может
содержать до шести строк, каждая из которых имеет до 80 символов длиной (вклю-
чая ограничивающий нулевой символ, автоматически добавляемый компилятором).
Инициализирующие строки массива заключены в кавычки и разделены запятыми.
Один из недостатков подобного применения массивов заключается в том, что значительная
часть памяти не используется. Все строки короче 80 символов и потому оставшиеся избы-
точные элементы массивов расходуются впустую.
Вы можете также позволить компилятору определить, сколько именно строк не-
обходимо поместить в массив, пропустив величину первого измерения массива и объ-
явив его следующим образом:
char stars[][80] = { "Robert Redford",
"Hopalong Cassidy",
"Lassie",
"Slim Pickens",
"Boris Karloff",
"Oliver Hardy"
Это заставит компилятор определить первое измерение так, чтобы уместить необ-
ходимое количество инициализированных строк. Поскольку их всего шесть, резуль-
тат будет тем же самым, но это исключит вероятность ошибки. Вы не можете про-
пустить оба измерения массива. В массиве из двух или более измерений самое правое
всегда должно быть определено.
Обратите внимание на точку с запятой в конце объявления. О ней легко забыть при иници-
ализации значений массива.
Когда вам необходимо сослаться на строку при выводе в следующем операторе,
достаточно указать только значение первого индекса:
cout « endl // Вывести имя звезды
« "Ваша счастливая звезда " « stars[dice - 1];
Значение с одним индексом выбирает конкретный 80-символьный подмассив, а
операция вывода отображает его содержимое вплоть до нулевого символа. Индекс
указан как dice - 1, потому что dice имеет значения от 1 до 6, в то время как значе-
ния индекса варьируются от 0 до 5.
Косвенный доступ к данным
Переменные, с которыми вы имели дело до сих пор, обеспечивали вам возмож-
ность именовать некоторое место в памяти, в котором можно сохранять данные
определенного типа. Содержимое переменной либо вводилось из некоторого внеш-
него источника, такого как клавиатура, либо вычислялось на основе других введен-
ных значений. Но есть и другой вид переменных C++, не хранящих в себе данные,
которые вы обычно вводите для вычислений, но значительно повышающих мощь и
гибкость ваших программ. Переменные такого рода называются указателями.
Массивы, строки и указатели 193
Что такое указатель?
Каждое место в памяти, которое вы используете для хранения данных, имеет
адрес. Адрес предоставляет возможность для оборудования компьютера обращаться
к определенному элементу данных. Указатель — это переменная, которая сохраняет
адрес другой переменной определенного типа. Переменная-указатель обладает име-
нем, как и любая другая переменная, и также имеет тип, определяющий то, на какого
рода данные она может указывать. Переменная, являющаяся указателем, содержащим
адрес местоположения в памяти, которое хранит значение типа int, называется “ука-
зателем на int”.
Объявление указателей
Объявления указателей подобно объявлению обычных переменных, за исключе-
нием того, что имя дополняется впереди звездочкой, говорящей о том, что данная
переменная является указателем. Например, чтобы объявить указатель pnumber типа
long, можно использовать следующий оператор:
long* pnumber;
Это объявление записано со звездочкой, расположенной ближе к имени перемен-
ной. Если хотите, можете также написать так:
long *pnumber;
Компилятору это безразлично. Однако типом переменной pnumber является “ука-
затель на long”, что часто обозначается помещением звездочки ближе к имени типа.
Не важно, какой стиль вы предпочтете — важно придерживаться его согласованно.
Можно смешивать объявления обычных переменных и указателей в одном опера-
торе, например:
long* pnumber, number = 99;
Этот оператор, как и ранее, объявляет указатель pnumber типа “указатель на
long”, но также объявляет переменную number типа long. В конечном итоге, вероят-
но, лучше все-таки объявлять указатели отдельно от других переменных; в противном
случае такой оператор может ввести в заблуждение в отношении типа объявляемых
переменных, особенно если вы предпочитаете размещать * рядом с именем типа.
Следующие операторы, безусловно, выглядят яснее, к тому же размещение объявле-
ния в отдельных строках позволяет вам добавить им индивидуальные комментарии,
что облегчает чтение программы.
long number - 99; // Объявление и инициализация переменной long
long* pnumber; // Объявление переменной типа указатель на long
В C++ существует общепринятое соглашение — называть переменные-указатели
именами, начинающимися с р. Это позволяет сразу увидеть, какие переменные явля-
ются указателями, что, в свою очередь, упрощает восприятие программ.
Давайте разберем пример, демонстрирующий работу указателей, не задумываясь
пока, для чего он нужен. Я продемонстрирую применение указателей очень кратко.
Предположим, что у вас есть целочисленная переменная типа long по имени number,
объявленная выше и содержащая значение 99. Кроме того, у вас есть указатель на
long по имени pnumber, который вы можете использовать для хранения адреса пере-
менной number. Но как получить адрес этой переменной?
194 Глава
Операция получения адреса
Что вам нужно для этого — операция получения адреса, &. Она представляет со-
бой унарную операцию, возвращающую адрес переменной — своего операнда. Он
также называется операцией ссылки — по причине, которую я поясню позже в этой
главе. Чтобы присвоить значение адреса переменной указателю, вы можете написать
следующий оператор присваивания:
pnumber = &number; // Сохранить адрес number в pnumber
Результат выполнения этой операции можно видеть на рис. 4.5.
&number <
Адрес: 1008
pnumber
1008 <—|
number
99
pnumber = &number;
Рис» 4.5, Результат выполнения операции получения адреса
Вы можете использовать операцию & для получения адреса любой переменной, но
для сохранения его вам понадобится переменная-указатель соответствующего типа.
Например, если нужно сохранить адрес переменной double, указатель должен быть
объявлен с типом double *, то есть “указатель на double”.
Использование указателей
Получение адреса переменной и сохранение его в переменной-указателе — это
очень хорошо, но более интересно, как его использовать. Основной смысл приме-
нения указателя состоит в возможности доступа к данным, на которые он указывает.
Это делается с помощью операции разыменования (*).
Операция разыменования
Для доступа к переменной, на которую указывает указатель, вы будете применять
операцию разыменования указателя (*), подставляя в качестве операнда имя пере-
менной-указателя. Название “операция разыменования” или “операция косвенного
доступа” говорит о том, что обращение к данным не прямое, и что для получения до-
ступа к данным, на которые указывает указатель, его следует “разыменовать”.
Один аспект этой операции, который может показаться запутанным, связан с суще-
ствованием разных применений одного и того же символа *. Во-первых, это — опера-
ция арифметического умножения, во-вторых — операция разыменования, и вдобавок
она используется при объявлении переменных-указателей. Всякий раз, встречая сим-
вол *, компилятор определяет его значение в каждом конкретном случае из контекста.
Когда вы перемножаете две переменных, например, А*В, не существует никакой дру-
гой осмысленной интерпретации этого выражения, кроме как операции умножения.
Массивы, строки и указатели 195
Зачем нужны указатели?
Вопрос, который обычно возникает при изучении указателей, звучит так: “Зачем
они вообще нужны?”. В конце концов, получить адрес переменной, которая вам уже
известна, сохранить его в указателе, чтобы потом разыменовать, выглядит излишним,
то есть чем-то таким, без чего вполне можно обойтись. Однако существует несколько
причин, объясняющих важность указателей.
Как вы вскоре увидите, нотацию указателей можно использовать для операций с
данными, хранимыми в массиве; иногда это выполняется быстрее, чем в случае при-
менения обычной нотации массивов. К тому же, когда позднее вы будете определять
свои собственные функции, то увидите, что указатели интенсивно используются для
обеспечения доступа функций к объемным блокам данных, таким как массивы, кото-
рые определены вне этих функций. Но как вы увидите позднее, еще более важно то,
что память для переменных может выделяться динамически, то есть во время выпол-
нения программы. Это средство позволяет программам адаптировать использование
памяти к объему обрабатываемых данных. Поскольку при этом вы не можете знать
наперед, сколько переменных придется создать динамически, основной способ рабо-
ты с такой памятью — через указатели. Так что будьте уверены, что указатели вам при-
годятся.
Практическое занятие
Использование указателей
В следующем примере вы можете испытать различные аспекты операций с указа-
телями.
//Ех4_05.срр
// Упражнение с указателями
#include <iostream>
using std::cout;
using std::endl;
using std::hex;
using std::dec;
int main ()
{
long* pnumber = NULL; // Объявление и инициализация указателя
long numberl = 55, number2 = 99;
pnumber = Snumberl; // Сохранить адрес в указателе
*pnumber +=11; // Увеличить numberl на 11
cout « endl
« "numberl = " « numberl
« " &numberl = " « hex « pnumber;
pnumber = &number2; // Изменить указатель на адрес number2
numberl = *pnumber*10; // 10 раз number2
cout « endl
« "numberl = " « dec << numberl
« " pnumber = " « hex << pnumber
« " *pnumber = " « dec << *pnumber;
cout « endl;
return 0;
}
На моем компьютере этот пример генерирует следующий вывод:
numberl = 66 &numberl = 0012FEC8
numberl = 990 pnumber = 0012FEBC *pnumber = 99
196 Глава 4
Описание полученных результатов
В этом примере нет ввода. Все операции выполняются с инициализированными
значениями переменных. После сохранения адреса numberl в указателе pnumber зна-
чение numberl увеличивается косвенно, через указатель:
*pnumber +=11; // Увеличить numberl на 11
Обратите внимание, что при объявлении указателя pnumber его значение инициализирова-
но NULL» В следующем разделе я расскажу об инициализации указателей.
Операция разыменования говорит о том, что вы добавляете 11 к содержимому пе-
ременной, на которую указывает pnumber, то есть к numberl. Если вы забудете напи-
сать символ * в этом операторе, то это будет означать попытку прибавить 11 к адресу,
который хранится в указателе.
Значение numberl и адрес numberl, который хранится в pnumber, отображаются
на экране. Для представления значения адреса в шестнадцатеричной форме применя-
ется манипулятор hex.
Вы можете получить значение обычной целочисленной переменной в шестнадца-
теричном виде, используя манипулятор hex. Вы посылаете его в выходной поток точ-
но таким же способом, как делаете это с endl, в результате чего весь последующий
вывод идет в шестнадцатеричном формате. Если вы хотите, чтобы последующий вы-
вод был в десятичном виде, то для этого должны применить манипулятор dec, чтобы
переключить режим вывода обратно в десятичный.
После вывода первой строки содержимое pnumber устанавливается в значение
адреса number2. Затем переменной numberl присваивается значение 10, умноженное
на number2:
numberl = *pnumber*10; // 10 раз number2
Это вычисление выполняется путем косвенного доступа к number2 через указа-
тель. Вторая строка вывода показывает результат этого вычисления.
Значения адресов, которые вы видите в выводе, могут существенно отличаться
от приведенных выше, поскольку они зависят от того, куда именно в память была
загружена программа, что, в свою очередь, зависит от того, как сконфигурирована
ваша операционная система. Префикс Ох в значениях адреса означает, что они ото-
бражаются в шестнадцатеричном формате. Обратите внимание, что адреса &numberl
и pnumber (когда он содержит &number2), отличаются на четыре байта. Это показы-
вает, что numberl и number2 занимают соседние участки памяти, поскольку каждое
значение типа long занимает 4 байта. Вывод доказывает, что все работает так, как и
следовало ожидать.
Инициализация указателей
Использование неинициализированных указателей чрезвычайно опасно. Вы може-
те легко перезаписать произвольную область памяти через неинициализированный
указатель. Полученный ущерб при этом зависит лишь от степени вашего везения, по-
этому инициализировать указатели — более чем хорошая идея. Очень легко инициа-
лизировать указатель адресом переменной, которая уже определена. Здесь вы можете
видеть, как я инициализировал указатель pnumber адресом переменной number, про-
сто применив операцию & к имени переменной:
pnumber = &numberl; // Сохранить адрес в указателе
*pnumber +=11; // Увеличить numberl на 11
Массивы, строки и указатели
197
Инициализируя указатель адресом другой переменной, помните, что эта перемен-
ная должна быть объявлена до объявления указателя.
Конечно, объявляя указатель, вы можете решить не инициализировать его адре-
сом определенной переменной. В этом случае его можно инициализировать указате-
лем, эквивалентным нулю. Для этого в Visual C++ предусмотрен символ NULL, кото-
рый уже определен как 0, поэтому указатель можно объявлять и инициализировать с
помощью следующего оператора вместо того, что вы видели ранее:
int* pnumber = NULL; // Указатель, не указывающий ни на что
Это гарантирует, что указатель не содержит адреса, который воспринимается, как
корректный, и представляет ему значение, которое можно проверять в операторе if,
вроде такого:
if (pnumber == NULL)
cout « endl « ’’pnumber есть null.’’;
Конечно, вы можете инициализировать указатель явно числом 0, что также га-
рантирует значение, не указывающее ни на что. Ни один объект не может быть раз-
мещен по адресу 0, поэтому 0 применяется, как адрес, означающий, что у указателя
нет цели. К тому же, если вы собираетесь собирать свой код другими компиляторами,
лучше использовать 0 для инициализации нулевых указателей.
К тому же это лучше согласовано с принятым 'хорошим тоном ’ ISO/ANSI C++, поскольку
если у вас есть именованный объект в C++, он должен иметь тип; однако NULL не имеет
типа — это просто псевдоним для 0. Как вы увидите дальше в этой главе, в C++/СЫ дела
обстоят несколько иначе.
Для использования 0 в качестве инициализирующего значения указателя, нужно
просто написать:
int* pnumber = 0; // Указатель, не указывающий ни на что
Чтобы проверить, содержит ли указатель корректный адрес, используйте следую-
щий оператор:
if (pnumber == 0)
cout « endl « ’’pnumber равен null.’’;
С тем же успехом можете применить и такой оператор:
if(!pnumber)
cout « endl « ’’pnumber равен null.”;
Этот оператор делает то же самое, что и предыдущий.
Конечно, вы также можете использовать и такую форму:
if(pnumber ’= 0)
I/ Указатель правильный, делать что-то полезное
Адрес, на который указывает NULL-указатель, содержит "мусорное” значение. Никогда не
пытайтесь разыменовывать нулевой указатель, поскольку это приведет к немедленному пре-
рыванию работы вашей программы.
Указатели на char
Указатель типа char * обладает интересным свойством — он может быть инициа-
лизирован строковым литералом. Так, например, можно объявить и инициализиро-
вать указатель следующим оператором:
char* proverb = "A miss is as good as a mile.’’;
198 Глава 4
Это выглядит похожим на инициализацию массива char, однако есть небольшое
отличие. Здесь создается строковый литерал (на самом деле, массив типа const
char), содержащий символьную строку, заключенную в кавычки, ограниченную сим-
волом \0, и его адрес присваивается указателю proverb. Адресом литерала будет
адрес его первого символа. Это показано на рис. 4.6.
1. Создается указатель proverb.
2. Адрес строки сохраняется в указателе.
Рис. 4.6. Создание и инициализация указателя
практическое занятие | Счастливые звезды и указатели
Теперь вы можете переписать ранее приведенный пример со счастливыми звезда-
ми, используя указатели вместо массива, чтобы увидеть, как это будет работать.
// Ех4_06.срр
// Инициализация указателей строками
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
char* pstrl = "Robert Redford";
char* pstr2 = "Hopalong Cassidy";
char* pstr3 = "Lassie";
char* pstr4 = "Slim Pickens";
char* pstr5 = "Boris Karloff";
char* pstr6 = "Oliver Hardy";
char* pstr = "Ваша счастливая звезда ";
int dice = 0;
cout « endl
« " Выберите счастливую звезду!"
« " Введите число от 1 до 6: ";
cin » dice;
cout « endl;
switch(dice)
{
case 1: cout « pstr « pstrl;
break;
Массивы, строки и указатели 199
case 2: cout « pstr << pstr2;
break;
case 3: cout « pstr « pstr3;
break;
case 4: cout « pstr « pstr4;
break;
case 5: cout « pstr « pstr5;
break;
case 6: cout « pstr « pstr6;
break;
default: cout « "Очень жаль, но у вас нет счастливой звезды.";
}
cout << endl;
return 0;
Описание полученных результатов
Массив из Ех4_04 . срр заменен шестью указателями, от pstrl до pstr6, каждый
из которых инициализирован строкой — именем. К тому же объявлен дополнитель-
ный указатель pstr, инициализированный фразой, которая должна появляться в
начале нормальной строки вывода. Поскольку здесь используются дискретные ука-
затели, легче применить оператор switch для выбора соответствующего выходно-
го сообщения, нежели использовать if, как вы это делали в оригинальной версии.
Любое неправильное введенное значение будет обработано опцией default опера-
тора switch.
Вывод строки, на которую указывает указатель, не может быть проще. Как види-
те, в операторе вывода вы просто записываете имя указателя. Может показаться, что
здесь, как и в Ех4_05 . срр, появление имени указателя в операторе вывода приведет
к отображению значения содержащегося в нем адреса, однако это не так. В чем раз-
ница? Ответ заключается в том, что операция вывода видит, что типом ее аргумента
является “указатель на char” и трактует указатели этого типа специальным образом —
как строку (которая есть массив char), поэтому выводится сама строка, а не ее адрес.
Использование указателей в этом примере исключает лишний расход памяти, ко-
торый имелся в предыдущей версии этой программы с массивами, но программа вы-
глядит слишком многословной. Должен существовать лучший способ. И конечно же
он есть — использование массива указателей.
Практическое занятие
Массивы указателей
В массиве указателей типа char каждый элемент может указывать на независимую
строку, причем длины этих строк могут быть разными. Вы можете объявить массив
указателей точно так же, как объявляете нормальный массив. Давайте перепишем
предыдущий пример, используя массив указателей.
// Ех4_07.срр
// Инициализация указателей строками
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
200 Глава 4
int main ()
char* pstr[] = {’’Robert Redford”, // Инициализация массива указателей
"Hopalong Cassidy”,
"Lassie",
"Slim Pickens",
"Boris Karloff",
"Oliver Hardy"
char* pstart = "Ваша счастливая звезда
int dice =0;
cout « endl
« " Выберите счастливую звезду!"
« " Введите число от 1 до 6: ";
cin » dice;
cout « endl;
if (dice >= 1 && dice <= 6) // Проверить правильность ввода
cout « pstart « pstr[dice - 1]; // Вывести имя звезды
else
cout « "Очень жаль, но у вас нет счастливой звезды."; // Неверный ввод
cout « endl;
return 0;
Описание полученных результатов
В данном случае вы получили практически наилучшее из возможных решений.
Имеется одномерный массив указателей на char, объявленный так, что компилятор
может самостоятельно определить его размер на основе списка инициализирующих
строк. Использование памяти в данном примере показано на рис. 4.7.
Если сравнить с применением “нормального” массива, то, наверное, этот пример
требует меньших накладных расходов памяти. Однако посмотрим, что получилось
на самом деле. В случае с массивами нужно было установить длину каждой строки
равной максимальной из них, и в результате шесть строк по семнадцать байт каждая
занимали 102 байта, поэтому, применив массив указателей, мы сэкономили всего -1
байт! Что не так? Дело в том, что для этого небольшого числа относительно корот-
ких строк становится существенным размер дополнительного массива указателей. Вы
получите экономию памяти, если будете работать с большим числом строк, большей и
разнообразной длины.
Экономия памяти — не единственное преимущество использования указателей.
Во множестве случаев также экономится время. Подумайте, что случится, если вы за-
хотите переместить “Oliver Hardy” на первую позицию, a “Robert Redford” — в конец
списка. В случае с массивом указателей нужно всего лишь обменять значения двух
указателей — сами строки при этом останутся там, где и были. Если же они будут хра-
ниться как массивы char, как это было сделано в Ех4_04 . срр, понадобится выпол-
нить значительный объем копирования — придется полностью скопировать строку
“Robert Redford” в какое-то временное место, затем скопировать на ее место “Oliver
Hardy”, после чего скопировать “Robert Redford” в конечную позицию. Это потребует
заметно больше времени для выполнения.
Поскольку здесь в качестве имени массива используется pstr, переменная, содер-
жащая начало выходного сообщения, должна быть другой; она называется pstart.
Строка для вывода выбирается очень простым оператором if, похожим на исходную
версию примера. Оно позволяет отобразить либо имя выбранной звезды, либо соот-
ветствующее сообщение, если пользователь ввел неверное значение.
Массивы, строки и указатели 201
15 байт
Общий объем памяти 103 байта
Рис. 4.7. Использование памяти одномерным массивом указателей на char
Слабым местом этой программы является то, что код предполагает, что всего есть
шесть вариантов выбора, даже несмотря на то, что компилятор выделяет простран-
ство для массива указателей на основании размера списка строки инициализации.
Поэтому если вы добавите в него новую строку, то вам придется изменять все части
программы, чтобы принять это во внимание. Было бы здорово добавлять строки и
позволить программе автоматически адаптироваться к их количеству.
Операция sizeof
И здесь на помощь приходит новая операция. Операция sizeof возвращает целое
значение типа size t, которое означает количество байт, занятых ее операндом.
Вспомните из того, что было сказано ранее, что size t — это тип, определенный в
стандартной библиотеке, и обычно он основан на базовом типе unsigned int. •
Взгляните на следующий оператор, который ссылается на переменную dice из
предыдущего примера:
cout « sizeof dice;
Значение выражения sizeof dice равно 4, поскольку переменная dice объявле-
на как int, а потому занимает 4 байта. Поэтому приведенный оператор выведет на
экран значение 4.
Операция sizeof может быть применена к элементу массива или к массиву в
целом. Когда она применяется к имени массива, то возвращает количество байт, за-
нятых всем массивом, в то время как примененная к отдельному элементу с соответ-
ствующим индексом, она возвращает число байт, занятых данным элементом. Таким
образом, в последнем примере можно получить количество элементов массива pstr
следующим выражением:
cout « (sizeof pstr)/(sizeof pstr[0]);
202 Глава 4
Выражение (sizeof pstr) / (sizeof pstr [0] ) делит число байт, занятых масси-
вом указателей на число байт, занятых первым его элементом. Поскольку каждый эле-
мент массива занимает одно и то же место в памяти, в результате получается число
элементов массива.
Помните, что pstr — это массив указателей. Применение операции sizeof ко всему мас-
сиву или отдельным его элементам ничего не скажет нам о памяти, занятой текстовыми
строками.
Операцию sizeof также можно применять к имени типа вместо переменной — в
этом случае результат означает количество байт, занятых каждой переменной данно-
го типа. И в этом случае имя типа должно быть заключено в скобки. Например, после
выполнения оператора:
size_t size = sizeof(long);
переменная size получает значение 4. Эта переменная объявлена с типом size t,
чтобы обеспечить соответствие типу значения, которое возвращает операция sizeof.
Использование другого целочисленного типа для этой переменной может привести к
появлению предупреждающих сообщений компилятора.
Практическое занятие | ИСПОЛЬЗОВЗНИе ОПерЭЦИИ Sizeof
Вы можете усовершенствовать последний пример, используя операцию sizeof, что-
бы код автоматически адаптировался к произвольному количеству строк для выбора.
// Ех4_08.срр
// Гибкое управление массивом с применением sizeof
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main()
{
char* pstr[] = {’’Robert Redford”, // Инициализация массива указателей
"Hopalong Cassidy",
"Lassie",
"Slim Pickens",
"Boris Karloff",
"Oliver Hardy"
};
char* pstart = "Your lucky star is ";
int count = (sizeof pstr) / (sizeof pstr[0]); // Количество элементов массива
int dice = 0;
cout « endl
« " Укажите счастливую звезду!"
« ’’ Введите число между 1 и " « count « ’’: ’’ ;
cin » dice;
cout « endl;
if (dice >= 1 && dice <= count) // Проверить правильность ввода
cout « pstart « pstr [dice - 1] ; // Вывести имя звезды
else
cout « "Очень жаль, но у вас нет счастливой звезды."; // Неправильный ввод
cout « endl;
return 0;
}
Массивы, строки и указатели 203
Описание полученных результатов
Как видите, необходимое изменение в этом примере очень простое. Мы вычисля-
ем количество элементов в массиве указателей pstr и сохраняем результат в перемен-
ной count. Затем везде, где было указано общее количество элементов массива (6),
мы просто вставляем переменную count. Теперь вы можете добавлять новые имена в
список счастливых звезд, и программа “подгоняется” автоматически.
Константные указатели и указатели на константы
Очевидно, что массив pstr в последнем примере не предназначен для того, чтобы
быть модифицированным, как не должны модифицироваться строки, на которые ука-
зывают его элементы, как и не должна модифицироваться переменная count. Было
бы неплохо каким-то образом гарантировать, чтобы все эти вещи не могли быть оши-
бочно изменены в программе. Переменную count очень легко защитить от непредна-
меренной модификации, написав так:
const int count = (sizeof pstr)/(sizeof pstr[0]);
Однако массив указателей требует более тщательного рассмотрения. Массив объ-
явлен следующим образом:
char* pstr[] = {"Robert Redford", // Инициализация массива указателей
"Hopalong Cassidy",
"Lassie",
"Slim Pickens",
"Boris Karloff",
"Oliver Hardy"
Каждый указатель в массиве инициализирован адресом строчного литерала —
“Robert Redford”, “Hopalong Cassidy” и так далее. Типом строчного литерала являет-
ся массив const char, поэтому получается, что вы сохраняете адрес константного
массива в не константном указателе. Причина, по которой компилятор позволяет
использовать строчные литералы для инициализации элементов массива char *, за-
ключается в необходимости обеспечения обратной совместимости с существующим
кодом.
Если вы попытаетесь изменить символьный массив оператором вроде следующего:
*pstr[0] ~ "Stan Laurel";
то программа не скомпилируется.
Если вы сбросите один из элементов массива в указатель на символ посредством
оператора:
*pstr[0] = ’X';
то программа скомпилируется, но потерпит крах при выполнении этого оператора.
Конечно, вы не хотели бы получить непредсказуемое поведение программы, вро-
де краха во время выполнения, и вы можете предотвратить его. Более удачный спо-
соб написания объявления массива будет таким:
const char* pstr[] = {"Robert Redford", // Инициализация массива указателей
"Hopalong Cassidy",
"Lassie",
"Slim Pickens",
"Boris Karloff",
"Oliver Hardy"
204 Глава 4
В этом случае устраняется противоречие между константностью строк, на кото-
рые указывают элементы массива указателей и не константностью самих указателей.
Если теперь попытаться изменить строки, компилятор обнаружит это и выдаст со-
общение об ошибке во время компиляции.
Однако вы все еще можете написать такой оператор:
pstr[0] =pstr[l];
Те счастливчики, которые выберут мистера Редфорда (Redford), получат вместо
него мистера Кессиди (Cassidy), поскольку оба указателя теперь указывают на одно
и то же имя. Обратите внимание, что это не изменяет значения объектов, на кото-
рые указывают указатели-элементы массива, но изменяет значение самого указателя,
хранящегося в pstr [0]. Поэтому вы должны запрещать подобного рода изменения,
поскольку некоторые люди считают, что старина Хоппи (Hopland Cassidy) не столь
импозантен, как Роберт (Robert Redford). И сделать это можно с помощью следующе-
го оператора:
// Массив константных указателей на константы
const char* const pstr[] = {"Robert Redford",//Инициализация массива указателей
"Hopalong Cassidy",
"Lassie",
"Slim Pickens",
"Boris Karloff",
"Oliver Hardy"
Дабы подытожить все сказанное: необходимо различить три ситуации, связанные
с константами, указателями и объектами, на которые они указывают:
□ указатель на константный объект;
□ константный указатель на объект;
□ константный указатель на константный объект.
В первой ситуации объект, на который указывает указатель, не может быть моди-
фицирован, но можно установить указатель на что-нибудь другое:
const char* pstring = "Некоторый текст";
Во второй ситуации адрес, сохраненный в указателе, не может быть изменен, но
объект, на который он указывает, может:
char* const pstring = "Некоторый текст";
И, наконец, в третьей ситуации как указатель, так и объект, на который он указы-
вает, определены как константы, а потому никогда не могут быть изменены:
const char* const pstring = Некоторый текст";
Конечно, все это касается указателей любого типа. Указатель на тип char использован
нами исключительно для примера.
Указатели и массивы
Имена массивов в некоторых случаях могут вести себя как указатели. В большин-
стве ситуаций, если вы используете имя одномерного массива само по себе, оно ав-
томатически преобразуется в указатель на первый элемент этого массива. Обратите
внимание, что это не касается того случая, когда имя массива выступает в качестве
операнда sizeof.
Массивы, строки и указатели 205
Если имеются следующие объявления:
double* pdata;
double data[5];
то вы можете написать такое присваивание:
pdata = data; // Инициализация указателя адресом массива
Это присваивает адрес первого элемента массива data указателю pdata. Приме-
нение имени массива самого по себе означает ссылку на его адрес. Если вы использу-
ете имя массива data с индексным значением, то это означает ссылку на содержимое
элемента, соответствующего значению индекса. Поэтому, если вы хотите сохранить
адрес элемента в указателе, то должны использовать операцию взятия адреса:
pdata = &data[l];
Здесь указатель pdata получает адрес второго элемента массива.
Арифметика указателей
Над указателями можно выполнять арифметические операции. Правда, они огра-
ничены только сложением и вычитанием, но можно также сравнивать значения ука-
зателей, получая логический результат. Арифметика над указателями неявно предпо-
лагает, что указатель указывает на массив, и арифметические операции выполняются
над адресом, содержащимся в указателе. Так, например, указателю pdata можно при-
своить адрес третьего элемента массива data с помощью следующего оператора:
pdata = &data[2];
В этом случае выражение pdata + 1 будет ссылаться на адрес data [ 3 ] — четверто-
го элемента массива data, поэтому вы можете переставить указатель на этот элемент
следующим образом:
pdata += 1; // Инкремент указателя pdata переносит его на следующий элемент
Этот оператор увеличивает адрес, содержащийся в pdata, на количество байт, ко-
торое занимает каждый элемент массива data. В общем случае выражение pdata + п,
где п — любое целочисленное выражение, добавляет n*sizeof (double) к адресу, со-
держащемуся в pdata, потому что pdata объявлен как указатель на double. Это про-
иллюстрировано на рис. 4.8.
double data[5];
data[O] data[1] data[2] data[3] data [4]
Каждый элемент
занимает 8 байт
Адрес
pdata+1
pdata+2
pdata = &data[2];
Puc. 4.8. Операции над указателем
206 Глава 4
Другими словами, инкремент и декремент указателя работает в терминах типа
объекта, на который он указывает. Увеличение на единицу указателя на long изме-
няет его содержимое на адрес следующего long, то есть увеличивает его адрес на че-
тыре. Аналогично, инкремент указателя на short на единицу увеличивает значение
адреса на два. Более распространенная нотация для увеличения указателя использует
операцию инкремента. Например:
pdata++; // Увеличить pdata до следующего элемента
Это эквивалентно форме +=, к тому же более часто применяется. Однако я исполь-
зовал форму +=, дабы подчеркнуть, что хотя обычно значение инкремента равно еди-
нице, эффект от его применения к указателю выражается в увеличении адреса боль-
ше чем на единицу, за исключением случая указателя на char.
Адрес, полученный в результате применения арифметической операции к указателю, может
изменяться от адреса первого элемента массива до адреса, лежащего сразу за его последним
элементом. Вне этих пределов поведение указателя не определено.
Вы можете, конечно, разыменовать указатель, к которому применено арифмети-
ческое действие (а иначе в нем не было бы особого смысла). Например, если предпо-
ложить, что pdata все еще указывает на data [2], то оператор:
* (pdata + 1) = * (pdata + 2);
эквивалентен следующему:
data[3] = data[4];
Когда вы хотите разыменовать указатель после увеличения адреса, который он со-
держит, скобки необходимы, поскольку приоритет операции разыменования выше,
чем приоритет арифметических операций + или -. Если вы напишете выражение
*pdata + 1 вместо * (pdata + 1), это добавит единицу к значению, находящемуся по
адресу, хранящемуся в pdata, что эквивалентно выполнению data [2] + 1. Поскольку
это не lvalue, его применение в предыдущем операторе присваивания заставит ком-
пилятор сгенерировать сообщение об ошибке.
Вы можете использовать имя массива, как если бы это был указатель, для обра-
щения к его элементам. Если у вас есть одномерный массив вроде того, что раньше,
объявленный, как:
long data[5];
то, применив нотацию указателя, вы можете сослаться на элемент data [3], напри-
мер, так: * (data + 3). Этот вид нотации может применяться совершенно свободно,
так что для доступа к элементам data [ 0 ], data [ 1 ], data [ 2 ] вы можете писать *data,
* (data + 1), * (data+2) и так далее.
Практическое занятие ИМвНЭ МЭССИВОВ КЗ К уКаЗЭТвЛИ
Испытать этот аспект адресации массивов можно на примере следующей програм-
мы, которая находит простые числа (простое число — то, которое делится только на
себя и на единицу).
// Ех4_09.срр
// Вычисление простых чисел
#include <iostream>
#include <iomanip>
Массивы, строки и указатели 207
4
using std::cout;
using std::endl;
using std::setw;
int main ()
const int MAX = 100; //
long primes [MAX] = { 2,3,5 }; //
long trial = 5; //
int count =3; //
int found =0; //
do
trial += 2; //
found =0; //
for (int i = 0; i < count; i++) //
//
Количество простых чисел
Первые три определены
Кандидат на простое число
Количество найденных простых чисел
Признак обнаружения простого числа
Следующее значение для проверки
Установка признака обнаружения
Попытка деления на существующие
простые числа
found = (trial % * (primes + i)) ®= 0;// Истинно при делении без остатка
if (found) // Если делится без остатка,
break; //то это — не простое число
if (found == 0) // Нашли...
* (primes + count++) = trial;//...сохраним его в массиве простых чисел
}while(count < MAX);
// Вывести значение primes по 5 в строке
for (int i = 0; i < MAX; i++)
if (i % 5 == 0) // Перевод строки перед 1-й и после каждой 5-й
cout « endl;
cout « setw (10) « * (primes + i);
cout « endl;
return 0;
Если скомпилировать и запустить этот пример, то получится следующий вывод:
2 3 5 7 11
13 17 19 23 29
31 37 41 43 47
53 59 61 67 71
73 79 83 89 97
101 103 107 109 113
127 131 137 139 149
151 157 163 167 173
179 181 191 193 197
199 211 223 227 229
233 239 241 251 257
263 269 271 277 281
283 293 307 311 313
317 331 337 347 349
353 359 367 373 379
383 389 397 401 409
419 421 431 433 439
443 449 457 461 463
467 479 487 491 499
503 509 521 523 541
208 Глава 4
Описание полученных результатов
Здесь присутствуют обычные операторы #include с заголовочным файлом
<iostream> для ввода и вывода, а также с <iomanip>, поскольку используются мани-
пуляторы потока для установки ширины полей при выводе.
Константа МАХ определяет количество простых чисел, которые нужно получить
от программы. Массив primes, в котором сохраняются результаты, инициализирован
первыми тремя простыми числами, чтобы было с чего запустить процесс. Вся работа
выполняется в двух циклах: внешнем do-while, который указывает следующее прове-
ряемое значение и добавляет найденное значение в массив primes, если оно является
простым числом, и внутренним циклом for, который в действительности проверяет
значение на принадлежность к множеству простых чисел.
Алгоритм цикла for очень прост и основан на том факте, что если число не про-
стое, то оно должно делиться на одно из ранее найденных простых — каждое из ко-
торых меньше проверяемого, поскольку все числа являются либо простыми, либо
произведениями простых. Фактически, нужно проверить только деление на простые
числа, меньшие или равные корню квадратному из проверяемого числа, поэтому дан-
ный пример еще не настолько эффективен, насколько он мог бы быть.
found = (trial % * (primes + i) ) == 0;// Истинно при делении без остатка
Этот оператор присваивает переменной found значение 1, если нет остатка от де-
ления значения trial на текущее простое число * (primes + i) (напомним, что это
эквивалентно primes [i]), и 0 — в противном случае. Оператор if прерывает цикл
for, если found равно 1, поскольку число-кандидат в trial в этом случае не является
простым.
После завершения цикла for (по любой причине) необходимо решить, является
ли текущее значение trial простым. Это определяется значением индикаторной пе-
ременной found.
* (primes + count++) = trial; // ...сохраним его в массиве простых чисел
Если trial действительно содержит простое число, этот оператор сохраняет его
значение в primes [count], после чего увеличивает count с помощью постфиксной
операции инкремента.
После того, как найдено МАХ простых чисел, они выводятся в поле шириной 10
символов, по 5 в строке, в результате выполнения такого оператора:
if (i % 5 == 0) // Перевод строки перед 1-й и после каждой 5-й
cout « endl;
Это начинает новую строку, когда i получает значения 0, 5, 10 и так далее.
Практическое занятие | ПОДСЧбТ символов
Чтобы увидеть, как обрабатываются строки в нотации указателей, можно напи-
сать версию программы, рассмотренной ранее, которая предназначалась для подсче-
та символов в строке:
// Ех4_10.срр
// Подсчет символов строки с использованием указателя
tfinclude <iostream>
using std::cin;
using std::cout;
using std::endl;
Массивы, строки и указатели 209
int main()
const int MAX = 80; // Максимальный размер массива
char buffer[MAX]; // Входной буфер
char* pbuffer = buffer; // Указатель на буферный массив
cout « endl // Приглашение ввода
« "Введите строку не длиннее "
« МАХ « " символ (-а, -ов) :"
« endl;
cin.getline(buffer, MAX, ’\n’); // Читать строку до \n
while(*pbuffer) // Продолжать до \0
pbuffer++;
cout « endl
« "Строка \"" « buffer
« "\" содержит " « pbuffer - buffer « " символов.";
cout « endl;
return 0;
Вот пример типичного вывода этого примера:
Введите строку не длиннее 80 символов:
Ничего на свете лучше нету, чем бродить друзьям по белу свету.
Строка "Ничего на свете лучше нету, чем бродить друзьям по белу свету,
содержит 62 символ (-а, -ов) .
Описание полученных результатов
Здесь программа работает с указателем pbuffer, а не с именем массива buffer.
Не нужна переменная count, поскольку указатель увеличивается в цикле while, пока
не найдет \ 0. Когда обнаруживается символ \ 0, pbu f f е г содержит адрес его положе-
ния в строке. Счетчик количества символов строки, таким образом, вычисляется, как
разница между адресом, записанным в pbuffer, и адресом начала массива, который
обозначен buffer.
Вы могли бы также выполнять инкремент указателя в цикле следующим образом:
while(*pbuffer++); // Считать до \0
Этот цикл не содержит в себе никаких операторов, а только проверочное условие.
Это должно работать адекватно, за исключением того факта, что значение счетчика
увеличивается после достижения \0, поэтому адрес будет на единицу больше, чем по-
следняя позиция в строке. Поэтому в данном случае количество символов в строке
должно быть вычислено как pbuffer — buffer — 1.
Обратите внимание, что здесь вы не можете применять имя массива так же, как ис-
пользуете указатель. Выражение buf fer++ совершенно неправильно, поскольку нель-
зя модифицировать значение адреса, которое представляет имя массива. Даже несмо-
тря на то, что вы можете использовать имя массива в выражениях вместо указателя,
все же это — не указатель, поскольку адрес, который оно представляет, фиксирован.
Применение указателей с многомерными массивами
Применение указателя для хранения адреса одномерного массива относительно
просто, но когда речь идет о многомерных массивах, все несколько усложняется.
Если вы не собираетесь делать это, можете пропустить настоящий раздел, так как он
немного запутан; однако если у вас есть предварительный опыт использования языка
С, на него стоит взглянуть.
210 Глава 4
Если вам нужно применять указатель с многомерными массивами, то нужно ясно
представлять, что будет происходить. В качестве иллюстрации можете взять массив,
объявленный следующим образом:
double beans [3] [4];
Вы можете объявить и присвоить значение указателю pbeans так:
double* pbeans;
pbeans = &beans[0][0];
Здесь вы устанавливаете указатель на адрес первого элемента массива, который
имеет тип double. Вы также можете установить указатель на адрес первой строки
массива с помощью следующего оператора:
pbeans = beans[01;
Это эквивалентно применению имени одномерного массива, который заменяется
его адресом. Мы использовали это в предыдущих дискуссиях; однако поскольку beans —
двумерный массив, вы не можете присвоить указателю адрес таким оператором:
pbeans - beans; // Вызовет ошибку!
Проблема заключается в типе. Тип указателя определен как double*, но массив
имеет тип double [ 3 ] [ 4 ]. Указатель, который может сохранить адрес этого массива,
должен быть double* [4]. C++ ассоциирует измерения массива с его типом, и опера-
тор, приведенный выше, был бы легальным только в том случае, если бы указатель
был объявлен вместе с необходимым измерением. Это делается с применением не-
сколько более сложной нотации, чем вы видели до сих пор:
double (*pbeans)[4];
Скобки здесь важны; без них это означало бы объявление массива указателей. С
таким объявлением предыдущий оператор корректен, но указатель может использо-
ваться только для сохранения адресов массивов с указанным измерением.
Нотация указателей с многомерными массивами
Вы можете использовать нотацию указателей с именами массивов для обращения
к их элементам. Обратиться к элементам массива beans, который был объявлен ра-
нее, и который имеет три строки по четыре элемента, можно двумя способами.
□ Используя имя массива с двумя значениями индексов.
□ Используя имя массива в нотации указателей.
Таким образом, следующие два оператора эквивалентны:
beans[i][j]
* (* (beans + i) + j)
Давайте разберемся, как это работает. В первой строке используется нормальная
индексация массива для ссылки на элемент со смещением j в строке i массива.
Вы можете определить значение второй строки, разбирая ее изнутри наружу,
beans ссылается на адрес первой строки массива, поэтому beans + i ссылается на
строку номер i. Выражение * (beans + i) — это адрес первого элемента строки i, по-
этому * (beans + i) + j — адрес элемента в строке i со смещением j. Таким образом,
полное выражение ссылается на конкретный элемент массива.
Если вы действительно хотите все запутать (хотя это не рекомендуется), то следу-
ющие два оператора, в которых смешивается нотация массивов с нотацией указате-
лей, также легально ссылаются на тот же элемент массива:
Массивы, строки и указатели 211
*(beans[i] + j)
(*(beans + i)) [j]
Существует еще один аспект применения указателей, который на самом деле важ-
нее всех остальных: возможность динамического выделения памяти для переменных.
Мы рассмотрим это в следующем разделе.
Динамическое выделение памяти
Работа с фиксированным набором переменных в программе может серьезно стес-
нять разработчика. Часто в приложениях возникает необходимость в оперативном
принятии решений относительно динамического выделения места для размещения
переменных различных типов непосредственно во время выполнения — в зависимо-
сти от входных данных, полученных программой. Для одного набора данных может
быть разумно применять большой массив целых чисел, в то время как другой набор
входных данных может потребовать большого массива чисел с плавающей точкой.
Понятно, что поскольку динамически распределенные переменные не могут быть
определены во время компиляции, они не имеют имен в исходном тексте програм-
мы. Когда они создаются, то идентифицируются адресами в памяти, которые сохра-
няются в указателях. Благодаря мощности указателей и средствам динамического
управления памятью в Visual C++ 2005, написание программ, обладающих такого рода
гибкостью, выполняется легко и быстро.
Свободное хранилище, псевдоним “куча”
Во многих случаях, когда выполняется ваша программа, у компьютера есть неис-
пользуемая память. Эта неиспользуемая память в C++ называется кучей или, иногда,
свободным хранилищем. Вы можете выделить память внутри этого хранилища для
новой переменной заданного типа с помощью специальной операции C++, которая
возвращает адрес выделенного пространства. Этой операцией является new, и ее до-
полняет операция delete, которая освобождает память, ранее выделенную new.
Вы можете выделить память в свободной хранилище для некоторой переменной в
одной части программы, а затем освободить выделенное пространство и вернуть его
в свободное хранилище после того, как необходимость в этой переменной отпадет.
Это позволяет позже в той же программе повторно задействовать эту память для дру-
гих динамически распределяемых переменных.
Память из свободного хранилища используется всякий раз, когда возникает по-
требность в выделении памяти для элементов, которые могут быть определены лишь
во время выполнения. Одним из примеров может служить выделение памяти для
строк, вводимых пользователем вашего приложения. Нет способа заранее знать, на-
сколько большими должны быть эти строки, поэтому имеет смысл выделять память
для них во время выполнения, используя для этого операцию new. Позднее вы озна-
комитесь с примером применения свободного хранилища для динамического распре-
деления памяти массива, где измерения массива задаются пользователем во время
работы программы.
Это может быть очень мощная техника, которая позволяет расходовать память
очень эффективно, и во многих случаях разрабатывать программы, способные ре-
шать намного более масштабные проблемы, обрабатывая большие объемы данных,
чем это возможно без ее применения.
212 Глава 4
Операции new и delete
Предположим, что вам необходимо пространство для переменной типа doub 1 е. Вы
можете определить указатель на тип double и затем запрашивать память во время вы-
полнения. Вы можете делать это, используя операцию new в следующих операторах:
double* pvalue = NULL; // Указатель, инициализированный null
pvalue = new double; // Запрос памяти для переменной double
Это хороший повод, чтобы напомнить, что все указатели должны, быть инициализи-
рованы. Динамическое использование памяти обычно предполагает появление множе-
ства “плавающих” указателей, поэтому важно, чтобы они не содержали неправильных
значений. Вы должны стараться следовать правилу, что если указатель не содержит
корректного значения адреса, то он устанавливается в 0.
Операция new во второй строке приведенного выше кода должна вернуть адрес
памяти из свободного хранилища, выделенный для размещения переменной типа
double, и этот адрес сохраняется в pvalue. Затем вы можете применять этот указа-
тель для ссылки на переменную, используя операцию разыменования, как уже было
показано ранее. Например:
*pvalue = 9999.0;
Конечно, может случиться, что память не будет выделена, поскольку все свобод-
ное хранилище занято, или же потому, что оно настолько фрагментировано в резуль-
тате предыдущего использования, что не находится непрерывного участка свободной
памяти, достаточного для размещения переменной указанного типа. Однако вам не
стоит слишком беспокоиться об этом. В соответствии с требованием стандарта ANSI
C++, операция new возбудит исключение, если память не может быть выделена по ка-
кой-либо причине, что прервет выполнение программы. Исключения — это механизм
сообщения об ошибках в C++, и вы познакомитесь с ними в главе 6.
Вы можете сразу инициализировать переменную операцией new при ее создании.
Если взять пример с переменной double, которая распределяется операцией new и
чей адрес сохраняется в pvalue, то можно сразу установить ей значение 999.0 с по-
мощью следующего оператора:
pvalue = new double(999.0); // Выделить память под double и инициализировать ее
Когда отпадает необходимость в переменной, распределенной динамически, вы
можете освободить память, которую она занимала в свободном хранилище, операци-
ей delete:
delete pvalue;
// Освободить память, на которую указывает pvalue
Это обеспечивает возможность последующего использования этой памяти другими
переменными. Если вы не применяете delete и позже присвоите указателю pvalue
другое значение адреса, то станет невозможным освободить память или использовать
переменную, которая в ней содержится, поскольку доступ к этому адресу будет утерян.
В такой ситуации происходит то, что называется утечкой памяти (memory leak) — осо-
бенно если данная ситуация в программе повторяется многократно.
Динамическое распределение памяти для массивов
Динамически выделить памяти для массива очень просто. Если вы хотите распре-
делить массив типа char, то, предполагая, что pstr — указатель на char, можно на-
писать следующий оператор:
pstr = new char[20]; // Распределение строки в 20 символов
Массивы, строки и указатели 213
Это выделяет память для символьного массива размером в 20 символов и сохраня-
ет указатель на него в pstr.
Чтобы освободить распределенный таким образом массив и вернуть память в сво-
бодное хранилище, вы должны применить операцию delete. Оператор должен вы-
глядеть так:
delete [] pstr; // Удалить массив, на который указывает pstr
Обратите внимание на квадратные скобки, свидетельствующие о том, что то, что
вы удаляете — массив. Когда освобождается массив, распределенный в свободном хра-
нилище, всегда нужно указывать квадратные скобки, иначе результат будет непредска-
зуемым. Обратите внимание, что при этом не нужно указывать размер массива — до-
статочно просто скобок [ ].
Конечно, указатель pstr после этого будет содержать адрес памяти, которая мо-
жет быть уже распределена для каких-то других целей, поэтому, конечно же, он не
должен никак использоваться. Поэтому когда вы применяете операцию delete для
того, чтобы освободить некоторую память, которая была распределена ранее, то всег-
да должны сбрасывать значение указателя в 0, как показано ниже:
pstr = 0; // Установить указатель в null
Практическое занятие
Использование свободного хранилища
Чтобы увидеть на практике, как работает динамическое выделение памяти, вы мо-
жете переписать программу вычисления простых чисел, используя память из свобод-
ного хранилища для их размещения.
// Ех4_11.срр
/ / Вычисление простых чисел с применением динамического распределения памяти
tfinclude <iostream>
#include <iomanip>
using std::cin;
using std::cout;
using std::endl;
using std::setw;
int main()
{
long* pprime = 0;
long trial = 5;
int count =3;
int found = 0;
int max = 0;
// Указатель на массив простых чисел
// Кандидат в простые числа
/ / Счетчик найденных простых чисел
/ / Признак обнаружения простого числа
// Количество требуемых простых чисел
cout « endl
« "Введите количество простых чисел, которые хотите получить (минимум 4) : ";
cin >> max;
// Число необходимых простых чисел
if(max < 4) // Проверка ввода пользователя, чтобы значение было не меньше 4
max =4; // Принудительное указание количества, если указано меньше
pprime = new long[max];
*pprime = 2;
* (pprime + 1) = 3;
* (pprime + 2) =5;
// Вставить три начальных
// простых числа
214 Глава 4
do
trial += 2; // Следующее значение для проверки
found =0; // Установка признака ’’найдено”
for (int i = 0; i < count; i++) //Деление на ранее найденные простые числа
found = (trial % * (pprime + i)) = 0;//Истинно, если делится без остатка
if(found) // Если делится без остатка,
break; // значит, число не простое
if (found == 0) // Найдено...
*(pprime + count++) = trial; //...сохраняем в массиве простых чисел
} while(count < max) ;
// Вывести значение primes по 5 в строке
for (int i = 0; i < max; i++)
if (i % 5 == 0) // Перевод строки перед первым и каждым пятым элементом
cout « endl;
cout « setw(10) « * (pprime + i);
delete [] pprime; // Освободить память
pprime =0; //и сбросить указатель
cout « endl;
return 0;
Ниже показан пример вывода этой программы:
Введите количество простых чисел, которые хотите получить (минимум 4) : 20
2 3 5 7 11
13 17 19 23 29
31 37 41 43 47
53 59 61 67 71
Описание полученных результа тов
Фактически эта программа очень похожа на предыдущую версию. После получе-
ния необходимого количества простых чисел в int-переменной max, вы распределяе-
те массив этого размера в свободном хранилище, используя операцию new. Обратите
внимание, что программа гарантирует, что max будет не меньше 4. Это потому, что
ей необходимо распределить пространство в свободном хранилище, как минимум для
трех начальных простых чисел, плюс еще одно. Вы специфицируете необходимый
размер массива, помещая переменную max между квадратными скобками, которые
следуют за спецификацией типа массива:
pprime = new long[max];
Вы помещаете адрес области памяти, выделенной операцией new в указатель
pprime. Если память не удастся выделить, то в этой точке программа должна быть
прервана по исключению.
После того, как память для хранения значений простых чисел успешно выделе-
на, первым трем элементам массива присваиваются значения первых трех простых
чисел:
*pprime =2;
* (pprime + 1)
* (pprime + 2)
// Вставить три начальных
// простых числа
Массивы, строки и указатели
215
Вы используете операцию разыменования для доступа к первым трем элементам
массива. Как было показано ранее, скобки во втором и третьем операторе необходи-
мы потому, что приоритет операции * выше, чем операции +.
Вы не можете специфицировать начальные значения элементов массива, который распре-
делен динамически. Если нужно установить начальные значения элементов такого массива,
это следует сделать явным присваиванием.
Вычисление простых чисел выполняется точно так же, как и раньше; единствен-
ное отличие в том, что вместо имени массива primes, использованного в предыдущей
версии, подставляется указатель pprime. Процесс вывода — тот же. Динамическое по-
лучение пространства памяти — вообще не проблема. После того, как память выделе-
на, она никак не влияет на методику вычислений.
По завершении работы с массивом он освобождается операцией delete. Не за-
будьте о квадратных скобках, чтобы указать, что удаляется массив.
delete f] pprime; // Освободить память
Хотя в данном случае это и не существенно, все же указатель устанавливается в 0:
pprime =0; //и сбросить указатель
Вся память, выделенная из свободного хранилища, освобождается по завершении
программы, но лучше выработать привычку сбрасывать значение указателя в ноль,
когда он уже не указывает на корректную область памяти.
Динамическое распределение многомерных массивов
Выделение памяти в свободном хранилище для многомерного массива предпо-
лагает использование операции new в несколько более сложной форме, чем для од-
номерного массива. Если предположить, что у вас уже есть соответствующим обра-
зом объявленный указатель pbeans, то получение пространства памяти для массива
beans [ 3 ] [ 4 ], с которым вы имели дело ранее в этой главе, может выглядеть так:
pbeans = new double [3] [4]; // Выделение памяти для массива 3x4
Вы просто специфицируете оба измерения между квадратными скобками после
имени типа элементов массива.
Выделение памяти для трехмерного массива просто требует указания дополни-
тельного измерения с операцией new, как показано в следующем примере:
pBigArray = new double [5][10][10];// Выделение памяти для массива 5x10x10
Однако сколько бы ни было измерений в созданном массиве, для того, чтобы уни-
чтожить его и вернуть память обратно в свободное хранилище, потребуется написать
такой оператор:
delete [] pBigArray;
// Освобождение памяти массива
То есть при освобождении памяти массива после операции delete вы должны
указать лишь одну пару квадратных скобок, независимо от того, сколько измерений
было в этом массиве.
Вы уже видели, что в качестве спецификации размера при распределении памяти
для одномерного массива операцией new можно указывать переменную. Это касает-
ся двух и более измерений, но с тем ограничением, что переменной можно задавать
лишь размер самого левого измерения.
216 Глава 4
Все прочие измерения должны быть заданы константами или константными вы-
ражениями. Поэтому вы можете записать следующим образом:
pBigArray ® new double[max][10][10];
где max — переменная; однако спецификация переменной для всех других измере-
ний массива кроме крайнего левого, вызовет сообщение компилятора об ошибке.
Использование ссылок
Ссылка (reference) во многих отношениях подобна указателю — вот почему я пред-
ставляю ее здесь, хотя на самом деле это совсем другая вещь. Истинная ценность ссы-
лок становится ясной, только когда рассматривается их применение с функциями, в
частности в контексте объектно-ориентированного программирования. Не позволяй-
те ввести себя в заблуждение их кажущейся простотой и тривиальностью концепции.
Как вы увидите позже, ссылки являются чрезвычайно мощным средством, и в некото-
рых контекстах позволяют достичь таких результатов, которые без них вообще были
бы невозможными.
Что такое ссылка?
Ссылка — это псевдоним для другой переменной. Она имеет имя, которое может
использоваться вместо исходного имени переменной. Поскольку это псевдоним, а
не указатель, переменная, для которой она определена, должна быть указана, когда
ссылка объявляется, и в отличие от указателя, ссылка не может быть изменена, что-
бы представлять другую переменную.
Объявление и инициализация ссылок
Предположим, вы объявили переменную следующим образом:
long number = 0;
После этого вы можете объявить ссылку на эту переменную с помощью такого
оператора объявления:
long& rnumber = number; // Объявление ссылки на переменную number
Знак амперсанд, следующий за именем типа long и предшествующий имени пе-
ременной rnumber, говорит о том, что это объявление ссылки, а имя переменной,
которую она представляет — number — специфицировано как инициализирующее зна-
чение, следующее за знаком равенства; таким образом, переменная rnumber имеет
тип “ссылка на long”. После этого вы можете использовать ссылку вместо имени ис-
ходной переменной. Например, оператор:
rnumber += 10;
в качестве эффекта дает увеличение значения переменной numbe г на 10.
Сравните ссылку rnumber с указателем pnumber, объявленным в операторе:
long* pnumber = &number; // Инициализация указателя адресом
Здесь объявляется указатель pnumber, который инициализируется адресом пере-
менной number. Это позволяет увеличивать значение number оператором вроде:
*pnumber +=10; // Инкремент number через указатель
Массивы, строки и указатели
217
Есть существенная разница между использованием указателя и использованием
ссылки. Указатель должен быть разыменован, и адрес, который он содержит, служит
для доступа к переменной, участвующей в выражении. В случае ссылки нет необходи-
мости в разыменовании. В некотором смысле ссылка подобна указателю, который уже
разыменован, хотя его и нельзя заставить ссылаться на другую переменную. Ссылка
полностью эквивалентна переменной, на которую она ссылается. Ссылка может пока-
заться просто альтернативной нотацией для данной переменной, и здесь она опреде-
ленно ведет себя именно таким образом. Однако когда я буду говорить о функциях
C++, вы убедитесь, что это не совсем так, и что это средство предлагает некоторые
весьма впечатляющие дополнительные возможности.
Программирование на C++/CLI
Динамическое выделение памяти в CLR работает иначе, и CLR поддерживает
свою собственную “кучу” памяти, которая полностью независима от кучи родного
C++. CLR автоматически очищает память, которая выделена в куче CLR и необходи-
мость в которой отпала, поэтому вам не нужно использовать операцию delete в про-
граммах, написанных для CLR. CLR может также время от времени упорядочивать
память кучи, дабы избежать фрагментации. Таким образом, CLR исключает вероят-
ность утечек памяти и ее фрагментации. Управление очисткой кучи, которое пред-
усматривает CLR, описывается как сборка мусора (мусор представляет собой отбро-
шенные переменные и объекты, а куча, которой управляет CLR, называется кучей,
очищаемой сборщиком мусора). Вы используете операцию gcnew вместо new для вы-
деления памяти в программе C++/CLI, и префикс дс отражает тот факт, что память
выделяется в очищаемой куче, а не в родной куче C++, где за все хозяйство вы отвеча-
ете самостоятельно.
Сборщик мусора CLR способен удалять объекты и освобождать память, которую
они занимали, когда необходимость в них отпадает. Возникает очевидный вопрос:
как может знать сборщик мусора о том, когда объект кучи более не нужен? Ответ до-
статочно прост. CLR отслеживает каждую переменную, которая ссылается на каждый
объект кучи, и когда не остается переменных, содержащих адрес данного объекта,
это значит, что на него невозможно сослаться в программе, а потому он может быть
удален.
Поскольку процесс сборки мусора может включать в себя сжатие области памяти
кучи для исключения фрагментации неиспользуемых блоков, адрес элемента данных,
который вы сохраняете в куче, может измениться. Следовательно, вы не можете при-
менять обычные родные указатели C++ с очищаемой кучей, поскольку местоположе-
ние указываемых данных изменяется, и такие указатели становятся недействитель-
ными. Необходим способ доступа к объектам в куче, который позволяет обновлять
адреса, когда сборщик мусора перемещает в ней элементы данных. Эта возможность
обеспечивается двумя способами: с помощью отслеживаемых дескрипторов, пред-
ставляющих собой некоторые аналоги указателей из родного C++, и посредством от-
слеживаемых ссылок — эквивалента родных ссылок C++ в программах CLR.
Отслеживаемые дескрипторы
Отслеживаемые дескрипторы (tracking handle) имеют сходство с родными указа-
телями C++, однако есть и существенные отличия. Дескриптор хранит адрес, и адрес,
который в нем содержится, автоматически обновляется сборщиком мусора, если объ-
218 Глава 4
ект, на который он ссылается, перемещается во время сжатия кучи. Однако вы не мо-
жете применять арифметику адресов к таким указателям, как вы делаете это с “род-
ными” указателями, кроме того, не разрешается приведение дескрипторов.
Все объекты, созданные в куче CLR, должны снабжаться дескрипторами. Все объ-
екты ссылочных типов классов сохраняются в куче и потому созданные вами перемен-
ные, которые ссылаются на такие объекты, должны быть дескрипторами. Например,
тип класса String — это ссылочный тип класса, поэтому переменные, которые ссыла-
ются на объекты String, должны быть отслеживаемыми дескрипторами. Память для
типов классов значений по умолчанию выделяется в стеке, но вы можете также раз-
местить их в куче, используя для этого операцию gcnew. Стоит также напомнить то, о
чем говорилось в главе 2: переменные, размещенные в куче CLR, включая ссылочные
типы CLR, не могут быть объявлены в глобальном контексте.
Объявление отслеживаемых дескрипторов
Вы специфицируете дескриптор для типа, помещая символ Л (часто называемый
“шляпой”) следом за именем типа. Например, вот как можно объявить отслеживае-
мый дескриптор по имени proverb, который может хранить адрес объекта String:
String"4 proverb;
Это определяет переменную proverb как отслеживаемый дескриптор типа
StringA. Когда вы объявляете дескриптор, он автоматически инициализируется ну-
лем, поэтому ни на что не ссылается. Для явной установки дескриптора в ноль слу-
жит ключевое слово nullpt г:
proverb = nullptr; // Установить дескриптор в null
Обратите внимание, что вы не можете здесь использовать 0, как это можно делать
с родными указателями. Если вы инициализируете дескриптор нулем, то значение О
преобразуется в тип объекта, на который будет ссылаться дескриптор, и в него будет
помещен адрес этого нового объекта.
Конечно, можно явно инициализировать дескриптор при его объявлении. Вот
другой оператор, определяющий дескриптор объекта String:
String"4 saying = Ь”Некоторая длинная строка";
Этот оператор создает в куче объект String, который содержит строку справа
от операции присваивания; адрес нового объекта помещается в saying. Типом стро-
кового литерала здесь является wchar_t*, а не String. Определение класса String
дает возможность такому литералу быть использованным для создания объекта типа
String.
А вот как создается дескриптор для типа значения:
int"4 value = 99;
Этот оператор создает дескриптор value типа intА, и значение в куче, на кото-
рое он указывает, инициализируется числом 99. Помните, что вы создаете разновид-
ность указателя, поэтому value не может участвовать в арифметических операциях
без его разыменования. Для разыменования отслеживаемого дескриптора служит
операция * — точно так же, как это делается с указателями. Например, ниже пред-
ставлен оператор, использующий значение, указанное отслеживаемым дескриптором
в арифметической операции:
int result = 2*(*value)+15;
Массивы, строки и указатели 219
Выражение *value между скобками обращается к целому, сохраненному по адресу,
содержащемуся в дескрипторе, поэтому переменная result получает значение 213.
Обратите внимание, что когда вы используете дескриптор справа от операции
присваивания, нет необходимости явно разыменовывать его, чтобы сохранить ре-
зультат; об этом позаботится компилятор. Например:
intA result = 0;
result = 2*(*value)+15;
Здесь сначала создается дескриптор result, указывающий на значение 0 в куче.
Следует отметить, что это вызывает предупреждение компилятора, поскольку он
предполагает, что вы можете иметь намерение инициализировать дескриптор значе-
нием null, и это неправильный способ. Поскольку result появляется слева от опе-
рации присваивания в следующем операторе, а правая часть производит значение,
компилятор в состоянии определить, что result должен быть разыменован для со-
хранения значения. Конечно, вы можете написать явно:
*result = 2*(*value)+15;
Но заметьте, что это работает только тогда, когда result уже определен. Если же
result только объявлен, то во время выполнения кода возникнет ошибка времени
выполнения. Например:
intA result; // Объявление, но не определение
*result = 2*(*value)+15; // Сообщение об ошибке — необработанное исключение
Поскольку вы разыменовываете result во втором операторе, предполагается, что
указанный им объект уже существует. Но поскольку это не так, возникает ошибка вре-
мени выполнения. Первый оператор — это объявление дескриптора result, который
устанавливается в null по умолчанию, а вы не можете разыменовать null-дескрип-
тор. Если же вы не станете явно разыменовывать result во втором операторе, все
будет работать должным образом, поскольку результат выражения справа имеет тип
класса значения, и его адрес присваивается result.
Массивы CLR
Массивы CLR отличаются от массивов родного C++. Память для массива CLR вы-
деляется в управляемой куче, но это еще не все. Как вы вскоре увидите, массивы CLR
обладают встроенной функциональностью, которой нет у массивов родного C++. Тип
переменной массива специфицируется ключевым словом array. При этом вы также
должны указать тип элементов массива между угловыми скобками, следующими за
ключевым словом array, поэтому общая форма переменной, ссылающейся на одно-
мерный массив — аггау<тип_элемента>Л. Поскольку массивы CLR создаются в куче,
переменные массивов — это всегда отслеживаемые дескрипторы. Вот пример объяв-
ления переменной массива:
array<int>/x data;
Переменная массива data может хранить ссылку на одномерный массив элемен-
тов типа int.
Вы можете создать массив CLR, используя операцию gcnew, одновременно с объяв-
лением переменной массива:
array<int>A data = gcnew array<int>(100); // Создать массив для
// хранения 100 целых чисел
220 Глава 4
Этот оператор создает одномерный массив по имени data (обратите внимание,
что переменная массива — отслеживаемый дескриптор, поэтому нельзя забывать о
символе “шляпы”, следующем за спецификацией типа элемента, заключенной в угло-
вые скобки). Количество элементов указывается в круглых скобках, следующих за
спецификацией типа массива, также заключенное в угловые скобки. То есть в данном
случае это будет массив, содержащий 100 элементов типа int.
Как и в массивах родного C++, элементы массивов CLR индексируются, начиная
с нуля, поэтому вы можете установить значения элементов массива data следующим
образом:
for (int i = 0 ; i<100 ; i++)
data[i] = 2*(i+1);
Цикл устанавливает значения элементов равными 2, 4, 6, и так далее — до 200.
Элементы массива CLR являются объектами, поэтому здесь вы сохраняете в массиве
объекты типа Int32. Конечно, они ведут себя подобно обыкновенным целым в ариф-
метических выражениях, поэтому тот факт, что они являются объектами, в таких си-
туациях прозрачен.
В предыдущем цикле количество обрабатываемых элементов в цикле задано как
литеральное значение. Но лучше использовать свойство Length массива, которое
хранит число элементов массива:
for (int i - 0 ; i < data->Length ; i++)
data[i] = 2*(i+1);
Чтобы обратиться к свойству Length, здесь используется операция ->, потому что
data — отслеживаемый дескриптор, который работает подобно указателю. Это свой-
ство хранит количество значений как 32-битное целое. Если нужно, вы можете полу-
чить длину массива как 64-битное целое через свойство LongLength.
Можно также выполнить итерацию по всем элементам массива, воспользовавшись
циклом for each:
array<int>A values = { 3, 5, 6, 8, 6);
for each (int item in values)
item = 2*item +1;
Console::Write(”{0,5}”,item);
Внутри цикла переменная item ссылается на каждый элемент массива по очереди.
Первый оператор в теле цикла заменяет значение текущего элемента удвоенным ста-
рым его значением плюс единица. Второй оператор цикла выводит новое значение,
выравнивая его вправо в поле шириной пять символов, поэтому в результате получа-
ется следующий вывод:
7 11 13 17 13
Переменная массива может хранить адрес любого массива того же ранга (ранг —
это количество измерений, которое в случае массива data равно 1) и типа элементов.
Например:
data = gcnew array<int>(45);
Этот оператор создает новый одномерный массив из 45 элементов типа int и со-
храняет его адрес в data. Исходный массив отбрасывается.
Вы также можете создать массив из набора начальных значений элементов:
array<double>A samples = { 3.4, 2.3, 6.8, 1.2, 5.5, 4.9. 7.4, 1.6};
Массивы, строки и указатели 221
Размер массива определяется количеством инициирующих значений, заключен-
ных в фигурные скобки, в данном случае — 8, причем значения присваиваются эле-
ментам последовательно.
Конечно, элементы массива могут быть любого типа, поэтому очень легко можно
создать массив строк:
array<StringA>A names = { "Jack”, "Jane", "Joe", "Jessica", "Jim", "Joanna"};
Элементы этого массива инициализируются строками, которые заключены в
фигурные скобки, и количество этих строк определяет число элементов в массиве.
Объекты String размещаются в куче CLR, поэтому типом элементов этого массива
является тип отслеживаемых дескрипторов — StringА.
Если вы объявите переменную массива, не инициализируя ее, то должны потом
явно создать массив, если хотите использовать список инициирующих значений.
Например:
array<StringA>A names; // Объявление переменной массива
names = gcnew array<StringA>{ "Jack", "Jane", "Joe", "Jessica", "Jim", "Joanna"};
Второй оператор создает массив и инициализирует его строками в фигурных скоб-
ках. Без явного вызова gcnew этот оператор не скомпилируется.
Вы можете использовать функцию Clear (), которая определена в классе Array
для установки любой последовательности числовых элементов массива в нулевое зна-
чение. Статическая функция вызывается с указанием имени класса, вы узнаете боль-
ше о таких функциях, когда мы будем в деталях разбирать классы. Ниже показан при-
мер вызова функции Clear ():
Array::Clear(samples, 0, samples->Length); // Установить все элементы в ноль
Первый аргумент Clear () — массив, который должен быть очищен, второй — ин-
декс первого очищаемого элемента, а третий — количество элементов, подлежащих
очистке. Таким образом, в этом примере всем элементам массива samples присваива-
ется значение 0.0. Если вы примените функцию Clear () к массиву дескрипторов, та-
ких как String^, то элементы устанавливаются в null, а если применить ее к массиву
объектов bool, то они получат значение false.
Теперь самое время разобрать пример использования массива CLR.
практическое занятие | Использование массива CLR
В этом примере генерируется массив, содержащий случайные значения, и затем
среди них ищется максимальное. Ниже показан код.
// Ех4_12.срр : главный файл проекта.
// Использование массива CLR
#include "stdafx.h"
using namespace System;
int main(array<System::String A> Aargs)
{
array<double>A samples = gcnew array<double> (50) ;
// Генерировать случайные значения элементов
Random* generator = gcnew Random;
for (int i = 0 ; i< samples->Length ; i++)
samples[i] = 100.0*generator->NextDouble();
222 Глава 4
// Вывести содержимое samples
Console: :WriteLine (L "Массив содержит следующие значения:");
for (int i = 0 ; i< samples->Length ; i++)
{
Console::Write(L"{0,10:F2}", samples[i]);
if ((i+l)%5 = 0)
Console: :WriteLine();
}
// Найти максимальное Значение
double max
= 0;
for each (double sample in samples)
if(max < sample)
Console: :WriteLine(L"Максимальное значение в массиве равно {0:F2}", max);
return 0;
Типичный вывод этой программы выглядит следующим образом:
Массив содержит следующие значения:
30.38 73.93 29.82 93.00 78.14
89.53 75.87 5.98 45.29 89.83
5.25 53.86 11.40 3.34 83.39
69.94 82.43 43.05 32.87 59.50
58.89 96.69 34.67 18.81 72.99
89.60 25.53 34.00 97.35 55.26
52.64 90.85 10.35 46.14 82.03
55.46 93.26 92.96 85.11 10.55
50.50 8.10 29.32 82.98 76.48
83.94 56.95 15.04 21.94 24.81
Максимальное значение в массиве равно 97.35
Press any key to continue . . .
Для продолжения нажмите любую клавишу . . .
Описание полученных результатов
Сначала вы создаете массив, способный хранить 50 элементов типа double:
array<double>^ samples = gcnew array<double>(50);
Переменная массива samples должна быть отслеживаемым дескриптором, потому
что массивы CLR создаются в управляемой сборщиком мусора куче.
Вы наполняете массив псевдослучайными значениями типа double в следующих
операторах:
Random^ generator = gcnew Random;
for (int i = 0 ; i< samples->Length ; i++)
samples[i] = 100.0*generator->NextDouble ();
Первый оператор создает объект типа Random в куче CLR. Объект Random включа-
ет функции, генерирующие псевдослучайные значения. Здесь вы в цикле используете
функцию Next Double (), которая возвращает случайное значение типа double, лежа-
щее в пределах от 0,0 до 1,0. Умножая это значение на 100,0, вы получаете значение
между 0,0 и 100,0. Цикл for сохраняет случайное значение в каждом элементе масси-
ва samples.
Массивы, строки и указатели 223
Объект Random также имеет функцию Next (), которая возвращает случайное неотрица-
тельное значение типа int. Если передать целочисленный аргумент при вызове функции
Next (), то она вернет случайное неотрицательное значение, меньшее значения этого аргу-
мента. Вы можете также передать два целочисленных аргумента, представляющих мини-
мальное и максимальное значения, в пределах которых должно находиться случайное число.
Следующий цикл выводит содержимое массива, по пять элементов в строке:
Console::WriteLine(Ь”Массив содержит следующие значения:”);
for (int i = 0 ; i< samples->Length ; i++)
Console::Write(L”{0,10: F2}”, samples[i]);
if((i+l)%5 == 0)
Console::WriteLine();
Внутри цикла значение каждого элемента выводится в поле шириной 10, с двумя
десятичными разрядами. Спецификация ширины поля гарантирует выравнивание
значений в столбцах. Символ перевода строки выводится всякий раз, когда выраже-
ние (i+1) % 5 принимает значение 0, что происходит после вывода значения каждого
пятого элемента, поэтому получается по пять значений в строке вывода.
И, наконец, максимальное значение элементов массива определяется следующим
образом:
double max = 0;
for each(double sample in samples)
if(max < sample)
max = sample;
Здесь применяется цикл for each просто для того, чтобы продемонстрировать,
что подобное возможно. Цикл сравнивает значение max с каждым элементом по оче-
реди, и когда оказывается, что элемент больше текущего значения max, то его зна-
чение присваивается max, так что в конечном итоге max содержит максимальное из
всех значений элементов массива.
Вы можете здесь использовать цикл for, если хотите записать позицию индекса
максимального элемента вместе с его значением, например:
double max =0;
int index = 0;
for (int i = 0 ; i < sample->Length ; i++)
if(max < samples[i])
max = samples [i];
index = i;
Сортировка одномерных массивов
В классе Array из пространства имен System определена функция Sort (), кото-
рая сортирует элементы одномерного массива так, что они располагаются в поряд-
ке возрастания. Чтобы отсортировать массив, вы просто передаете его дескриптор
функции Sort (). Ниже показан пример.
array<int>* samples = { 27, 3, 54, 11, 18, 2, 16};
Array::Sort(samples); // Сортировать элементы массива
for each(int value in samples) // Вывести элементы массива
Console::Write(L"{0, 8}", value);
Console::WriteLine();
224 Глава 4
Вызов функции Sort () переупорядочивает значения элементов массива samples
по возрастанию. Результат выполнения этого фрагмента будет таким:
2 3 11 16 18 27 54
Можно также сортировать диапазон элементов массива, передав в двух аргументах
функции Sort () индекс первого элемента и количество элементов, подлежащих со-
ртировке. Например:
array<int>A samples = { 27, 3, 54, 11, 18, 2, 16};
Array::Sort(samples, 2, 3); // Сортировать элементы с 2 по 4
Этот оператор сортирует три элемента массива samples, начиная с позиции 2.
После выполнения этих операторов элементы массива будут иметь следующие значе-
ния:
27 3 11 18 54 2 16
Существует также несколько других версий функции Sort (), описание которых
вы можете найти в документации, но я представлю только одну, которая особенно
полезна. Эта версия предполагает, что у вас есть два массива, которые ассоциирова-
ны так, что элементы первого из них представляют собой ключи к соответствующим
элементам второго массива. Например, вы можете сохранить имена людей в одном
массиве, а вес каждого — во втором. Функция Sort () сортирует массив names в по-
рядке возрастания и также переупорядочивает элементы массива weights, так что
показатели веса по-прежнему соответствуют соответствующим персонам. Рассмотрим
это на примере.
Практическое занятие
Сортировка двух ассоциированных массивов
Следующий пример создает массив имен и сохраняет вес каждой персоны в со-
ответствующем элементе второго массива. Затем он сортирует оба массива в одной
операции. Ниже показан его код.
// Ех4_13.срр : main project file.
// Сортировка массива ключей (names) и массива объектов (weights)
#include "stdafx.h"
using namespace System;
int main(array<System::String A> Aargs)
{
array<StringA>A names = { "Jill", "Ted", "Mary", "Eve", "Bill", "Al"};
array<int>A weights = { 103, 168, 128, 115, 180, 176};
Array: :Sort( names,weights) ; // Сортировать массивы
for each(StringA name in names) // Вывести имена
Console::Write(L"{0, 10}", name);
Console::WriteLine();
for each (int weight in weights) // Вывести показания веса
Console:: Write (L“{0, 10}", weight);
Console::WriteLine();
return 0;
}
Вывод этой программы:
Al Bill Eve Jill Mary Ted
176 180 115 103 128 168
Press any key to continue . . .
Массивы, строки и указатели 225
Описание полученных результатов
Значения массива weights соответствуют весу персон в тех же позициях индекса,
что и их имена в массиве names. Вызываемая здесь функция Sort () сортирует оба
массива, используя первый из них (names) в качестве ключа сортировки, определя-
ющего порядок обоих массивов. Вы можете видеть по выходной информации, что
после сортировки имени каждого лица соответствует его правильный вес во втором
массиве.
Поиск в одномерном массиве
В классе Array также предусмотрены функции поиска элементов в одномерном
массиве. Версии функции BinarySearch () используют алгоритм бинарного поиска
для нахождения позиции индекса заданного элемента во всем массиве либо в указан-
ном диапазоне его элементов. Алгоритм бинарного поиска требует, чтобы элементы
были упорядочены, поэтому перед выполнением поиска в массиве их следует отсо-
ртировать.
Вот как можно выполнять поиск во всем массиве:
array<int>" values = { 23, 45, 68, 94, 123, 127, 150, 203, 299};
int toBeFound = 127;
int position = Array::BinarySearch(values, toBeFound);
if(position<0)
Console::WriteLine(L”{0} не найдено.’’, toBeFound);
else
Console:: WriteLine (L” {0} найдено в позиции индекса {1}.’’, toBeFound, position);
Искомое значение сохраняется в переменной toBeFound. Первый аргумент функ-
ции BinarySearch () — это дескриптор массива, в котором выполняется поиск, а вто-
рой специфицирует то, что необходимо найти. Результат поиска возвращается функ-
цией BinarySearch () как значение типа int. Если второй аргумент функции найден
в массиве, указанном в ее первом аргументе, возвращается индекс его позиции, в
противном случае возвращается отрицательное значение. Таким образом, вы можете
проверить значение возврапГ, чтобы определить, найдено ли нужное значение в мас-
сиве. Поскольку значения в массиве values уже упорядочены по возрастанию, нет
необходимости сортировать его перед началом поиска. Приведенный фрагмент кода
генерирует следующий вывод:
127 найдено в позиции индекса 5.
Чтобы искать в заданном диапазоне элементов массива, можно применить версию
функции BinarySearch () с четырьмя аргументами. Первый аргумент — дескриптор
массива, в котором выполняется поиск, второй — позиция индекса элемента, с кото-
рого нужно начинать поиск, третий аргумент — количество элементов, среди кото-
рых нужно искать, а четвертый — собственно, искомое значение. Вот как это можно
использовать:
аггау<1пГ>Л values = { 23, 45, 68, 94, 123,
int toBeFound = 127;
int position = Array::BinarySearch(values,
127, 150, 203, 299};
3, 6, toBeFound);
Здесь выполняется поиск в массиве, начиная с его четвертого элемента и до по-
следнего. Как и предыдущая версия BinarySearch (), эта возвращает позицию индек-
са найденного элемента или отрицательное значение, если поиск не удался.
Рассмотрим пример.
Практическое занятие | |-|оиск в МаССИВе
Это — вариант предыдущего примера с добавлением операции поиска.
// Ех4_14.срр : главный файл проекта.
// Поиск в массиве
#include "stdafx.h"
using namespace System;
int main(array<System::String A> Aargs)
{
array<StringA>A names = { "Jill", "Ted", "Mary", "Eve", "Bill",
"Al", "Ned", "Zoe", "Dan", "Jean"};
array<int>A weights = { 103, 168, 128, 115, 180,
176, 209, 98, 190, 130 };
array<StringA>A toBeFound = {"Bill", "Eve", "Al", "Fred"};
Array: :Sort( names, weights) ; // Сортировка массива
int result = 0; // Хранит значение возврата
for each(StringA name in toBeFound) // Поиск весов
{
result = Array: :BinarySearch(names, name) ;// Поиск в массиве имен
if (result<0) // Проверка результата поиска
Console::WriteLine(L"{0} не найден.", name);
else
Console::WriteLine(L"{0} весит {1} фунтов.", name, weights[result]);
}
return 0;
}
Эта программа генерирует следующий вывод:
Bill весит 180 lbs.
Eve весит 115 lbs.
Al весит 176 lbs.
Fred не найден.
Press any key to continue . . .
Описание полученных результатов
Создаются два ассоциированных массива (массив имен и массив соответствующих
весов в фунтах). Кроме того, создается массив toBeFound, содержащий имена людей,
вес которых нужно узнать.
Массивы names и weights сортируются с использованием names в качестве ключа
сортировки. Затем в цикле for each выполняется поиск в массиве имен каждого из
имен, содержащихся в массиве toBeFound. Переменная цикла name по очереди по-
лучает значение каждого из имен массива toBeFound. Внутри цикла осуществляется
поиск текущего имени с помощью следующего оператора:
result = Array::BinarySearch(names, name); // Поиск в массиве имен
Это возвращает индекс элемента из массива names, который равен name, или от-
рицательное значение, если таковой не найден. Результат поиска проверяется и вы-
водится соответствующее сообщение:
if(result<0) // Проверка результата поиска
Console::WriteLine (L" {0} не найден.", name);
else
Console::WriteLine(L"{0} весит {1} фунтов.", name, weights[result]);
Поскольку порядок элементов массива weights определяется порядком элементов
names, для получения результата из weights можно обратиться к элементу этого мас-
сива по индексу, найденному при поиске name в массиве names. Из вывода программы
видно, что “Fred” не был найден в массиве names.
Когда операция бинарного поиска завершается неудачно, то возвращается не
просто какое-то случайное отрицательное значение. На самом деле это значение
представляет собой битовое дополнения позиции индекса первого элемента, ко-
торый больше искомого значения. Зная это, вы можете воспользоваться функцией
BinarySearch () для нахождения места в массиве, куда следовало бы вставить новый
объект, не нарушив при этом порядка сортировки элементов. Предположим, что вы
хотите вставить “Fred” в массив names. Вы можете найти позицию индекса, куда он
должен быть вставлен, следующими операторами:
array<StringA>A names = { "Jill", "Ted", "Mary", "Eve", "Bill",
"Al", "Ned", "Zoe", "Dan", "Jean"};
Array::Sort(names); // Сортировать массив
StringA name = L"Fred";
int position = Array::BinarySearch(names, name);
if(position<0) // Если результат отрицательный
position = ^position; // перебросить биты, чтобы получить индекс вставки
Если результат поиска отрицательный, замена всех битов на противоположное зна-
чение (0 на 1 и наоборот) дает позицию индекса, куца следует вставить новое имя. Если
результат положительный, значит, новое имя идентично имени в позиции с индексом,
равным результату, поэтому это значение можно использовать непосредственно.
Теперь можно скопировать массив names в новый массив, размером на один эле-
мент больше, и использовать вычисленную позицию для вставки имени в соответству-
ющее место:
array<StringA>A newNames = gcnew array<StringA>(names->Length+l);
// Копировать элементы из names в newNames
for (int i = 0 ; i<position ; i++)
newNames[i] = names[i];
newNames[position] = name; // Копировать новый элемент
if (position<names->Length) // Если еще остались элементы names
for(int i = position ; i<names->Length ; i++)
newNames[i+1] = names[i]; // копировать их в newNames
Это создает новый массив длиной на один элемент больше, чем старый. Затем
копируются элементы из старого массива в новый — от начала до позиции индекса
posit ion-1. После этого копируется новое имя, а за ним — ставшиеся элементы из
старого массива. Чтобы отбросить старый массив, можно написать просто:
names = nullptr;
Многомерные массивы
Вы можете создавать массивы с двумя и более измерениями; максимальное ко-
личество измерений массива — 32, чего вполне достаточно в большинстве случаев.
Количество измерений массива указывается в угловых скобках сразу после типа эле-
мента и отделяется от него запятой. По умолчанию массив имеет одно измерение,
вот почему мы не специфицировали его до сих пор. Вот как можно создать двумер-
ный массив целочисленных элементов:
array<int, 2>А values = gcnew array<int, 2> (4, 5);
228 Глава 4
Этот оператор создает двумерный массив из четырех строк и пяти столбцов, так
что всего он вмещает 20 элементов. Чтобы обратиться к элементу многомерного мас-
сива, вы специфицируете набор значений индекса — по одному для каждого измере-
ния; они указываются между квадратными скобками вслед за именем массива и разде-
ляются запятыми. Вот как можно установить значения элементов двумерного массива
целых чисел:
int nrows = 4;
int ncols = 5;
array<int, 2>Л values = gcnew array<int, 2> (nrows, ncols);
for (int i = 0 ; i<nrows ; i++)
for (int 3=0; 3<ncols ; j++)
values[i,j] = (i+l)*(j+l);
Вложенный цикл проходит по всем элементам массива. Внешний цикл проходит
по строкам, а внутренний — по каждому элементу в текущей строке. Как видите, зна-
чение каждого элемента устанавливается равным значению, полученному в результа-
те вычисления выражения (i+l)*(j+l), поэтому элементы первой строки будут уста-
новлены в 1, 2, 3, 4, 5, второй строки — 2, 4, 6, 8, 10 и так далее, вплоть до последней
строки, элементы которой будут равны 4, 6, 12, 16, 20.
Наверняка вы заметили, что нотация доступа к элементам двумерного массива
отличается от нотации, используемой с массивами “родного” C++. Это не случайно.
Массив C++/CLI не является массивом, подобным массивам родного C++. Как я уже
упоминал, размерность массива называется его рангом (rank), поэтому ранг масси-
ва values из предыдущего примера равен 2. Конечно, вы можете объявлять массивы
C++/CLI с рангом 3 и более, вплоть до массивов с рангом 32. В отличие от этого, мас-
сивы родного C++ в действительности всегда имеют ранг 1, поскольку родные масси-
вы C++ с двумя и более измерениями в действительности являются массивами масси-
вов. Как вы увидите позже, массивы массивов также можно объявлять и в C++/CLI.
Попробуем воспользоваться многомерным массивом в следующем примере.
Практическое занятие
Использование многомерного массива
Это консольное приложение CLR создает таблицу умножения 12x12 в двумерном
массиве.
// Ех4_15.срр : main project file.
// Использование двумерного массива
#include "stdafx.h"
using namespace System;
int main(array<System::String Л> Aargs)
{
const int SIZE = 12;
array<int, 2>A products = gcnew array<int, 2>(SIZE,SIZE) ;
for (int i = 0 ; i < SIZE ; i++)
for (int j = 0 ; j < SIZE ; j++)
products[i,j] = (i+l)*(j+l);
Console::WriteLine(L"Таблица умножения размером {0} SIZE) ;
// Вывести горизонтальную разделительную линию
for (int i = 0 ; i <= SIZE ; i++)
Console: :Write(L"_____") ;
Console: :WriteLine() ; // Вывести новую строку
Массивы, строки и указатели 229
// Вывести верхнюю строку таблицы
Console: :Write (L" | ") ;
for (int i = 1 ; i <= SIZE ; i++)
Console::Write(L"{0,3} |”, i) ;
Console: :WriteLine() ; // Вывести новую строку
// Вывести горизонтальную разделительную линию с вертикальными метками
for (int i = 0 ; i <= SIZE ; i++)
Console: :Write(L"____| ”) ;
Console: :WriteLine() ; // Вывести новую строку
// Вывести остальные строки
for (int i = 0 ; i<SIZE ; i++)
{
Console: :Write(L’’{0,3} |”, i+1);
for (int j = 0 ; j<SIZE ; j++)
Console::Write(L"{0,3} | ”, products[i,j]);
Console: :WriteLine() ; // Вывести новую строку
}
// Вывести горизонтальную разделительную линию
for (int i = 0 ; i <= SIZE ; i++)
Console: :Write (L”______");
Console: :WriteLine(); // Вывести новую строку
return 0;
Этот пример должен сгенерировать следующий вывод:
Таблица умножения размером 12:
1 I 1 I 2
2 | 2 | 4
3 | 3 | 6
4 | 4 | 8
5| 5 | 10
61 6 | 12
7 | 7 | 14
8| 8 | 16
9| 9 | 18
10 | 10 | 20
11 | 11 | 22
12 I 12 I 24
3
6
9
12
15
18
21
24
27
30
33
36
4
8
12
16
20
24
28
32
36
40
44
48
5
10
20
25
30
35
40
45
50
55
60
6
12
18
24
30
36
42
48
54
60
66
72
7 18 19
7 | 8 | 9
14 | 16 | 18
21 | 24 | 27
28 | 32 | 36
35 | 40 | 45
42 | 48 | 54
49 | 56 | 63
56 | 64 | 72
63 | 72 | 81
70 | 80 | 90
77 | 88 | 99
84 I 96 1108
10 I 11 I 12
10 | 11 | 12
20 I 22 | 24
30 | 33 | 36
40 | 44 | 48
50 I 55 | 60
60 I 66 | 72
70 | 77 | 84
80 | 88 I 96
90 | 99 |108
100 |110 |120
110 |121 1132
120 1132 1144
Press any key to continue . . .
Описание полученных результатов
Код выглядит довольно объемным, но большая его часть обеспечивает красивый
вывод. С помощью показанных ниже операторов создается двумерный массив.
const int SIZE = 12;
array<int, 2>A products = gcnew array<int, 2> (SIZE,SIZE);
Первая строка определяет константное целочисленное значение, которое специ-
фицирует количество элементов в каждом измерении массива. Вторая строка опреде-
ляет массив с рангом 2, состоящий из 12 строк и 12 столбцов. В нем размещаются
значения произведений таблицы умножения размером 12x12.
230 Глава 4
Значения элементов массива формируются во вложенном цикле:
for (int i = 0 ; i < SIZE ; i++)
for (int j = 0 ; j < SIZE ; j++)
products[i,j] = (i+l)*(j+l);
Внешний цикл выполняет итерацию по строкам, внутренний — по столбцам.
Значение каждого элемента равно произведению индекса строки на индекс столбца
после того, как каждый из них увеличен на 1. Остальная часть кода функции main ()
занята исключительно генерацией вывода на экран.
После вывода заголовка таблицы формируется линия, обозначающая вершину та-
блицы следующим образом:
for (int i = 0 ; i <= SIZE ; i++)
Console::Write(L" ");
Console::WriteLine(); // Вывести новую строку
Каждая итерация цикла выводит пять символов подчеркивания. Обратите внима-
ние, что верхний предел счетчика цикла определен как SIZE включительно, то есть
ыводится 13 наборов знаков подчеркивания, чтобы в дополнение к 12 столбцам зна-
чений сформировать начальный столбец заголовков строк.
Дальше в следующем цикле выводится строка заголовков столбцов таблицы:
// Вывести верхнюю строку таблицы
Console::Write(L" I”);
i <= SIZE ; i++)
Console::Write(L"{0,3} |", i);
Console::WriteLine(); // Вывести новую строку
В начале перед выводом заголовков столбцов отдельно выводится группа пробе-
лов, потому что это — специальный случай без выходного значения; эти пробелы на-
ходятся над начальным столбцом заголовков строк. Каждая метка столбца выводится
в цикле, затем печатается символ новой строки, подготавливая следующую строку вы-
вода.
Строки значений выводятся с помощью вложенного цикла:
// Вывести остальные строки
for (int i = 0 ; i<SIZE ; i++)
Console::Write(L"{0,3} |”, i+1) ;
for (int j = 0 ; j<SIZE ; j++)
Console::Write (L" {0,3} I", products[i,j]);
Console::WriteLine(); // Вывести новую строку
Внешний цикл проходит по строкам, и код внутри внешнего цикла формирует вы-
вод целой строки, включая ее метку в левой части. Внутренний цикл выводит зна-
чения элементов массива products, соответствующих i-й строке, разделяя их верти-
кальными линиями.
Оставшийся код выводит нижнюю линию таблицы.
Массив массивов
Элементы массива могут быть любого типа, поэтому вы можете создавать массивы,
элементы которых представляют собой отслеживаемые дескрипторы, ссылающиеся
на массивы. Это дает возможность создавать так называемые зубчатые массивы (jag-
ged arrays), поскольку элементы-дескрипторы могут ссылаться на массивы с разным
количеством элементов. Проще всего это понять, взглянув на пример. Предположим,
Массивы, строки и указатели 231
что требуется сохранить имена детей в классе в соответствии с уровнем их успевае-
мости, причем существует пять уровней классификации — А, В, С, D и Е. Сначала вы
создаете массив из пяти элементов, где каждый элемент сохраняет массив имен. Вот
оператор, которое это делает:
array< array< String74 >А >А grades = gcnew array< array< StringA >A > (5) ;
He пугайтесь обилия “шляп”, на самом деле все проще, чем выглядит. Переменная
массива grades — это дескриптор типа array<type>A. Каждый элемент этого масси-
ва также является дескриптором массива, так что тип элементов массива имеет ту же
форму (array<type>A), потому это и указывается между угловыми скобками специфи-
кации типа исходного массива, в результате чего получается array< array<type>A >А.
Элементы, сохраненные в массивах, также являются дескрипторами объектов String,
поэтому в последнем выражении type нужно заменить на StringA; таким образом, в
результате получаем тип массива array< array< StringA >А >А.
Имея такой массив массивов, теперь можно создавать массивы имен. Вот пример
того, как это может выглядеть:
grades[0] = gcnew array<StringA>{"Louise", "Jack"}; // Уровень A
grades[1] = gcnew array<StringA>{"Bill", "Mary", "Ben", "Joan"};// Уровень В
grades[2] = gcnew array<StringA>{"Jill", "Will", "Phil"}; // Уровень C
grades[3] = gcnew array<StringA>{"Ned", "Fred", "Ted", "Jed", "Ed"};//Уровень D
grades[4] = gcnew array<StringA>{"Dan", "Ann"}; // Уровень E
Выражение grades [n] обращается к n-ному элементу массива grades, который
представляет собой дескриптор массива дескрипторов StringA. Таким образом, каж-
дый из пяти операторов создает массив дескрипторов объектов String и сохраняет
его адрес в одном из элементов массива grades. Как видите, эти массивы строк име-
ют разную длину, поэтому понятно, что подобным образом можно управлять набором
массивов произвольной длины.
Можно создать и инициализировать весь такой массив массивов в одном операторе:
};
array< array< String74 >А >А grades = gcnew array< array< String74 >A >
{
gcnew array<StringA: >{"Louise", "Jack"}, // Уровень А
gcnew array<StringA: >{"Bill", "Mary", "Ben", "Joan"}, // Уровень В
gcnew array<StringA: >{"Jill", "Will", "Phil"}, // Уровень С
gcnew array<StringA: >{"Ned", "Fred", "Ted", "Jed", "Ed"}, // Уровень D
gcnew array<StringA: >{"Dan", "Ann"} H Уровень Е
Начальные значения элементов заключены в фигурные скобки.
А теперь давайте соберем все это в работающий пример, который продемонстри-
рует обработку массива массивов.
Практическое занятие
Использование массива массивов
Создайте проект консольной программы CLR и модифицируйте ее следующим об-
разом:
// Ех4_16.срр : main project file.
// Использование массива массивов
#include "stdafx.h"
using namespace System;
232 Глава 4
int main(array<System::String A> Aargs)
array< array< String74 >A >A grades = gcnew array< array< String74 >A >
gcnew array<StringA>{"Louise", "Jack"}, // Уровень A
gcnew array<StringA>{"Bill", "Mary", "Ben", "Joan"}, // Уровень В
gcnew array<StringA>{"Jill", "Will", "Phil"}, // Уровень C
gcnew array<StringA>{ "Ned", "Fred", "Ted", "Jed", " Ed"},//Уровень D
gcnew array<StringA>{"Dan", "Ann"} // Уровень E
wchar t gradeLetter = 'A';
for each(array< String74 >A grade in grades)
{
Console: :WriteLine ("Студенты с уровнем успеваемости {0}: " ,
gradeLetter++);
for each( String74 student in grade)
Console: :Wri te("{0,12}", student) ; // Вывести текущее имя
Console: :WriteLine(); // Новая строка
return 0;
Этот пример сгенерирует следующий вывод:
Студенты с уровнем успеваемости А:
Louise Jack
Студенты с уровнем успеваемости В:
Bill Mary Ben
Студенты с уровнем успеваемости С:
Jill Will Phil
Студенты с уровнем успеваемости D:
Ned Fred Ted
Студенты с уровнем успеваемости Е:
Dan Ann
Press any key to continue . . .
Joan
Jed
Ed
Описание полученных результатов
Определение массива написано точно так, как было предложено в предыдущем
разделе. После него определяется переменная gradeLetter типа wchar_t с началь-
ным значением 1А ’. Она используется для представления классификации уровня
успеваемости при выводе.
Студенты и их уровни перечисляются вложенными циклами. Внешний цикл про-
ходит по элементам массива grades:
for each(array< String74 >Л grade in grades)
// Обработать студентов текущего уровня...
Переменная цикла grade имеет тип array< StringA >А, поскольку такой тип у
элементов массива grades. Переменная grade ссылается на каждый массив дескрип-
торов StringA по очереди, поэтому на первой итерации цикла она ссылается на мас-
сив имен студентов уровня А, на второй — массив студентов уровня В и так далее — до
тех пор, пока не дойдет до массива имен студентов уровня Е.
При каждой итерации внешнего цикла выполняется следующий код:
Массивы, строки и указатели 233
Console::WriteLine("Студенты с уровнем успеваемости {0}:", gradeLetter++);
for each( StringA student in grade)
Console::Write("{0,12}",student); // Вывести текущее имя
Console::WriteLine(); // Новая строка
Первый оператор выводит строку, которая включает текущее значение gradeLetter,
которое начинается с ’ А ’. Оператор также выполняет инкремент этой переменной,
так что она последовательно получает значения ’В1, ’С’, • D ’ и ’ Е ’ на каждой итера-
ции внешнего цикла.
Далее следует внутренний цикл for each, который перебирает все имена из те-
кущего массива grade по очереди. Оператор вывода вызывает функцию Console: :
Write (), так что все имена текущего grade выводятся в одной строке. Имена пред-
ставлены в выходном поле шириной 12 символов, выровненные вправо. После завер-
шения внутреннего цикла WriteLine () просто выводит символ новой строки, пото-
му имена студентов следующего уровня начинаются с новой строки.
Внутренний цикл также можно было бы записать так:
for (int i = 0 ; i < grade->Length ; i++)
Console::Write ("{0,12}",grade [i])
II
Цикл ограничен значением свойства Length текущего массива имен, на который
ссылается переменная grade.
Внешний цикл тоже можно было оформить как for вместо for each, и в этом слу-
чае внутренний также пришлось бы исправить следующим образом:
{
for (int j = 0 ; j < grades->Length ; j++)
Console::WriteLine("Students with Grade {0}:", gradeLetter+j);
for (int i = 0 ; i < grades [ j ] ->Length ; i++)
Console: :WriteLine();
}
Теперь grades [ j ] ссылается на j-й массив имен, так что выражение grades [ j ] [i]
ссылается на i-e имя из j -го массива имен.
Строки
Вы уже видели, что тип класса String, определенный в пространстве имен
System, представляет строку в C++/CLI (фактически строки состоят их символов
Unicode). Выражаясь точнее, он представляет строку, состоящую из последовательно-
сти символов типа System: :Char. Объекты класса String предоставляют в ваше рас-
поряжение громадный объем мощной функциональности, что значительно облегчает
обработку строк. Начнем с начала — с создания строки.
Объект String можно создать следующим образом:
System::StringA saying = L”MHoro рук выполняют мало работы.”;
Переменная saying — отслеживаемый дескриптор, который ссылается на объект
String, инициализированный строкой, находящейся справа от знака =. Вы всегда
должны использовать отслеживаемые дескрипторы для сохранения ссылок на объек-
ты String. Представленный здесь строковый литерал является строкой, состоящей
из широких символов, поскольку снабжен префиксом L. Если вы пропустите префикс
L, то получите строковый литерал, состоящий из 8-битовых символов, а так компиля-
тор гарантирует, что он будет преобразован в строку широких символов.
234 Глава 4
Вы можете обращаться к индивидуальным символам в строке, используя нотацию
массивов, и первый символ строки имеет индекс 0. Вот как вы могли бы вывести тре-
тий символ строки saying:
Console::WriteLine (’’Третий символ в строке: {0}”, saying[2]);
Обратите внимание, что вы можете лишь читать отдельные символы строки, обра-
щаясь к ним по индексу; обновлять строку подобным образом не удастся. Строковые
объекты являются неизменяемыми (immutable), а потому не могут быть модифици-
рованы.
Вы можете получить количество символов строки, обратившись к ее свойству
Length. Вывести длину saying можно с помощью следующего оператора:
Console::WriteLine ("Строка содержит {0} символов.’’, saying->Length);
Поскольку saying — отслеживаемый дескриптор (который, как вы знаете, явля-
ется разновидностью указателя), вы должны использовать операцию ->, чтобы об-
ратиться к свойству Length (или любому другому члену объекта). Подробнее о свой-
ствах вы узнаете, когда мы начнем детальное рассмотрение классов C++/CLI.
Объединение строк
Для объединения строк вы можете использовать операцию +, чтобы сформиро-
вать новый объект String. Вот пример:
String* namel
String* name2
String* патеЗ
= L”IIaT";
= Ь’’Паташон’’;
= namel + L" и ’’ + name2;
После выполнения этих операторов патеЗ содержит строку ”Пат и Паташон”.
Обратите внимание на использование операции + для объединения объектов String
со строковыми литералами. Также вы можете объединять объекты String с числовы-
ми значениями, или значениями bool, при этом получая автоматическое их преобра-
зование в строковый вид. Следующие операторы иллюстрируют сказанное.
String* str = 1’’3начение:
String* strl = str + 2.5;
String* str2 = str + 25;
String* str3 = str + true;
f
// В результате — новая
// В результате — новая
//В результате — новая
строка: "Значение: 2.5"
строка: "Значение: 25"
строка: "Значение: True"
Можно также соединять String с символом, но результат при этом зависит от
типа символа:
char ch = ’ Z ’;
wchar t wch = ’ Z ’
String* str4 = str + ch;
String* str5 = str + wch;
// В результате — новая
//В результате — новая
строка "Значение: 90"
строка ’’Значение: Z"
В комментариях отражены результаты этих операций. Символ типа char трактует-
ся как числовое значение, так что вы получаете код символа, присоединенный к стро-
ке. Символ wchar_t имеет тот же тип, что и символы в объекте String (тип Char),
поэтому символ добавляется к строке.
Не забывайте, что объекты String не изменяемы; будучи однажды созданы, из-
меняться они не могут. Это значит, что все операции, которые предположительно из-
меняют объекты String, всегда в результате порождают новые объекты String.
В классе String также определена функция Join (), которую вы можете исполь-
зовать, когда хотите объединить серию строк, сохраненных в массиве, в одну общую
Массивы, строки и указатели 235
строку с разделителями. Вот как можно объединить вместе имена, разделив их запя-
тыми:
array<StringA>A names = { "Джил", "Тед", "Мери", "Ева", "Билл" };
StringA separator = ",
String74 joined = String:: Join (separator, names);
После выполнения этих операторов joined ссылается на строку "Джил, Тед,
Мери, Ева, Билл". Строка separator вставляется между объединяемыми фрагмента-
ми— исходными строками массива names. Конечно, строка-разделитель может быть
любой, какая вам понравится (например, она могла бы быть " и ", тогда в результате
получилось бы "Джил и Тед и Мери и Ева и Билл").
Давайте попробуем написать полный пример, работающий с объектами String.
Практическое занятие
и _______________ Работа со строками
Предположим, что имеется массив целых чисел, которые вы хотите вывести, вы-
равнивая по столбцам. Вам нужно выровнять значения, но ширину столбцов подо-
брать такую, чтобы ее было достаточно для размещения самого большого значения в
массиве, оставляя между столбцами один пробел. Ниже показана эта программа.
// Ех4_17.срр : main project file.
// Создание настраиваемой форматной строки
#include "stdafx.h"
using namespace System;
int main(array<System::String A> Aargs)
{
array<int>A values = { 2, 456, 23, -46, 34211, 456, 5609, 112098,
234, -76504, 341, 6788, -909121, 99, 10};
StringA formatStrl = "{0,"; // 1-я половина форматной строки
StringA formatstr2 = "}"; // 2-я половина форматной строки
StringA number; // Сохранить число в строке
// Определение максимальной длины значения в строковом виде
int maxLength = 0; // Максимальная найденная длина
for each (int value in values)
{
number - "" + value; // Создать строку из значения
if(maxLength<number->Length)
maxLength = number->Length;
}
// Создать форматную строку, используемую для вывода
StringA format = formatStrl + (maxLength+1) + formatStr2;
// Вывод значений
int numberPerLine = 3;
for (int i = 0 ; i< values->Length ; i++)
{
Console::Write(format, values[i]);
if((i+1)%numberPerLine — 0)
Console::WriteLine();
}
return 0;
}
236 Глава 4
Вывод этой программы выглядит следующим образом:
2 456 23
-46 34211 456
5609 112098 234
-76504 341 6788
-909121 99 10
Press any key to continue . . .
Описание полученных результатов
Назначение этой программы — создать форматную строку для выравнивания вы-
вода целых чисел из массива в столбцах, имеющих ширину, достаточную для разме-
щения строки максимальной длины, которая представляет целые числа. Изначально
форматная строка создается из двух частей:
String* formatStrl = ”{0,"; // 1-я половина форматной строки
String74 formatStr2 = // 2-я половина форматной строки
Эти две строки представляют собой начальную и конечную части требуемой фор-
матной строки. Вам необходимо найти длину максимальной числовой строки, вло-
жить ее между formatStrl и formatStr2, чтобы сформировать полную строку фор-
мата.
Эта длина находится с помощью следующего кода:
int maxLength =0; // Максимальная найденная длина
for each(int value in values)
number = ”” + value; // Создать строку из значения
if(maxLength<number->Length)
maxLength = number->Length;
Внутри цикла каждое число из массива преобразуется в его String-представле-
ние посредством объединения его с пустой строкой. Свойство Length каждой строки
сравнивается с maxLength и большее из них снова помещается в maxLength, форми-
руя подобным образом максимальную длину.
Создание форматной строки очень просто:
String* format = formatStrl + (maxLength+1) + formatStr2;
К maxLength добавляется единица, чтобы добавить один дополнительный про-
бел к полю, когда отображается строка максимальной длины. Помещение выражения
maxLength+1 в скобки гарантирует, что оно будет оценено как арифметическая опе-
рация, прежде чем выполнится операция объединения строки.
В конце полученная описанным образом строка format используется к коде для
вывода значений массива:
int numberPerLine =3;
for (int i = 0 ; i< values->Length ; i++)
Console::Write(format, values[i]);
if((i+1)%numberPerLine == 0)
Console::WriteLine();
Оператор вывода в этом цикле использует format в качестве строки вывода.
Благодаря тому, что maxLength включена в строку format, вывод выполняется в
столбцы, шириной на единицу больше, чем максимальная длина выводимого значе-
Массивы, строки и указатели 237
ния. Переменная numberPerLine определяет, сколько значений появится в строке,
поэтому цикл достаточно универсален — в том смысле, что вы можете изменять коли-
чество столбцов, изменяя значение numberPerLine.
Модификация строк
Наиболее часто возникает потребность в усечении пробелов в начале и конце
строки. Функция Trim () объекта String выполняет эту задачу:
String* str = { " Точность - вежливость королей... "} ;
String* newStr = str->Trim();
Функция Trim () во втором операторе удаляет все пробелы из начала и конца str
и возвращает в качестве результата новый объект String, ссылка на который поме-
щается в newStr. Конечно, если вам не нужно сохранять исходное значение str, вы
можете поместить результат обратно в str.
Существует другая версия функции Trim (), позволяющая специфицировать симво-
лы, которые должны быть исключены из начала и конца строки. Эта функция очень
гибка, поскольку представляет более одного способа указания удаляемых символов.
Вы можете специфицировать их в массиве и передать дескриптор массива в качестве
аргумента функции:
String* toBeTrimmed - L"wool wool sheep sheep wool wool wool";
array<wchar_t>* notWanted = {L’w’,L’o’,L’l’,L’
Console::WriteLine (toBeTrimmed-->Trim(notWanted)) ;
Здесь мы имеем строку toBeTrimmed, которая состоит из “sheep” (овцы), покры-
той “wool” (шерстью). Массив усекаемых символов строки определен в переменной
notWanted, поэтому передача его функции Trim () удаляет любые символы, содержа-
щиеся в массиве, из обоих концов строки. Напомним, что объекты String не изменя-
емы, поэтому исходная строка останется прежней, а функция Trim () вернет новый
экземпляр строки. Выполнение этого фрагмента кода выдаст следующий вывод:
sheep sheep
Если вы укажете символьные литералы без префикса L, они будут иметь тип char
(что соответствует классу типа значения SByte), однако компилятор неявно преоб-
разует их в тип wchar t.
Вы можете также специфицировать символы, которые функция Trim () должна
удалить, как отдельные аргументы, поэтому последний оператор предыдущего фраг-
мента можно переписать следующим образом:
Console::WriteLine(toBeTrimmed->Trim(L’w’, L’o’, L’l’, L’ ’));
Это даст тот же вывод, что и предыдущая версия оператора. Вы можете переда-
вать столько аргументов типа wchar_t, сколько хотите, но если символов слишком
много, то массив будет более удачным решением.
Если вы хотите усечь только один конец строки, то для этого можете использо-
вать функции TrimEnd () или TrimStart (). Они имеют такие же версии, что и функ-
ция Trim (), поэтому без аргументов усекают пробелы, с аргументом-массивом — от-
секают символы, содержащиеся в массиве, а с явными аргументами wchar_t удаляет
переданные в аргументах символы.
Противоположность усечению строки — ее дополнение на любом конце пробела-
ми или другими символами. Для выполнения этой операции предусмотрены функции
PadLef t () и PadRight (), которые дополняют строку слева и справа соответствен-
но. Главное назначение этих функций — форматирование вывода, когда нужно вы-
238 Глава 4
вести строки, выравнивая их вправо или влево в пределах фиксированной ширины.
Простейшие версии функций PadLeft () и PadRight () принимают один аргумент,
указывающий длину строки, которая должна получиться в результате этой операции.
Например:
String* value = L"3.142";
String* leftPadded = value~>PadLeft(10); // Результат: ” 3.142”
String* rightPadded = value->PadRight(10); // Результат: "3.142 "
Если длина, переданная в аргументе, меньше или равна длине исходной строки,
обе функции возвращают новый объект String, идентичный оригиналу.
Чтобы дополнить строку символом, отличным от пробела, можно специфици-
ровать дополняющий символ в качестве второго аргумента функций PadLeft () и
PadRight (). Ниже показана пара примеров.
String* value = L" 3.142";
String* leftPadded = value->PadLeft(10, L'*'); // Результат: "*****3.142"
String* rightPadded = value->PadRight (10, L’#’); // Результат: "3.142#####"
Конечно, во всех этих случаях можно поместить результирующую строку обрат-
но в дескриптор, ссылающийся на исходную строку, что приведет к потере исходной
версии строки.
Класс String также включает в себя функции ToUpper () и ToLower (), предназна-
ченные для преобразования всей строки в верхний и нижний регистр соответствен-
но. Вот как это работает:
String* proverb = Ъ"Много рук выполняют мало работы.";
String* upper = proverb->ToUpper();//Результат: "МНОГО РУК ВЫПОЛНЯЮТ МАЛО РАБОТЫ."
Функция ToUpper () возвращает новую строку, которая повторяет исходную, но
преобразует все ее символы в заглавные.
Функция Insert () служит для вставки строки в заданную позицию существующей
строки. Вот пример этой операции:
String* proverb = Ь"Много рук выполняют мало работы.";
String* newProverb « proverb->Insert(6, Ь"кривых ");
Функция вставляет строку, указанную во втором аргументе, в исходную, начиная с
позиции, переданной первым аргументом. В результате получается новая строка сле-
дующего содержания:
Много кривых рук выполняют мало работы.
Вы можете также заменить все вхождения заданного символа или подстроки в
строке на другой символ или подстроку. Следующий фрагмент кода иллюстрирует эту
возможность.
String* proverb = Ь”Много рук выполняют мало работы.";
Console::WriteLine(proverb->Replace(L’ ’, L’*’);
Console::WriteLine(proverb->Replace(L"MHoro рук", Ь”Нажатия кнопок");
Выполнение этого фрагмента даст следующий вывод:
Много *рук* выполняют *мало *работы.
Нажатия кнопок выполняют мало работы.
Первый аргумент функции Replace () специфицирует символ или подстроку, ко-
торая должна быть заменена, а второй аргумент описывает замену.
Массивы, строки и указатели 239
Поиск строк
Возможно, простейшая операция поиска — это проверка того, начинается ли
либо заканчивается заданная строка указанной подстрокой. Это делают функции
StartsWith () и EndsWith (). Обеим функциям передается дескриптор подстроки,
которую нужно найти, и они возвращают булевское значение, указывающее на то,
присутствует подстрока в исходной строке или нет. Вот фрагмент, демонстрирующий
применение функции StartsWith ():
String* sentence = Ь"3акон суров, но это закон.";
if(sentence->StartsWith(и’Закон"))
Console::WriteLine("Фраза начинается с ’Закон’.");
Выполнение этого фрагмента даст такой вывод:
Фраза начинается с ’Закон'.
Конечно, вы можете также применить функцию EndsWith () к строке sentence:
Console::WriteLine("Фраза {0}заканчивается словом ’закон’.",
sentence~>EndsWith (Ь’’закон") ? L"" : 1"не ’’);
Результат выражения с условной операцией вставляется в выходную строку. Это
будет пустая строка, если EndsWith () вернет true, и "не ", если она вернет false. В
данном случае функция возвращает false (из-за точки в конце исходной строки).
Функция IndexOf () ищет в строке первое вхождение указанного символа или под-
строки и возвращает индекс вхождения, если оно есть, либо -1 в противном случае.
Искомый символ или подстрока передается в аргументе функции. Например:
String* sentence = Ь"3акон суров, но это закон.";
int ePosition = sentence->IndexOf(L'о'); // Возвращает 3
int thePosition = sentence->IndexOf(L"ho"); // Возвращает 13
В первом случае ищется буква “о”, а во втором — слово “но”. Возвращаемые значе-
ния IndexOf () указаны в комментариях.
Гораздо чаще вам понадобится находить все вхождения заданного символа или под-
строки. Для этого подойдет другая версия функции IndexOf (), которая предназначе-
на для повторяющегося вызова. Она принимает второй аргумент, указывающий индекс
позиции начала поиска. Ниже показан пример применения этой версии функции.
int index =0;
int count = 0;
while((index = words->IndexOf(word,index)) >= 0)
index += word->Length;
++count;
Console::WriteLine(L"'{0}' найдено {1} раз в:\п{2}", word, count, words);
Этот фрагмент подсчитывает количество вхождений фрагмента "wool" в строку
words. Операция поиска выполняется в проверочном условии цикла while, а резуль-
тат сохраняется в index. Цикл повторяется до тех пор, пока значение index неот-
рицательно, поэтому, когда IndexOf () возвращает -1, он завершается. Внутри тела
цикла значение index увеличивается на длину word, что перемещает позицию начала
поиска на символ, следующий за найденным экземпляром word, подготавливая сле-
дующую итерацию поиска. Переменная count увеличивается внутри цикла, поэтому,
когда цикл завершается, она содержит накопленное общее количество вхождений
word в words. В результате выполнения этого фрагмента получается такой вывод:
240 Глава 4
'wool' найдено 5 раз в:
wool wool sheep sheep wool wool wool
Функция LastlndexOf () во всем подобна IndexOf (), за исключением того, что
выполняет поиск в обратном порядке, начиная с конца строки или с заданной пози-
ции. Вот как операция, выполняемая предыдущим фрагментом, может быть реализо-
вана с помощью функции LastlndexOf ():
int index = words->Length - 1;
int count = 0;
while(index >= 0 && (index = words->LastIndexOf(word,index)) >= 0)
{
—index;
++count;
}
При условии, что значения строк word и words те же самые, что и раньше, этот
фрагмент генерирует тот же результат. Поскольку LastlndexOf () ищет в обратном
порядке, здесь начальной позицией поиска является последний символ строки, кото-
рый имеет индекс words->Length-l. Когда найдено вхождение word, значение index
уменьшается на 1, так что следующий обратный поиск начинается с символа, предше-
ствующего текущему вхождению word. Если word встретится прямо в начале words
(в позиции индекса 0), то уменьшение index приведет к тому, что он станет равным
-1, что не является допустимым аргументом для функции LastlndexOf (), поскольку
начальная позиция поиска всегда должна находиться внутри строки. Дополнительная
проверка отрицательного значения index в условии цикла предотвращает описанную
ситуацию. Если левый операнд && равен false, то правый операнд не оценивается.
И последняя функция поиска, о которой следует упомянуть — это IndexOf Any ();
она ищет в строке первое вхождение любого символа из массива array<wchar_t>,
который передается в аргументе. Подобно функции IndexOf (), эта функция име-
ет версии, которые ищут от начала строки или от указанной позиции индекса.
Давайте рассмотрим полный работающий пример, в котором используется функция
IndexOfAny().
Практическое занятие
Поиск любого из множества символов
Этот пример ищет в строке знаки препинания:
// Ех4_18.срр : main project file.
// Поиск знаков препинания
#include "stdafx.h"
using namespace System;
int main(array<System::String A> Aargs)
{
array<wchar_t>* punctuation = {L"" , L'\' ', L'.', L',', L': ', L';', L' ?', L' ?'} ;
String* sentence = L"\"It's chilly in here\", the boy's mother said coldly.";
// Создать массив пробелов с длиной sentence
array<wchar_t>A indicators = gcnew array<wchar_t>(sentence->Length) {L* '} ;
int index = 0; 11 Индекс найденного символа
int count = 0; // Счетчик знаков препинания
while((index = sentence->IndexOfAny(punctuation, index)) >= 0)
{
indicators [index] = L'*' ; // Установить маркер
Массивы, строки и указатели 241
++index;
++count;
// Инкремент до следующего символа
// Увеличить счетчик
}
Console::WriteLine(L"Найдено {0} знаков препинания в строке: ", count) ;
Console::WriteLine(L"\n{0}\n{1}", sentence, gcnew String(indicators));
return 0;
Этот пример выдает следующее:
Найдено 6 знаков препинания в строке:
"It’s chilly in here", the boy’s mother said coldly.
Press any key to continue . . .
Описание полученных результатов
Сначала в этом примере создается массив, содержащий символы, которые должны
быть найдены и строка, в которой будет выполняться поиск:
array<wchar_t>A punctuation = {L’ ’” , L’\’’, L’.’, L’,’, L’:’, L’;’, L’!’, L’?’};
-StringA sentence = L"\"It’s chilly in here\", the boy’s mother said coldly.’’;
Обратите внимание, что вы должны специфицировать символ одной кавычки, ис-
пользуя управляющую последовательность с обратным слешем, поскольку одиночная
кавычка — ограничитель символьного литерала. Двойную кавычку можно задавать
обычным образом, поскольку в данном контексте нет никакого риска ее неправиль-
ной интерпретации.
Далее определяется массив символов, в котором все элементы инициализированы
пробелами:
array<wchar_t>A indicators = gcnew array<wchar_t>(sentence->Length){L*
Этот массив содержит столько элементов, сколько символов в строке sentence.
В нем элементы, находящиеся в позициях, где в исходной строке будут найдены знаки
препинания, заменяются на А. Обратите внимание на то, что единственный символ-
инициализатор в фигурных скобках может применяться для инициализации элемен-
тов массива.
Поиск выполняется в цикле while:
while((index = sentence->IndexOfAny(punctuation, index)) >= 0)
indicators[index] = L’A’; // Установить маркер
++index; // Инкремент до следующего символа
++count; // Увеличить счетчик
Условие цикла — то же самое, что вы видели в предыдущих фрагментах кода.
Внутри тела цикла обновляется элемент массива indicators в позиции index — ему
присваивается символ маркера А перед тем, как значение index увеличивается для
следующей итерации. Когда цикл завершается, count содержит количество найден-
ных знаков препинания, а в indicators проставлены маркеры А в тех позициях, где
стоят знаки препинания в исходной строке.
Вывод результата формируется операторами:
Console::WriteLine(Ь"Найдено {0} знаков препинания в строке:", count);
Console::WriteLine(L”\n{0}\n{1}’’, sentence, gcnew String(indicators));
242 Глава 4
Второй оператор создает в куче новый объект String на основе массива
indicators путем передачи этого массива конструктору класса String. Конструктор
класса — это функция, которая создает объект класса. Подробнее о конструкторах вы
узнаете, когда будете создавать собственные классы.
Отслеживающие ссылки
Отслеживающие ссылки (tracking references) являются средством, подобным ссыл-
кам “родного” C++ в том смысле, что оно предоставляет псевдоним для чего-то, на-
ходящегося в куче CLR. Вы можете создавать такие ссылки на типы значений в стеке
и на дескрипторы объектов управляемой кучи. Сами отслеживающие ссылки всегда
создаются в стеке и автоматически обновляются, если объекты, на которые они ссы-
лаются, перемещаются сборщиком мусора.
Отслеживающая ссылка определяется с помощью операции %. Например, вот как
создается такая ссылка на элемент типа значения:
int value = 10;
int% trackvalue = value;
Второй оператор определяет trackvalue как отслеживающую ссылку на перемен-
ную value, созданную в стеке. После этого вы можете модифицировать value, ис-
пользуя trackValue:
trackvalue *= 5;
Console::WriteLine(value);
Поскольку trackValue — псевдоним для value, второй оператор выдаст 50.
Внутренние указатели
Хотя вы не можете выполнять арифметические действия над адресами отслежива-
ющих дескрипторов, C++/CLI все же предусматривает форму указателя, к которому
можно применять арифметические операции; он называются внутренним указате-
лем (interior pointer) и определяется ключевым словом interior_ptr. Адрес, сохра-
ненный во внутреннем указателе, может быть автоматически обновлен сборщиком
мусора CLR, когда это необходимо. Внутренний указатель — это всегда автоматиче-
ская переменная, которая локальна по отношению к функции.
Ниже показано, как определяется внутренний указатель, содержащий адрес перво-
го элемента массива:
array<double>A data = {1.5, 3.5, 6.7, 4.2, 2.1};
interior_ptr<double> pstart = &data[0];
Тип объекта, на который указывает внутренний указатель, специфицируется меж-
ду угловыми скобками, следующими за ключевым словом interior_ptr. Во втором
операторе приведенного фрагмента указатель инициализируется адресом первого
элемента массива, полученного операцией & — точно так же, как это делается в “род-
ном” C++. Если вы не задаете начального значения внутреннего указателя, он иници-
ализируется по умолчанию nullpt г. Массив всегда размещается в куче CLR, поэтому
это как раз та ситуация, когда сборщик мусора может изменить адрес, содержащийся
во внутреннем указателе.
Существуют ограничения, накладываемые на спецификацию типа для внутреннего
указателя. Внутренний указатель может содержать адрес объекта класса значения в
стеке или адрес дескриптора объекта в куче CLR; он не может содержать адрес цело-
Массивы, строки и указатели 243
го объекта в куче CLR. Внутренний указатель также может указывать на объект “род-
ного” класса, или “родной” указатель.
Можно использовать внутренний указатель для того, чтобы хранить адрес объекта
класса значения, являющегося частью объекта из кучи, такого как элемент массива
CLR. Вы можете создать внутренний указатель для хранения адреса отслеживающего
дескриптора объекта System: : String, но вы не можете создать внутренний указа-
тель для хранения адреса самого объекта String. Например:
interior_ptr<StringA> pstrl; // нормально — указатель на дескриптор
interior_ptr<String> pstr2; //не компилируется — указатель на объект String
Все арифметические операции, применимые к указателям “родного” C++, мож-
но также применять к внутренним указателям. Можно использовать инкремент и
декремент, чтобы изменять адреса, на которые они указывают, чтобы обращаться к
предыдущим или последующим элементам данных. Вы можете также прибавлять или
вычитать целые значения, а также сравнивать внутренние указатели между собой.
Разберем пример, который демонстрирует кое-что из сказанного.
практическое занятие | Создание и использование в нутре НН их
указателей
В этом примере предложено упражнение с внутренними указателями на числовые
значения и строки.
// Ех4_19.срр : main project file.
// Создание и использование внутренних указателей
#include "stdafx.h"
using namespace System;
int main(array<System::String A> Aargs)
{
// Доступ к элементам массива через указатель
array<double>A data = {1.5, 3.5, 6.7, 4.2, 2.1};
interior_ptr<double> pstart = &data[0];
interior_ptr<double> pend = &data[data->Length - 1];
double sum = 0.0;
while (pstart <= pend)
sum += *pstart++;
Console: .-WriteLine (L" Сумма элементов данных массива = {0}\n", sum) ;
//Просто чтобы показать возможность — доступ к строкам через внутренний указатель
array<StringA>A strings = {L"Land ahoy?",
L"Splice the mainbrace?",
L"Shiver me timbers! ",
L"Never throw into the wind?"
};
for (interior_ptr<StringA> pstrings = &strings[0] ;
pstrings-fistrings[0] < strings->Length ; ++pstrings)
Console: -.WriteLine(*pstrings) ;
return 0;
}
244 Глава 4
Вывод этого примера будет таким:
Сумма элементов данных массива =18
Land ahoy!
Splice the mainbrace!
Shiver me timbers!
Never throw into the wind!
Press any key to continue . . .
Описание полученных результатов
После создания массива элементов типа double определяются два внутренних ука-
зателя:
interior_ptr<double> pstart = &data[0];
interior_ptr<double> pend = &data[data->Length - 1];
Первый оператор создает pstart как указатель на тип double и инициализирует
его адресом первого элемента массива — data [ 0 ]. Внутренний указатель pend ини-
циализируется адресом последнего элемента массива — data [data->Length - 1].
Поскольку data->Length представляет количество элементов массива, а вычитание
1 из этого значения дает индекс последнего элемента.
Цикл while накапливает сумму элементов массива:
while(pstart <= pend)
sum += *pstart++;
Цикл продолжается до тех пор, пока внутренний указатель pstart содержит
адрес, не превышающий значение адреса pend. Проверочное условие цикла также
можно было бы выразить в виде ! pstart > pend.
Внутри цикла pstart сначала получает адрес первого элемента массива. Его зна-
чение извлекается с помощью операции разыменования, как *pstart, и это значение
прибавляется к sum. Затем значение адреса в указателе увеличивается операцией ++.
На последней итерации цикла pstart содержит адрес последнего элемента массива,
который совпадает с адресом, записанным в pend, поэтому следующее увеличение
pstart приводит к тому, что условие цикла оценивается как false, поскольку pstart
становится больше pend. После завершения цикла значение sum выдается на экран,
так что вы можете убедиться, что цикл while работает так, как и должен.
Далее создается массив строк:
array<StringA>A strings = {L”Land ahoy!”,
L”Splice the mainbrace!”,
L"Shiver me timbers!",
L"Never throw into the wind!"
Затем цикл for выдает каждую строку на экран:
for(interior_ptr<StringA> pstrings = &strings[0] ;
pstrings-&strings[0] < strings->Length ; ++pstrings)
Console::WriteLine(*pstrings);
Первое управляющее выражение в цикле for объявляет внутренний указатель
pstrings и инициализирует его адресом первого элемента массива strings. Второе
выражение, определяющее условие продолжения цикла, выгладит следующим обра-
зом:
pstrings-&strings [0] < strings->Length
Массивы, строки и указатели 245
До тех пор, пока pstrings содержит адрес корректного элемента массива, раз-
ница между адресом pstrings и адресом первого элемента массива остается меньше
количества элементов в массиве, которое сообщает выражение strings->Length. То
есть, когда эта разница становится равной длине массива, цикл завершается. Взглянув
на вывод программы, вы можете убедиться, что все работает, как ожидалось.
Чаще всего внутренние указатели используются для обращения к объектам, явля-
ющимся частью объектов кучи CLR, и позже из нашей книги вы узнаете об этом под-
робнее.
Резюме
Вы ознакомились со всеми базовыми типами значений C++, с тем, как создавать и
использовать массивы этих типов, и как создавать и использовать указатели. Вы так-
же получили представление о концепции ссылки. Однако все эти темы раскрыты не
полностью. Позднее в этой книге я еще вернусь к теме массивов, указателей и ссылок.
Ниже приведен краткий перечень тем этой главы, касающихся программирования на
“родном” C++.
□ Массив позволяет управлять множеством однотипных переменных с использо-
ванием одного общего имени. Каждое измерение массива задается между ква-
дратными скобками, следующими за именем массива при его объявлении.
□ Каждое измерение массива индексируется, начиная с нуля. Таким образом, пя-
тый элемент одномерного массива имеет значение индекса 4.
□ Массивы могут инициализироваться в объявлении списком значений, заклю-
ченным в фигурные скобки.
□ Указатель — это переменная, содержащая адрес другой переменной. Указатель
объявляется как “указатель на тип” и может получать значения адресов пере-
менных этого типа.
□ Указатель может указывать на константный объект. Такой указатель можно пе-
реустановить на другой объект. Но указатель также может быть объявлен как
константный — в этом случае ему нельзя присвоить другой адрес.
□ Ссылка — это псевдоним другой переменной, который может использоваться
в тех же местах, что и переменная, на которую она ссылается. Ссылка должна
быть инициализирована в объявлении.
□ Ссылка не может быть переназначена на другую переменную.
□ Операция sizeof возвращает количество байт, занятых объектом, специфи-
цированным в ее аргументе. Аргументом может служить переменная или имя
типа в скобках.
□ Операция new динамически выделяет память в свободном хранилище приложе-
ний “родного” C++. Когда блок памяти выделен, операция возвращает адрес его
начала. Если память не может быть выделена по какой-то причине, возбуждает-
ся исключение, которое по умолчанию прерывает выполнение программы.
Механизм указателя иногда выглядит несколько запутанным, потому что он рабо-
тает на разных уровнях в пределах одной программы. Иногда он ведет себя как адрес,
а в других случаях работает со значением, расположенным по этому адресу. Очень
важно, чтобы вы научились правильно понимать способы применения указателей,
поэтому если вам не все ясно, напишите несколько собственных примеров, использу-
ющих указатели, чтобы чувствовать себя уверенно в их применении.
246 Глава
Далее перечислены ключевые моменты, с которыми вы ознакомились в этой гла-
ве, касающиеся программирования CLR.
□ В программах CLR память выделяется операцией gcnew в управляемой куче,
которую обслуживает сборщик мусора.
□ Объекты классов ссылок вообще и объекты String в частности всегда разме-
щаются в куче CLR.
□ При работе со строками в программах CLR используются объекты String.
□ CLR имеет собственные типы массивов, предлагающие более богатую функци-
ональность, чем родные типы массивов.
□ Отслеживаемый дескриптор — это разновидность указателя, служащего для об-
ращения к переменным, размещенным в куче CLR. Отслеживаемые дескрип-
торы автоматически обновляются, когда объекты, на которые они указывают,
перемещаются в куче сборщиком мусора.
□ Переменные, ссылающиеся на объекты и массивы в куче, всегда являются от-
слеживаемыми дескрипторами.
□ Отслеживаемая ссылка подобна обычной ссылке “родного” C++ во всем, за ис-
ключением того, что адрес, на который она ссылается, автоматически обновля-
ется при перемещении объекта в куче сборщиком мусора.
□ Внутренний указатель — это тип указателя C++/CLI, к которому можно приме-
нять те же операции, что и к обычному указателю “родного” C++.
□ Адрес, содержащийся во внутренней ссылке, может модифицироваться ариф-
метическими операциями и оставаться корректным, даже если он ссылается на
нечто, размещенное в куче CLR.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Напишите программу на “родном” C++, которая позволит ввести неограничен-
ное количество значений и сохранить их в массив, размещенный в свободном
хранилище. Программа должна вывести введенные значения, по пять в строке,
после чего вычислить и показать их среднюю величину. Начальный размер мас-
сива должен составлять пять элементов. При необходимости программа должна
создавать массив с пятью добавочными элементами и копировать содержимое
старого массива в новый.
2. Повторите предыдущий пример, применив нотацию указателей вместо нотации
массивов.
3. Объявите символьный массив и инициализируйте его подходящей стро-
кой. Используйте цикл для изменения регистра каждого символа на верхний.
Подсказка: в наборе символов ASCII значения кодов символов верхнего реги-
стра на 32 меньше соответствующих кодов символов нижнего регистра.
4. Напишите программу на C++/CLI, которая создает массив случайных чисел
типа int. Массив должен иметь от 10 до 20 элементов. Присвойте элементам
случайные значения от 100 до 1000. Выведите их на экран, по пять в строке,
в порядке возрастания, без сортировки массива; например, найдите самый ма-
Массивы, строки и указатели 247
ленький элемент и выведите его, затем следующий самый маленький, исключая
первый, и так далее.
5. Напишите программу на C++/CLI, которая генерирует случайное целое число
больше 10 000. Выведите его на экран цифрами, а затем — словами, перечисля-
ющими десятичные цифры этого числа. Например, если будет сгенерировано
число 345 678, вывод должен быть таким:
Значение числа 345678
три четыре пять шесть семь восемь
6. Напишите программу на C++/CLI, создающую массив следующих строк:
"Madam I’m Adam."
"Don’t cry for me, Marge and Tina."
"Lid off a daffodil."
"Red lost soldier."
"Cigar? Toss it in a can. It is so tragic."
Программа должна проверить каждую строку по очереди, вывести ее и отме-
тить, является ли она палиндромом (то есть последовательностью символов, ко-
торые читаются одинаково слева направо и справа налево, если игнорировать
пробелы и знаки препинания).
5
Структурная организация
программ
До настоящего момента вы не были готовы организовывать код своих программ в
модульном стиле, поскольку могли конструировать программу как единственную функ-
цию — main (); однако вы использовали библиотечные функции различного рода, а
также функции, принадлежащие объектам. Всякий раз, начиная писать программу на
C++, вы должны продумывать ее модульную структуру с самого начала, и как вы уви-
дите, хорошее понимание того, как должны быть реализованы функции, существенно
для объектно-ориентированного программирования на C++. В этой главе вы изучите
следующие вопросы.
□ Как объявлять и писать собственные функции C++.
□ Как определять и использовать аргументы функций.
□ Как передавать в функции и получать от них массивы.
□ Что означает передача по значению.
□ Как передавать в функции указатели.
□ Как использовать ссылки в качестве аргументов функций, и что означает пере-
дача по ссылке.
□ Как модификатор const влияет на аргументы функции.
□ Как возвращать значения из функций.
□ Как использовать рекурсию.
О структурировании программ C++ можно говорить достаточно много, поэтому во
избежание несварения желудка, вы не должны пытаться проглотить все сразу. После
того, как вы хорошенько разжуете и прочувствуете аромат маленького кусочка, то
сможете двинуться дальше, к следующей главе, где получите более подробное пред-
ставление об этой теме.
250 Глава 5
Что такое функции
Для начала рассмотрим общие принципы работы функций. Функция — это изоли-
рованный блок кода, имеющий определенное специфическое назначение. Функция
обладает именем, которое как идентифицирует ее, так и служит для вызова на вы-
полнение внутри программы. Имя функции глобально, но не обязательно уникально
в C++, как вы увидите это в следующей главе; однако функции, которые выполняют
различные действия, обычно должны иметь разные имена.
Имена функций подчиняются тем же правилам, что и имена переменных. То есть
имя функции — это последовательность букв и цифр, начинающаяся с буквы, причем
знак подчеркивания тоже считается буквой. Имя функции должно обычно отражать
то, что она делает, поэтому, например, вы можете назвать функцию, которая подсчи-
тывает бобы, count beans ().
Вы передаете информацию функции с помощью аргументов, специфицированных
при ее вызове. Эти аргументы должны соответствовать параметрам, появляющимся
в определении функции. Когда функция выполняется, то указанные вами аргументы
заменяют параметры, использованные в ее определении. Код функции выполняется
так, как будто он был написан с применением значений ваших аргументов. На рис. 5.1
показаны отношения между аргументами при вызове функции и параметрами, специ-
фицированными в ее определении.
Аргументы
cout << add_ints( 2,3);
Значение аргументов заменяют соответствующие
параметры в определении функции
Определение
функции
int addjnts( int i, int j)
return i + j;
Параметры
Возвращается значение 5
Puc. 5.1. Отношения между аргументами и параметрами
Структурная организация программ 251
В этом примере функция возвращает сумму двух переданных ей аргументов.
В общем случае функция возвращает либо одно значение в точку программы, откуда
она была вызвана, либо вообще ничего — в зависимости от того, как она определена.
Можно подумать, что возврат единственного значения из функции ограничивает ее
возможности, но это единственное значение может быть указателем, содержащим,
например, адрес массива. Чуть позднее в этой главе вы узнаете подробнее о возврате
данных из функции.
Зачем нужны функции?
Одно из главных преимуществ, предоставляемых функцией, состоит в том, что
она может быть выполнена столько раз, сколько необходимо, в разных точках про-
граммы. Без такой возможности упаковывать блоки кода в функции, программы были
бы намного больше, поскольку тогда пришлось бы повторять один и тот же код вез-
де, где он может понадобиться. Но реальная необходимость в функциях вызвана тем,
чтобы можно было разбивать программу на легко управляемые фрагменты для неза-
висимой разработки и тестирования.
Представьте себе действительно большую программу — скажем, в миллион строк
кода. Программу такого размера практически невозможно написать без функций.
Функции позволяют сегментировать программу так, что ее можно писать по частям, и
тестировать каждую часть независимо, прежде чем соединять ее с прочими частями.
Это также дает возможность распределить работу между членами команды разработ-
чиков, где каждый член команды отвечает за четко определенную часть программы, с
хорошо определенным функциональным интерфейсом для остального кода.
Структура функции
Как вы уже видели, когда писали функции ma i п (), функция состоит из заголовка
функци:
:, который идентифицирует ее, а за ним следует тело функции, заключен-
ное в фигурные скобки и содержащее ее исполняемый код. Рассмотрим пример.
Попробуем написать функцию, которая будет возводить значение в заданную степень,
то есть вычислять результат умножения значения х на себя п раз, что в математике
записывается как хп.
// Функция для вычисления х в степени п, где п больше или равно О
double power(double х, int n) // Заголовок функции
{ // Тело функции начинается здесь...
double result = 1.0; // Здесь сохраняется результат
result *= х;
return result;
} // ...и заканчивается здесь
Заголовок функции
Сначала рассмотрим в этом примере заголовок функции. Следующая строка — пер-
вая строка функции:
double power(double х, int n) // Заголовок функции
Она состоит из трех частей, которые описаны ниже.
□ Тип возвращаемого значения (в данном случае — double).
□ Имя функции (в данном случае — power).
252 Глава 5
□ Параметры функции, заключенные в скобки (в данном случае — х и п, типа do-
uble и int соответственно).
Возвращаемое значение возвращается вызывающей функции, поэтому, когда дан-
ная функция вызывается, то ее результат типа double подставляется в выражение, из
которого она вызвана.
Наша функция имеет два параметра: х — значение типа double, которое нужно
возвести в степень, и п — значение степени типа int. Эти переменные параметры
участвуют в вычислениях, выполняемых функцией, вместе с другой переменной —
result, объявленной в ее теле. Имена параметров и любые переменные, определен-
ные в теле функции являются локальными по отношению к ней.
Обратите внимание, что в конце заголовка функции и после закрывающей фигурной скобки
ее тела точка с запятой не требуется
Общая форма заголовка функции
Общая форма заголовка функции может быть записана следующим образом:
тип_возврата имя_функции (список_параметров)
тип_возврата может быть любым легальным типом. Если функция не возвращает
значения, то тип возврата указывается ключевым словом void. Ключевое слово void
также применяется для обозначения отсутствия параметров, поэтому функция, кото-
рая не имеет параметров и не возвращает значения, должна иметь следующую форму
заголовка:
void my_function(void)
Пустой список параметров также означает, что функция не имеет аргументов, по-
этому вы можете пропустить ключевое слово void между скобками:
void my_function()
Функция с типом возврата void, не должна использоваться в составе выражений в вызы-
вающей программе. Поскольку она не возвращает значения, то, по сути, не может быть
значимой частью выражения, поэтому применение ее в таком виде заставит компилятор
генерировать сообщение об ошибке.
Тело функции
Все необходимые вычисления функции выполняются операторами в ее теле, кото-
рое следует за заголовком. Тело функции из нашего последнего примера начинается
с объявления переменной result, инициализированной значением 1.0. Переменная
result локальна по отношению к функции, как и все автоматические переменные,
объявленные в ее теле. Это значит, что переменная result прекращает существова-
ние после того, как функция завершит работу. Скорее всего, у вас немедленно воз-
никнет вопрос: если переменная result прекращает существование по завершении
работы функции, то как она может быть возвращена? Ответ в том, что при этом авто-
матически делается копия значения, подлежащего возврату, и эта копия возвращает-
ся в программу.
Вычисление производится в цикле for. Управляющая переменная цикла i объяв-
лена в самом цикле, и предполагается, что она последовательно принимает значения
от 1 до п. Переменная result умножается на х при каждой итерации цикла, поэтому
это происходит п раз, чтобы сгенерировать необходимое значение. Если п равно 0,
Структурная организация программ
253
то оператор цикла не будет выполнен ни разу, поскольку условное выражение сразу
возвратит false, и result останется равным 1.0.
Как я сказал, параметры и все переменные, объявленные в теле функции, локаль-
ны по отношению к ней. Ничто не мешает вам использовать те же имена перемен-
ных в других функциях, для других целей. В самом деле, иначе было бы чрезвычайно
трудно обеспечить уникальность имен переменных внутри программы, состоящей из
множества функций, особенно, если эти функции разрабатывает не один человек.
Область видимости переменных, объявленных внутри функции, определяется та-
ким же образом, как уже упоминалось. Переменная создается в точке ее объявления
и прекращает свое существование в конце блока, в котором была объявлена. Однако
существует разновидность переменных, которая составляет исключение из этого пра-
вила— переменные, объявленные как static. Позднее в этой главе мы поговорим о
статических переменных.
Будьте осторожны с маскированием глобальных переменных одноименными ло-
кальными. Вы уже встречались с такой ситуацией в главе 2, где использовали опера-
цию разрешения контекста :: для доступа к глобальным переменным.
Оператор return
Оператор return возвращает значение result в точку вызова функции. Общая
форма оператора return такова:
return выражение;
где выражение должно вычисляться как значение типа, специфицированного в заго-
ловке функции для возврата значения. Выражение может быть любым, какое хоти-
те, до тех пор, пока оно в результате отдает значение требуемого типа. Выражение
может включать вызовы функций — даже вызов той самой функции, в которой оно
появляется, и вы увидите это дальше в настоящей главе.
Если тип возврата функции специфицирован как void, то за оператором return
не должно следовать никакого выражения. Оно должна записываться очень просто:
return;
Использование функций
В точке, где в программе используется функция, компилятор должен знать кое-что
о ней, чтобы скомпилировать ее вызов. Ему нужна достаточная информация, чтобы
идентифицировать функцию, и убедиться, что вы применяете ее корректно. И если
только функция, которую вы намерены использовать, не появилась где-то ранее в том
же исходном файле, вы должны объявить функцию с помощью оператора, который
называется прототипом функци:
Прототипы функций
Прототип функции предоставляет базовую информацию, которую компилятор
должен проверить, чтобы убедиться, что функция используется корректно. Он специ-
фицирует параметры, передаваемые функции, ее имя и тип возвращаемого значения.
По сути, прототип содержит ту же информацию, что содержится в заголовке функ-
ции, с добавлением точки с запятой. Понятно, что количество параметров и их типы
в прототипе функции должны быть такими же, как в ее заголовке.
Прототипы функций, которые вызываются из другой функции, должны появлять-
ся перед операторами, где эти функции вызываются, и потому обычно помещаются
254 Глава 5
в начале исходного файла программы. Заголовочные файлы, которые вы включаете
для использования стандартных библиотечных функций, помимо прочего, включают
в себя прототипы этих библиотечных функций.
Для примера функции power () вы можете написать следующий прототип:
double power(double value, int index);
He забывайте о точке с запятой в конце прототипа функции. Без этого вы получите от
компилятора сообщение об ошибке.
Обратите внимание, что я специфицировал имена параметров в прототипе функ-
ции, отличающиеся от тех, что применялись в заголовке функции при ее определе-
нии. Это просто для того, чтобы показать, что такое возможно. Чаще в прототипах
указываются те же имена, что и в заголовке определения функции, но это не обяза-
тельно должно быть так. Вы можете применять более длинные и выразительные име-
на параметров в прототипе функции, чтобы пояснить их назначение, а потом указать
более короткие имена тех же параметров в определении функции, где длинные име-
на могли бы загромоздить код и сделать его менее читабельным.
При желании вы можете вообще пропустить имена параметров в прототипе функ-
ции и просто написать так:
double power(value, index);
Это предоставляет достаточно информации компилятору, чтобы он выполнил
свою работу; однако лучше использовать некоторые осмысленные имена в прототи-
пе, потому что это повышает читабельность и в некоторых случаях вообще отличает
ясный код от запутанного. Если у вас есть функция с двумя параметрами одного и
того же типа (предположим, к примеру, что у нас index в функции power () также
был бы типа double), то выбор для них осмысленных имен показывает, какой пара-
метр идет первым, а какой вторым.
Практическое занятие | ИСПОЛЬЗОВЭНИв ФУНКЦИИ
В следующем примере применения функции power () вы можете увидеть, как все
это работает вместе.
// Ех5_01.срр
// Объявление, определение и применение функции
#include <iostream>
using std::cout;
using std::endl;
double power(double x, int n) ; // Прототип функции
int main(void)
{
int index = 3; // Возвести в эту степень
double х = 3.0; // Другая переменная х, отличная от используемой в power()
double у = 0.0;
у = power(5.0, 3); // Передача констант в виде аргументов
cout « endl
« "5.0 в кубе = " « у;
cout « endl
« "3.0 в кубе = "
« power(3.0, index); // Вывод возвращенного значения
х = power(х, power(2.0, 2.0)); // Применение функции в качестве аргумента
Структурная организация программ 255
cout « endl
« "х =
cout « endl
return 0;
//с автоматическим приведением 2-го параметра
// Функция для вычисления х в степени п, где п больше или равно О
double power (double х, int n) // Заголовок функции
{ // Тело функции начинается здесь...
double result = 1.0; // Здесь сохраняется результат
result *= х;
return result;
} // . . .и заканчивается здесь
В этой программе демонстрируются некоторые способы использования функции
power () путем передачи ей аргументов различными способами. Если запустить этот
пример, получится следующий вывод:
5.О в кубе = 125
3 . О в кубе = 27
х = 81
Описание полученных результатов
После обычного оператора #include для поддержки ввода-вывода и объявления
using идет прототип функции power (). Если вы удалите его и попытаетесь пере-
компилировать программу, то компилятор не сможет обработать вызовы функции в
main (), а выдаст вместо этого серию сообщений об ошибках:
error С3861: ’power’: identifier not found
ъбка C3861: 'power': идентификатор не найден
и еще одно сообщение:
error С2365: ’power’ : redefinition; previous definition was
’formerly unknown identifier’
ошибка C2365: 'power' : повторное определение; предыдущее определение было
'ранее неизвестный идентификатор'
В отличие от предыдущих примеров, я использовал в этом примере новое ключе-
вое слово void в функции main (), где обычно появляется список параметров, чтобы
показать, что параметры не применяются. Ранее я оставлял скобки, включающие па-
раметры, пустыми, что также интерпретировалось C++ как признак отсутствия пара-
метров; но все же лучше явно указать на этот факт ключевым словом void. Как вы
уже видели, ключевое слово void также может применяться в качестве типа возврата
функции, чтобы показать, что функция не возвращает значения. Если вы специфици-
руете тип возврата как void, то не должны помещать никакого значения рядом с опе-
ратором return внутри функции; в противном случае вы получите от компилятора
сообщение об ошибке.
Думаю, вы сделали вывод из предыдущих примеров, что применять функции
очень просто. Чтобы использовать функцию power () для вычисления 5,03 и сохра-
нить результат в переменной у в нашем примере используется следующий оператор:
у = power(5.0, 3);
Здесь значения 5.0 и 3 — аргументы функции. В данном случае они являются кон-
стантами, но вы можете использовать любые выражения в качестве аргументов, если
256 Глава 5
они дают результат требуемого типа. Аргументы функции power () подставляются
вместо параметров х и п, которые используются в определении функции. Вычисление
производится с применением этих значений, а копия результата, 125, возвращается
вызывающей функции main (), где присваивается переменной у. Вы можете думать
о функции как о значении в операторе или выражении, где она появляется. Затем в
примере значение переменной у выводится на экран:
cout « endl
« ”5.0 в кубе =
Далее вызов функции применяется прямо в составе оператора вывода:
cout « endl
« "3.0 в кубе = "
« power(3.0, index); // Вывод возвращенного значения
Здесь значение, возвращаемое функцией, передается непосредственно в выход-
ной поток. Поскольку вы нигде не сохраняете это значение, оно никаким другим
способом вам недоступно. Первый аргумент в этом вызове функции — константа, а
второй — переменная.
После этого функция power () используется еще раз в операторе:
х = power(х, power(2.0, 2.0)); // Использование функции, как аргумента
Здесь функция power () вызывается дважды. Первый вызов — правый в выраже-
нии, и его результат служит вторым аргументом для второго, левого вызова. Хотя оба
аргумента в подвыражении power (2.0, 2.0) являются литералами типа double, на
самом деле функция вызывается с первым аргументом 2.0 и вторым — целочислен-
ным литералом 2. Компилятор преобразует значение double, приведенное в качестве
второго аргумента, к типу int, поскольку знает на основании прототипа, что типом
второго аргумента должен быть int:
double power(double x, int n) ; // Прототип функции
Результат 4.0 типа double возвращается первым вызовом функции power () и по-
сле преобразования к типу int значение 4 передается в качестве второго аргумента
следующему вызову функции, где первый аргумент — х. Поскольку х имеет значение
3.0, значение вычисляется 3,04 и результат, равный 81.0, присваивается х. Эта по-
следовательность событий проиллюстрирована на рис. 5.2.
Этот оператор заключает в себе два неявных преобразования типа double в тип
int, вставку которых обеспечивает компилятор. При таком преобразовании данных
возможна потеря данных, поэтому компилятор издает предупреждающие сообщения,
когда такое случается, хотя он сам и вставил это преобразование. Обычно полагаться
на автоматическое преобразование типов, потенциально чреватое потерей данных —
опасная практика в программировании, и совсем не очевидно вытекает из кода, что
такое преобразование было намеренным. Гораздо лучше, когда необходимо, явно ука-
зывать в коде преобразование типа, используя для этого операцию static_cast. То
есть последний оператор в этом примере лучше переписать так:
х = power(х, static_cast<int>(power(2.О, 2)));
Такое кодирование оператора позволяет избежать обоих предупреждений компи-
лятора, которые вызывает исходная версия. Применение статического приведения
не исключает возможности потери данных при преобразовании одного типа в дру-
гой. Но поскольку оно указано явно, то компилятору ясно, что это входит в ваши на-
мерения.
Структурная организация программ 257
х = power( х, power( 2.0,2.0));
начальное значение ® Преобразовано
3.0 к типу int
(§) результат сохранен ▼
обратно в х @ power( 2.0,2)
4.0 (type double)
Преобразовано
к типу int
@ power( 3.0,4)
81.0 (типа double)
Рис. 5.2. Результат вызова функции power ()
Передача аргументов в функцию
Очень важно понимать, как аргументы передаются в функцию, потому что это
влияет на то, как вы их пишете, и то, как они в конечном итоге работают. Здесь есть
множество ловушек, которых следует избегать, поэтому рассмотрим этот механизм
поближе.
Аргументы, которые вы специфицируете при вызове функции, обычно должны
соответствовать по типу и последовательности параметрам, заданным в ее опреде-
лении. Как вы видели из последнего примера, если тип аргумента, указанного при
вызове функции, не соответствует типу параметра, указанному в ее определении, то
там, где возможно, происходит преобразование к требуемому типу в соответствии
с правилами приведения операндов, описанными в главе 2. Если такое приведение
невозможно, вы получаете от компилятора сообщение об ошибке. Однако даже если
приведение возможно и код компилируется, это может привести к потере данных
(например, когда тип long приводится к short), а потому его следует избегать.
Существуют два механизма, обычно используемые C++ для передачи аргументов в
функции. Первый применяется, когда параметры в определении функции специфи-
цированы как обычные переменные (не ссылки). Это называется методом передачи
по значению (pass-by-value) данных в функцию, и его мы рассмотрим первым.
Механизм передачи по значению
Когда применяется такой механизм, то переменные или константы, которые вы
специфицируете в качестве аргументов, вообще не передаются в функцию. Вместо
этого создаются копии аргументов, и эти копии используются в качестве передавае-
мых значений. На рис. 5.3 показана диаграмма применительно к функции power ().
258 Глава 5
int index = 2;
double value = 10.0;
double result = power(value, index);
double power ( double x , int n )
{
Исходные аргументы не доступны
здесь, а только копии
}
Рис, 5,3, Передача по значению
Каждый раз, когда вызывается функция power (), компилятор создает копии пере-
данных аргументов и размещает их во временном участке памяти. Во время выпол-
нения функции все ссылки на ее параметры отображаются на эти временные копии
аргументов.
Практическое занятие
Передача по значению
Одно из последствий механизма передачи по значению состоит в том, что функ-
ция не может напряк^ю модифицировать переданные ей аргументы. В этом можно
убедиться на следующем примере:
// Ех5_02.срр
// Бесполезная попытка модификации аргументов вызывающего кода
#include <iostream>
using std::cout;
using std::endl;
int incrlO(int num); // Прототип функции
int main(void)
{
int num = 3;
cout « endl
« "incrlO(пищ) = ” « incrlO(num)
« endl
Структурная организация программ 259
« "num = " « num;
cout « endl;
return 0;
}
// Функция для увеличения переменной на 10
int incrlO(int num) // Применение такого имени может помочь...
{
num +=10; // Попытка инкрементировать аргумент вызова
return num; // Возврат измененного значения
}
Конечно, эта программа обречена на неудачу. Если вы запустите ее, то получите
следующий вывод:
incrlO(num) = 13
num = 3
Описание полученных результатов
Вывод подтверждает, что исходное значение num остается незатронутым. Инкре-
менту подвергается копия num, которая была создана и затем отброшена при выходе
из функции.
Понятно, что механизм передачи по значению обеспечивает высокую степень
защиты аргументов вызова от повреждения недобросовестной функцией, но все же
может случиться, что вы действительно хотите, чтобы переданные аргументы были
модифицированы функцией. Конечно, существует способ сделать это. Вспомните, на-
сколько удобными могут оказаться указатели.
Указатели как аргументы функций
Когда вы используете указатели в качестве аргументов, механизм передачи по зна-
чению работает, как и прежде; однако указатель — это адрес другой переменной, и
если вы возьмете копию этого адреса, то она будет по-прежнему указывать на ту же
переменную. Таким образом, применение указателя в качестве параметра позволяет
вашей функции получить сам аргумент, а не его копию.
практическое занятие | передача по указателю
Для демонстрации эффекта можно изменить последний пример, чтобы он исполь-
зовал указатель:
// Ех5_03.срр
// Успешная попытка модифицировать аргумент
#include <iostream>
using std::cout;
using std::endl;
int incrlO (int* num) ; // Прототип функции
int main (void)
{
int num = 3;
int* pnum = # // Указатель на num
cout « endl
« "Передан адрес = " « pnum;
260 Глава 5
cout « endl
« "incrlO (pnum) = ” « incrlO (pnum) ;
cout « endl
« "num = " « num;
cout « endl;
return 0;
// Функция для увеличения переменной на 10
int incrlO (int* num) // Функция с аргументом-указателем
cout « endl
« "Получен адрес = ” « num;
*num +=10; / / Инкремент аргумента
return *num; / / Возврат увеличенного значения
Ниже показан вывод этого примера.
Передан адрес = 0012FF6C
Получен адрес = 0012FF6C
incrlO(pnum) = 13
num =13
Точное значение адреса на вашем компьютере может отличаться от показанного
здесь, но два отображенных значения адреса должны быть идентичными.
Описание полученных результатов
Принципиальное отличие этого примера от предыдущей версии заключается в*
передачи указателя pnum вместо исходной переменной num. Прототип функции те-
перь специфицирует тип параметра как указатель на int, а в функции main () объяв-
лен указатель pnum, инициализированный адресом num. Функция main () и функция
incrlO () выводят соответственно отправленный и принятый адрес, чтобы показать,
что в обоих местах используется один и тот же адрес.
Выход программы показывает, что на этот раз значение num было изменено, и
имеет значение, идентичное тому, что возвратила функция.
В измененной версии функции incrlO () теперь и оператор, выполняющий ин-
кремент переданного значения, и оператор return выполняют разыменование указа-
теля, чтобы обратиться к значению, хранящемуся в нем.
Передача массивов в функцию
В функцию можно передавать массивы, но в этом случае массив не копируется,
даже несмотря на то, что при этом по-прежнему применяется передача аргумента
по значению. Имя массива преобразуется в указатель, и копия указателя на начало
массива передается в функцию по значению. Это достаточно выгодно, потому что
копирование больших массивов требует значительных затрат времени. Однако, как
вы можете обнаружить в этом случае, элементы массива могут быть изменены внутри
функции, и потому массив — единственный тип, который не может быть передан по
значению.
Структурная организация программ 261
Практическое занятие | Передача МаССИВОВ
Плюсы и минусы этого можно проиллюстрировать на примере функции, вычисля-
ющей среднюю величину значений, переданных в массиве.
// Ех5_04.срр
// Передача массива в функцию
#include <iostream>
using std::cout;
using std::endl;
double average(double array[], int count); // Прототип функции
int main(void)
{
double values [] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 };
cout « endl
« "Среднее = "
« average(values, (sizeof values)/(sizeof values[0]));
cout « endl;
return 0;
}
// Функция для вычисления среднего
double average(double array[], int count)
{
double sum =0.0; // Здесь накапливается сумма
for (int i = 0; i < count; i++)
sum += array[i]; // Суммировать элементы массива
return sum/count; // Вернуть среднее
}
Программа генерирует следующий вывод:
Среднее =5.5
Описание полученных результатов
Функция average () предназначена для работы с массивами любой длины. Как
видно из ее прототипа, она принимает два аргумента: массив и счетчик количества
элементов. Поскольку мы хотим, чтобы она работала с массивами произвольной дли-
ны, параметр массива указан без спецификации измерения.
Функция вызывается в main () следующим оператором:
cout « endl
« "Среднее = "
« average(values, (sizeof values)/(sizeof values[0]));
Первым аргументом передается имя массива, values, а вторым — выражение, ко-
торое вычисляется, как элементов массива.
Вы должны вспомнить это выражение, использующее операцию sizeof— оно уже появля-
лось в главе 4, когда речь шла о массивах.
Внутри тела функции вычисление выполняется так, как можно было ожидать.
Нет особого отличия от того, как оно могло бы быть реализовано непосредственно в
main ().
Вывод программы подтверждает, что все работает, как надо.
262 Глава 5
Практическое занятие
Использование нотации указателей при
передаче массивов
Но этим мы не исчерпали все возможности. Как было сказано, имя массива пе-
редается в виде указателя (точнее говоря, в виде копии указателя), поэтому внутри
функции вы вообще не обязаны работать с данными как с массивом. Можно так моди-
фицировать функцию в примере, чтобы повсюду использовать нотацию указателей,
несмотря на тот факт, что речь идет о массиве.
// Ех5_05.срр
// Сокрытие массива в функции указателем
#include <iostream>
using std::cout;
using std::endl;
double average(double* array, int count); // Прототип функции
int main (void)
{
double valuesf] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 };
cout « endl
« "Среднее = "
« average (values, (sizeof values) I (sizeof values [0])) ;
cout « endl;
return 0;
}
// Функция для вычисления среднего
double average(double* array, int count)
{
double sum =0.0; // Здесь накапливается сумма
for(int i = 0; i < count; i++)
sum += *array++; // Суммировать элементы массива
return sum/count; // Вернуть среднее
}
Вывод программы выглядит точно так же, как в предыдущем примере.
Описание полученных результатов
Как видите, для того, чтобы программа работала с массивом как с указателем, в нее
требуется внести совсем немного изменений. Изменяется прототип и заголовок функ-
ции, хотя эти изменения не являются абсолютно необходимыми. Если вернуть их к
исходному виду, где первый параметр специфицирован как массив double, и оставить
тело функции в терминах указателя, она будет работать так же хорошо. Самый инте-
ресный аспект этой версии тела функции, кроется в операторе цикла for:
sum += *array++; // Суммировать элементы массива
Можно подумать, что здесь нарушается правило о том, что нельзя модифициро-
вать адрес, специфицированный как имя массива, поскольку увеличивается адрес,
хранящийся в array. Но на самом деле это правило вовсе не нарушается. Вспомните,
что механизм передачи по значению создает копию исходного адреса массива и пере-
дает его, а потому в теле функции модифицируется лишь эта копия (исходный адрес
массива остается неизменным). В результате всякий раз, когда вы передаете одномер-
ный массив в функцию, то вольны трактовать переданное значение в любом смысле
как указатель, а также произвольным образом изменять адрес, хранящийся в нем.
Структурная организация программ 263
Передача в функцию многомерных массивов
Передача в функцию многомерного массива достаточно проста. Следующий опе-
ратор объявляет двумерный массив beans:
double beans[2][4];
Вы можете написать прототип гипотетической функции yield () примерно так:
double yield(double beans [2] [4]);
Вас может удивить — как компилятор может знать, что это определение массива с раз-
мерностью, указанной в квадратных скобках, а не отдельного элемента массива? Ответ
прост — вы не можете написать отдельный элемент массива в качестве параметра в опреде-
лении или прототипе функции, хотя можете передать его в качестве аргумента при ее вы-
зове. Параметр, принимающий отдельный элемент массива в качестве аргумента, должен
быть объявлен как отдельная переменная. Контекст массива здесь не применим.
При определении многомерного массива в качестве параметра вы можете также
пропустить первое измерение массива. Конечно, функция должна как-то узнать о раз-
мере первого измерения. Например, вы можете написать так:
double yield(double beans[] [4], int index);
Здесь второй параметр может передать необходимую информацию относительно
величины первого измерения. В этом случае функция может оперировать двумерным
массивом со значением первого измерения, указанным вторым аргументом функции,
и фиксированным значением второго измерения, равным 4.
практическое занятие | передача многомерных массивов
Использование подобной функции показано в следующем примере.
// Ех5_06.срр
// Передача в функцию двумерного массива
#include <iostream>
using std::cout;
using std::endl;
double yield(double array[] [4], int n) ;
int main(void)
{
double beans[3][4] = { { 1.0, 2.0, 3.0, 4.0 },
{ 5.0, 6.0, 7.0, 8.0 },
{ 9.0, 10.0, 11.0, 12.0 } };
cout « endl
« "Урожай = " « yield(beans, sizeof beans/sizeof beans[0]);
cout « endl;
return 0;
}
// Функция для вычисления всего объема урожая
double yield(double beans [] [4], int count)
{
double sum = 0.0;
for (int i = 0; i < count; i++) // Цикл по строкам
for (int j = 0; j < 4; j++) // Цикл по элементам строки
sum += beans [ i ] [ j ] ;
return sum;
}
264 Глава 5
Вывод этого примера выглядит следующим образом:
Урожай =78
Описание полученных результатов
Я вновь использовал разные имена параметров в прототипе и заголовке функции,
просто чтобы напомнить, что это возможно (правда, в данном случае это ничем не
улучшает программу). Первый параметр определен как массив с произвольным коли-
чеством строк, каждая из которых содержит четыре элемента. Функция вызывается
с массивом beans, состоящим из трех строк. Второй аргумент специфицирован как
частное от деления общего размера массива в байтах на размер его первой строки.
В результате вычисления это дает количество строк в массиве.
Вся вычислительная работа функции выполняется во вложенных циклах for, где
внутренний цикл суммирует элементы отдельной строки, а внешний цикл повторяет-
ся для каждой строки.
Использование указателя вместо многомерного массива в качестве аргумента
функции в этом примере не очень подходит. Когда передается массив, то на самом
деле передается значение адреса, которое в данном случае указывает на первую стро-
ку — массив из четырех элементов. Это само по себе не обеспечивает простых опера-
ций с указателем внутри функции. В этом случае придется модифицировать оператор
во вложенном цикле for следующим образом:
sum += * (* (beans + i) + j) ;
поэтому вычисления в нотации массива все-таки выглядят яснее.
Ссылки как аргументы функции
Теперь перейдем ко второму из двух механизмов передачи аргументов в функцию.
Спецификация параметра функции в виде ссылки изменяет метод передачи параме-
тра. Этот метод не передает аргумент по значению, когда он копируется перед тем,
как быть переданным в функцию, а передается по ссылке — то есть параметр выступа-
ет псевдонимом передаваемого аргумента. Это исключает всякое копирование и обе-
спечивает функции прямой доступ к аргументу. Это также означает, что нет необходи-
мости в разыменовании, которое необходимо при передаче указателей на значения.
Практическое занятие | Передача п0 ССыЛКе
Вернемся к очень простому примеру Ех5_03. срр и попробуем переписать его так,
чтобы использовать параметры-ссылки:
// Ех5_07.срр
// Использование ссылки для модификации аргумента
#include <iostream>
using std::cout;
using std::endl;
int incrlO(int& num); / / Прототип функции
int main (void)
{
int num = 3;
int value = 6;
cout « endl
« "incrlO (num) = " « incrlO (num) ;
Структурная организация программ 265
cout « endl
« "num = " « num;
cout « endl
« "incrlO (value) = " « incr 10 (value) ;
cout « endl
« "value = " « value;
cout « endl;
return 0;
// Функция для увеличения переменной на 10
int incrlO (int& num) // Функция с аргументом - ссылкой
cout « endl
num +=10; // Увеличить значение аргумента
return num; // Вернуть увеличенное значения
Эта программы генерирует следующий вывод:
Получено значение = 3
incrlO(num) = 13
num =13
Получено значение = 6
incrlO(value) = 16
value = 16
Описание полученных результа тов
Вы должны согласиться с тем, что способ работы этого примера замечателен. По
сути, это то же самое, что и Ех5_03. срр, за исключением того, что функция в каче-
стве параметра использует ссылку. Этот факт отражает измененный прототип. Когда
функция вызывается, аргумент передается ей точно так же, как и в случае передачи
по значению, поэтому он используется точно так же, как и в предыдущей версии.
Значение аргумента функции не передается. Параметр функции инициализируется
адресом аргумента, поэтому всякий раз, когда параметр num используется в функции,
он непосредственно обращается к переданному аргументу.
Дабы исключить неправильное толкование использования аргумента num в
main () — одноименного с тем, что используется в функции — второй раз она вызыва-
ется с переменной value в качестве аргумента. На первый взгляд это может создать у
вас впечатление некоторого противоречия с тем, что я говорил раньше — о том, что
основное свойство ссылки таково, что после ее объявления и инициализации она не
может быть переназначена на другую переменную. Но здесь никакого противоречия
нет, потому что ссылка, как параметр функции, создается и инициализируется при
каждом вызове функции и разрушается по ее завершении, поэтому каждый раз вы по-
лучаете совершенно новую ссылку.
Внутри функции полученное ею значение отображается на экране. Хотя оператор,
по сути, тот же самый, что и использованный ранее для вывода адреса, сохраненного в
указателе, на этот раз выводятся значение num, а не адрес, потому что num — ссылка.
Это наглядно демонстрирует разницу между ссылкой и указателем. Ссылка — псевдоним дру-
гой переменной, а потому может использоваться в качестве ее альтернативного имени. Это
полностью эквивалентно применению ее исходного имени.
266 Глава 5
Вывод доказывает, что функция incrlO () непосредственно модифицирует пере-
менную, переданную ей в аргументе.
Но если вы попытаетесь использовать числовое значение, такое как 20, в качестве
аргумента incrlO (), то компилятор выдаст сообщение об ошибке. Дело в том, что
компилятор обнаруживает, что параметр-ссылка может быть модифицирован внутри
функции, но изменять значения констант нельзя. Это может внести в программы не-
которое излишнее волнение, без которого имеет смысл обойтись.
Эта защита хороша, но если функция не собирается модифицировать переданное
ей по ссылке значение, то вряд ли вы захотите, чтобы компилятор каждый раз выда-
вал сообщения об ошибках, когда вы передаете константу. Должен же быть какой-то
способ добиться этого? Разумеется, способ есть.
Использование модификатора const
Чтобы сообщить компилятору, что вы не собираетесь никоим образом модифици-
ровать параметр функции, вы можете применить модификатор const. Это заставит
компилятор проверить код функции на предмет того, действительно ли он не изме-
няет значения аргумента, и если нет, то никаких сообщений об ошибках не выдается
при использовании константного аргумента.
Практическое занятие | ПврвДаЧа ПЭрЭМеТрЭ Const
Чтобы увидеть, как модификатор const изменяет ситуацию, модифицируем пред-
ыдущую программу.
// Ех5_08.срр
// Использование константной ссылки
#include <iostream>
using std::cout;
using std::endl;
int incrlO (const int& num) ; // Прототип функции
int main(void)
{
const int num = 3; // Объявлено константой, чтобы протестировать
// временное создание
int value =6;
cout « endl
« "incrlO(num) = " « incrlO (num);
cout « endl
« "num = " « num;
cout « endl
« "incrlO(value) = " « incrlO(value);
cout « endl
« "value = " « value;
cout « endl;
return 0;
}
// Функция для увеличения переменной на 10
int incrlO (const int& num) // Функция с константным аргументом-ссылкой
{
cout « endl
« "Получено значение = " « num;
Структурная организация программ 267
4
// num +=10; // теперь этот оператор является незаконным
return num+10; // Возвратить увеличенное значение
Ниже показан вывод этой программы.
Получено значение = 3
incrlO (num) = 13
num = 3
Получено значение = 6
incrlO(value) = 16
value = 6
Описание полученных результатов
Вы объявляете переменную num в main () как const, чтобы показать, что когда
параметр функции incrlO () объявлен как const, то компилятор более не выдает со-
общений об ошибке при передаче константного объекта.
Необходимо также закомментировать оператор, который увеличивает значение
num в функции incrlO (). Если вы уберете комментарий с этой строки, программа
перестанет компилироваться, потому что компилятор не допустит появления num в
левой части присваивания. Когда вы специфицируете num как const в заголовке и в
прототипе функции, это значит, что вы обязуетесь не модифицировать его, и компи-
лятор проверяет, как вы держите слово.
Все работает как прежде, за исключением того, что переменные из main () более
не изменяются в функции.
Используя аргументы-ссылки, вы берете лучшее из двух миров. С одной стороны,
вы можете написать функцию, которая получает прямой доступ к исходному аргумен-
ту вызывающего кода, позволив избежать копирования, которое подразумевает меха-
низм передачи по значению. С другой стороны, когда вы не намерены модифициро-
вать аргумент, вы получаете полную защиту от непреднамеренной его модификации,
используя модификатор const со ссылкой.
Аргументы main ()
Функцию main () можно объявлять без аргументов (или лучше со списком пара-
метров void), либо же специфицировать список параметров, которые позволяют
этой функции получать аргументы из командной строки. Значения, передаваемые в
качестве аргументов main (), всегда интерпретируются как строки. Если вы хотите в
main () получить данные из командной строки, то должны определить ее следующим
образом:
int main(int argc, char* argv[])
// Код main () . . .
Первый параметр — счетчик строк, переданных программе в командной строке,
включая имя самой программы, а второй параметр — массив указателей на эти строки
плюс один дополнительный нулевой указатель. Таким образом, значение argc всегда
не меньше 1, поскольку при запуске программы всегда, как минимум, указывается ее
имя. Количество полученных аргументов зависит от того, что вы вводите в команд-
ной строке для запуска программы. Например, предположим, что вы выполняете
программу DoThat с помощью следующей команды:
DoThat.ехе
268 Глава 5
Это просто имя исполняемого файла .ехе программы, поэтому argc равно 1, и
массив argv содержит два элемента — argv [ 0 ], указывающий на строку "DoThat. ехе",
и argv [ 1 ], содержащий null.
Предположим, вы вводите в командной строке следующее:
DoThat or else ’’my friend" 999.9
Теперь argc равно 5, a argv содержит шесть элементов, из которых последний
равен 0, а остальные пять указывают на строки:
"DoThat" "or" "else" "my friend" "999.9"
Отсюда видно, что если вы хотите передать как одно целое строку, включающую
пробелы, то должны заключить ее в двойные кавычки. Кроме того, вы можете ви-
деть, что числовые значения читаются как строки, поэтому если вам нужно преобра-
зовать их в числа, то вам придется это делать самостоятельно.
Практическое занятие | Прием аргуМенТОВ КОМЭНДНОЙ СТРОКИ
Эта программа просто перечисляет переданные ей в командной строке аргументы.
// Ех5_09.срр
// Чтение аргументов командной строки
#include <iostream>
using std::cout;
using std::endl;
int main(int argc, char* argv[])
{
cout << endl « "argc = " « argc « endl;
cout « "Приняты следующие аргументы командной строки:" « endl;
for (int i = 0 ; i <argc ; i++)
cout << "аргумент " « (i+1) « ": " « argvfi] « endl;
return 0;
}
У вас есть два варианта ввода аргументов командной строки. После того, как при-
мер программы собран в IDE, можно открыть командное окно в папке, содержащей
файл .ехе, и затем ввести имя программы с последующими аргументами командной
строки. Альтернативно вы можете специфицировать аргументы командной строки в
IDE, прежде чем запускать программу. Для этого просто откройте окно свойств проек-
та, выбрав Projects Properties (ПроектФСвойства) в главном меню, и затем разверни-
те дерево Configuration Properties (Свойства конфигурации) в левой панели, щелкнув
на значке “плюс” (+). Затем щелкните на папке Debugging (Отладка), чтобы увидеть
на правой панели место, куда можно ввести аргументы командной строки.
Вот типичный пример запуска этого примера в командном окне:
C:\Visual C++ 2005\Examples\Ex5_09 trying multiple "argument values" 4.5 0.0
argc = 6
Приняты следующие аргументы командной строки:
аргумент 1: Ех5_09
аргумент 2: trying
аргумент 3: multiple
аргумент 4: argument values
аргумент 5: 4.5
аргумент 6: 0.0
Структурная организация программ 269
Описание полученных результатов
Во-первых, программа выводит значение argc, а затем значения каждого аргумен-
та из массива argv в цикле for. Можно воспользоваться тем фактом, что последний
элемент argv равен null, тогда код вывода значений аргументов будет таким:
int i = 0;
while(argv[i] != 0)
cout << "аргумент " << (i+1) « ": " « argv[i++] « endl;
Выполнение цикла завершится по достижении элемента массива argv [argc], ко-
торый равен null.
Прием функцией переменного количества аргументов
Можно определить функцию так, что она сможет принимать любое количество
переданных ей аргументов. Переменное число аргументов функции задается в ее
определении посредством многоточия в конце списка параметров, например:
int sumValues (int first,...)
{
// Код функции
}
При этом должен быть указан как минимум один обычный параметр, но можно и
больше. Многоточия всегда помещаются в конце списка параметров.
Очевидно, что в переменном списке параметров нет информации о числе и типах
аргументов, поэтому ваш код должен каким-то образом определять, что ему передано
при вызове функции. Стандартная библиотека “родного” C++ определяет в заголовоч-
ном файле stdarg.h макросы va start, va arg и va end, которые могут в этом по-
мочь. Легче всего продемонстрировать их применение на примере.
практическое занятие | прием переменного списка аргументов
Эта программа использует функцию, которая суммирует значения переданных ей
аргументов.
// Ех5_10.срр
// Обработка переменного количества аргументов
#include <iostream>
#include "stdarg.h"
using std::cout;
using std::endl;
int sum(int count, ...)
{
if(count <= 0)
return 0;
va_list arg_ptr; //Объявление указателя на список аргументов
va_start(arg_ptr, count);//Установить arg_ptr на 1-й необязательный аргумент
int sum =0;
for (int i = 0 ; iccount ; i++)
sum += va_arg(arg_ptr, int); // Прибавить значение int из arg_ptr
va_end(arg_ptr); // Сбросить указатель в null
return sum;
}
270 Глава 5
int main(int argc, char* argv[])
cout « sum(2, 4, 6, 8, 10, 12) « endl;
cout « sum(ll, 22, 33, 44, 55, 66, 77, 66, 99) « endl;
Этот пример сгенерирует следующий вывод:
10
172630728
Press any key to continue . . .
Описание полученных результатов
Функция ma i n () вызывает функцию s um () в двух операторах вывода, в первом
случае с шестью аргументами, а во втором — с девятью.
Функция sum () принимает один нормальный параметр типа int, который пред-
ставляет счетчик числа аргументов, следующих за ним. Многоточие в списке параме-
тров функции указывает на то, что ей может быть передано произвольное количество
аргументов. В основном, у вас есть два способа определения того, сколько аргументов
было передано при вызове функции — можно потребовать явной передачи числа ар-
гументов фиксированным параметром, как это сделано в случае функции sum (), или
же можно передавать в последнем аргументе специальный маркер, который можно
проверить и распознать.
Для начала обработки переменного списка аргументов объявляется указатель типа
va_list:
va_list arg_ptr; // Объявление указателя на список аргументов
Тип va list определен в заголовочном файле stdarg.h, и этот указатель исполь-
зуется для указания на каждый аргумент в списке по порядку.
Макрос va_start служит для инициализации arg ptr, так что он указывает на
первый аргумент в списке:
va_start(arg_ptr, count); // Установить arg_ptr на 1-й необязательный аргумент
Второй аргумент этого макроса — имя фиксированного параметра, который пред-
шествует многоточию в списке параметров; он используется макросом для определе-
ния того, где находится первый аргумент из переменного списка.
Значения аргументов из списка извлекаются в цикле for:
int sum =0;
for (int i = 0 ; i<count ; i++)
sum += va_arg(arg_ptr, int); // Прибавить значение int из arg_ptr
Макрос va_arg возвращает значение аргумента, находящегося по адресу arg_ptr, и
увеличивает его с тем, чтобы он указывал на следующее по порядку значение. Второй
аргумент макроса va arg — тип аргумента, и он определяет интерпретацию значе-
ния, которое вы получаете, а также размер инкремента arg ptr, потому что если он
будет неправильным, вы получите хаос; программа в этом случае, возможно, будет вы-
полняться, но полученные значения окажутся мусором, и arg ptr, увеличенный не-
правильно, только породит еще больше мусора.
Когда завершается извлечение значений аргументов, с помощью следующего опе-
ратора arg_ptr сбрасывается:
va__end (arg_ptr); // Сбросить указатель в null
Структурная организация программ 271
Макрос va end устанавливает указатель типа va_list, который передается ему в
качестве аргумента, равным null. Это нужно делать всегда, потому что после обра-
ботки arg_ptr указывает на адрес, который не содержит корректных данных.
Возврат значений функциями
Все примеры функций, который вы создавали, возвращали единственное значе-
ние. Можно ли вернуть что-то другое, отличное от единственного значения? Вообще-
то нет, но как я уже говорил, возвращаемое единственное значение не обязательно
должно быть числовым; оно может быть адресом, что является ключом к возврату лю-
бого объема данных. Вы просто используете указатель. К сожалению, здесь начинают-
ся ловушки, поэтому вам нужно быть заранее готовым к некоторым “приключениям”.
Возврат указателя
Вернуть значение указателя легко. Значение указателя — это просто адрес, поэто-
му если вы хотите вернуть адрес некоторой переменной value, то можете просто за-
писать так:
return &value; // Возвращение адреса
До тех пор, пока заголовок функции и ее прототип правильно и согласованно
указывают тип возврата, у вас нет никаких проблем — по крайней мере, никаких ви-
димых проблем. Предполагая, что переменная value имеет тип double, прототип
функции по имени treble, которая может содержать приведенный выше оператор
return, должен быть таким:
double* treble(double data);
Список параметров я указал произвольным образом.
Давайте рассмотрим функцию, возвращающую указатель. Заранее должен предупре-
дить — эта функция работать не будет, но она нам понадобиться в качестве наглядного
пособия. Предположим, что вам нужна функция, которая возвращает указатель на ме-
стоположение в памяти, где находится значение ее аргумента, умноженное на 3. Первая
попытка реализовать такую функцию могла бы выглядеть следующим образом:
// Функция для утроения значений - вариант 1
double* treble(double data)
{
double result = 0.0;
result = 3.0*data;
return &result;
}
Практическое занятие | Возврат ПЛОХОГО уКаЗЭТвЛЯ
Для того чтобы посмотреть, что получится, можно написать небольшую тестовую
программу (напомню, что функция treble не будет работать, как ожидается).
//Ех5_11.срр
#include <iostream>
using std::cout;
using std::endl;
double* treble(double); // Прототип функции
272 Глава 5
int main(void)
double num =5.0; // Тестовое значение
double* ptr =0; // Указатель на возвращенное значение
ptr = treble(num);
cout « endl
« "Утроенное num = " « 3.0*num;
cout « endl
« "Результат = " « *ptr; // Отобразить 3*num
cout « endl;
return 0;
// Функция для утроения значений - вариант 1
double* treble(double data)
double result = 0.0;
result = 3.0*data;
return &result;
При компиляции сразу возникает подсказка, что все не так, как хотелось бы — в
виде предупреждающего сообщения компилятора:
warning С4172: returning address of local variable or temporary
предупреждение C4172: возврат адреса локальной или временной переменной
Вывод, который я получил при запуске этой программы, был таким:
Утроенное num = 15
Результат = 4.10416е-230
Описание полученных результатов (или почему это не работает)
Функция main () вызывает treble () и сохраняет возвращенный адрес в указателе
ptr, который по идее должен указывать на утроенное значение аргумента num. Затем
отображается результат простого умножения num на три, за которым следует значе-
ние адреса, возвращенного функцией.
Ясно, что вторая строка вывода не показывает корректного значения — 15, но в
чем же ошибка? Вообще-то это не секрет, потому что компилятор ясно предупредил
о проблеме. Ошибка возникает потому, что переменная result в функции treble ()
создается, когда функция начинает выполнение, а уничтожается при выходе из функ-
ции. Поэтому память, на которую указывает указатель, уже не содержит исходного
корректного значения. Память, ранее выделенная result, становится доступной для
других целей, и здесь она как раз использована для чего-то другого.
Железное правило возврата адресов
Существует абсолютное железное правило относительно возвращаемых адресов:
Никогда не возвращать из функции адрес локальной автоматической переменной.
Очевидно, что нельзя применять функцию, которая не работает, но как же это ис-
править? Можно использовать ссылочный параметр и модифицировать в функции
его исходное значение, но это не совсем то, что нужно. Вы пытаетесь вернуть указа-
тель на нечто полезное, потому что в конечном итоге, вам может понадобиться воз-
вращать нечто более сложное, чем отдельный элемент данных. Ответ заключается в
динамическом выделении памяти (вы видели это в действии в предыдущей главе).
Структурная организация программ
273
С помощью операции new вы создаете новую переменную в свободном хранили-
ще, которая продолжает существовать до тех пор, пока не будет уничтожена операци-
ей delete или пока не завершится программа. С таким подходом функция выглядит
так:
// Функция для утроения значении - вариант 2
double* treble(double data)
double* result = new double(0.0);
*result = 3.0*data;
return result;
Вместо объявления result типа double теперь эта переменная объявлена с ти-
пом double* и ей присваивается адрес, возвращенный операцией new. Поскольку
результат — указатель, остальная часть функции изменена соответствующим образом,
и адрес, записанный в result, в конечном итоге возвращается вызывающей програм-
ме. Можете проверить эту версию, заменив ею функцию из предыдущего примера.
Необходимо помнить, что при динамическом распределении памяти в функции
“родного” C++, вроде этой, при каждом вызове выделяется новый фрагмент памяти.
Ответственность за освобождение выделенной памяти, когда она более не нужна, ло-
жится на программу, которая вызывает эту функцию. На практике очень легко забыть
об этом, в результате чего функция будет последовательно “отгрызать” куски памяти
от свободного хранилища до тех пор, пока в определенный момент не израсходует
ее всю, и программа потерпит крах. Как уже упоминалось, проблема подобного рода
известна как утечка памяти.
Ниже приведен пример использования новой версии функции. Единственное не-
обходимое отличие от исходного кода — применение delete для освобождения памя-
ти, возвращенной функцией treble ().
#include <iostream>
using std::cout;
using std::endl;
double* treble(double); // Прототип функции
int main(void)
double num =5.0; // Тестовое значение
double* ptr =0; // Указатель на возвращенное значение
ptr = treble(num);
cout « endl
« "Утроенное num = " « 3.0*num;
cout « endl
« "Результат = " « *ptr; // Отобразить 3*num
delete ptr; //He забудьте освободить память
cout « endl;
return 0;
}
// Функция для утроения значении - вариант 2
double* treble (double data)
{
double* result = new double(0.0) ;
♦result = 3.0*data;
return result;
274 Глава 5
Возврат ссылки
Функция может возвращать ссылку. Это также чревато потенциальными ошиб-
ками, как и в случае возврата указателя, поэтому здесь вы также должны быть осто-
рожны. Поскольку ссылка не является какой-то отдельной сущностью (это всегда
псевдоним чего-то другого), вы должны быть уверены, что объект, на который она
ссылается, все еще существует после завершения выполнения функции. Очень легко
забыть об этом, используя в функции ссылки, поскольку они выглядят как обычные
переменные.
Ссылки, как возвращаемые типы, особенно важны в контексте объектно-ори-
ентированного программирования. Как вы увидите позднее в этой книге, они по-
зволяют делать такие вещи, которые невозможно осуществить без их применения
(в частности, это касается “перегрузки операций”, о которой речь пойдет в главе 9).
Принципиальная характеристика возвращаемого значения типа ссылки в том, что
оно является lvalue. Это значит, что вы можете использовать результат функции, ко-
торая возвращает ссылку, в левой части оператор присваивания.
Практическое занятие | Возврат ССЫЛКИ
Теперь рассмотрим пример, иллюстрирующий использование возвращаемых ти-
пов ссылок, а также демонстрирующий, как функция может применяться в левой ча-
сти операции присваивания, когда она возвращает lvalue. Этот пример предполагает,
что у вас есть массив, который содержит смешанный набор значений. Всякий раз,
когда вы хотите вставить новое значение в массив, вы заменяете элемент с наимень-
шим значением.
// Ех5_12.срр
// Возврат ссылки
#include <iostream>
#include <iomanip>
using std::cout;
using std::endl;
using std::setw;
doubles lowest(double values[], int length); // Прототип функции
// возвращающей ссылку
int main(void)
{
double array[] = { 3.0, 10.0, 1.5, 15.0, 2.7, 23.0,
4.5, 12.0, 6.8, 13.5, 2.1, 14.0 };
int len = sizeof array/sizeof array [0]; // Инициализировать числом элементов
cout << endl;
for (int i = 0; i < len; i++)
cout « setw(6) « array[i];
lowest(array, len) = 6.9; // Изменить минимальное на 6.9
lowest (array, len) = 7.9; // Изменить минимальное на 7.9
cout « endl;
for (int i = 0; i < len; i++)
cout << setw(6) << array[i];
cout << endl;
return 0;
Структурная организация программ 275
doubles lowest(double а[], int len)
int j = 0; // Индекс наименьшего элемента
for (int i = 1; i < len; i++)
if (a [ j ] > a [i]) // Поиск минимального значения...
j = i; // ...если так, обновить j
return a[j]; // Вернуть ссылку на наименьший элемент
Ниже показан вывод этого примера.
3 10 1.5 15 2.7 23 4.5 12 6.8 13.5 2.1 14
3 10 6.9 15 2.7 23 4.5 12 6.8 13.5 7.9 14
Описание полученных результатов
Посмотрим сначала, как реализована функция. Прототип функции lowest () ис-
пользует doubles в качестве спецификации возвращаемого типа, который, таким
образом, является “ссылкой на double”. Возвращаемое значение ссылочного типа
пишется точно так же, как это вы видели в случае объявления ссылочных перемен-
ных — с добавлением s к имени типа. Функция принимает два параметра — одномер-
ный массив типа double и параметр типа int, указывающий длину массива.
Тело функции содержит цикл for, в котором определяется, какой элемент пере-
данного массива содержит минимальное значение. Индекс j найденного элемента с
минимальным значением изначально равен 0, а затем модифицируется внутри цикла,
если текущий элемент а [ i ] меньше а [ j ]. Таким образом, по завершении цикла j рав-
но индексу элемента массива с минимальным значением. Оператор return выглядит
следующим образом:
return а[j]; / / Вернуть ссылку на наименьший элемент
Несмотря на тот факт, что это выглядит точно так же, как оператор, возвращаю-
щий значение, поскольку тип возврата объявлен как ссылка, здесь возвращается не
значение элемента а [ j ], а ссылка на него. Адрес а [ j ] используется для инициализа-
ции возвращаемой ссылки. Эта ссылка создается компилятором, потому что возвра-
щаемый тип объявлен как ссылка.
Не путайте возврат Sa [ j ] с возвратом ссылки. Если вы укажете в качестве возвра-
щаемого значения Sa [ j ], это будет означать адрес а [ j ], то есть указатель. Если вы
сделаете это после спецификации типа возврата как ссылки, то получите от компиля-
тора сообщение об ошибке. Если конкретно, вы получите:
error С2440: ’return* : cannot convert from ’double * w64 ’ to ’double S’
Iff
>тбка C2440
'return'
не удается преобразовать 'double * w64 ' в 'double &'
Функция ma i n (), которая вызывает 1 owe s t (), очень проста. Здесь объявляется
массив типа double и инициализируется 12-ю произвольными значениями, а пере-
менная len инициализируется длиной массива. Начальные значения массива выво-
дятся на экран для сравнения.
Опять-таки, использование в программе манипулятора потока setw () для выравнивания
выводимых значений по ширине требует директивы #include <iomanip>.
Функция main () использует функцию lowest () в левой части операции присваи-
вания, чтобы изменить элемент, содержащий минимальное значение в массиве. Это
делается дважды, чтобы продемонстрировать, что все это действительно работает, и
написано не случайно. Затем содержимое массива снова выводится на дисплей, с той
Глава 5
же шириной полей, что и раньше, так что соответствующие значения оказываются
друг под другом.
Как вы можете видеть из вывода перед первым вызовом 1 owe s t (), третий элемент
массива, array [2], содержит минимальное значение, так что функция возвращает
ссылку на него и его значение изменяется на 6.9. Аналогично, после второго вызова
array [10] изменяется на 7.9. Это достаточно ясно демонстрирует, что возврат ссыл-
ки позволяет использовать функцию в левой части оператора присваивания.
Конечно, при желании вы можете использовать ее и в правой части присваива-
ния или в составе любых других подходящих выражений. Если есть два массива — X
и Y — с количеством элементов, указанным соответственно в lenx и leny, вы можете
присвоить минимальному элементу массива х удвоенное значение удвоенного мини-
мального элемента у в следующем операторе:
lowest(х, lenx) = 2.0*lowest (у, leny);
Этот оператор должен вызвать функцию lowest () дважды — один раз с аргумен-
тами у и leny в выражении правой части присваивания и один — с аргументами х и
lenx для получения адреса, куда должен быть сохранен результат правого выраже-
ния.
Еще одно железное правило: возврат ссылок
То же правило, которое применяется к возврату указателей из функций, также ка-
сается возврата ссылок.
Никогда не возвращайте из функции ссылку на локальную переменную.
Пока мы оставим тему возврата ссылок из функций, но пока не будем закрывать
ее окончательно. Мы вернемся к ней опять в контексте определяемых пользователем
типов и объектно-ориентированного программирования и узнаем еще о нескольких
волшебных вещах, которые можно делать со ссылками.
Статические переменные в функциях
Есть несколько вещей, которые невозможно делать с автоматическими переменны-
ми внутри функций. Например, невозможно подсчитать, сколько раз функция вызы-
валась, поскольку нельзя накапливать значение счетчика от вызова к вызову. Однако
существует не один способ обойти это при необходимости. Например, вы можете
использовать параметр-ссылку для обновления счетчика в вызывающей программе,
хотя это не поможет, если функция вызывается из множества разных мест програм-
мы. Вы можете применить глобальную переменную, значение которой увеличивать
внутри функции, но глобальные объекты — рискованная вещь, поскольку доступны в
любой точке программы, что открывает путь к их непреднамеренному изменению.
Глобальные переменные также опасно применять в многопоточных программах,
когда несколько одновременно выполняющихся потоков управления имеют доступ к
ним, и вы должны специально позаботиться об управлении доступом из разных по-
токов. Основная проблема, связанная с тем, что более одного потока имеют доступ
к глобальной переменной, заключается в том, что один поток может изменить значе-
ние глобальной переменной в то время как другой работает с ней. Наилучшее реше-
ние в таких случаях состоит в том, чтобы вообще избегать применения глобальных
переменных.
Чтобы создать переменную, чье значение сохраняется от одного вызова функции
до другого, можно объявить ее внутри функции с ключевым словом static. При этом
Структурная организация программ 277
используется точно такая же форма объявления static переменной, как вы уже ви-
дели в главе 2. Например, чтобы объявить переменную count как static, можно вос-
пользоваться таким оператором:
static int count = 0;
Это также инициализирует ее нулем.
Инициализация статической переменной в функции происходит только при первом вызове
функции. Фактически, именно при первом вызове статическая переменная создается и ини-
циализируется. Затем она продолжает существовать на протяжении всего времени выпол-
нения программы, и любое значение, которое она имеет при завершении функции, остается
доступным при следующем ее вызове.
Практическое занятие
Использование статических переменных
в функциях
Следующий простой пример демонстрирует поведение статической переменной
в функции.
// Ех5_13.срр
// Использование статической переменной в функции
#include <iostream>
using std::cout;
using std::endl;
void record(void); // Прототип функции, без аргументов и возвращаемого значения
int main(void)
{
record();
for (int i = 0; i <= 3; i++)
record();
cout « endl;
return 0;
}
// Функция, которая запоминает, сколько раз она была вызвана
void record(void)
{
static int count = 0;
cout « endl
« "This is the " « ++count;
if ( (count > 3) && (count < 21)) // Все это....
cout «"th";
else
switch(count%10) // просто для получения...
{
case 1: cout « "st";
break;
case 2: cout « "nd";
break;
case 3: cout « "rd";
break;
default: cout « "th"; // правильного окончания для...
} // 1st (1-й), 2nd (2-й), 3rd (3-й), 4st (4-й) и так далее.
cout « " time I have been called";
return;
278 Глава 5
Функция record () служит только для фиксации того факта, что она была вызва-
на. В результате запуска этого примера получается следующий вывод:
This is the 1st time
This is the 2nd time
This is the 3rd time
This is the 4th time
This is the 5th time
I have been called
I have been called
I have been called
I have been called
I have been called
Описание полученных результатов
Статическая переменная count инициализируется нулем и увеличивается в пер-
вом же приложении вывода внутри функции. Поскольку операция инкремента пре-
фиксная, отображается уже увеличенное значение. То есть оно будет равно 1 при
первом вызове, 2 — при втором и так далее. Поскольку переменная count является
статической, она продолжает существовать и сохраняет свое значение от одного вы-
зова функции до другого.
Остаток функции сосредоточен на обработке окончания числового прилагатель-
ного, то есть выборе окончания ’ st ’, • nd ’, ’ rd • или ’ th ’, которое должно быть до-
бавлено при отображении count. Эти окончания в английском языке удивительно не-
регулярны. (Я вот думаю, 101-й вызов должен отобразить 101st, или все-таки 101th?).
Обратите внимание на оператор return. Поскольку типом возврата функции является
void, включение сюда какого-то значения вызовет ошибку компиляции. Вообще в данном
конкретном случае указывать оператор return не обязательно, поскольку выход управле-
ния за закрывающую скобку тела функции эквивалентен оператору return без значения.
Программа должна компилироваться и запускаться без ошибок, даже если не включить в
эту функцию return.
Рекурсивные вызовы функции
Когда функция содержит вызов самой себя, такая функция называется рекурсив*
ной. Рекурсивный вызов функции может быть непрямым, когда функция funl вызы-
вает функцию fun2, которая в свою очередь, вызывает funl.
Может показаться, что рекурсия — прямой путь к бесконечному циклу, и если вы
будете невнимательны, так и случится. Бесконечный цикл заблокирует вашу машину
и потребует нажатия комбинации клавиш <Ctrl+Alt+Del>, чтобы прервать программу,
а это всегда неприятно. Предпосылкой избегания бесконечных циклов является на-
личие в функции некоторого способа, прерывающего процесс.
Если вы не сталкивались с этой техникой ранее, то случаи, когда может пригодить-
ся рекурсия, не слишком очевидны. Однако в физике и математике есть много вещей,
которые для своего описания или управления предполагают применение рекурсии.
Простой пример — факториал целого числа, который для некоторого заданного N ра-
вен произведению 1x2x3x...xN. Этот пример очень часто приводится для демонстра-
ции рекурсии в действии. Рекурсия также применяется для анализа программы во
время процесса компиляции. Однако мы рассмотрим нечто более простое.
Структурная организация программ 279
Практическое занятие
Рекурсивная функция
В начале этой главы (см. Ех5_01. срр) мы написали функцию возведения значения
в целочисленную степень — то есть для вычисления хп. Это эквивалентно х, умножен-
ному на себя п раз. Теперь в качестве элементарного примера применения рекурсии
мы можем реализовать это в виде рекурсивной функции. Можно также при этом усо-
вершенствовать реализацию этой функции, чтобы она могла работать с отрицатель-
ными значениями степени, когда х-n эквивалентно 1/хп.
// Ех5_14.срр (основан на Ех5_01.срр)
// Рекурсивная версия возведения х в степень п
#include <iostream>
using std::cout;
using stdirendl;
double power(double x, int n) ; // Прототип функции
int main(void)
{
double x = 2.0; // Переменная x, отличающаяся от используемой внутри power
double result = 0.0;
// Вычислить x в степенях от -3 до +3 включительно
for(int index = -3 ; index<=3 ; index++)
cout « x « ’’ в степени ” « index « ” равно ’’ « power (x, index)« endl;
return 0;
}
// Рекурсивная функция для вычисления целой степени значения типа double
// Первый аргумент - значение, второй - показатель степени
double power(double х, int n)
{
if(n < 0)
{
x = 1.0/x;
n = -n;
}
if(n > 0)
return x*power(x, n-1);
else
return 1.0;
}
Вывод этой программы будет таким:
2 в степени -3 равно 0.125
2 в степени -2 равно 0.25
2 в степени -1 равно 0.5
2 в степени 0 равно 1
2 в степени 1 равно 2
2 в степени 2 равно 4
2 в степени 3 равно 8
Описание полученных результатов
Теперь наша функция поддерживает положительные и отрицательные степени х,
поэтому первое, что необходимо сделать — проверить, не является ли степень п, в
которую нужно возвести х, отрицательной:
280 Глава 5
х = 1.0/х;
Поддержка отрицательных степеней проста; здесь просто используется тот факт,
что хп может быть вычислено как (1/х)п. Поэтому если п отрицательно, то х присва-
ивается 1.0/х, а знак п меняется на положительный.
В следующем операторе i f принимается решение о том, должна ли функция
power () вызывать саму себя еще раз:
if(п > 0)
return x*power(x, n-1);
else
return 1.0;
В случае если п равно нулю, функция возвращает 1.0, в других случаях возвра-
щает результат вычисления выражения x*power (х, п-1), то есть функция power ()
вызывается еще раз с показателем степени, на 1 меньше. Таким образом, ветвь else
оператора if обеспечивает механизм, необходимый для прерывания бесконечной по-
следовательности рекурсивных вызовов функции.
Понятно, что если внутри функции power () значение п больше нуля, то выпол-
няется следующий вызов power (). Фактически для любого заданного значения п,
отличного от 0, функция вызывается п раз, независимо от знака п. Этот механизм
проиллюстрирован на рис. 5.4, где предполагается, что аргумент — показатель степе-
ни — равен 3.
Как видите, функция power () вызывается четыре раза, чтобы сгенерировать х3,
из которых три вызова рекурсивны, то есть, функция три раза вызывает саму себя.
Использование рекурсии
Если только вы не имеете дело с проблемой, которая сама по себе диктует не-
обходимость использования рекурсивных функций, или существуют очевидные аль-
тернативы, то лучше применить другой подход, например, цикл. Это намного более
эффективно, чем рекурсивные вызовы функций. Подумайте о том, что происходит в
последнем примере, когда нужно вычислить простое произведение х*х* . . . х, причем
праз. При каждом вызове компилятор генерирует копии двух аргументов функции и
отслеживает место в памяти, куда нужно вернуть значение, при каждом выполнении
return. Кроме того, ему необходимо сохранить содержимое различных регистров
компьютера, чтобы они использовались внутри функции power (), и конечно, восста-
навливать их значение при каждом возврате. При сравнительно небольшой глубине
рекурсивных вызовов накладные расходы будут заметно больше, чем при использова-
нии цикла.
Однако из этого не следует, что вы никогда не должны использовать рекурсию.
Когда проблема для своего решения требует применения рекурсивных вызовов функ-
ций, это может оказаться весьма мощной техникой, значительно упрощающей исхо-
дный код. Пример такого случая будет показан в следующей главе.
Структурная организация программ 281
Результат: х3
power( х , 3 )
хх х
double power( double x, int n )
return x*power( x, n -1 );
double power( double x, int n )
return x*power( x, n -1 );
double power( double x, int n )
return x*power( x, n -1);
double power( double x, int n )
return 1.0;
Puc. 5.4. Пример рекурсивной функции
Программирование на C++/CLI
В основном функции в программах C++/CLI работают точно так же, как и в про-
граммах “родного” C++. Конечно, здесь вам приходится иметь дело с дескрипторами
и отслеживаемыми ссылками вместо родных указателей и ссылок, и это ведет к неко-
торым отличиям. Существуют несколько моментов, которые слегка отличаются, по-
пробуем систематизировать их.
□ Параметры функций и возвращаемые значения в программе CLR могут быть
типами классов значений, отслеживаемыми дескрипторами, отслеживаемыми
ссылками и внутренними указателями.
□ Когда параметром является массив, нет необходимости в дополнительном па-
раметре, передающем его длину, поскольку массивы C++/CLI хранят в себе ин-
формацию о собственной длине — в свойстве Length.
□ Нельзя применять адресную арифметику с параметрами-массивами в програм-
мах C++/CLI, как это делается в программах на родном C++, то есть всегда нуж-
но использовать индексацию массивов.
282 Глава 5
□ Возврат дескриптора памяти, выделенной в куче CLR — не проблема, потому
что сборщик мусора позаботится об освобождении памяти, когда необходи-
мость в ней отпадет.
□ Механизм приема переменного списка аргументов в C++/CLI отличается от ме-
ханизма родного C++.
□ Доступ к аргументам командной строки в функции main () программы C++/CLI
также отличается от механизма родного C++.
Последние два отличия мы рассмотрим более подробно.
Функции, принимающие переменное
количество аргументов
Язык C++/CLI разрешает передачу переменного количества аргументов, позволяя
специфицировать список параметров как массив с предшествующим многоточием.
Вот пример функции с переменным числом параметров:
int sum(... array<int>A args)
{
// Код sum
}
Функция sum () принимает любое число аргументов типа int. Чтобы обработать
аргументы, вы просто обращаетесь к элементам массива args. Поскольку это — мас-
сив CLR, количество его элементов записано в свойстве Length, и у вас нет проблем
с определением числа аргументов в теле функции. Этот механизм представляет собой
усовершенствование подобного механизма родного C++, который вы видели ранее,
поскольку является безопасным в отношении типов. Тип аргументов, принимаемых
данной функцией, четко указан как int. Рассмотрим вариацию на тему Ех5_10. срр,
чтобы увидеть этот механизм CLR в действии.
Практическое занятие
Функция с переменным количеством
аргументов
Вот код проекта CLR:
// Ех5_15.срр : главный файл проекта.
// Передача в функцию переменного количества аргументов
#include "stdafx.h"
using namespace System;
double sum(... array<double>A args)
{
double sum = 0.0;
£or each (double arg in args)
sum += arg;
return sum;
int main(array<System::String A> Aargs)
{
Console: '.WriteLine(sum(2.0, 4.0, 6.0, 8.0, 10.0, 12.0));
Console: :WriteLine(sum(1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9));
return 0;
}
Структурная организация программ
283
Этот пример генерирует следующий вывод:
42
49.5
Press any key to continue . . .
Описание полученных результатов
Функция sum () принимает аргументы типа double. Многоточие, предшествующее
параметру, сообщает компилятору, что можно ожидать произвольное количество аргу-
ментов, и что эти аргументы помещаются в массив элементов типа double. Конечно,
без двоеточия функция при вызове ожидала бы только один аргумент, который дол-
жен был быть дескриптором массива.
По сравнению с “родной” версией Ех5_10, данное определение функции sum () за-
метно проще. Здесь, в версии C++/CLI, исчезают все проблемы, связанные с типом и
количеством аргументов. Сумма элементов накапливается в простом цикле for each,
который проходит по всем элементам массива.
Аргументы main ()
Ниже показана версия C++/CLI примера Ех5_09.
// Ех5_16.срр : главный файл проекта.
// Прием множества аргументов командной строки.
#include "stdafx.h"
using namespace System;
int main(array<System::String Л> Aargs)
Console::WriteLine(Ь"Принято {0} аргументов командной строки."f args->Length);
Console::WriteLine(Ь"Приняты следующие аргументы командной строки:");
int i = 1;
for each (StringA str in args)
Console::WriteLine(L"ApryMeHT {0}: {1}", i++, str);
return 0;
Вы можете ввести аргументы командной строки в командном окне или через окно
свойств проекта, как было описано ранее в этой главе. Пример выдает на экран сле-
дующее:
C:\Visual C++ 2005\Examples\>Ex5_16 trying multiple "argument values" 4.5 0.0
Принято 5 аргументов командной строки.
Приняты следующие аргументы командной строки:
Аргумент 1: trying
Аргумент 2: multiple
Аргумент 3: argument values
Аргумент 4: 4.5
Аргумент 5: 0.0
Описание полученных результатов
Как видно из вывода этого примера, имеется одно отличие от версии “родного”
C++ — имя самой программы не передается в аргументах main (), и это не слишком
большой недостаток, а в некоторых случаях даже и достоинство. Здесь доступ к аргу-
ментам командной строки — тривиальная задача, которая решается простой итераци-
ей по элементам массива args.
284 Глава 5
Резюме
В этой главе вы изучили основы структурной организации программ. Вы долж-
ны были получить хорошее представление о том, как определяются функции, как
им передаются данные и как возвращаются результаты в вызывающую программу.
Функции — фундаментальная концепция в программировании на C++, поэтому все,
что вы будете делать, начиная с этого момента, будет включать использование мно-
жества функций в каждой программе. Ниже перечислены ключевые моменты, о кото-
рых вам следует помнить при написании своих собственных функций.
□ Функции должны быть компактными единицами кода, предназначенными для
четко определенных целей. Типичная программа должна состоять из множе-
ства небольших функций, а не из малого количества крупных.
□ Для каждой функции, определенной в программе, всегда следует представлять
прототип, располагая его перед ее вызовами.
□ Передача значений в функцию с применением ссылок позволяет избежать не-
явного копирования, которое выполняется при передаче аргументов по значе-
нию. Параметры, переданные по ссылке, которые не должны быть модифици-
рованы в функции, следует специфицировать как const.
□ При возврате ссылки или указателя из функции родного C++ убедитесь, что
возвращаемый объект имеет правильную область определения. Никогда не воз-
вращайте указатель или ссылку на локальный объект функции родного C++.
□ В программах C++/CLI не возникает проблем с возвратом дескриптора памяти,
которая была выделена динамически, потому что сборщик мусора позаботится
о его удалении, когда он будет не нужен.
□ Когда вы передаете массив C++/CLI в функцию, нет необходимости в дополни-
тельном параметре, указывающем его длину, поскольку количество элементов
массива можно получить в теле функции через свойство Length массива.
Использование ссылок в качестве аргументов — очень важная концепция, поэтому
убедитесь, что вы уверены в том, что делаете. Когда мы будем говорить об объектно-
ориентированном программировании, вы узнаете намного больше о ссылках в каче-
стве аргументов.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Факториал 4 (записывается, как 4!) равен 4*3*2*1=24, а 3! равен 3*2*1=6, отсю-
да следует, что 4!=4*3!, или, в более общем виде:
fact(n) = n*fact(n — 1)
Ограничивающим условием является случай, когда п=1, при этом 1! = 1. Напишите
рекурсивную функцию для вычисления факториала и протестируйте ее.
2. Напишите функцию, которая обменивает значениями две целочисленных пере-
менных, используя указатели. Напишите программу, вызывающую эту функцию,
и протестируйте, чтобы убедиться, что она работает правильно.
Структурная организация программ 285
3. Тригонометрические функции (sin (), cos () и tan ()) из стандартной матема-
тической библиотеки принимают аргументы в радианах. Напишите три эквива-
лентных функции sind (), cosd () и tand (), которые принимают аргументы в
градусах. Все аргументы и возвращаемые значения должны быть типа double.
4. Напишите программу на родном C++, которая читает с клавиатуры число (це-
лое) и имя (до 15 символов). Спроектируйте программу так, чтобы данные вво-
дились в одной функции, а выводились в другой. Сохраняйте данные в главной
программе. Программа должна завершаться, когда пользователь введет в каче-
стве числа 0. Подумайте о том, как передавать данные между функциями — по
значению, по указателю или по ссылке?
5. (Усложненное.) Напишите функцию, которая, получив строку, состоящую из
слов, разделенных пробелами, возвращает первое слово; следующий ее вызов с
аргументом NULL должен вернуть второе слово, и так далее до тех пор, пока вся
строка не будет успешно обработана, когда должен быть возвращен NULL. Это
должна быть упрощенная версия того, как работает функция стандартной би-
блиотеки родного C++ strtok (). То есть, когда функция получает строку "один
два три’, то при первом вызове должна вернуть ‘один’, при втором — "два’, и
наконец — ‘три’. Передача новой строки должна прекратить обработку старой
и отбросить ее прежде, чем начать обрабатывать новую.
6
Дополнительные сведения
о структурах программ
В предыдущей главе вы изучили основы определения функций и узнали о различ-
ных способах передачи информации в функции. Вы также видели, как функции воз-
вращают результаты в вызвавшую их программу.
В этой главе мы раскроем дополнительные аспекты правильного применения
функций, включая'перечисленные ниже вопросы.
□ Что такое указатель на функции.
□ Как определять и использовать указатели на функции.
□ Как определять и использовать массивы указателей на функции.
□ Что такое исключение, и как создавать обработчики исключений.
□ Как создавать множество функций с одинаковыми именами для автоматиче-
ской обработки разнотипных данных.
□ Что такое шаблоны функций, как их определять и использовать.
□ Как написать реальный пример программы на родном C++, использующей не-
сколько функций.
□ Что собой представляют обобщенные функции в C++/CLI.
□ Как написать реальный пример программы на C++/CLI, использующей не-
сколько функций.
Указатели на функции
Указатель сохраняет значение адреса, и все рассмотренные нами до настоящего
времени указатели хранили адреса других переменных с тем же базовым типом, что и
конкретный указатель. Это обеспечивало относительную гибкость в смысле того, что
288 Глава 6
можно было обращаться в разное время к разным переменным через один и тот же
указатель. Но указатель также может хранить адрес функции. Это дает возможность
вызывать функцию через указатель, адрес которой был присвоен данному указателю.
Очевидно, что указатель на функцию должен содержать адрес памяти, где находит-
ся функция, которую необходимо вызвать. Чтобы работать правильно, однако, такой
указатель должен поддерживать информацию о списке параметров соответствующей
функции и типе ее возвращаемого значения. Таким образом, когда вы объявляете
указатель на функцию, то в дополнение к имени указателя должны специфицировать
типы параметров и тип возврата функции, на которую он может указывать. Ясно,
что это необходимо для того, чтобы ограничить то, что можно присваивать каждому
конкретному указателю функции. Если вы объявили указатель на функцию, принима-
ющую один аргумент типа int и возвращающую значение типа double, то сможете
хранить в нем только адрес функции, в точности соответствующей упомянутой сиг-
натуре. Если же вы хотите хранить адрес функции, принимающей два аргумента типа
int и возвращающей значение типа char, то должны определить другой указатель с
соответствующими характеристиками.
Объявление указателей на функции
Попробуем объявить указатель pf un, который может указывать на функцию, при-
нимающую два аргумента типа char* и int и возвращающую значение типа double.
Объявление должно выглядеть так:
double (*pfun)(char*, int); // Объявление указателя на функцию
Во-первых, вы наверняка обратите внимание на то, что скобки выглядят несколь-
ко загадочно. Этот оператор объявляет указатель по имени pf un, который может ука-
зывать на функции, принимающие два аргумента — типа указателя на char и типа int
и возвращающие значение типа double. Скобки вокруг имени указателя, pfun, как и
звездочка — необходимы; без них этот оператор было бы просто объявлением функ-
ции, а не объявлением указателя. В этом случае оно выглядело бы так:
double *pfun(char*, int); // Прототип функции
// возвращающей тип double*
Такой оператор является прототипом функции pfun (), принимающей два пара-
метра и возвращающей указатель на значение типа double. Поскольку вы намерены
объявить указатель, это совсем не то, что вам нужно в данный момент.
Общая форма объявления указателя на функцию выглядит так:
тип_возврата (*имя_указателя) (список_типов_параметров);
Указатель может указывать только на функции с тем же типом возврата и списком типов
параметров, что заданы в его объявлении.
Это показывает, что объявление указателя на функцию состоит из трех перечис-
ленных ниже компонентов.
□ Тип возврата функции, на которую он указывает.
□ Имя указателя, предваренное звездочкой, говорящей о том, что это — указа-
тель.
□ Типы параметров функции, на которую он указывает.
Если вы попытаетесь присвоить адрес функции указателю, который не соответствует ти-
пам в его объявлении, компилятор выдаст сообщение об ошибке.
Дополнительные сведения о структурах программ 289
Вы можете инициализировать указатель на функцию именем функции непосред-
ственно в операторе объявления этого указателя. Ниже приведены примеры этого.
long sum(long numl, long num2); // Прототип функции
long (*pfun) (long, long) = sum; // Указатель указывает на функцию sum()
В общем случае вы можете установить объявленный здесь указатель pf un так, что-
бы он указывал на любую функцию, принимающую два аргумента типа long и возвра-
щающую значение типа long. В данном случае он инициализирован адресом функции
sum (), которая имеет прототип, представленный в первом операторе.
Конечно, вы также можете инициализировать указатель на функцию с помощью
оператора присваивания. Предполагая, что указатель pf un объявлен, как показано
выше, вы можете установить значение указателя равным адресу другой функции в
следующих операторах:
long product(long, long); // Прототип функции
pfun = product; // Установить указатель на product ()
Как и с указателями на переменные, вы должны обеспечить инициализацию указа-
телей на функции перед тем, как использовать их для вызова функций. Без инициали-
зации такой вызов гарантирует катастрофический сбой вашей программы.
Практическое занятие УкаЗЭТвЛИ НЭ фуНКЦИИ
Чтобы прочувствовать эти новые для вас указатели и увидеть их в действии, рас-
смотрим следующий пример программы.
// Ех6_01.срр
// Упражнение с указателями на функции
#include <iostream>
using std::cout;
using std::endl;
long sum(long a, long Ь) ; // Прототип функции
long product(long a, long b); // Прототип функции
int main(void)
{
long (*pdo_it)(long, long); // Объявление указателя на функцию
pdo_it = product;
cout « endl
« "3*5 = " « pdo_it(3, 5); // Вызов product через указатель
pdo_it = sum; // Переназначение указателя на sum()
cout << endl
« "3* (4 + 5) + 6 = "
« pdo_it (product (3, pdo_it(4, 5)), 6);//Дважды вызвать через указатель
cout « endl;
return 0;
}
// Функция для умножения двух значений
long product(long a, long b)
{
return a*b;
}
// Функция для сложения двух значений
long sum(long a, long b)
{
return a + b;
}
290 Глава 6
Эта программа генерирует следующий вывод:
3*5 = 15
3* (4 + 5) + 6 = 33
Описание полученных результатов
Это не особенно полезная программа, однако на очень простом примере она по-
казывает, как объявить указатель на функцию, как ему присвоить значение и затем
использовать для вызова функции.
После обычной преамбулы вы объявляете указатель на функцию pdo_i t, который
может указывать на любую из двух функций, которую вы определили — sum () или
product (). Указателю присваивается адрес функции product () в следующем опера-
торе:
pdo_it = product;
В качестве присваиваемого значения используется просто имя функции — без ско-
бок и каких-либо других украшений. Имя функции автоматически преобразуется ком-
пилятором в ее адрес, который и сохраняется в указателе.
Функция product () вызывается неявно, через указатель pdo_it в операторе вы-
вода:
cout « endl
« ”3*5 = ” « pdo_it(3, 5); // Вызов product через указатель
Здесь вы используете имя указателя, как если бы он был именем функции, с по-
следующими аргументами в скобках — точно так же, как они задавались бы при непо-
средственном вызове функции по ее имени.
Далее, исключительно для того, чтобы показать, что так можно сделать, указателю
присваивается адрес функции sum ():
pdo_it = sum;
// Переназначение указателя на sum()
После этого вы используете его в следующем, хитро закрученном операторе, вы-
полняющем несложную арифметику:
cout « endl
« ”3* (4 + 5) + 6 = ”
« pdo_it(product(3, pdo_it(4, 5)), 6);//Дважды вызвать через указатель
Этот код демонстрирует, что указатель на функцию может использоваться точно
таким же образом, как и функция, на которую он указывает. Последовательность дей-
ствий этого выражения показана на рис. 6.1.
Указатель на функцию в качестве аргумента
Поскольку указатель на функцию — вполне законный тип, любая функция может
иметь параметр типа указателя на функцию. Такая функция затем может вызывать
функцию, указатель на которую ей передан в аргументе. Поскольку указатель может
быть переустановлен на другую функцию в других обстоятельствах, это позволяет
определять вызывающей программе определять, какая функция должна быть вызвана
из данной. В этом случае вы можете передать функцию явно в виде аргумента.
Дополнительные сведения о структурах программ 291
pdojt (27,6)
эквивалентно
sum (27,6) — дает в результате —► 33
Рис. 6.1. Указатель на функцию
Практическое занятие
Передача указателя на функцию
В сказанном можно убедиться на следующем примере. Предположим, что вам нуж-
на функция, обрабатывающая массив чисел, в одних случаях вычисляя сумму квадра-
тов этих чисел, а в других — сумму кубов этих чисел. Одним из способов обеспечить
такую возможность является применение указателя функции в качестве аргумента.
//Ех6_02.срр
// Указатель на функцию в качестве аргумента
#include <iostream>
using std::cout;
using std::endl;
// Прототипы функции
double squared(double);
double cubed(double);
double sumarray(double array[], int len, double (*pfun)(double));
int main(void)
{
double array[] = { 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5 };
int len = sizeof array/sizeof array[0];
cout « endl
« "Сумма квадратов = ”
« sumarray(array, len, squared);
cout « endl
« "Сумма кубов = "
« sumarray(array, len, cubed);
cout « endl;
return 0;
}
292 Глава 6
// Функция для возведения значения в квадрат
double squared(double х)
return х*х;
// Функция для возведения значения в куб
double cubed(double х)
return х*х*х;
//Сумма для аккумулирования результатов вызова функции для всех элементов массива
double sumarray(double array[], int len, double (*pfun)(double))
double total = 0.0; // Здесь накапливается итоговая сумма
for (int i = 0; i < len; i++)
total += pfun (array [i]);
return total;
Если вы скомпилируете и запустите этот код, то увидите следующий вывод:
Сумма квадратов = 169.75
Сумма кубов = 1015.88
Описание полученных результатов
Первый оператор, который представляет интерес — это прототип функции
s игла г г а у (). Ее третий параметр — указатель на функцию, принимающую параметр
типа double и возвращающую значение типа double.
double sumarray(double array[], int len, double (*pfun)(double));
Функция sumarray () обрабатывает каждый элемент массива, переданного ей в
первом аргументе, функцией, на которую указывает третий аргумент. Функция воз-
вращает сумму обработанных элементов массива.
Функция sumarray () в main () вызывается дважды — первый раз с именем функ-
ции squared в качестве третьего аргумента, а второй — с именем функции cubed.
В каждом случае адрес функции соответствующей имени, использованному в качестве
аргумента, подставляется вместо указателя функции в теле sumarray (), поэтому соот-
ветствующая функция вызывается в теле цикла for.
Очевидно, что существует масса более простых способов получения того же ре-
зультата, что и в этом примере, но применение указателя на функцию обеспечивает
высокую степень применимости. Вы можете передать любую функцию sumarray (),
какую пожелаете объявить, если только она принимает один аргумент типа double и
возвращает значение типа double.
Массивы указателей на функции
Точно так же, как и с обычными указателями, разрешено объявлять массивы ука-
зателей на функции. Вы также можете инициализировать их в объявлении. Ниже по-
казан пример объявления массива указателей.
double sum(double, double);
double product(double, double);
double difference(double, double);
double (*pfun[3])(double,double) =
{ sum, product, difference };
// Прототип функции
// Прототип функции
// Прототип функции
// Массив указателей на функции
Дополнительные сведения о структурах программ 293
Каждый из элементов массива инициализируется соответствующим адресом функ-
ции, указанным в фигурных скобках. Чтобы вызвать функцию product (), используя
второй элемент массива указателей на функции, вы должны записать так:
pfun[l] (2.5, 3.5) ;
Квадратные скобки, в которых указывается индекс элемента массива указателей
на функции, появляется немедленно после имени массива и перед аргументами вы-
зываемой функции. Конечно, вы можете поместить вызов функции через обращение
к элементу массива указателей на функции в любое выражение, где можно легитимно
применять имя исходной функции, а значением индекса, выбирающего конкретный
указатель из массива, может служить любое выражение, порождающее корректное
целочисленное значение индекса.
Инициализация параметров функций
Со всеми функциями, которые использовались до сих пор, вы должны были поза-
ботиться о передаче аргументов, соответствующих каждому из параметров вызывае-
мой функции. Было бы довольно удобно, если бы при вызове функции можно было
пропускать один или более аргументов, имеющих значения по умолчанию, чтобы эти
значения передавались автоматически. Этого можно добиться, инициализируя пара-
метры функции в ее прототипе.
Например, представим, что вы пишете функцию для отображения сообщения, где
отображаемое сообщение передается в аргументе. Вот ее определение:
void showit(const char message[])
{
cout « endl
« message;
return;
}
Вы можете инициализировать параметр этой функции, специфицируя начальное
строковое значение в прототипе функции, как показано ниже:
void showit(const char message[] = "Что-то не так.");
Здесь параметр message инициализируется указанным строковым литералом. Если
вы инициализируете параметр функции в прототипе, то в случае пропуска аргумента
при ее вызове, используется это значение инициализации.
Практическое занятие ПрОПуСК ЭрГуМеНТОВ фуНКЦИИ
Пропуск аргумента при вызове функции приводит к тому, что она выполняется с
параметром по умолчанию. Если вы применяете аргумент, он заменяет значение по
умолчанию. Вы можете использовать функцию showit () для вывода широкого раз-
нообразия сообщений.
//Ех6_03.срр
// Пропуск аргументов функции
#include <iostream>
using std::cout;
using std::endl;
void showit(const char message[] = "Что-то не так.");
294 Глава 6
int main(void)
const char mymess[] = "Близится конец рабочего дня.";
showit(); // Отобразить базовое сообщение
showit("Что-то ужасно не так’");// Отобразить альтернативное сообщение
showitO; // Опять отобразить сообщение по умолчанию
showit(mymess); // Отобразить предопределенное сообщение
cout « endl;
return 0;
void showit(const char message[])
cout « endl
« message;
return;
Если выполнить этот пример, вы увидите следующий вывод:
Что-то не так.
Что-то ужасно не так!
Что-то не так.
Близится рабочего дня.
Описание полученных результатов
Как видите, вы получаете сообщение по умолчанию, специфицированное в про-
тотипе, когда аргумент не указывается явно; в противном случае функция ведет себя
как обычно.
Если у вас есть функция с несколькими аргументами, вы можете указать началь-
ные значения для любого количества из них. Если вы хотите пропустить более одно-
го аргумента, чтобы воспользоваться выгодами от значений по умолчанию, все аргу-
менты справа от крайнего левого из пропущенных, также должны быть пропущены.
Например, предположим, что имеется следующая функция:
int do_it(long argl = 10, long arg2 = 20, long arg3 = 30, long arg4 = 40);
и вы хотите пропустить один аргумент при вызове ее, то сможете это сделать
только с последним из них — arg4. Если вы хотите пропустить агдЗ, то придется про-
пустить также arg4. Если вы пропускаете arg2, то агдЗ и агд4 также должны быть
пропущены, а если вы хотите использовать значение по умолчанию argl, то при вы-
зове функции придется пропустить все ее аргументы.
Из этого можно сделать вывод, что аргументы со значениями по умолчанию в про-
тотипе функции следует помещать вместе, последовательно начиная с конца списка
параметров, причем те из них, которые наиболее вероятно будут пропущены, долж-
ны располагаться в конце.
Исключения
Если вы выполняли упражнения, приведенные в конце предыдущих глав, то бо-
лее чем вероятно, что при этом сталкивались с ошибками и предупреждениями
компилятора, а равно с ошибками во время выполнения программы. Исключения
(exceptions) — это способ пометки ошибок или неожиданных условий, появляющихся
в ваших программах C++, и вы уже знаете, что операция new возбуждает исключение,
если запрошенная вами память не может быть выделена.
Дополнительные сведения о структурах программ 295
До сих пор вы обычно обрабатывали ошибочные условия в программах, используя
оператор if для проверки некоторого выражения, а затем выполняя некоторый спе-
цифический код, предназначенный для обработки ошибки. Но C++ также представ-
ляет другой, более общий механизм обработки ошибок, позволяющий отделить код,
имеющий дело с этими ошибочными ситуациями, от кода, выполняющегося, когда
такие условия не возникают. Важно понимать, что исключения не предназначены в
качестве альтернативного способа обычной проверки и верификации данных, кото-
рые выполняются программой. Код, генерируемый при использовании исключений,
требует некоторых накладных расходов, поэтому на самом деле исключения пред-
назначены для применения в контексте исключительных, почти катастрофических
условий, которые могут возникнуть, но чье появление не является частью нормально-
го хода событий. Для использования исключения подойдет ситуация ошибки чтения
диска. Случай некорректного значения введенных данных — не самый подходящий
кандидат для применения исключений.
В механизме исключений используются три новых ключевых слова:
О try — идентифицирует блок кода, в котором может возникнуть исключение;
О throw — вызывает возникновение исключительного условия;
О catch — идентифицирует блок кода, в котором обрабатывается исключение.
В следующем разделе вы увидите, как исключения работают на практике.
практическое занятие | Возбуждение и перехват исключений
Легче всего объяснить применение исключений на примере. Возьмем для этого
очень простой контекст. Предположим, что вам нужно написать программу, кото-
рая вычисляет время в минутах, необходимое для изготовления детали на станке.
Известно количество деталей, изготовляемых в час, но вы должны иметь в виду, что
станок периодически ломается, и тогда детали не производятся.
Вы можете закодировать это с использованием обработки исключений следующим
образом:
// Ех6_04.срр
// Обработка исключений
#include <iostream>
using std::cout;
using std::endl;
int main(void)
{
int counts [ ] = {34, 54, 0, 27, 0, 10, 0};
int time = 60; // Количество минут в часе
for (int i = 0 ; i < sizeof counts/sizeof counts [0] ; i++)
try
{
cout « endl
« "Час " « i+1;
if(counts [i] == 0)
throw "Нулевое количество — вычисление невозможно.";
cout « " минут на деталь: "
« static_cast<double>(time)/counts[i];
}
catch(const char aMessage[])
{
296 Глава 6
cout « endl
« aMessage
« endl;
return 0;
Если вы запустите этот пример, то получите следующее:
Час 1 минут на деталь: 1.76471
Час 2 минут на деталь: 1.11111
Час 3
Нулевое количество — вычисление невозможно.
Час 4 минут на деталь: 2.22222
Час 5
Нулевое количество — вычисление невозможно.
Час 6 минут на деталь: 6
Час 7
Нулевое количество — вычисление невозможно.
Описание полученных результатов
Код в блоке try выполняется в нормальной последовательности. Блок try служит
для определения той части программы, где может возникнуть исключение. Вы може-
те видеть по результату работы программы, что когда возбуждается исключение, вы-
полнение продолжается в блоке catch, а после того, как код блока catch выполнен,
дальше выполняется следующая итерация цикла. Конечно, когда не исключение воз-
буждается, блок catch не выполняется. Как блок try, так и блок catch рассматрива-
ются компилятором как единое целое, поэтому оба они формируют блок — тело цикла
f о г — и работа цикла продолжается после возбуждения исключения.
Деление выполняется в операторе вывода, следующем за оператором if, который
проверяет делитель. Когда выполняется оператор throw, управление немедленно пе-
редается первому оператору блока catch, так что оператор, выполняющий деление,
в случае возникновения исключения пропускается. После того, как выполнится опе-
ратор блока catch, цикл продолжается со следующей итерации, если проверочное
условие цикла истинно.
Возбуждение исключений
Исключения могут быть возбуждены в любом месте блока try, и операнд опе-
ратора throw определяет тип исключения — исключение, которое возбуждается в
этом примере, является строковым литералом, а потому имеет тип const char [ ].
Операнд, следующий за ключевым словом throw, может быть любым выражением, и
тип результата вычисления этого выражения определяет тип возбужденного исклю-
чения.
Исключения также могут быть возбуждены из функций, вызываемых изнутри бло-
ка try, и перехвачены блоком catch, следующим за этим блоком try. Чтобы проде-
монстрировать это, к предыдущему примеру вы можете добавить функцию со следую-
щим определением:
void testThrow(void)
throw ’’Нулевое количество — вычисление невозможно.
Дополнительные сведения о структурах программ 297
Затем вы помещаете вызов этой функции в предыдущий пример на место опера-
тора throw:
if(counts[i] == 0)
testThrow(); // Вызов функции, возбуждающей исключение
Исключение возбуждается функцией testThrow () и перехватывается блоком
catch всякий раз, когда встречается элемент массива, равный нулю, поэтому вывод
программы останется без изменений. Не забудьте о прототипе функции, если добави-
те определение testThrow () в конец кода.
Перехват исключений
Блок catch, следующий в нашем примере за блоком try, перехватывает любые ис-
ключения типа const char [ ]. Это определяется спецификацией параметра, которая
появляется в скобках, следующих за ключевым словом catch. Вы должны предусмо-
треть как минимум один блок catch для блока try, и этот блок должен немедленно
следовать за блоком try. Блок catch перехватывает все исключения (корректного
типа), которые возникают в любой точке кода, непосредственно предшествующего
блока try, включая те, что могут быть возбуждены в любых функциях, вызванных
прямо или непрямо изнутри блока try.
Если вы хотите специфицировать, что блок catch должен обрабатывать любое
исключение, возбужденное в блоке try, то должны поместить многоточие в скобки,
ограничивающие объявление исключения:
catch (...)
{
// Код для обработки любых исключений
}
Такой блок catch должен появляться последним, если у вас определены и другие
блоки catch для данного блока try.
Практическое занятие) ВЛОЖеННЫв бЛОКИ try
Вы можете вкладывать несколько блоков try друг в друга. В такой ситуации, если
исключение возбуждается из внутреннего блока try, за которым не следует блок
catch для перехвата исключения соответствующего типа, выполняется поиск обработ-
чика catch для внешнего блока. Сказанное демонстрируется в следующем примере.
// Ех6_05.срр
// Вложенные блоки try
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
int main(void)
{
int height = 0;
const double inchesToMeters = 0.0254;
char ch = ’ у ’ ;
try // Внешний блок try
{
298 Глава 6
while(ch == •у•||ch ==’Y’)
cout « "Введите высоту в дюймах: ";
cin » height; // Читать высоту, подлежащую преобразованию
try // Определение блока try, в котором
{ // могут быть возбуждены исключения
if (height > 100)
throw "Высота превышает максимум"; // Исключение возбуждено
if (height < 9)
throw height; // Исключение возбуждено
cout « static_cast<double>(height)*inchesToMeters
« " метров"
« endl;
catch(const char aMessage[J) // начало блока catch, который
{ 11 перехватывает исключения типа
cout « aMessage « endl; // const char[]
cout « "Хотите продолжить (у или n)?";
cin » ch;
catch(int badHeight)
cout « badHeight « " значение в дюймах меньше допустимого" « endl;
return 0;
}
Здесь присутствует блок try, включающий в себя цикл while, и вложенный блок
try, в котором могут возбуждаться исключения двух разных типов. Исключение типа
const char [ ] перехватывается блоком catch внутреннего блока try, а исключение
типа int не имеет ассоциированного обработчика для внутреннего блока try, поэто-
му выполняется блок catch внешнего блока try. В этом случае программа немедлен-
но прекращается, поскольку за блоком catch следует оператор return.
Обработка исключений в MFC
Теперь наступило время поговорить о библиотеке MFC и исключениях, поскольку
там они используются несколько необычным образом. Если вы просмотрите докумен-
тацию, поставляемую с Visual C++ 2005, то встретите там в предметном указателе TRY,
THROW и CATCH. Все это — макросы, определенные в MFC, которые были созданы еще
до того, как обработка исключений была реализована в языке C++. Они имитируют
операции try, throw и catch языка C++, но новые средства языка, предназначенные
для обработки исключений, на самом деле сделали их устаревшими, поэтому вы не
должны пользоваться этими макросами. Однако они остаются в библиотеке по двум
причинам. Существует большое количество программ, все еще использующих эти ма-
кросы, и важно как можно дольше поддерживать возможность компиляции старого
кода. К тому же большая часть кода самой MFC, гду возбуждаются исключения, была
реализована в терминах макросов. Но как бы то ни было, любая новая программа
должна использовать ключевые слова try, throw и catch в C++, потому что они ра-
ботают и с MFC.
Есть одна небольшая аномалия, о которой следует помнить, когда вы работаете
с функциями MFC, которые возбуждают исключения. Эти функции обычно возбуж-
Дополнительные сведения о структурах программ 299
дают исключения типов классов (вы узнаете о типах классов перед тем, как начне-
те использовать MFC). Даже несмотря на то, что исключения, которые возбуждают
функции MFC, относятся к данному типу класса (скажем, CDBException), вы должны
перехватывать исключение как указатель, а не тип исключения. То есть если было
возбуждено исключение типа CDBException, типом, который появляется в виде пара-
метра блока catch, будет CDBException*. Примеры этого вы увидите далее в книге.
Обработка ошибок выделения памяти
Когда вы используете операцию new для выделения памяти для переменных (как
было показано в главах 4 и 5), то игнорируете вероятность того, что память не будет
выделена. Если не удается выделить память, возбуждается исключение, которое при-
водит к завершению программы. Игнорирование этого исключения подходит в боль-
шинстве ситуаций, поскольку отсутствие памяти — это ситуация, в которой ничего
другого не остается, кроме как прекратить выполнение программы. Однако бывают
случаи, когда вы можете с этим что-то сделать, если есть шанс, или вы можете сооб-
щить пользователю о проблеме каким-то особым способом. В этой ситуации вы може-
те перехватить исключение, которое возбуждает операция new. Давайте рассмотрим
пример, который продемонстрирует, как это происходит.
Практическое занятие | ПврвХВаТ ИСКЛЮЧвНИЯ, ВОЗбуЖДвННОГО
операцией new
Исключение, которое возбуждает операция new, когда не удается выделить память,
имеет тип bad alloc. bad_alloc — тип класса, определенный в стандартном заголо-
вочном файле <new>, поэтому вам понадобится директива #include. Ниже показан
код примера.
// Ехб_06.срр
#include<new> // Для типа bad_alloc
#include<iostream>
using std::bad_alloc;
using std::cout;
using std::endl;
int main( )
{
char* pdata = 0;
size_t count = ~static_cast<size_t>(0)/2;
try
{
pdata = new char [count];
cout « "Память выделена." « endl;
}
catch(bad_alloc &ex)
{
cout « "He удалось выделить память." « endl
« "Информация из объекта исключения: "
« ex.whatO « endl;
}
delete[] pdata;
return 0;
}
300 Глава 6
На моей машине этот пример выдал следующее:
Не удалось выделить память.
Информация из объекта исключения: bad allocation
Если вам повезло иметь на компьютере много гигабайт памяти, то может случить-
ся, вы не получите исключения нехватки памяти.
Описание полученных результатов
Пример динамически выделяет память для массива типа char [ ], где длина указана
переменной count, которая определена следующим образом:
size_t count = ~static_cast<size_t>(0)/2;
"Размер массива — целое число типа size_t, поэтому переменная count объ-
явлена с этим типом. Значение count генерируется довольно сложным выраже-
нием. Значение 0 имеет тип int, поэтому значение, порожденное выражением
static__cast<size_t> (0) — это ноль типа size t. Применение операции ~ к этому
значению переключает все его биты так, что в результате получаем значение типа
size t со всеми битами, установленными в 1, что соответствует максимальному зна-
чению, которое может быть представлено как size t, поскольку size t — беззна-
ковый тип. Это значение превышает максимальный объем памяти, который может
выделить операция new за один шаг, поэтому мы делим его на 2, чтобы привести за-
прошенный объем в допустимые пределы. Это все равно будет очень большое значе-
ние, поэтому если только ваша машина не обладает таким огромным объемом памяти,
запрос должен завершиться неудачей.
Выделение памяти происходит в блоке try. Если оно произойдет успешно, вы
увидите соответствующее сообщение, но если оно не удастся, как и следует ожидать,
то операцией new будет возбуждено исключение bad alloc. Это приведет к выпол-
нению кода из блока catch. Вызов функции what () для ссылки на объект ех типа
bad__alloc возвращает строку, описывающую проблему, которая была причиной ис-
ключения, и вы увидите результат этого в выводе программы. В большинстве классов
исключений реализована функция what () для строкового описания причины возник-
новения исключения.
Чтобы обработать ситуацию переполнения памяти с некоторым положительным
эффектом, ясно, что вы должны иметь какие-то средства для возврата памяти в сво-
бодное хранилище. В большинстве практических случаев это влечет за собой серьез-
ную работу над программой, направленную на управление памятью, которая предпри-
нимается нечасто.
Перегрузка функций
Предположим, вы написали функцию, определяющую максимальное значение в
массиве значений типа double:
// Функция для нахождения максимального значения в массиве типа double
double maxdouble(double array[], int len)
double max = array[0];
for (int i = 1; i < len; i++)
if(max < array[i])
max == array [i];
return max;
дополнительные сведения о структурах программ
301
Теперь вам нужно создать функцию, которая выдает максимальное значение из
массива типа long, поэтому вы пишете другую функцию, похожую на первую, со сле-
дующим прототипом:
long maxlong(long array[], int len);
Вы выбрали имена функций, отражающее конкретно выполняемые ими задачи,
что вполне адекватно для этой пары функций, но вам может понадобиться такая
же функция для нескольких других типов аргументов. Придумывать имена им всем
может оказаться довольно-таки утомительно. В идеале хотелось бы иметь одно имя
функции — main (), независимо от типа аргументов, и чтобы в каждом случае выпол-
нялась нужная версия. Возможно, это уже не будет для вас сюрпризом, что вы дей-
ствительно можете это сделать, и механизм C++, предназначенный для этого, называ-
ется перегрузкой функций.
Что такое перегрузка функций?
Перегрузка функций позволяет вам использовать одно и то же имя для определе-
ния нескольких функций — до тех пор, пока они принимают различные списки пара-
метров. Когда вызывается функция, компилятор находит подходящую для конкретно-
го случая версию на основе списка аргументов, который вы применяете. Очевидно,
что компилятор должен всегда быть в состоянии недвусмысленно решить, какая
именно функция должна быть выбрана в каждом конкретном случае вызова, поэтому
список параметров для каждой из множества перегруженных функций должен быть
уникальным. Возвращаясь к примеру с функциями max (), вы могли бы создать пере-
груженные функции со следующими прототипами:
int max(int array[], int len);
long max(long array[], int len);
double max(double array[], int len);
// Прототипы
// набора перегруженных
// функции
Эти функции разделяют одно общее имя, но имеют разные списки параметров.
Вообще перегруженные функции можно различать по наличию параметров разного
типа либо по различному количеству параметров.
Обратите внимание, что разные типы возврата не могут адекватно различать
функции. Вы не можете добавить следующую функцию к приведенному выше набору:
double max(long array[], int len); // Недопустимая перегрузка
Причина в том, что эта функция для компилятора неотличима от функции с таким
прототипом:
long max(long array[], int len);
Если вы определите функцию вроде этой, компилятор выдаст ошибку:
error С2556: 'double max(long [],int)' : overloaded function differs only
by return type from 'long max (long [],int) '
ошибка C2556: 'double max (long [], int) ' : перегруженная функция отличается
только типом возрата от 'long max (long [],int) '
и программа не скомпилируется. Это может казаться несколько неразумным, пока вы
не вспомните, что можно написать так:
long numbers!] = {1, 2, 3, 3, 6, 7, 11, 50, 40};
int len = sizeof numbers/sizeof numbers[0];
max(numbers, len);
302 Глава 6
Фактически, хотя этот вызов функции шах () не имеет особого смысла, потому
что возвращаемое значение игнорируется, все же он не является незаконным. Если
бы тип возврата служил признаком различия перегруженных функций, то в данном
случае компилятор не мог бы решить, какую версию функции следует вызвать — воз-
вращающую значение long или же возвращающую double. По этой причине тип воз-
врата не рассматривается как средство различия перегруженных функций.
Фактически о каждой функции (не только перегруженной) говорят, что она имеет
сигнатуру, определяемую ее именем и списком параметров. Все функции в програм-
ме должны иметь уникальные сигнатуры, иначе программа не компилируется.
Практическое занятие | ИСПОЛ ЬЗОВЭНИе ПврвГруЖвННЫХ ФУНКЦИЙ
Вы можете испытать средство перегрузки на примере описанной функции шах ().
Следующий пример включает три версии — для массивов типа int, long и double.
// Ех6_07.срр
// Использование перегруженных функций
#include <iostream>
using std::cout;
using std::endl;
int max(int array[], int len); // Прототип для
long max(long array[], int len); // набора перегруженных
double max(double array[], int len); // функций
int main(void)
{
int small[] = {1, 24, 34, 22};
long medium[] = {23, 245, 123, 1, 234, 2345};
double large[] = {23.0, 1.4, 2.456, 345.5, 12.0, 21.0};
int lensmall = sizeof small/sizeof small[0];
int lenmedium = sizeof medium/sizeof medium[0];
int lenlarge = sizeof large/sizeof large[0];
cout « endl « max (small, lensmall);
cout « endl « max (medium, lenmedium) ;
cout « endl « max (large, lenlarge);
cout « endl;
return 0;
}
// Максимум из массива int
int max(int x[], int len)
{
int max = x[0] ;
for (int i = 1; i < len; i++)
if (max < x [i])
max = x [ i ] ;
return max;
}
11 Максимум из массива long
long max (long x[], int len)
{
long max = x [0] ;
for (int i = 1; i < len; i++)
if (max < x [i])
max = x [ i ] ;
return max;
Дополнительные сведения о структурах программ 303
// Максимум из массива double
double max(double x[], int len)
double max = x [ 0 ];
for (int i = 1; i < len; i++)
if (max < x [i])
max = x [ i ];
return max;
Этот пример работает так, как и следовало ожидать, и выдает следующий результат:
34
2345
345.5
Описание полученных результатов
В этом примере представлены три прототипа трех перегруженных версий функ-
ции max (). В каждом из трех операторов вывода компилятором выбирается соответ-
ствующая версия функции max () на основе списка аргументов. Это работает потому,
что каждая из версий функции max () имеет уникальную сигнатуру, так как список ее
параметров отличается от прочих функций max ().
Когда нужно перегружать функции
Перегрузка функций предоставляет вам средство обеспечить, чтобы ее имя опи-
сывало предназначение, не будучи замутненным избыточной информацией о типе об-
рабатываемых данных. Это сродни тому, как работают базовые операции C++. Чтобы
сложить два числа, вы используете одну и ту же операцию, независимо от типа ее
операндов. Наши перегруженные функции max () имеют одно и то же имя, независи-
мо от типа обрабатываемых данных. Это помогает сделать код более читабельным и
облегчает использование функций.
Назначение перегрузки функций понятно: разрешить выполнять одну и ту же операцию с
разными операндами, используя единственное имя функции. Поэтому всякий раз, когда у вас
есть серия функций, которые, по сути, делают одно и то же, но с разными типами аргумен-
тов, вы должны перегрузить их и использовать одно и то же имя.
Шаблоны функций
Последний пример был несколько скучным, потому что пришлось повторить, по
сути, один и тот же код в каждой функции, но с разными переменными и типами
параметров. Однако есть способ избежать этого. У вас имеется возможность выпи-
сать рецепт, по которому компилятор автоматически сгенерирует похожие функции
с различными типами параметров. Код, описывающий такой “рецепт” для генерации
определенной группы функций называется шаблоном функции (function template).
Шаблон функции имеет один или более параметров типа, и вы генерируете опре*
деленную функцию, подставляя аргумент — конкретный тип для каждого из параме-
тров шаблона. Таким образом, все функции, сгенерированные по шаблону, имеют
один и тот же базовый код, но настраиваются типом аргумента, который вы приме-
няете. Как это работает на практике, вы можете увидеть, определив шаблон функции
max () для предыдущего примера.
304 Глава 6
Использование шаблона функции
Шаблон для функции max () можно определить следующим образом:
template<class Т> Т max(T х[], int len)
Т max = х [ 0 ];
for (int i = 1; i < len; i++)
if (max < x [i])
max = x[i];
return max;
Ключевое слово template указывает на то, что это — определение шаблона.
Угловые скобки, следующие за ключевым словом template, заключают в себе пара-
метры типа, используемые для создания определенного экземпляра функции и разде-
ленные запятыми; в данном случае имеется только один параметр типа — Т. Ключевое
слово class перед Т указывает на то, что Т — параметр типа для шаблона; здесь
class — обобщенный термин, означающий “тип”. Позднее в этой книге вы увидите,
что определение класса — это, по сути, определение вашего собственного типа дан-
ных. То есть у вас есть фундаментальные типы C++, такие как тип int и тип char,
и есть типы, определенные вами. Обратите внимание, что вы можете использовать
ключевое слово type name вместо class для идентификации параметров шаблона
функции. В этом случае определение шаблона выглядит так:
template<typename Т> Т max(T х[], int len)
Т max = х [ 0 ];
for (int i = 1; i < len; i++)
if (max < x [i])
max = x [ i ];
return max;
Некоторые программисты предпочитают применять ключевое слово typename,
поскольку слово class ассоциируется с типом, определенным пользователем, в то
время как typename — более нейтральное, а потому легче воспринимается и подраз-
умевает фундаментальные типы наряду с типами, определяемыми пользователем. На
практике оба ключевых слова используются одинаково широко.
Всякий раз, когда Т появляется в определении шаблона функции, оно заменяется
специфическим аргументом типа, таким как long, который вы применяете, создавая
экземпляр функции из шаблона. Если вы попытаетесь сделать вручную, включив long
вместо Т в определение шаблона, то увидите, что это позволит сгенерировать заме-
чательную функцию для поиска максимального значения в массиве типа long:
long max (long x[], int len)
long max = x [0];
for (int i = 1; i < len; i++)
if (max < x [i])
max = x [ i ];
return max;
Создание конкретного экземпляра функции называется реализацией (instantia-
tion) .
Дополнительные сведения о структурах программ 305
Всякий раз, когда вы используете функцию max () в своей программе, компилятор
проверяет, не существует ли уже функция с такими типами аргументов, как вы ис-
пользовали. Если требуемая функция не существует, компилятор создает ее, подстав-
ляя тип аргумента, использованный вами в ее вызове, на место параметра Т по всему
исходному коду в соответствующем определении шаблона. Вы можете поупражняться
в применении шаблона функции max () в той же функции main (), что была использо-
вана в предыдущем примере.
практическое занятие I использование шаблона функции
Ниже приведена версия предыдущего примера, модифицированная с целью ис-
пользования шаблона функции max ().
// Ехб_08.срр
// Использование шаблона функции
#include <iostream>
using std::cout;
using std::endl;
11 Шаблон функции для поиска максимального элемента массива
template<typename Т> Т max (Т х [ ], int len)
{
Т шах - х[0] ;
for (int i = 1; i < len; i++)
if (max < x[i])
max = x[i] ;
return max;
}
int main (void)
{
int small[] = { 1, 24, 34, 22};
ilong medium[] = { 23, 245, 123, 1, 234, 2345};
double large[] = { 23.0, 1.4, 2.456, 345.5, 12.0, 21.0};
int lensmall = sizeof small/sizeof small[0];
int lenmedium = sizeof medium/sizeof medium[0];
int lenlarge = sizeof large/sizeof large[0];
cout « endl « max (small, lensmall) ;
cout « endl « max (medium, lenmedium);
cout « endl « max (large, lenlarge) ;
cout « endl;
return 0;
}
Если вы запустите эту программу, то получите тот же результат, что и в предыду-
щем примере.
Описание полученных результатов
Для каждого из операторов, выводящих максимальное значение из массива, на
базе шаблона создается экземпляр новой версии max (). Конечно, если вы добавите
еще один оператор, вызывающий функцию max () с одним из типов, использованных
ранее, новая версия кода функции не генерируется.
Обратите внимание, что использование шаблона не уменьшило размера скомпи-
лированной программы. Компилятор генерирует версию исходного кода для каждой
функции, которую вы запросили. На самом деле применение шаблонов обычно увели-
306 Глава 6
чивает размер ваших программ, поскольку версии функций создаются автоматически,
даже несмотря на то, что существующие версии могут быть успешно применены с
соответствующим приведением типов аргументов. Вы можете принудительно форси-
ровать создание определенных экземпляров шаблона, явно включив их объявления.
Например, если вы хотите гарантировать создание экземпляра функции шах (), соот-
ветствующего типу float, то можете поместить следующее объявление после опреде-
ления шаблона:
float max(float, int);
Это форсирует создание такой версии шаблонной функции. Для нашего примера
это не имеет особого значения, но может оказаться удобным, когда вы знаете, что
некоторые версии шаблонной функции могут быть сгенерированы, но хотите фор-
сировать генерацию подмножества, которое планируете использовать с приведением
аргументов к соответствующему типу при необходимости.
Пример использования функций
До настоящего момента вы уже получили солидную базу знаний о C++, и только
из одной этой главы узнали достаточно много о функциях. После преодоления вброд
целого моря разнообразных языковых средств не всегда бывает легко увидеть, как
они связаны друг с другом. Теперь настал подходящий момент, чтобы посмотреть, как
некоторые из изученных нами средств можно использовать вместе, чтобы создать не-
что более сложное, чем простенькая демонстрационная программа.
Давайте рассмотрим более реалистичный пример, чтобы увидеть, как одна про-
блема может быть разбита на несколько функций. Процесс включает формулирова-
ние проблемы, которую нужно решить, ее анализ, направленный на прояснение того,
как это можно реализовать на C++, и, наконец, написание кода. Подход к следующему
примеру направлен на то, чтобы проиллюстрировать, как различные функции могут
работать совместно для достижения финального результата, а не представлять руко-
водство по разработке программ.
Реализация калькулятора
Предположим, что вам нужна программа, которая работает подобно калькулято-
ру — но только не одно их этих причудливых устройств с множеством кнопок и шту-
чек, придуманных для тех, кого легко этим порадовать, а такое устройство, которое
предназначено для людей, знающих, куда они идут, выражаясь арифметически. Вы
сможете просто запустить его и инициировать вычисление с клавиатуры как единое
арифметическое выражение, получая результат немедленно. Вот пример того, что
можно будет ввести:
2*3.14159*12.6*12.6 / 2 + 25.2*25.2
Чтобы пока избежать ненужного усложнения, мы не будем разрешать скобки в вы-
ражениях, кроме того, все выражение должно быть введено в одной строке; однако,
чтобы обеспечить пользователю ввод выражений, выглядящих более привлекатель-
но, мы разрешим вставлять куда угодно пробелы. Введенное выражение может содер-
жать операции умножения, деления, сложения и вычитания, представленные, соот-
ветственно, символами *, /, + и -. Выражения будут вычисляться согласно обычным
правилам арифметики, поэтому умножение и деление будут иметь приоритет перед
сложением и вычитанием.
дополнительные сведения о структурах программ
307
Программа должна обеспечивать выполнение стольких последовательных вы-
числений, сколько потребуется, и должна прекращать свою работу при вводе пустой
строки. Кроме того, она должна уметь выдавать полезные и дружественные сообще-
ния об ошибках.
Анализ проблемы
Хорошим местом старта является ввод. Программа читает арифметическое выра-
жение любой длины из одной строки, которое может представлять собой любую кон-
струкцию в пределах описанных терминов. Поскольку никак элементы, составляю-
щие выражение, не фиксируются, вы должны читать его как строку символов, а затем
в программе разбирать и обрабатывать соответствующим образом. Можно решить
произвольно, что строка выражения не превысит по длине 80 символов, поэтому вы
сможете поместить ее в символьный массив, объявленный следующим образом:
const int MAX = 80;
char buffer[MAX];
// Максимальная длина выражения, включая • \0 ’
// Область ввода вычисляемого выражения
Чтобы изменить максимальную длину обрабатываемой программой строки, вам
придется изменить только начальное значение МАХ.
Вам нужно понять базовую структуру информации, поступающей из входной стро-
ки, поэтому давайте разберем ее шаг за шагом.
Для начала надо исключить из строки все лишнее, поэтому перед тем, как присту-
пить к ее анализу, нужно удалить все пробелы. Назовем функцию, которая сделает это,
eat spaces (). Эта функция будет проходить по входному буферу, коим является мас-
сив buffer [ ], и сдвигать символы, перекрывая все пробелы. Этот процесс потребует
двух индексов для буферного массива, i и j, оба начнутся с начала буфера; в общем
случае мы будем помещать элемент j в позицию i. По мере прохода по элементам
массива, всякий раз, находя пробел, будем увеличивать j, не трогая i, так что пробел
в позиции i будет перезаписываться следующим символом, найденным в позиции j,
который не является пробелом. На рис. 6.2 иллюстрируется описанная логика.
Это — процесс копирования содержимого буфера buf fer [ ] в самого себя, исклю-
чая пробелы. На рис. 6.2 показан массив buffer перед процессом копирования и по-
сле него. Стрелки указывают, какие символы копируются и в какие позиции.
После того, как мы удалили пробелы из строки, можно вычислять выражение.
Определим функцию ехрг (), которая вернет результат вычисления полного выраже-
ния из входного буфера. Чтобы решить, что должно происходить внутри функции
ехрг (), необходимо рассмотреть структуру ввода более детально.
Буферный массив перед копированием его содержимого на себя
Индекс] 0 1 2 3 4 5 6 7
Индекс i не увеличивается
в этой позиции, потому что
обнаружен пробел.
Пробелы перезаписыва-
ются следующим непро-
бельным символом,
найденным в буфере
Индекс i 0 1 2 3 4 5
Буферный массив после копирования его содержимого на себя
Рис. 6.2. Изъятие пробелов из введенной пользователем строки
308 Глава 6
Операции сложения и вычитания имеют наименьший приоритет, а потому вы-
полняются последними. Можно рассматривать строку, как состоящую из одного или
более элементов, соединенных между собой операциями, которые могут быть либо +,
либо -. Будем называть эти операции addop. По этой терминологии можно предста-
вить общую форму входного выражения следующим образом:
выражение: элемент addop элемент ... addop элемент
Выражение содержит как минимум один элемент и может иметь произвольное
число следующих за ним комбинаций addop элемент. Фактически, если предполо-
жить, что удалены все пробелы, существует только три легальных возможности по-
явления символов, следующих за элементом.
□ Следующий символ — 1 \ 01, то есть, обнаружен конец строки.
□ Следующий символ — • - •. В этом случае нужно вычесть следующий элемент из
значения выражения, полученного до этой точки.
□ Следующий символ — • +1. В этом случае нужно прибавить значение следующе-
го элемента к значению выражения, накопленного до этого момента.
Если за элементом следует что-то другое, значит, строка содержит нечто неожи-
данное, поэтому следует выдать сообщение об ошибке и завершить выполнение про-
граммы. На рис. 6.3 показана структура простого выражения.
Рис. 6.3. Структура простого выражения
Далее нам понадобится более детальное и точное определение элемента.
Элемент — это просто последовательность чисел, соединенных операцией *, либо
операцией /. Таким образом, элемент (в общем виде) выглядит так:
элемент: число multop число ... multop число
Здесь multop представляет либо операцию умножения, либо операцию деления.
Можно определить функцию term (), которая будет возвращать значение элемента.
Для этого потребуется сканировать строку до обнаружения первого числа, а затем
найти mu 1 top, за которым следует другое число. Если будет обнаружен символ, не яв-
ляющийся mu 1 top, то функция term() должна предположить, что это addop, и вер-
нуть значение, которое было вычислено до этого момента.
Последнее, что нам нужно решить перед тем, как писать программу — это как рас-
познать число. Чтобы упростить код, будем распознавать только беззнаковые числа;
таким образом, число состоит из последовательности десятичных цифр, за которыми
необязательно может следовать десятичная точка и еще несколько десятичных цифр.
Чтобы определить значение числа, следует пройти по буферу в поисках десятичных
цифр. Если обнаружится нечто, не являющееся десятичным числом, нужно прове-
рить, не является ли это нечто десятичной точкой. Если это не точка, то с числом
Дополнительные сведения о структурах программ 309
далее делать нечего, поэтому должно быть возвращено то, что получено до этого мо-
мента. Если же обнаружится десятичная точка, потребуется продолжить поиск деся-
тичных цифр. Как только будет найдено нечто, не являющееся десятичным числом,
значит, следует завершить формирование числа и вернуть его.
Предположим, что мы назовем функцию, распознающую число и возвращающую
его значение, как number (). На рис. 6.4 показан пример разбиения выражения на
элементы и числа.
Элемент addop
Число
Цифра Цифра
Выражение
Элемент
addop Элемент
Значение выражения, возвращаемое
функцией ехрг()
Значение каждого элемента, возвращаемое
функцией term()
Значение каждого числа, возвращаемое
функцией number()
Конец ввода
Рис. 6.4. Пример разбиения выражения на элементы и числа
Теперь мы достигли достаточного понимания проблемы, чтобы написать некото-
рый код. Сначала разработаем все необходимые функции, а затем напишем функцию
main (), в которой соберем их все вместе. Первая, и, возможно, самая простая функ-
ция, которую потребуется написать — это eat spaces (), которая исключит все про-
белы из входной строки.
Удаление пробелов из строки
Прототип функции eat spaces () будет таким:
void eatspaces(char* str); // Функция для удаления пробелов
Эта функция не должна возвращать никаких значений, потому что пробелы мож-
но удалить из строки по месту, модифицируя исходную строку непосредственно, че-
рез указатель, переданный в качестве аргумента. Процесс удаления пробелов очень
прост. Вы копируете строку в саму себя, перезаписывая все пробелы, как объяснялось
выше.
Определение функции может выглядеть следующим образом:
// Функция для удаления пробелов из строки
void eatspaces(char* str)
int i = 0; // Индекс места в
int j = 0; // Индекс места в
while((*(str + i) = * (str + j++)) != ’\0’)
строке, ’куда копировать’
строке, ’откуда копировать’
// Цикл, пока очередной
// символ не ’ \0’
return;
// Инкремент i до тех пор,
// пока символ — не пробел
310 Глава 6
Как работает функция
Все действие происходит в цикле while. Условие цикла копирует строку, переме-
щая символ из позиции j в позицию i, а затем увеличивая j до следующего символа.
Если скопирован символ ’ \ 0 ', значит, достигнут конец строки и работа окончена.
Единственное действие в теле цикла — увеличение i до следующего символа, если
последний скопированный символ не был пробелом. Если же этот символ — пробел,
инкремент i не выполняется, и потому пробел будет перезаписан символом, скопиро-
ванным на следующей итерации.
Не так уж трудно, не правда ли? Теперь попробуем написать функцию, которая
вернет результат вычисления выражения.
Вычисление выражения
Функция ехрг () возвращает значение выражения, специфицированного в строке,
переданной в качестве аргумента, поэтому вы можете написать ее прототип следую-
щим образом:
double ехрг(char* str); // Функция, вычисляющая выражение
Объявленная здесь функция принимает строку как аргумент и возвращает резуль-
тат типа double. На основе структуры выражения, которую мы исследовали ранее,
можно нарисовать логическую диаграмму процесса вычисления выражения, как по-
казано на рис. 6.5.
Используя это базовое описание логики, можем написать функцию:
// Функция для вычисления арифметического выражения
double ехрг(char* str)
double value = 0.0; // Здесь сохранять результат
int index =0; // Текущая позиция символа
value = term(str, index); // Получить первый элемент
for(;;) // Бесконечный цикл, выход - внутри
switch(*(str + index+t)) // Выбрать действие на основе текущего символа
case
case
• \0 ’:
return value;
найден конец строки,
поэтому возвращаем то, что имеем
value += term(
break;
найден знак плюс
index)
значит - сложение
следующий элемент
case ’-’:
value -= term
break;
default:
cout « endl
// найден знак минус, значит - вычитание
index); //
следующий элемент
// Если то, что найдено в строке -
// мусор
"Эй, повнимательней можно?! Здесь обнаружена ошибка"
endl;
exit (1);
Дополнительные сведения о структурах программ
311
Рис. 6.5. Логическая диаграмма процесса вычисления выражения
Как работает функция
Учитывая, что эта функция выполняет анализ переданного ей арифметическо-
го выражения (при условии, что в нем используются только описанные операции),
в ней не так много кода. Мы определили переменную index типа int для отслежи-
вания текущей позиции в строке, с которой мы работаем, и инициализировали ее
значением 0, что соответствует позиции первого символа строки. Также определена
переменная value типа double, в которой накапливается значение выражения, пере-
данного в функцию в символьном (типа char) массиве str.
312 Глава 6
Поскольку выражение должно содержать как минимум один элемент, первое
действие этой функции — получение значения первого элемента вызовом функции
term (), которую еще предстоит написать. Это позволяет сразу сформулировать три
требования к функции term ().
1. Она должна принимать в качестве параметров указатель char* и переменную
int, причем второй параметр — это индекс первого символа элемента в строке.
2. Она должна обновлять переданное ей значение индекса, устанавливая его в по-
зицию, следующую за последним символом найденного элемента.
3. Она должна возвращать значение элемента типа double.
Остальная часть программы — это бесконечный цикл for. Внутри цикла текущее
действие определяется оператором switch, который управляется текущим символом
строки. Если этот символ — • + •, вызывается функция term (), чтобы получить значе-
ние следующего элемента выражения и прибавить его к переменной value. Если же
этот символ — ’ - ’, то значение, возвращенное term (), вычитается из value. Если
очередной символ строки — • \ 0 •, значит, мы достигли ее конца, поэтому возвращаем
текущее значение value вызывающей программе. Если же вдруг очередной символ
строки окажется чем-то другим, чем он быть не должен, программа извещает об этом
пользователя и немедленно завершается.
Цикл продолжается до тех пор, пока в строке находятся либо 1 + •, либо 1 - •.
Каждый вызов term () перемещает значение index на символ, следующий за вычис-
ленным выражением, и этот символ должен быть либо • + •, либо • - ’, либо символом
конца строки 1 \0 •. Таким образом, функция либо завершается нормально по дости-
жении ’ \0 •, либо ненормально — вызовом exit (). Когда мы соберем всю програм-
му вместе, нужно не забыть включить директиву #include для заголовочного файла
<cstdlib>, в котором приведено определение функции exit ().
Можно было бы проанализировать арифметическое выражение с использовани-
ем рекурсивной функции. Если представить себе определение выражения несколько
иначе, то можно специфицировать его либо как просто элемент, либо как элемент,
за которым следует выражение. Такое определение является рекурсивным (то есть,
определение включает понятие, которое должно быть определено), и такой подход
очень часто применяется в структурах языков программирования. Это определение
представляет почти столько же гибкости, что и первое, но, используя его в качестве
базовой концепции, вы можете разработать рекурсивную версию ехрг () вместо при-
менения цикла, как это сделано в приведенной реализации. После того, как мы по-
кончим с первой версией, вы можете попробовать написать альтернативный вариант
этого примера в качестве самостоятельного упражнения, используя рекурсию.
Получение значения элемента
Функция term () возвращает значение элемента типа double и принимает два ар-
гумента: анализируемую строку и индекс текущей позиции в ней. Есть и другие спо-
собы сделать то же самое, но приведенный вариант достаточно показателен. Таким
образом, прототип функции term () будет выглядеть следующим образом:
double term(char* str, int& index); // Функция для анализа элемента
Мы специфицируем второй аргумент как ссылку. Это необходимо для того, чтобы
функция могла модифицировать значение переменной index из вызывающей про-
граммы, изменяя текущую позицию в строке на символ, следующий немедленно по-
еле найденного элемента. Можно было бы вернуть значение index как значение, но
тогда пришлось бы другим способом возвращать значение элемента, поэтому предло-
женный вариант выглядит достаточно естественным.
Логика анализа элемента подобна структуре выражения. Элемент — это число, за
которым потенциально может следовать одна или более комбинаций операции умно-
жения или деления и другого числа. Определение функции term () можно написать
так:
// Функция, вычисляющая значение элемента
double term(char* str, int& index)
double value =0.0; // Здесь накапливается значение результата
value = number(str, index); // Получить первое число элемента
// Выполнять цикл, пока имеем допустимую операцию
while((*(str + index) ==•*•) || (*(str + index) == ’/’))
if (* (str + index) == '*')
value *= number(str, ++index);
if(*(str + index) == '/’)
value /= number(str, ++index);
// Если это знак умножения,
// умножить на следующее число
// Если знак деления,
// разделить на следующее число
return value;
// Готово, возвращаем то, что получилось
Как работает функция
Сначала мы объявляем локальную переменную value типа double, в которой бу-
;ем накапливать значение текущего элемента. Поскольку элемент должен содержать,
как минимум, одно число, первое действие этой функции — получение значения пер-
вого числа элемента вызовом функции number () и сохранением результата в value.
Предполагается, что number () в качестве аргументов принимает строку и индекс по-
зиции в строке, а возвращает значение найденного числа. Поскольку number () может
обновить индекс позиции в строке после того, как прочтет в ней число, при опреде-
лении этой функции мы опять-таки специфицируем второй параметр как ссылку.
Остальная часть функции term () — это цикл while, который продолжается до тех
пор, пока следующий символ строки будет знаком умножения — • * •, либо деления —
’ / ’. Внутри цикла, если в текущей позиции найден ’ * ’, значение index увеличива-
ется до позиции начала следующего числа, вызывается функция number (), чтобы по-
лучить значение следующего числа, после чего текущее значение value умножается
на возвращенное значение. Аналогично, если текущий символ — • /•, значение index
также увеличивается, а значение value делится на число, возвращенное number ().
Поскольку функция number () автоматически изменяет значение переменной index,
так что оно устанавливается на символ, следующий за найденным числом, index ока-
зывается установленным на следующий символ строки, и все готово к следующей ите-
рации.
Цикл прерывается, когда обнаруживается символ, отличный от знаков умножения
и деления, и накопленное текущее значение value возвращается вызывающей про-
грамме.
Последняя аналитическая функция, которая нам понадобится — это number (),
определяющая очередное содержащееся в строке числовое значение.
314 Глава 6
Анализ числа
Основываясь на способе использования функции number () внутри функции
term (), объявим ее прототип:
double number(char* str, int& index); // Функция, распознающая число
Спецификация второго параметра как ссылки позволяет функции обновлять зна-
чение переданного ей аргумента непосредственно, что и требовалось.
Здесь можно использовать функцию из стандартной библиотеки C++. Заголо-
вочный файл <cctype> предоставляет определение диапазона функций для тестиро-
вания отдельных символов. Эти функции возвращают значения типа int, причем по-
ложительные значения соответствуют true, а ноль — false. Четыре из этих функций
перечислены в табл. 6.1.
Таблица 6.1. Некоторые функции из <cctype>
Функция из <cctype> Результат тестирования отдельных символов
int isalpha(int с)
int isupper(int с)
int islower(int с)
int isdigit(int c)
Возвращает true, если аргумент — буква, иначе — false.
Возвращает true, если аргумент — буква верхнего регистра, иначе — false.
Возвращает true, если аргумент — буква нижнего регистра, иначе — false.
Возвращает true, если аргумент — десятичная цифра, иначе — false.
Существует множество других функций, представленных <cctype>, но пока я не хотел бы
погружаться в детали. Если интересуетесь, загляните в справочную систему MSDN Library
Help. Поиск по строке “is routines” должен найти их.
Итак, нам осталось написать последнюю функцию для программы. Запомните, что
isdigit () проверяет символ, например, такой как ’ 9 ’ (ASCII-код 57 в десятичной
нотации), а не число 9, поскольку на входе появляется строка.
Определение numbe г () может выглядеть следующим образом:
// Функция для распознавания числа в строке
double number(char* str, int& index)
double value =0.0; // Хранит результирующее значение
while(isdigit(*(str + index))) // Цикл накапливает ведущие цифры
value = 10*value + (*(str + index++) - '0');
if (* (str + index) != 1 . ’)
return value;
Если не цифра,
то проверяем на десятичную точку,
и если это не точка, возвращаем значение
double factor = 1.0;
// Множитель для десятичных разрядов
while(isdigit(*(str + (++index))))
factor *= 0.1;
value = value + (*(str + index) -
// Выполнять цикл, пока идут цифры
// Уменьшить factor в 10 раз
'0’)*factor; // Добавить десятичную
// позицию
return value;
//По выходе из цикла возвращаем значение
Дополнительные сведения о структурах программ 315
Как работает функция
Объявляем локальную переменную value типа double, которая будет хранить зна-
чение найденного числа. Инициализируем ее 0.0, поскольку значение будет накапли-
ваться по мере чтения цифр.
Поскольку число представлено в строке как последовательность символов ASCII,
функция проходит по строке, накапливая значение числа разряд за разрядом. Это
происходит в две фазы — в первой накапливаются цифры до десятичной точки, а по-
сле обнаружения десятичной точки начинается вторая фаза — накопление разрядов
после нее.
Первый шаг в цикле while продолжается до тех пор, пока текущий выбранный
символ строки является десятичной цифрой. Значение цифры извлекается и прибав-
ляется к значению переменной value в теле цикла:
value = 10*value + (*(str + index++) - '0');
Эта конструкция требует более пристального рассмотрения. Символы десятичных
цифр имеют ASCII-коды между 48, что соответствует 10 •, и 57, соответствующий 1 91.
Таким образом, если вычесть ASCII-код символа ’ 0 ’ из кода цифры, это преобразует
ее в эквивалентное число от 0 до 9. Подвыражение * (str + index++) - ’ 0 ’ взято
в скобки; это не существенно, но скобки несколько проясняют то, что здесь проис-
ходит. Значение переменной value умножается на 10, чтобы сдвинуть его на один
десятичный разряд влево перед тем, как добавить значение цифры, поскольку они
выбираются слева направо — то есть, самая значащая цифра идет первой. Описанный
процесс показан на рис. 6.6.
цифры в числе
ASCII-коды как десятичные значения
Начальное значение = 0
Первая
цифра
value = 10*value + (53 - 48)
= 10*0.0 + 5
= 5.0
53 49 51
Вторая
цифра
value = 10*value + (49 - 48)
= 10*5.0+1
= 51.0
Третья
цифра
value = 10*value + (51 - 48)
= 10*51.0 + 3
= 513.0
Puc, 6.6, Получение целой части десятичного числа из строки
316 Глава 6
Как только встречается что-то, отличное от десятичной цифры, это будет либо де-
сятичная точка, либо нечто другое. Если это не десятичная точка, обработка заверша-
ется, и в вызывающую программу возвращается значение переменной value. Если же
очередной символ — десятичная точка, то в следующем цикле начинается накопление
цифр, составляющих дробную часть числа. В этом цикле используется переменная
factor, имеющая начальное значение 1.0, чтобы устанавливать текущую десятичную
позицию, и эта переменная последовательно умножается на 0.1 при каждой найден-
ной цифре после точки. То есть, первая цифра после точки умножается на 0.1, вто-
рая — на 0.01, третья — на 0.001 и так далее. Этот процесс показан рис. 6.7.
десятичные цифры
целой части числа
десятичные цифры
дробной части числа
ASCII-коды как десятичные значения
До десятичной точки
value = 513.0
factor = 1.0
Первая
цифра
factor = 0.1 *factor ▼
value = value + factor*(54 - 48)
= 513.0 + 0.1*6
= 513.6
Вторая
цифра
factor = 0.1 ‘factor ▼
value = value + factor*(49 - 48)
= 513.6 + 0.01*0
= 513.60
Третья
цифра
factor = 0.1‘factor v
value = value + factor*(56 - 48)
= 513.60 + 0.001*8
= 513.608
Puc. 6.7. Получение дробной части десятичного числа из строки
Как только обнаруживается символ, не являющийся десятичным числом, обработ-
ка завершается, поэтому после второго цикла значение переменной value возвра-
щается в вызывающую программу. Теперь почти все готово. Осталось написать лишь
функцию main (), чтобы прочесть ввод и запустить процесс.
Собираем программу вместе
Мы можем собрать вместе все операторы #include и прототипы всех функций в
начале программы:
// Ехб_09.срр
// Программа, реализующая калькулятор
#include <iostream> // Для потокового ввода-вывода
#include <cstdlib> // Для функции exit()
#include <cctype> // Для функции isdigitO
Дополнительные сведения о структурах программ 317
1
using std::cin;
using std::cout;
using std::endl;
void eatspaces(char* str);
double expr(char* str);
double term(char* str, int& index);
double number(char* str, int& index);
// Функция удаления пробелов
// Функция вычисления выражения
// Функция анализа элемента
// Функция распознавания числа
const int MAX = 80;
// Максимальная длина выражения, включая ’ \0 ’
Мы также определили глобальную переменную МАХ, хранящую максимальное ко-
личество символов в выражении, обрабатываемом программой (включая ограничива-
ющий символ ’ \ 0').
Теперь мы можем добавить определение функции main (), и программа будет го-
това. Функция main () должна читать строку и завершаться, если эта строка пуста, в
противном случае вызывать ехрг () для вычисления ввода и отображения результата.
Этот процесс должен повторяться бесконечно. Звучит несколько сложно, поэтому да-
вайте попытаемся реализовать сказанное.
int main ()
char buffer[MAX] = {0}; // Область хранения вычисляемого входного выражения
cout « endl
« "Добро пожаловать в дружественный калькулятор!"
« endl
« "Введите выражение или пустую строку для завершения.
« endl;
cin.getline(buffer, sizeof buffer); // Читать входную строку
eatspaces(buffer); // Удалить пробелы из строки
if(!buffer[0]) // Пустая строка — признак завершения работы
return 0;
cout « "\t= " « ехрг(buffer) // Вывести значение выражения
« endl « endl;
Как работает функция
В main () объявляется символьный массив buffer, который будет принимать вы-
ражения до 80 символов длиной (включая символ — ограничитель строки). Выражение
читается внутри бесконечного цикла for посредством функции ввода getline (), и по-
сле получения ввода с помощью вызова eatspaces () из строки удаляются пробелы.
Все остальное, что делается в функции main (), происходит внутри цикла. Здесь
проверяется пустая строка, которая состоит из нулевого символа • \ 0 ’ — в этом слу-
чае программа завершается, и здесь же выводится значение вычисленного функцией
ехрг () выражения.
После того, как в программу будет введен код всех функций и программа скомпи-
лирована, то запустив ее, мы получим:
2/3 + 3/4 + 4/5 + 5/6 + 6/7
= 3.90714
318 Глава 6
Вы можете выполнять столько вычислений, сколько захотите, и когда надоест —
просто нажмите <Enter> для завершения программы.
Расширение программы
Теперь, когда мы имеем работающий калькулятор, можно подумать о его усовер-
шенствовании. Не правда ли, было бы неплохо, если бы он понимал скобки? Это же
должно быть не сложно? Давайте попробуем.
Подумаем об отношении между тем, что находится в скобках, и способом анализа
выражений, использованном до сих пор. Возьмем пример выражения, которое хоте-
лось бы обрабатывать:
2*(3 + 4) / 6 - (5+6) / (7+8)
Обратите внимание, что выражение в скобках всегда формирует часть, которую
можно воспринимать как элемент в нашей исходной терминологии. Какого бы рода
вычисления ни пришлось выполнять, это всегда верно. Фактически, если мы сможем
подставлять значение выражений в скобках обратно в исходную строку, то получим
нечто такое, с чем мы уже имели дело. Это указывает на возможный подход к обра-
ботке скобок. Выражение в скобках можно трактовать как еще одно число, и модифи-
цировать функцию number () таким образом, чтобы она упорядочивала любое значе-
ние, появляющееся в скобках.
Это звучит, как неплохая идея, но об “упорядочивании” выражения в скобках
нужно немного задуматься: ключ к успеху лежит в используемой здесь терминоло-
гии. Выражение, появляющееся в скобках — исключительно удачный пример полно-
ценного выражения, а у нас уже есть функция ехрг (), которая возвращает значения
выражений. Если мы научим функцию number () обрабатывать содержимое скобок и
извлекать его из строки, то можно будет передавать эту подстроку функции ехрг (),
то есть рекурсия должна действительно упростить проблему. Что еще лучше — не при-
дется заботиться о вложенных скобках. Поскольку любая пара скобок содержит то,
что определяется как выражение, их обработка выполнится автоматически. Опять
побеждает рекурсия.
Попробуем переписать функцию number () так, чтобы она распознавала выраже-
ния в скобках.
// Функция для распознавания выражения
//в скобках или числа в строке
double number(char* str, int& index)
double value = 0.0;
if (* (str + index) == • (•)
char* psubstr = 0;
// Хранит результирующее значение
// Открытие скобки
// Указатель на подстроку
psubstr = extract(str,
value = ехрг(psubstr);
delete[]psubstr;
return value;
++index); // Извлечь подстроку в скобках
//
//
И
Получить значение выражения-подстроки
Освободить память в свободном хранилище
Вернуть значение выражения-подстроки
while(isdigit(*(str + index))) // Накапливать в цикле ведущие цифры
value = 10*value + (*(str + index++) - 48);
// Обнаружена не цифра
if(*(str + index)!=’.’) // Проверка на равенство десятичной точке
return value; // Если не точка, вернуть значение
Дополнительные сведения о структурах программ 319
double factor =1.0; // Множитель для десятичных разрядов
while(isdigit(*(str + (++index)))) // Цикл по оставшимся цифрам
factor *=0.1; // Уменьшить factor в 10 раз
value = value + (*(str + index) - 48) *factor;//Добавить десятичный разряд
}
return value; //По выходу из цикла обработка закончена
Пока это еще не все, поскольку нужна функция extract (), но пока отложим ее
рассмотрение.
Как работает функция
Обратите внимание, насколько мало понадобилось изменить, чтобы добавить
поддержку скобок. Хотя в использовании функции, которая еще не была написана
(extract ()) есть определенный момент мошенничества, но все-таки всего лишь одна
добавочная функция позволяет обработать любое количество вложенных скобок. Это
на самом деле — сливки на пирожном, и в этом проявляется волшебство рекурсии!
Первое, что делает обновленная функция number () — проверяет символ на равен-
ство символу левой скобки. Если таковая обнаружена, вызывается другая функция —
extract (), служащая для извлечения подстроки, заключенной в скобки из исходной
строки. Адрес этой подстроки сохраняется в указателе psubstr, так что вы можете
затем применить ехрг (), передавая ей этот указатель в аргументе. Результат сохра-
няется в value, и после освобождения памяти, выделенной в свободном хранилище
в функции extract () (в конце концов, мы ее реализуем), возвращается как обычное
число. Конечно, если открывающая скобка не найдена, функция number () работает,
как и ранее.
Извлечение подстроки
Теперь нам нужно написать функцию extract (). Это не трудно, но и не так три-
виально. Главная сложность происходит от того факта, что выражение в скобках
может также содержать другие множества скобок, поэтому нельзя просто так искать
первую закрывающую скобку. Нужно отследить вложенные левые скобки и проигно-
рировать соответствующие им правые. Это можно сделать, установив счетчик левых
скобок, который будет расти при встрече каждой открывающей скобки в подстроке.
Если значение счетчика не равно нулю, то при каждом появлении правой скобки, его
следует уменьшать на единицу. Конечно, если счетчик левых скобок равен нулю, и вы
встречаете правую, это означает, что достигнут конец подстроки. Механизм извлече-
ния подстрок, заключенных в скобки, показан на рис. 6.8.
Поскольку извлеченная строка содержит подвыражения, заключенные в скобки,
extract () нужно вызвать для каждого из них.
Функция extract () также нуждается в выделении памяти для подстроки и возвра-
щает указатель на нее. Конечно, индекс текущей позиции в оригинальной строке дол-
жен в конце быть установлен на символ, следующий за подстрокой, поэтому параметр
индекса должен быть ссылкой. Таким образом, прототип extract () будет выглядеть
следующим образом:
char* extract(char* str, int& index); // Функция для извлечения подстроки
320
Глава 6
Отмечает начало подстроки
Обнаружение ’)’ при значении счетчика V
равным 0 указывает на достижение
конца подстроки в скобках
Исходная строка выражения
счетчик ’('0 0 0 0 0
0
ООО
0
копирование
замена на ’\0’
) \0
Подстрока, находившаяся в скобках
Рис. 6.8. Механизм извлечения подстрок, заключенных в скобки
Теперь можно определить и саму функцию:
/ / Функция для извлечения подстроки, заключенной в скобки
// (требует заголовочного файла <cstring>)
char* extract(char* str, int& index)
char buffer[MAX]; // Временное пространство для подстроки
char* pstr = 0; // Указатель на новую строку для возврата
int numb =0; // Счетчик найденных левых скобок
int bufindex = index; // Сохранить начальное значение index
do
buffer[index - bufindex] = *(str + index);
switch(buffer[index - bufindex])
case ')•:
if (numb == 0)
buffer[index - bufindex] = ’\0’; // Заменить ’) ’ на '\0 ’
++index;
pstr = new char [index - bufindex];
if(’pstr)
cout « ’’Выделение памяти не удалось,”
« " программа прервана. ";
exit(1);
strcpy_s(pstr,
return pstr;//
else
numL—; //
break;
case '(’:
numL++; //
break;
index~bufindex, buffer);//Копировать подстроку
//в новую память
Вернуть подстроку в новой памяти
Уменьшить значение
Увеличить значение
счетчика
счетчика ’ (’
} while (*(
+ index++) •= ’ \0’); // Цикл до конца строки
Дополнительные сведения о структурах программ 321
cout « "Выход за пределы выражения, возможно, плохой ввод.
« endl;
return pstr;
Как работает функция
Здесь объявлен массив char для временного хранения подстроки. Мы не знаем, на-
сколько длинной будет подстрока, но она не может быть длиннее, чем МАХ символов.
Мы не можем вернуть адрес buffer вызывающей функции, потому что это локальная
переменная, которая уничтожается при выходе из функции, поэтому нужно выделить
некоторое пространство памяти из свободного хранилища, когда становится ясно,
какова длина строки. Это делается объявлением переменной pstr типа “указатель на
char”, который будет возвращен по значению, когда подстрока будет скопирована в
память из свободного хранилища.
Мы объявляем счетчик numL, который отслеживает левые скобки в подстроке (как
я объяснил выше). Начальное значение index (когда функция начинает работу) со-
храняется в переменной buf index. Она используется в комбинации с увеличенным
значением index для индексации массива buffer.
Исполняемую часть функции составляет один большой цикл do-while. Подстрока
копируется из str в buffer по одному символу за каждую итерацию цикла, с провер-
кой появления левых и правых скобок на каждом шаге. Если обнаружена левая скоб-
ка, значение numL увеличивается, а если правая, и при этом numL больше нуля, то ее
значение уменьшается. Если же при обнаружении правой скобки значение numL равно
О, то это значит, что достигнут конец подстроки. Затем содержимое buffer копирует-
ся в область памяти, полученную операцией new, посредством функции strcpy_s (),
объявленной в заголовочном файле <cstring>; это безопасная версия функции
strep у (), объявленной в том же заголовочном файле. Эта функция копирует стро-
ку, специфицированную третьим аргументом — buffer, в место, указанное адресом в
первом аргументе — pstr. Второй аргумент — это длина строки назначения pstr.
Если получается “провал” за пределы цикла, это означает, что встретился символ
' \ 0' в конце выражения str ранее, чем была найдена закрывающая правая скобка,
поэтому выдается соответствующее сообщение и программа прерывается.
Запуск модифицированной программы
После замены функции number () из старой версии программы, добавления опера-
тора #include для заголовка <cstring> и включения прототипа и определения толь-
ко что написанной новой функции extract (), все готово для запуска калькулятора,
умеющего “петь и танцевать”. Если программа будет собрана без ошибок, то при ее
запуске получим нечто вроде следующего:
Добро пожаловать в дружественный калькулятор!
Введите выражение или пустую строку для завершения.
1/(1+1/(1+1/(1+1)))
= 0.6
(1/2-1/3)*(1/3-1/4)*(1/4-1/5)
= 0.000694444
3.5*(1.25-3/(1.333-2.1*1.6))-1
= 8.55507
2,4-3.4
Эй, повнимательней можно?! Здесь обнаружена ошибка
322 Глава 6
Дружественное и информативное сообщение об ошибке в последней строке выво-
да вызвано применением запятой вместо десятичной точки в последнем введенном
выражении, где должно было быть 2.4. Как видите, мы получили возможность об-
работки скобок любого уровня вложенности за счет относительно простого расшире-
ния программы — и все это благодаря невероятной мощи рекурсии.
Программирование C++/CLI
Почти все, что было сказано до сих пор относительно функций “родного” C++,
в равной степени применимо к коду на языке C++/CLI, правда, с одной оговоркой:
типы параметров и типы возврата будут относиться к фундаментальным типам, кото-
рые, как вы знаете, эквивалентны типам классов значений в программе CLR, типам
отслеживаемых дескрипторов или типам отслеживаемых ссылок. Поскольку не раз-
решается применять арифметические действия к адресам, хранимым в отслеживае-
мых дескрипторах, к массивам C++/CLI не применима продемонстрированная выше
техника кодирования, в которой параметры-массивы родного C++ трактуются как ука-
затели, над которыми можно совершать арифметические операции. Многие сложно-
сти, связанные с аргументами функций родного C++, исчезают, но остаются ловушки,
связанные с непродуманным применением C++/CLI. CLR-версия калькулятора помо-
жет понять, как выглядят функции, написанные на C++/CLI.
Механизм исключений throw и catch работает в программах CLR точно так же,
как в программах на родном C++, хотя существуют и некоторые отличия. Исключения,
которые возбуждаются в программах C++/CLI, должны всегда использовать отслежи-
ваемые дескрипторы. Следовательно, вы всегда должны возбуждать объекты исклю-
чений и насколько возможно, избегать при этом литералов — особенно строковых.
Например, рассмотрим следующий код try-cat ch:
throw Ь"Поимаи меня, если сможешь.
catch(String* ex) // Исключение здесь не будет перехвачено
Console::WriteLine(L"StringA:
Блок catch не может здесь перехватить возбужденный объект, потому что опера-
тор throw генерирует исключение типа const wchar_t *, а не String*. Чтобы пере-
хватить такое исключение, блок catch должен быть таким:
throw L”Поймай меня, если сможешь
catch (const wchar__t* ex) //OK. Возбуждено исключение этого типа
{
String* exc = gcnew String (ex);
Console::WriteLine (L"wchar_t: {0}", exc);
Этот блок catch перехватывает исключение, поскольку теперь имеет правильный
тип.
Чтобы исключение было перехвачено исходным блоком catch, следовало бы из-
менить код блока try:
Дополнительные сведения о структурах программ 323
throw gcnew String(Ь"Поймай меня, если сможешь.");
catch(String* ex) // ОК. Возбуждено исключение этого типа
Console::WriteLine(L"String*: {0}", ex);
Теперь исключение представляет собой объект String, и брошено, как тип
String*, — дескриптор, ссылающийся на строку.
Вы можете использовать шаблоны функций в программах C++/СИ, но в вашем рас-
поряжении также имеется дополнительная возможность, называемая обобщенными
функциями (generic functions), которые выглядят похоже, однако представляют со-
бой нечто отличающееся.
Что такое обобщенные функции
Хотя обобщенные функции делают то же самое, что и шаблоны функций, а пото-
му на первый взгляд выглядят излишними, все же, по сути, они работают иначе, чем
шаблоны функций, и это отличие делает их полезным дополнением к программам
CLR. Когда вы применяете шаблон функции, компилятор генерирует исходный код
конкретных требуемых экземпляров функций; затем этот генерированный код ком-
пилируется вместе с остальным кодом вашей программы. В некоторых случаях это
приводит к генерации большого числа функций и существенно увеличивает размер
исполняемого кода. С другой стороны, код обобщенной функции сам по себе ком-
пилируется, и когда вызывается функция, отвечающая спецификации обобщенной
функции, действительные параметры типа подставляются во время выполнения. Во
время выполнения уе генерируется никакого дополнительного кода, отсюда не про-
исходит такого “разбухания” кода, как в случае применения шаблонов функций.
Некоторые аспекты определения обобщенных функций требуют дополнительных
знаний, которые будут изложены в последующих главах, и в конечном итоге вам все
станет ясно. Пока что я приведу минимум объяснений тех вещей, которые покажутся
новыми, а детали вы изучите позднее.
Определение обобщенных функций
Обобщенные функции определяются с параметрами типов, которые заменяются
действительными типами при вызове функций. Ниже показан пример определения
обобщенной функции.
generic<typename Т> where Т:IComparable
Т MaxElement(array<T>* х)
Т max = х [ 0 ];
for (int i = 1; i < x->Length; i++)
if(max->CompareTo(x[i]) < 0)
max = x [ i ];
return max;
Эта обобщенная функция делает то же самое, что и шаблон функции родного C++,
который вы видели ранее в этой главе. Ключевое слово generic в первой строке
идентифицирует последующую обобщенную спецификацию, и первая строка опреде-
ляет параметр типа функции как Т; ключевое слово typename в угловых скобках гово-
324 Глава 6
рит о том, что Т — имя параметра типа обобщенной функции, и этот параметр типа
заменяется действительным типом при использовании данной функции. Для обоб-
щенной функции с несколькими параметрами типа имена параметров заключаются в
угловые скобки, снабжаются ключевым словом type name и отделяются друг от друга
запятыми.
Ключевое слово where, следующее за закрывающей угловой скобкой, представля-
ет ограничение (constraint), накладываемое на действительный тип, подставляемый
вместо Т при вызове функции. Это конкретное ограничение говорит о том, что лю-
бой тип, который заменяет Т при вызове обобщенной функции, должен реализовы-
вать интерфейс I Comparable. Об интерфейсах вы узнаете позднее в этой книге, а
пока я скажу, что он предполагает, что подставляемый тип должен определять функ-
цию CompareTo (), которая позволяет сравнивать два объекта данного типа. Без это-
го ограничения компилятор не может знать, какие операции можно использовать с
типом, заменяющим Т, потому что до того, как функция будет вызвана, это остает-
ся совершенно неизвестным. С этим ограничением вы можете использовать функ-
цию CompareTo () для сравнения max со значениями элементов массива. Функция
CompareTo () возвращает целочисленное значение, которое меньше нуля, если зна-
чение объекта, для которого она вызвана (в данном случае — max), меньше значения
аргумента, ноль, если значение объекта равно значению аргумента, и больше ноля,
если значение объекта больше значения аргумента.
Вторая строка специфицирует имя обобщенной функции MaxElement, которая
возвращает тип Т, и список ее параметров. Это выглядит почти как заголовок обыч-
ной функции, с тем отличием, что здесь участвует обобщенный параметр типа Т. Тип
возврата обобщенной функции и тип элемента массива являются частью специфика-
ции параметра типа, и оба имеют тип Т, поэтому оба эти типа определяются в мо-
мент вызова обобщенной функции.
Использование обобщенных функций
Простейший способ вызова обобщенной функции — просто использовать ее, как
любую другую обычную функцию. Например, обобщенную функцию MaxElement ()
из предыдущего раздела можно вызвать следующим образом:
array<double>^ data = {1.5, 3.5, 6.7, 4.2, 2.1};
double maxData = MaxElement (data);
Компилятор может определить, что в данном случае аргумент обобщенной функ-
ции имеет тип double, и генерирует соответствующий код для ее вызова. Функция
выполняется со всеми экземплярами Т, замененными на double. Как я сказал раньше,
это не то же самое, что шаблон функции; здесь не происходит создания экземпляра
функции во время компиляции. Скомпилированная обобщенная функция в состоя-
нии обработать подстановку типов аргументов при вызове.
Обратите внимание, что если вы передадите строковый литерал в качестве аргу-
мента обобщенной функции, компилятор определит его тип как String74, независимо
от того, будет ли это “узкая” строковая константа вроде "Hello! ” или широкая — та-
кая как L”Hello!".
Может случиться так, что компилятор не сможет определить тип аргумента по вы-
зову обобщенной функции. В таких ситуациях можно специфицировать типы аргу-
ментов явно, указав их между угловыми скобками, следующими за именем функции
при ее вызове. Например, вы можете написать вызов в предыдущем фрагменте следу-
ющим образом:
double maxData = MaxElement<double>(data) ;
Дополнительные сведения о структурах программ 325
При явном указании аргумента типа исключается любая неоднозначность.
Существуют ограничения на то, какие типы вы можете применять в качестве ар-
гументов к обобщенным функциям. Тип аргумента не может быть типом класса “род-
ного” C++ либо родным указателем или ссылкой на класс значений вроде intA. То
есть, здесь применимы только типы классов значений, такие как int или double, и
отслеживаемые дескрипторы наподобие StringA (но не дескрипторы классов типа
значений).
Рассмотрим рабочий пример.
Практическое занятие
Использование обобщенной функции
В следующем примере показано, как определять и использовать обобщенные
функции.
// Ех6_10А.срр : main project file.
// Определение и использование обобщенной функции
#include "stdafx.h”
using namespace System;
// Обобщенная функция для нахождения максимального элемента массива
generic<typename Т> where Т:IComparable
Т MaxElement(аггау<Т>Л х)
{
Т max = х [ 0 ] ;
for (int i ® 1; i < x->Length; i++)
if(max->CompareTo (x[i]) < 0)
max = x [ i ] ;
return max;
}
// Обобщенная функция для удаления элемента массива
generic<typename Т> where Т:IComparable
array<T>A RemoveElement(Т element, array<T>A data)
{
array<T>A newData = gcnew array<T>(data->Length - 1);
int index =0; //Индекс элемента в массиве newData
bool found = false;//Признак нахождения элемента, который должен быть удален
for each(T item in data)
{
// Проверка на недопустимость индекса или факта нахождения элемента
if((!found) && item->CompareTo(element) == 0)
{
found = true;
continue;
}
else
{
if(index == newData->Length)
{
Console::WriteLine(Ъ"Элемент для удаления не найден");
return data;
}
newData[index++] = item;
}
}
return newData;
326 Глава 6
// Обобщенная функция для вывода списка элементов массива
generic<typename Т> where T:IComparable
void ListElements(array<T>A data)
for each (T item in data)
Console::Write(L"{0,10}", item);
Console::WriteLine();
int main(array<System::String A> Aargs)
array<double>A data = {1.5, 3.5, 6.7, 4.2, 2.1};
Console::WriteLine(L”MaccnB содержит:");
ListElements(data);
Console::WriteLine(Е"\пМаксимальный элемент = {0}\n", MaxElement(data));
array<double>A result = RemoveElement(MaxElement(data), data);
Console::WriteLine(L" После удаления максимума массив содержит:");
ListElements(result);
array<int>A numbers = {3, 12, 7, 0, 10,11};
Console::WriteLine(L"\nMaccnB содержит:");
ListElements(numbers);
Console::WriteLine(Е"\пМаксимальныи элемент = {0}\n", MaxElement(numbers));
Console::WriteLine(L”\n После удаления максимума массив содержит:");
ListElements(RemoveElement(MaxElement(numbers), numbers));
array<StringA>A strings = {L"Many”, L"hands", L"make", L"light", L"work"};
Console::WriteLine(L"\nMaccnB содержит:");
ListElements(strings);
Console::WriteLine(L"\пМаксимальныи элемент = {0}\n", MaxElement (strings));
Console::WriteLine(L"\n После удаления максимума массив содержит:");
ListElements(RemoveElement(MaxElement(strings), strings));
return 0;
Массив содержит:
1.5 3.5 6.7 4.2 2.1
Максимальный элемент =6.7
После удаления максимума, массив содержит:
1.5 3.5 4.2 2.1
Массив содержит:
3 12 7 0 10 11
Максимальный элемент =12
После удаления максимума, массив содержит:
3 7 0 10 11
Массив содержит:
Many hands make light work
Максимальный элемент = work
После удаления максимума, массив содержит:
Many hands make light
Press any key to continue . . .
Дополнительные сведения о структурах программ 327
Описание полученных результатов
Первая обобщенная функция, определенная в этом примере — MaxElement (), на-
ходящая максимальный элемент массива — идентична той, что вы видели в предыду-
щем разделе, поэтому обсуждать ее снова нет особой необходимости.
Следующая обобщенная функция — RemoveElements () — удаляет элемент, передан-
ный ей в первом аргументе, из массива, указанного вторым аргументом, и возвращает
дескриптор нового массива, получившегося в результате этой операции. Из первых
двух строк определения функции вы можете видеть, что и тип параметра, и тип воз-
врата определены через параметр типа Т.
generic<typename Т> where Т:IComparable
array<T>A RemoveElement(Т element, array<T>A data)
На параметр типа Т здесь накладывается то же ограничение, что и в первой обоб-
щенной функции, и это ограничение предполагает, что тип, используемый в качестве
аргумента, должен реализовывать функцию Compare То (), чтобы позволить сравни-
вать объекты этого типа. Второй параметр и тип возврата — оба являются дескрипто-
рами массивов элементов типа Т. Первый же параметр просто относится к типу Т.
Сначала функция создает массив, который будет содержать результат:
array<T>A newData = gcnew array<T>(data->Length -1);
Массив newData — того же типа, что и второй аргумент (массив элементов Т), но
имеет на один элемент меньше, поскольку один элемент должен быть удален из ори-
гинала.
Элементы копируются из массива data в newData в цикле for each:
int index =0; //Индекс элемента в массиве newData
bool found = false;//Признак нахождения элемента, который должен быть удален
for each(T item in data)
// Проверка на недопустимость индекса или факта нахождения элемента
if((!found) && item->CompareTo(element) == 0)
found = true;
continue;
if(index == newData->Length)
Console::WriteLine(Ь"Элемент для удаления не найден");
return data;
newData[index++] = item;
Копируются все элементы, кроме того, что идентифицирован первым аргумен-
том функции. Переменная index служит для указания следующего элемента масси-
ва newData, которому присваивается значение следующего элемента массива data.
Каждый элемент data копируется, только если он не равен element, в этом случае
found устанавливается в true, и оператор continue пропускает следующую итера-
цию. Вполне может быть, что массив может содержать более одного элемента, равно-
го первому аргументу, и переменная found предотвращает пропуск при копировании
последующих элементов, равных element.
328 Глава 6
Необходимо также проверить переменную index на предмет вхождения в разре-
если значение, переданное в первом аргументе функции, не будет найдено в массиве
data. В этом случае нужно будет просто вернуть дескриптор исходного массива.
Третья обобщенная функция просто выводит список элементов массива типа
аггау<Т>:
generic<typename Т>
void ListElements(array<T>A data)
for each(T item in data)
Console::Write(L"{0,10}", item);
Console::WriteLine();
Это один из тех случаев, когда никаких ограничений на параметр типа не требует-
ся. Очень мало чего можно сделать с объектами, чей тип совершенно неизвестен, по-
этому обычно параметры типов обобщенных функций имеют ограничения. Действие
этой функции очень просто — в ней каждый элемент массива выводится в командную
строку в поле вывода шириной 10 символов цикла for each. При желании можно
было бы немного усложнить эту функцию, добавив параметр ширины поля и соз-
дав форматную строку, используемую как первый аргумент функции Write () класса
Console. Можно также добавить в цикл логику для вывода нескольких элементов в
строке, в зависимости от ширины поля.
Функция main () вызывает все эти обобщенные функции с параметрами типа
double, int и String*. Таким образом, вы можете видеть, как все три обобщенных
функции работают с типами значений и дескрипторами. Во втором и третьем приме-
рах обобщенные функции используются в комбинации, в одном операторе. Например,
взглянем на следующий оператор в третьем примере применения функций:
ListElements(RemoveElement(MaxElement(strings), strings));
Первый аргумент обобщенной функции RemoveElement () генерируется вызовом
обобщенной функции MaxElement (), поэтому эти обобщенные функции могут быть
использованы тем же способом, что и эквивалентные им обычные функции.
Программа калькулятора для CLR
Теперь давайте повторно реализуем калькулятор, но как пример программы C++/
CLI. Предположим, что программа будет иметь ту же структуру, что и та, которую вы
видели в примере на “родном” C++, но функции будут объявлены и определены как
функции C++/CLI. Хорошей начальной точкой может послужить набор прототипов
функций в начале исходного файла (я использовал Ех6_11 в качестве имени проекта
CLR):
// Ех6_11.срр : главный файл проекта.
// Калькулятор для CLR с поддержкой скобок
#include "stdafx.h"
#include <cstdlib> // Для exit()
using namespace System;
StringA eatspaces(StringA str); // Функция
double expr(StringA str); // Функция
double term(StringA str, intA index); // Функция
double number(StringA str, intA index); // Функция
StringA extract(StringA str, intA index);// Функция
для удаления пробелов
для вычисления выражения
анализа элемента
распознавания числа
извлечения подстроки
Дополнительные сведения о структурах программ 329
Все параметры теперь — дескрипторы; строковые параметры имеют тип String'4,
а параметры индекса, хранящие текущую позицию строки — также дескрипторы типа
intA. Конечно же, строки возвращаются как String^.
Ниже показана реализация функции main ().
int main(array<System::String A> Aargs)
StringA buffer; // Область хранения вычисляемого входного выражения
Console::WriteLine(I/'Добро пожаловать в дружественный калькулятор!");
Console: :WriteLine (Ъ"Введите выражение или пустую строку для завершения.");
buffer = eatspaces(Console::ReadLine()); //
if(String::IsNullOrEmpty(buffer)) //
11
return 0;
Console::WriteLine(L" - {0}\n\n",expr(buffer));
return 0;
Читать входную строку
Пустая строка — признак
завершения работы
//Вывести значение выражения
бесконечного цикла for вызывается функция класса String — IsNullOrEmpty ().
Удаление пробелов из входной строки
Функция для удаления пробелов также получается проще и короче:
/ / Функция для удаления пробелов
StringA eatspaces(StringA str)
// Массив для хранения строки
array<wchar_t>A chars = gcnew
int length = 0; // Количество
из строки
без пробелов
array<wchar_t>(str->Length);
пробелов в строке
// Копировать непробельные символы в массив char
for each (wchar_t ch in str)
if (ch != ' ’)
chars[length++] = ch;
// Возвратить символьный массив в виде строки
return gcnew String(chars, 0, length);
Здесь сначала создается массив, куда будут помещены символы строки без про-
белов. Это массив элементов типа wchar_t, поскольку строки в C++/CLI состоят из
символов Unicode. Процесс удаления пробелов очень прост — копируются все сим-
волы, не являющиеся пробелами, из строки str в массив chars, и количество ско-
пированных символов запоминается в переменной length. В конце создается новый
объект String посредством конструктора класса String, который создает объект из
элементов массива. Первый аргумент конструктора — массив, являющийся источни-
ком символов строки, второй — позиция индекса первого элемента массива, который
формирует строку, а третий — общее количество символов массива, которые должны
быть использованы. Класс String определяет ряд конструкторов для создания строк
разными способами.
330 Глава 6
Вычисление арифметического выражения
Функция вычисления выражения может быть реализована следующим образом:
// Функция для вычисления арифметического выражения
double expr(StringA str)
intA index =0; // Текущая позиция символа
double value = term(str, index); // Получить первый элемент
while(*index < str->Length)
switch(str[*index]) // Выбрать действие на основе текущего символа
case '+': //
++(*index); //
value += term(str, index); //
break;
case '-•: //
++(*index); //
value -= term(str, index); //
break;
найден +
увеличить индекс и прибавить
следующий элемент
найден -
увеличить индекс и
вычесть следующий элемент
default: // Неверный формат строки выражения
Console::WriteLine(Ь"Эй, повнимательней можно?!
Здесь обнаружена ошибка.\п");
exit(1);
return value;
ее функции term () и позволить ей модифицировать оригинальное значение index.
Если объявить index просто как int, то функция term() примет копию значения и
не сможет обратиться к оригинальной переменной, чтобы изменить ее.
Объявление index приводит к появлению предупреждающих сообщений компи-
лятора, поскольку оператор полагается на автоматическую упаковку значения 0 для
производства объекта класса значения Int32, на который ссылается дескриптор, и
предупреждение нужно потому, что люди часто пишут такое объявление, имея в виду
инициализацию дескриптора значением null. Конечно, чтобы правильно сделать
это, необходимо в качестве значения инициализации использовать nullpt г вместо 0.
Чтобы избежать предупреждающего сообщения компилятора, этот оператор потре-
буется переписать так:
intA index = gcnew int(0);
Этот оператор явно использует конструктор, чтобы создать объект и инициализи-
ровать его значением 0, поэтому никакого предупреждения компилятор не выдает.
После обработки начального элемента вызовом функции term () цикл while
проходит по строке в поисках операций + или -, следующих за другим элементом.
Оператор switch идентифицирует и обрабатывает операции. Если вы долгое время
работаете с родным C++, у вас может возникнуть соблазн написать операторы case
внутри switch несколько иначе. Например, так:
// Некорректный код!! Не работает!!
case • + •: // найден +
value += term (str, ++(*index));//увеличить index и прибавить следующий элемент
break;
Дополнительные сведения о структурах программ
331
Конечно, обычно подобное пишут без этого начального комментария. Код непра-
вильный, но почему? Функция term () ожидает дескриптора типа intA в качестве вто-
рого аргумента, и именно это применяется, хотя, может быть, это не то, чего можно
ожидать. Компилятор вычисляет выражение ++ (*index) и его результат помещает-
ся во временную область памяти. Выражение на самом деле обновляет значение, на
которое ссылается index, но дескриптор, передаваемый функции term () — это де-
in de х. Этот дескриптор порождается в результате автоматической упаковки, когда
значение сохраняется во временной области памяти. Когда функция term () обновля-
ет его, то обновляется значение временного объекта, а не значение index, поэтому
все обновления index, выполненные внутри term (), будут утеряны. Если вам нужно
обновить переменную из вызывающей программы, вы не должны использовать вы-
ражение в качестве аргумента функции — всегда нужно применять имя дескриптора
переменной.
Получение значения элемента
первом аргументе, начиная с позиции символа, указанного вторым аргументом:
// Функция для получения значения элемента
double term(StringA str, intA index)
double value = number(str, index); // Получить первое число элемента
// Выполнять цикл, пока есть
while(*index < str->Length)
if(str[*index] ==L'*')
и допустимые операции
++(*index);
value *= number(str, index);
else if( str[*index] == L'/')
++(*index);
value /= number(str, index);
else
break;
// Умножение
// Увеличить index и
// умножить на следующее число
/ / Деление
// Увеличить index и
// разделить на следующее число
/ / Выйти из цикла
// Готово, возвращаем то, что получилось
return value;
После вызова функции number () для получения значения первого числа или вы-
ражения в скобках в элементе функция проходит по строке циклом while. Цикл про-
должается до тех пор, пока остаются во входной строке есть символы операций * и /,
за которыми следует другое число или выражение в скобках.
Извлечение числа
Функция number () извлекает и оценивает выражение в скобках, если оно есть; в
противном случае она просто определяет и возвращает очередное числовое значение
из входной строки:
332 Глава 6
// Функция для распознавания числа
double number(StringA str, intA index)
double value = 0.0;
// Сохраняет значение результата
// Проверить выражение в скобках
if(str[*index] == L' (' )
++(*index);
StringA substr = extract(str, index);
return expr(substr);
// Открытие скобок
// Извлечь подстроку в скобках
// Вернуть значение подстроки
// Цикл, аккумулирующий ведущие цифры
while((*index < str->Length) && Char::IsDigit(str, *index))
value = 10.0*value + Char::GetNumericValue(str[(*index)]);
++(*index);
if((*index -= str->Length)
return value;
double factor = 1.0;
++(*index);
// Обнаружена не цифра
index] !=’.') // Проверить на
// десятичную точку
// Если нет, вернуть value
// Множитель для десятичных разрядов
/ / Перейти к цифре
// Выполнять цикл, пока идут цифры
while((*index < str->Length) && Char::IsDigit(str, *index))
factor *=0.1; // Уменьшить множитель в 10 раз
value = value + Char::GetNumericValue(str[*index])*factor; // Добавить
// десятичный разряд
++(*index);
return value; //По завершении цикла - возврат значения
Как и в версии на родном C++, функция extract () используется для извлечения
выражения в скобках и результат передается для вычисления функции ехрг (). Если
в строке нет выражения в скобках (что определяется отсутствием символа открыва-
ющей скобки), то выполняется сканирование числа, которое представлено последо-
вательностью из нуля или более десятичных цифр, за которой следует необязатель-
ная десятичная точка и разряды дробной части. Функция IsDigit () класса Char
возвращает true, если символ — десятичная цифра, и false — в противном случае.
Проверяемый символ здесь находится в строке, переданной функции в первом аргу-
менте, в позиции, заданной вторым аргументом. Существует и другая версия функции
IsDigit (), которая принимает единственный аргумент типа wchar_t, поэтому ее
можно использовать с аргументом str [*index]. Функция GetNumericValue () клас-
са Char возвращает значение переданной в аргументе десятичной цифры Unicode в
виде числового значения типа double. Существует и другая версия этой функции, ко-
торая принимает дескриптор строки и позицию индекса, указывающую символ.
Извлечение подстроки в скобках
Функцию extract (), возвращающую подстроку в скобках из входной строки, мож-
но реализовать так:
Дополнительные сведения о структурах программ
333
// Функция для извлечения подстроки в скобках
String^ extract(StringA str, intA index)
// Временное место для подстроки
array<wchar_t>A buffer = gcnew array<wchar_t>(str->Length);
StringA substr; // Возвращаемая подстрока
int numL = 0; // Счетчик левых открывающих скобок
int bufindex - *index; // Сохранить начальное значение index
while(*index < str->Length)
buffer[*index - bufindex] = str[*index];
switch(str[*index])
case ') ' :
if (numb == 0)
array<wchar_t>A substrChars = gcnew array<wchar_t>(*index - bufindex);
str->CopyTo(bufindex, substrChars, 0, substrChars->Length);
substr = gcnew String(substrChars);
++(*index);
return substr; // Возвратить подстроку в новой памяти
else
numb—; // Уменьшить значение счетчика 1('
break;
case ’(':
numL++; // Увеличить значение счетчика '('
break;
++(*index);
Console::WriteLine(L"Выход за конец выражения. Возможно, некорректный ввод”);
exit(1);
return substr;
Опять-таки стратегия та же, что и в версии родного C++, отличия — лишь в де-
талях. Чтобы найти соответствующую правую скобку, функция отслеживает, сколь-
ко найдено новых левых скобок, в переменной numL. Подстрока извлекается, ког-
да найдена правая скобка при значении счетчика левых скобок numL равным нулю.
Подстрока копируется в массив substrChars, используя функцию СоруТо () с объек-
том str типа String. Функция копирует символы, начиная с позиции индекса, специ-
фицированной первым аргументом, в массив, специфицированный вторым аргумен-
том; третий аргумент определяет начальный элемент массива, который принимает
символы, а четвертый аргумент — количество символов, которые должны быть скопи-
рованы. Возвращаемая строка создается конструктором класса String, который стро-
ит объект на основе всех элементов массива substrChars, переданных в аргументе.
Если вы соберете все функции в один консольный проект CLR, то получите реа-
лизацию калькулятора на C++/ CLI, выполняемую под управлением CLR. Вывод про-
граммы должен быть таким же, как и в версии на родном C++.
334 Глава 6
Резюме
Теперь вы имеете достаточно знаний, чтобы разрабатывать и использовать функ-
ции. В этой главе вы научились применять указатель на функцию в практическом кон-
ознакомил ись с перегрузкой, используемой для реализации набора функций, пред-
ставляющих одну и ту же операцию с параметрами разного типа. В следующих главах
вы узнаете больше о перегрузке функций.
Ниже перечислены важнейшие моменты, которые вы изучили в этой главе.
О Указатель на функцию хранит ее адрес, плюс информацию о количестве и ти-
пах аргументов, а также типе возвращаемого функцией значения.
□ Указатель на функцию можно использовать для хранения адреса любой функ-
ции с соответствующим типом возврата и количеством и типами параметров.
□ Указатель функции можно применять для вызова функции, адрес которой он со-
держит. Указатель на функцию можно передавать в виде параметра функции.
Исключение — это способ сигнализации об ошибке в программе, так что код,
обрабатывающий ошибку, может быть отделен от кода нормальных операций.
□ Чтобы возбудить исключение, следует применить оператор с ключевым словом
throw.
try, а код, обрабатывающий исключения определенного рода — в блок catch,
следующий непосредственно за блоком try. Может существовать несколько
блоков catch, следующих за блоком try — каждый перехватывает определен-
ный тип исключений.
□ Перегруженные функции — это функции с одинаковыми именами, но разными
списками параметров.
□ Когда вызывается перегруженная функция, конкретная версия подлежащей вы-
зову функции выбирается компилятором на основе количества и типов указан-
ных аргументов.
□ Шаблон функции — это рецепт для автоматической генерации перегруженных
функций.
□ Шаблон функции имеет один или более аргументов, представляющих пере-
менные типа. Экземпляр шаблона функции, то есть, определение функции,
создается компилятором для каждого вызова функции, который соответствует
уникальному набору типов — аргументов шаблона.
Вы можете заставить компилятор создать определенный экземпляр шаблона
функции, указав объявление прототипа требуемой функции.
Вы приобрели также некоторый опыт в использовании нескольких функций в
одной программе на примере программы калькулятора. Однако имейте в виду, что
все случаи применения функций, рассмотренные до сих пор, находятся в контексте
традиционного процедурного подхода к программированию. Когда мы перейдем к
рассмотрению объектно-ориентированного программирования, мы по-прежнему
будем интенсивно пользоваться функциями, но с очень отличающимся подходом к
структуре программ и дизайну решений проблем.
дополнительные сведения о структурах программ
335
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Пусть имеется следующая функция:
int ascVal(size_t i, const char* p)
// печатать ASCII-значение символа
if (!p || i > strlen(p))
return -1;
else
return p[i];
Напишите программу, которая вызовет эту функцию через указатель и протести-
рует ее работу. Вам понадобится включить директиву #include для заголовоч-
ного файла <cstring>, чтобы использовать библиотечную функцию strlen ().
2. Напишите семейство перегруженных функций по имени equal (), которые при-
нимают два аргумента одного типа, возвращают 1, если аргументы эквивалент-
ны, и 0 — в противном случае. Предложите версии для аргументов типов char,
int, double и char*. (Для проверки эквивалентности строк применяйте функ-
цию strcmp () из библиотеки времени выполнения. Если вы не знаете, как ис-
пользовать st гетр (), поищите в справочной системе. Вам также понадобится
директива #include с заголовочным файлом <cstring>.) Напишите тестовый
код для проверки того, что вызвана корректная версия.
3. Сейчас, когда калькулятор встречает неправильный входной символ, он печата-
ет сообщение об ошибке, но не показывает, в каком месте входной строки она
обнаружена. Напишите процедуру обработки ошибок, которая печатает вход-
ную строку, помещая символ А под местом обнаружения ошибки, например:
12 + 4,2*3
4. Добавьте в программу калькулятора операцию возведения в степень А, в допол-
нение к * и /. Каковы возможные ограничения ее реализации, и как их преодо-
леть?
5. (Усложненное.) Усовершенствуйте калькулятор так, чтобы он мог обрабатывать
тригонометрические и прочие математические функции, позволяя вводить вы-
ражения вроде:
2 * sin(0.6)
Все математические библиотечные функции работают с радианами; предложи-
те версии тригонометрических функций, использующих градусы:
2 * sin(30)
Определение собственных
типов данных
Настоящая глава посвящена созданию собственных типов данных, предназна-
ченных для решения конкретных проблем. Здесь также будет рассказано о создании
объектов — строительных блоков объектно-ориентированного программирования.
Объект может показаться чем-то мистическим для непосвященных, но, как вы увиди-
те в этой главе, объект может быть всего лишь экземпляром одного из собственных
типов данных.
В этой главе вы изучите перечисленные ниже вопросы.
□ Структуры и их использование.
□ Классы и их использование.
□ Базовые компоненты класса и определение типов классов.
□ Создание и использование объектов класса.
□ Управление доступом к членам класса.
□ Конструкторы и их создание.
□ Конструктор по умолчанию.
□ Ссылки в контексте классов.
□ Конструктор копирования и его реализация.
□ Чем отличается класс C++/CLI от классов “родного” C++.
□ Свойства классов C++/CLI, их определение и использование.
□ Литеральные поля, их определение и использование.
□ Поля in it only, их определение и использование.
□ Что такое статический конструктор.
338 Глава 7
Структуры в C++
Структура — это определяемый пользователем тип, который вы определяете, при-
меняя ключевое слово struct, поэтому их часто называют struct. Структуры впер-
вые появились в языке С, и C++ включает и расширяет понятие struct из С. В C++
структуры функционально заменяемы классами, - в том смысле, что все, что вы може-
те делать со структурой, можно также достичь, применяя классы. Однако поскольку
Windows была написана на С до того, как C++ достиг популярности, struct широко
распространены в программировании Windows. Они также широко применяются в
наши дни, так что вам действительно нужно знать кое-что о структурах. Первым де-
лом в настоящей главе мы рассмотрим struct (в стиле С), прежде чем перейти к бо-
лее развитым возможностям, предоставляемым классами.
Что такое структура?
Почти все переменные, которые вы видели до настоящего момента, могли хра-
нить единственный тип сущности — число определенного вида, символ или массив
элементов некоторого типа. Реальный мир несколько сложнее этой модели, и раз-
мышляя почти о любом физическом объекте, возникает необходимость описывать
его в, как минимум, нескольких единицах данных. Подумайте об информации, кото-
рая понадобится, чтобы описать нечто простое, например, книгу. Вы можете прини-
мать во внимание название, автора, издательство, дату публикации, количество стра-
ниц, цену, тему классификации и номер ISBN — и этот начальный список можно без
труда расширить. Вы должны специфицировать отдельные переменные для хранения
каждого из параметров, необходимых для описания книги, но в идеале вам понадо-
бится единый тип данных, скажем, BOOK, который включит в себя все /эти параметры.
Уверен, что для вас не будет неожиданностью узнать, что именно это и позволяет сде-
лать структура.
Определение структуры
Давайте остановимся на описании книги и предположим, что вы хотите включить
в него заголовок, автора, издательство и год публикации. Чтобы достичь этого, вы
могли бы объявить структуру следующего вида:
struct BOOK
char Title[80];
char Author [80];
char Publisher[80];
int Year;
Это не определяет никаких переменных, но на самом деле создает новый тип
переменных по имени BOOK. Ключевое слово struct определяет тип BOOK как тако-
вой, и элементы, составляющие объект этого типа определены в фигурных скобках.
Обратите внимание, что каждая строка, определяющая отдельный элемент структу-
ры, ограничена точкой с запятой, и что точка с запятой появляется также после за-
крывающей скобки. Элементы структуры могут быть любого типа, за исключением
самого типа структуры, внутри которой они определены. Вы не можете иметь вну-
три определения структуры BOOK элемент типа BOOK. Может показаться, что это се-
Определение собственных типов данных 339
рьезное ограничение, но отметьте, что вы можете включить в структуру указатель на
переменную типа BOOK, как будет показано несколько позже.
Элементы Title, Author, Publisher и Year, заключенные в фигурные скобки в
определении, приведенном выше, могут быть названы членами или полями струк-
туры BOOK. Каждый объект тира BOOK содержит члены Title, Author, Publisher и
Year. Вы можете создавать переменные типа BOOK точно таким же способом, как соз-
;аете переменные любого другого типа.
BOOK Novel; // Объявление переменной Novel типа BOOK
Это объявляет переменную по имени Novel, которую вы теперь можете исполь-
зовать для хранения информации о книге. Все, что вам сейчас нужно — понять, как
установить данные в различных членах, составляющих переменную типа BOOK.
Инициализация структуры
Первый способ установить данные в члены структуры — это определить их началь-
ные значения в объявлении. Предположим, что вы хотите инициализировать пере-
менную Novel, чтобы она содержала данные об одной из ваших любимых книг Paneless
Programming (Программирование без панелей), выпущенной в 1981 году издательством
Gutter Press. Это история о парне, предпринявшем героические усилия по разработке
кода, когда он жил в иглу, и, как вы, возможно, знаете, вдохновленной знаменитым
лидером кассовых сборов — голливудским фильмом Gone with the Window (Унесенные
окном). Она была написана Фингерсом (I.C. Fingers), который также был автором
плодотворного трехтомника The Connoisseur's Guide to the Paper Clip (Руководство по рез-
ке бумаги для экспертов). Вооруженные этой информацией, вы можете написать объ-
явление переменной Novel следующим образом:
BOOK Novel =
{
’’Paneless Programming”,
"I.C. Fingers”,
"Gutter Press",
1981
// Начальное значение Title
// Начальное значение Author
// Начальное значение Publisher
// Начальное значение Year
Инициализирующие значения появляются между скобками и разделены запяты-
ми — почти так же, как определяются начальные значения членов массива. Как и в
случае массивов, последовательность инициализирующих значений очевидно должна
совпадать с последовательностью членов структуры в ее определении. Каждый член
структуры Novel имеет соответствующее значение, присвоенное ему, на что указыва-
ют комментарии.
доступ к членам структуры
Чтобы обратиться к индивидуальным членам структуры, вы можете использовать
операцию выбора члена, которая обозначается точкой; иногда ее называют опера-
цией доступа к члену. Чтобы сослаться на определенный член структуры, вы пишете
имя переменной-структуры, за которым следует точка, а за ней — имя члена, к кото-
рому вы хотите обратиться. Для изменения члена Year структуры Novel вы можете
записать так:
Novel.Year = 1988;
340 Глава 7
Это установить значение члена Year равным 1988. Вы можете использовать член
структуры точно таким же образом, как любую другую переменную того же типа, что
и этот член. Чтобы увеличить Year на 2, например, можно написать:
Novel.Year += 2;
Это увеличит значение члена Year структуры, как увеличило бы значение любой
другой переменной.
Практическое занятие | ИСПОЛЬЗОВЭНИе СТРУКТУР
Теперь воспользуемся примером консольного приложения, чтобы разобраться,
как работает обращение к членам структур. Предположим, что вы хотите написать
программу, которая будет иметь дело с некоторыми вещами, находящимися у вас во
дворе — как это проиллюстрировано на плане, приведенном на рис. 7.1.
Я произвольно установил координаты 0, 0 для левого верхнего угла двора. Правый
нижний угол имеет координаты 100, 120. Аналогичным образом первое значение ко-
ординаты измеряется в горизонтальном направлении относительно левого верхнего
угла, и ее значение увеличивается слева направо, а вторая координата измеряется по
вертикали, начиная от той же точки, и ее значения возрастают сверху вниз.
Позиция 100,120
Рис. 7.1. План двора
Определение собственных типов данных 341
На рис. 7.1 также показана позиция бассейна и двух сараев относительно лево-
го верхнего угла двора. Поскольку двор, сараи и бассейн прямоугольны, вы можете
определить тип структуры для представления любого из этих объектов:
struct RECTANGLE
int Left;
int Top;
int Right;
int Bottom;
// Пара координат
// верхнего левого угла
// Пара координат
// нижнего правого угла
Первые два члена типа структуры RECTANGLE соответствуют координатам верхней
левой точки прямоугольника, а следующие две координаты — правой нижней точке.
Вы можете использовать это в элементарном примере, имеющем дело с объектами
двора, показанном ниже:
// Ех7_01.срр
// Использование структур для описания двора
#include <iostream>
using std::cout;
using std::endl;
/ / Определение структуры, описывающей прямоугольник
struct RECTANGLE
int Left; // Пара координат
int Top; // верхнего левого угла
int Right; // Пара координат
int Bottom; // нижнего правого угла
// Прототип функции вычисления площади прямоугольника
long Area(RECTANGLE& aRect);
/ / Прототип функции перемещения прямоугольника
void MoveRect (RECTANGLE& aRect, int x, int y) ;
int main(void)
RECTANGLE Yard = { 0, 0, 100, 120 };
RECTANGLE Pool = { 30, 40, 70, 80 };
RECTANGLE Hutl, Hut2;
Hutl.Left = 70;
Hutl .Top = 10;
Hutl.Right = Hutl.Left + 25;
Hutl.Bottom = 30;
Hut2 = Hutl; // Определить Hut2 таким же, как Hutl
MoveRect (Hut2, 10, 90); // Теперь переместить его в правильную позицию
cout « endl
« "Координаты Hut2: "
« Hut2.Left « "« Hut2.Top « " и "
« Hut2.Right « "," « Hut2.Bottom;
cout « endl
« "Площадь двора: "
« Area(Yard);
cout « endl
« "Площадь бассейна:
« Area(Pool)
« endl;
return 0;
342 Глава 7
// Функция вычисления площади прямоугольника
long Area(RECTANGLE& aRect)
return (aRect.Right - aRect.Left)*(aRect.Bottom - aRect.Top);
// Функция перемещения прямоугольника
void MoveRect(RECTANGLES aRect, int x, int y)
int length = aRect.Right - aRect.Left;// Получить длину прямоугольника
int width = aRect.Bottom - aRect.Top; // Получить ширину прямоугольника
aRect.Left = x; // Установить верхнюю левую точку
aRect.Top - у; // в новую позицию
aRect.Right = х + length; // Получить правую нижнюю точку как
aRect.Bottom = у + width; // инкремент относительно новой позиции
return;
Вывод этого примера будет выглядеть следующим образом:
Координаты Hut2: 10,90 и 35,110
Площадь двора: 12000
Площадь бассейна: 1600
Описание полученных результатов
Обратите внимание, что определение структуры в данном примере появляется в
глобальном контексте. Вы сможете увидеть ее на вкладке Class View (Представление
классов) проекта. Помещение определения структуры в глобальный контекст позво-
ляет объявлять переменные типа RECTANGLE в любом месте файла . срр. В программе
с более существенным объемом кода такое определение обычно должно быть поме-
щено в файл .h и затем добавлено в файл . срр с помощью директивы #include.
Здесь мы определили две функции для обработки объектов RECTANGLE. Функция
Area () вычисляет площадь объекта RECTANGLE, переданного в аргументе-ссылке, как
произведение длины и ширины, где длина — разница между горизонтальными пози-
циями конечных точек, а ширина — разница между их вертикальными позициями. За
счет передачи по ссылке код работает чуть быстрее, потому что аргумент не копиру-
ется. Функция MoveRect () модифицирует определения точек объекта RECTANGLE в
позицию х, у, которая передается в аргументах. Позицией объекта RECTANGLE счита-
ется позиция левой верхней точки Left, Тор. Поскольку объект RECTANGLE передает-
ся по ссылке, функция может модифицировать члены объекта RECTANGLE непосред-
ственно. После вычисления длины и ширины переданного объекта RECTANGLE, его
члены Left и Тор устанавливаются в х и у соответственно, а новые значения членов
Right и Bottom вычисляются увеличением х и у на длину и ширину исходного объ-
екта RECTANGLE.
В функции main () вы инициализируете переменные Yard и Pool типа RECTANGLE
координатами их позиций, как показано на рис. 7.1. Переменная Hutl представляет
сарайчик в верхней правой части рисунка, а его члены устанавливаются в соответ-
ствующие значения с помощью операций присваивания. Переменная Hat2, соответ-
ствующая второму сараю — в нижней левой части двора, сначала устанавливается точ-
но такой же, как Hut 1 с помощью такого оператора присваивания:
Hut2 = Hutl; // Определение Hut2 таким же, как Hutl
После выполнения этого оператора значения членов Hutl копируются в соответ-
ствующие члены Hut2. Вы можете присвоить структуру данного типа только другой
Определение собственных типов данных
343
структуре того же типа. Невозможно непосредственно выполнить инкремент структу-
ры или использовать ее в арифметической операции.
Чтобы изменить позицию Hat 2 для размещения его в нижней левой части двора,
вызывается функция MoveRect () с координатами нужной позиции, переданными в
аргументах. Этот окружной путь получения координат Hut 2 совершенно не обязате-
лен, и служит только для того, чтобы показать, как можно использовать структуру в
качестве аргумента функции.
Поддержка средства Intellisense при работе со структурами
Вероятно, вы заметили, что редактор в Visual C++ 2005 довольно-таки умный —
так, например, он знает типы переменных. Если в окне редактора вы наводите кур-
сор мыши на имя переменной, всплывает маленькая рамка, в которой показано его
определение. Редактор также может здорово помочь при работе со структурами (и,
как вы увидите позже, с классами), потому что он не только знает типы обычных пе-
ременных, но также и члены, входящие в переменные типа определенной структуры.
Если ваш компьютер достаточно быстр, то по мере ввода операции выбора члена,
вслед за именем структурной переменной редактор показывает окно со списком всех
членов данной структуры. Если вы щелкнете на одном из них, будет показан коммен-
тарий, указанный в исходном определении структуры, так что вы сразу видите, что
это такое. Это видно на рис. 7.2, где используется фрагмент предыдущего примера.
ssi }
595 -
I
60; /> Function tD Move a Rectangle
61 void MoveRect (RECTANGLE^ allect, int int
«I
64i
I
esi
67^
6s!
691
?oi
71;
73^
74i
I
int length • aRecr-Right - afreet rleft;
int vidth w aRect Bottom - aR.ect.Top;
afreet, belt = к;
afreet-Top " y;
aRe ct- Right = x
aRectj
coordinate par
// Get length of rectangle
// Get width of rectangle
// Sec tap left polnx
// tn new pasition
// Get battam right point as
Puc. 7.2. Работа средства Intellisense
Теперь есть реальный стимул добавлять комментарии и писать их кратко и выра-
зительно. Если выполняется двойной щелчок на члене в списке либо нажимается кла-
виша <Enter> при выделенном элементе списка, он автоматически вставляется после
операции выбора члена (точки), избавляя вас от необходимости вручную вводить его
с клавиатуры. Здорово, не правда ли?
При желании вы можете отключить любое или все средства Intellisense через меню
Tools Options (Сервис *=> Параметры), но я предполагаю, что для их отключения
может быть только одна причина — если ваша машина слишком медленна, что делает
эти средства неудобными. Вы можете отключить средства завершения операторов на
странице редактора C/C++, который выбираете в правой панели опций. Если вы их
отключите, то всегда можете вызвать их обратно — либо через меню Edit (Правка),
либо через клавиатуру. Нажатие <Ctrl+J>, например, вызывает отображение членов
объекта, на котором находится курсор. Редактор также показывает список параме-
тров для функции, когда вы вводите код для ее вызова
он выводит всплывающее
344 Глава 7
поле, как только нажата клавиша с левой скобкой перед списком аргументов. Это, в
частности, удобно с библиотечными функциями, поскольку напоминает списки пара-
метров каждой из них. Конечно, чтобы это средство работало, директива #include
для заголовочного файла уже должна присутствовать в исходном коде. Без этого ре-
дактор не имеет понятия о библиотечных функциях. При изучении классов вы увиди-
те и другие полезные возможности редактора, благодаря которым облегчается ввод
кода.
После этого небольшого интересного отступления давайте вернемся к структу-
рам.
Структура RECT
Прямоугольники в программах Windows используются очень широко. По этой при-
чине существует предопределенная структура RECT в заголовочном файле windows. h.
Ее определение, по сути, совпадает со структурой, которую вы видели в предыдущем
примере:
struct RECT
int top;
int right;
int bottom;
// Пара координат
// верхней левой точки
// Пара координат
// нижней правой точки
Эта структура обычно используется для определения прямоугольных областей на
вашем дисплее для множества различных целей. Поскольку RECT используется столь
интенсивно, windows .h также предоставляет функцию Inf lateRect () для увеличе-
ния размера прямоугольника и функцию EqualRect () для сравнения двух прямоу-
гольников.
В библиотеке MFC также определен класс по имени CRect, который эквивалентен
структуре RECT. После изучения классов вы отдадите ему предпочтение перед струк-
турой RECT. Класс CRect предлагает широкий набор функций для манипулирования
прямоугольниками, и вы используете многие из них, когда будете писать программы
Windows с применением MFC.
Использование указателей со структурами
Как и следовало ожидать, вы можете создавать указатель на переменную типа
структуры. Фактически многие функции, объявленные в windows .h, которые рабо-
тают с объектами RECT, требуют указателей на RECT в качестве аргументов, поскольку
это позволяет избежать копирования всей структуры при передаче в функцию аргу-
мента типа RECT. Чтобы определить указатель на объект RECT, используется вполне
предсказуемое объявление:
RECT* pRect = NULL; // Определение указателя на RECT
Предполагая, что у вас определен объект типа RECT по имени aRect, вы можете
установить указатель на адрес этой переменной обычным образом, используя опера-
цию взятия адреса:
pRect = &aRect; // Присвоить указателю адрес aRect
Определение собственных типов данных 345
Как вы видели, когда была представлена идея структуры, struct не может содер-
жать член того же типа, как она сама, однако она может содержать указатель на str-
uct того же типа. Например, вы можете определить структуру примерно так:
struct ListElement
RECT aRect;
ListElement* pNext;
// Член структуры RECT
// Указатель на элемент списка
Первый элемент структуры ListElement имеет тип RECT, а второй — указатель на
структуру типа ListElement — тот же тип, внутри которого он определен. (Следует
подчеркнуть, что этот элемент не относится к типу ListElement; его типом является
“указатель на ListElement”.) Это позволяет связывать объекты типа ListElement в
цепочки, в которых каждый ListElement может содержать адрес следующего объек-
та ListElement в цепочке, а последний элемент цепочки будет иметь нулевой указа-
тель. Эта конструкция показана на рис. 7.3.
Рис. 7.3. Связанные структуры ListElement
Каждый прямоугольник на диаграмме представляет объект типа ListElement, и
член pNext каждого объекта хранит адрес следующего объекта в цепочке, за исклю-
чением самого последнего, у которого pNext равно 0. Такого рода конструкцию часто
называют связным списком. Его преимущество состоит в том, что до тех пор, пока
известен первый элемент списка, можно найти все остальные. Это, в частности, важ-
но, когда переменные создаются динамически, поскольку связный список может ис-
пользоваться для отслеживания их всех. Всякий раз, когда создается новый элемент,
он просто добавляется в конец списка, сохранением его адреса в члене pNext послед-
него объекта в цепочке.
Доступ к членам структуры через указатель
Рассмотрим следующие операторы:
RECT aRect = { 0, 0, 100, 100 };
RECT* pRect = &aRect;
Первый оператор объявляет и определяет объект aRect типа RECT с первой па-
рой членов инициализированных (0, 0) и второй парой (100, 100). Второй оператор
объявляет pRect как указатель на тип RECT и инициализирует его адресом aRect.
346 Глава 7
Теперь вы можете обращаться к членам aRect через указатель, используя оператор
вроде следующего:
(*pRect) .Тор +=10; // Увеличит член Тор на 10
Скобки вокруг разыменования указателя здесь важны, потому что операция доступа
к члену имеет более высокий приоритет, чем операция разыменования. Без скобок вы
попытаетесь трактовать указатель как struct и разыменовывать ее член, так что этот
оператор компилироваться не будет. После выполнения этого оператора член Тор бу-
дет иметь значение 10, а остальные члены, конечно же, останутся без изменений.
Метод, используемый здесь для доступа к членам структуры через указатель, вы-
глядит достаточно громоздко. Поскольку операции подобного рода часто встречают-
ся в C++, язык включает специальную операцию, позволяющую вам выразить то же
самое в намного более читабельной и интуитивной форме, поэтому давайте рассмо-
трим следующую операцию.
Операция непрямого выбора члена
Операция непрямого выбора члена, ->, специально предназначена для доступа
к членам структур через указатель; эта операция также называется операцией не-
прямого доступа к члену. Она выглядит как маленькая стрелочка (->) и состоит из
знака “минус” (-), за которым следует символ “больше” (>). Вы можете использовать
ее, чтобы переписать оператор доступа к члену Тор структуры aRect через указатель
pRect, как показано ниже:
pRect->Top +=10; // Увеличить член Тор на 10
Это намного выразительнее, чем то, что мы видели выше, не правда ли? Операция
непрямого выбора члена также используется с классами, и вы еще встретите ее много
раз на протяжении книги.
Типы данных, объекты, классы и экземпляры
Прежде чем мы обратимся к синтаксису языка и приемам программирования клас-
сов, я начну с того, какое отношение приобретенные вами знания имеют к концеп-
ции классов.
До сих пор вы учили, что “родной” C++ позволяет вам создавать переменные,
которые могут быть любыми из диапазона фундаментальных типов данных: int,
long, double и так далее. Вы также видели, как можно использовать ключевое слово
struct для определения структур, которые можно трактовать как типы переменных,
представляющих группы других переменных.
Переменные фундаментальных типов не позволяют адекватно моделировать объ-
екты реального мира (или даже воображаемые объекты). Трудно, например, смодели-
ровать ящик в терминах int; однако вы можете использовать члены структуры для
определения набора атрибутов такого объекта. Вы можете определять переменные
length (длина), width (ширина) и height (высота), чтобы представить размеры ящи-
ка и связать их вместе в виде членов структуры Box, как показано ниже:
struct Box
double length;
double width;
double height;
Определение собственных типов данных 347
Имея такое определение нового типа данных по имени Box, вы определяете пе-
ременные этого типа — так же, как делаете это с переменными базовых типов. Вы
можете создавать, манипулировать и уничтожать столько объектов Box, сколько вам
нужно в вашей программе. Это значит, что вы можете моделировать объекты, исполь-
зуя структуры, и строить свои программы вокруг них. Так что, это и есть объектно-
ориентированное программирование?
Не совсем. Дело в том, что объектно-ориентированное программирование (ООП)
основано на нескольких фундаментальных понятиях (знаменитые инкапсуляция, поли-
морфизм и наследование), а то, что вы видели до сих пор, им не отвечает. Пока вам не
нужно беспокоиться о значении этих терминов — вы получите надлежащие поясне-
ния в оставшейся части этой главы и далее в книге.
Понятие структуры в C++ выходит далеко за пределы оригинальной концепции
структур языка С — оно включено в объектно-ориентированное понятие класса. Идея
классов, от которых вы можете создавать свои собственные типы данных и использо-
вать их подобно родным типам, является фундаментальной в C++, и новое ключевое
слово class было добавлено в язык именно для описания этой концепции. Ключевые
слова struct и class почти идентичны в C++, за исключением управления доступом
к членам, о чем вы узнаете далее в этой главе. Ключевое слово struct поддержива-
ется для обратной совместимости с языком С, но все, что можно делать со struct, и
много больше, можно также достичь с class.
Вот как вы можете определить класс, представляющий ящики:
class СВох
public:
double m_Length;
double m_Width;
double m_Height;
Когда вы определяете СВох как класс, то, по сути, вы определяете новый тип дан-
ных, подобный тому, что вы создали, когда определили структуру Box. Единственное
отличие в том, что используется ключевое слово class вместо struct, и в приме-
нении ключевого слова public, за которым следует двоеточие, предшествующее
определению членов класса. Переменные, которые вы определяете как часть класса,
называются данными-членами класса, потому что они являются переменными, хра-
нящими данные.
Кроме того, класс назван СВох вместо Box. Вы могли бы назвать класс Box, но в
MFC принято соглашение об использовании префикса С в именах классов, поэтому
вы можете также придерживаться его. MFC также предваряет имена данных-членов
префиксом т_, чтобы отличить их от других переменных, поэтому я также буду следо-
вать этому соглашению. Запомните, однако, что в других контекстах, где вы можете
использовать C++ и C++/CLI, эти соглашения могут не выдерживаться; в других сре-
дах разработки и на других платформах принципы именования классов и их членов
могут быть другими, а в некоторых может вообще не быть каких-либо соглашений
относительно именования.
Ключевое слово public — это фундаментальный момент, отличающий класс от
структуры. Оно просто объявляет члены класса общедоступными, как это имеет ме-
сто с членами структур. Члены структур являются public по умолчанию. Как вы уви-
дите чуть позже в настоящей главе, на доступность членов класса можно накладывать
ограничения.
348 Глава 7
Вы можете объявить переменную, скажем, bigBox, которая представляет экзем-
пляр класса СВох, следующим образом:
СВох bigBox;
Это выглядит в точности так же, как объявление переменной типа структуры или
любого другого типа. После того, как вы определили класс СВох, объявления пере-
менных этого типа достаточно стандартны.
Первый класс
Понятие класса было придумано англичанами, чтобы большая часть населения
чувствовала себя счастливой. Оно происходит из теории, что люди, которые знают
свое место и функцию в обществе, будут намного более защищенными и успешными в
жизни, чем те, которые его не знают. Знаменитый датчанин, Бьерн Страуструп, кото-
рый изобрел C++, без сомнения почерпнул глубокие знания о концепции классов во
время учебы в Кембриджском университете в Англии, и очень успешно применил эту
идею в своем новом языке.
Класс C++ подобен этой английской концепции — в том смысле, что каждый класс
обычно имеет очень точно определенную роль и допустимый набор действий. Однако
он и отличается от английской идеи, поскольку класс в C++ имеет некоторую “социа-
листическую окраску”, сосредоточивая внимание на рабочих классах. В самом деле, в
определенном смысле это противоположность английскому идеалу, потому что, как вы
увидите, рабочие классы в C++ часто живут за счет классов, которые не делают ничего.
Операции с классами
В C++ вы можете создавать новые типы данных как классы, чтобы представлять
объекты любого вида, который вам нужен. Как вы вскоре увидите, классы (и структу-
ры) не ограничены просто хранением данных; вы можете определять функции-члены
или даже операции, которые работают с объектами ваших классов, с использованием
стандартных операций C++. Вы, например, можете определить класс СВох, так что
будут работать следующие операторы и иметь тот смысл, который вы в них вложите:
СВох boxl;
СВох Ьох2;
if(boxl > box2) // Наполнить больший ящик
boxl.fill () ;
else
box2.fill();
Вы также можете реализовать операции как часть класса СВох для сложения, вы-
читания и даже умножения ящиков — фактически, почти любая операция, которой
вы можете приписать осмысленное значение в контексте ящиков.
Говоря о невероятной мощи этого волшебства, следует отметить, что оно требует
коренного изменения подхода к программированию. Вместо разбиения проблемы на
части в терминах привязки к машинно-ориентированным типам данных (целым чис-
лам, числам с плавающей точкой и так далее) с последующим написанием програм-
мы, вы переходите к программированию в терминах проблемно-ориентированных
типов данных — другими словами, в терминах классов. Эти классы могут именоваться
CEmployee, или CCowboy, или CCheese, или CChutney, каждый из которых определен
специально для типа проблем, которые вы хотите решить, полон функций и опера-
ций, необходимых для манипулирования экземплярами этих новых типов.
Определение собственных типов данных 349
Дизайн программы теперь начинается с решения о том, какие новые специфич-
ные для приложения типы данных понадобятся для решения проблемы, и написании
программы в терминах операций, специфичных для рассматриваемой проблемы,
будь то CCof f ins или CCowpokes.
Терминология
Для начала определим основную терминологию, которую будем использовать, об-
суждая классы в C++.
□ Класс — определенный пользователем тип данных.
□ Объектно-ориентированное программирование (ООП) — стиль програм-
мирования, основанный на идее определения собственных типов данных как
классов.
□ Объявление объекта типа класса иногда называется созданием экземпляра
(реализацией) класса.
□ Экземпляры класса известны как объекты.
□ Идея объекта, содержащего данные, указанные в его определении, вместе с
функциями, оперирующими этими данными, называется инкапсуляцией.
Когда я обращаюсь к деталям объектно-ориентированного программирования, это
может показаться несколько усложненным, но возврат к основам того, что вы делае-
те, часто может помочь прояснить ситуацию, поэтому всегда имейте в виду, для чего
предназначены объекты. Они предназначены для написания программ в терминах
объектов, специфичных для проблемной области. Все средства, присущие классам
C++, направлены на это и призваны сделать это как можно более всесторонним и гиб-
ким образом. Давайте займемся осмыслением классов.
Что такое класс?
Класс — это спецификация типа данных, определенного вами. Он может содер-
жать элементы данных, которые могут быть как переменными базовых типов C++, так
и других определенных пользователем типов. Элементы данных класса могут быть от-
дельными элементами данных, массивами, указателями, массивами указателей почти
любого рода, объектами других классов, так что в вашем распоряжении практически
неограниченная гибкость в отношении того, что можно включать в ваши типы дан-
ных. Класс также может содержать функции, которые оперируют объектами класса,
обращаясь к элементам данных, которые они включают в себя. Таким образом, класс
комбинирует определение элементарных данных, из которых состоит объект, и сред-
ства манипулирования данными, относящимися к индивидуальным объектам класса.
Данные и функции внутри класса называются членами класса. Довольно забавно,
что члены класса, которые являются элементами данных, называются данными-чле-
нами, а функции, принадлежащие классу, называются функциями-членами или чле-
нами-функциями. Функции-члены класса также иногда называются методами; я не
стану использовать этот термин в настоящей книге, но имейте в виду, что вы можете
встретить этот термин в других источниках.
Данные-члены также называются полями, и эта терминология используется в
C++/CLI, поэтому я тоже буду применять ее время от времени.
Когда вы определяете класс, то тем самым создаете “проект” типа данных. Это в
действительности не определяет никаких данных, но определяет смысл для имени
350 Глава 7
класса, то есть, что будет содержать объект этого класса и какие операции могут быть
выполнены над таким объектом. Это почти то же самое, что подготовить описание
базового типа double. Это не будет переменной типа double, но определением того,
как она создается и как с ней обращаться. Чтобы создать переменную базового типа,
вы должны использовать операторы объявления. Точно так же и с классами, и вы в
этом вскоре убедитесь.
Опре
еление класса
Давайте взглянем еще раз на пример класса, упомянутый ранее — класс ящиков. Вы
определили тип данных СВох, используя ключевое слово class, следующим образом:
class СВох
public:
double
double
double
m_Length;
m_Width;
m_Height;
/1 Длина ящика в дюймах
// Ширина ящика в дюймах
// Высота ящика в дюймах
Имя класса следует за ключевым словом class, и между фигурными скобками
определены три данных-члена. Данные-члены определены в классе с использованием
операторов объявления, которые вы уже знаете и любите, а все определение класса
завершается точкой с запятой. Имена всех членов класса локальны по отношению к
нему. Поэтому вы можете использовать те же имена еще где-то в программе, и это не
вызовет никаких проблем.
Управление доступом в классе
Ключевое слово public выглядит как метка, но на самом деле это нечто боль-
шее. Оно определяет атрибут доступа к членам класса, которые следуют за ним.
Спецификация данных-членов как public означает, что эти члены объекта класса
могут быть доступны в любой точке внутри области видимости объекта класса, к ко-
торому они относятся. Вы также можете специфицировать члены класса как private
или protected. Фактически, если вы вообще пропустите спецификатор доступа, то
члены будут иметь атрибут по умолчанию — private. (В этом заключается единствен-
ное отличие между классом и структурой в C++ — у структур спецификатором доступа
по умолчанию является public.) Чуть позже вы увидите, в чем состоит эффект от
этих ключевых слов в определении класса.
Запомните, что все, что вы определили до сих пор — это класс, который являет-
ся типом данных. Вы не объявляли никаких объектов типа класса. Когда я говорю о
доступе к членам класса, скажем, m_Height, то я говорю о доступе к членам данных
конкретного объекта, и этот объект должен быть где-то определен.
Определение объектов класса
Вы объявляете объекты класса точно таким же образом, как и объекты базовых
типов, поэтому вы можете объявлять объекты типа класса СВох с помощью следую-
щих операторов:
СВох boxl; // Объявление boxl типа СВох
СВох Ьох2; // Объявление Ьох2 типа СВох
Оба объекта — boxl и Ьох2 — конечно же, будут иметь свои собственные данные-
члены. Это проиллюстрировано на рис. 7.4.
Определение собственных типов данных 351
Ьох1
m_Length m_Width m.Height
8 байт 8 байт 8 байт
Ьох2
m_Length m_Width mJHeight
8 байт 8 байт 8 байт
Рис. 7.4. Объекты boxl и Ьох2
Имя объекта boxl воплощает целый объект, включая три его данных-члена. Они
никак не инициализированы и будут просто содержать мусор — случайные значения,
поэтому нужно посмотреть, как обратиться к ним, чтобы установить в них какие-то
определенные значения.
Доступ к данным-членам класса
Вы можете обращаться к данным-членам объектов класса, используя операцию
прямого выбора члена, которую использовали ранее для доступа к членам структур.
Поэтому, чтобы установить значение члена данных m_Height объекта Ьох2, скажем,
равным 18.0, вы можете написать следующий оператор присваивания:
box2.m_Height =18.0; // Установка значения члена данных
Вы можете таким образом обращаться к данным-членам в функции, находящейся
вне класса, потому что член m_Height специфицирован как имеющий доступ public.
Если бы он не был определен как public, этот оператор не скомпилировался бы.
Вскоре вы узнаете об этом больше.
Практическое занятие ПерВОб ПрИМвНеНИв КЛЭССОВ
Убедитесь в том, что вы можете использовать класс точно так же, как структуру.
Испытайте это в следующем консольном приложении:
// Ех7_02.срр
// Создание и использование ящиков
#include <iostream>
using std::cout;
using std::endl;
class СВох // Определение класса в глобальном контексте
{
public: double m_Length; double m_Width; double m_Height; / / Длина ящика в дюймах / / Ширина ящика в дюймах // Высота ящика в дюймах
}; int main () { СВох boxl; СВох Ьох2; double boxVolume = 0.0; boxl.m_Height = 18.0; boxl .m__Length = 78.0; boxl.m Width = 24.0; // Объявление boxl типа СВох / / Объявление Ьох2 типа СВох // Сохранить объем ящика // Определить значения // членов // объекта boxl
box2 .m__Height = boxl .m_Height - 10; // Определить члены Ьох2
box2.m_Length = boxl.m_Length/2.0; box2.m_Width = 0.25*boxl.m_Length; / / в терминах boxl
352 Глава 7
/ / Вычислить объем boxl
boxVolume « boxl.m_Height*boxl.m_Length*boxl.m_Width;
cout « endl
< < "Объем boxl = " « boxVolume;
cout « endl
< < "box2 имеет сумму сторон "
« box2.m_Height+ box2.m_Length+ box2.m_Width
« " дюймов.";
cout « endl // Отобразить размер ящика в памяти
« "Объект СВох занимает "
« sizeof boxl « " байт.";
cout «endl;
return 0;
При вводе кода функции ma i п () вы должны наблюдать подсказки редактора со
списком имен членов при каждом вводе операции выбора члена вслед за именем
объекта класса. Затем вы можете выбирать нужный член в списке двойным щелчком
мыши. Наведение курсора мыши на любую переменную в вашем коде приведет к ото-
бражению ее типа.
Описание полученных результатов
Поскольку все примеры пока оформлены в виде консольных приложений, про-
ект должен быть определен соответствующим образом. Все здесь работает так, как
и можно ожидать, основываясь на опыте применения структур. Определение класса
находится вне функции main (), и потому относится к глобальному контексту. Это по-
зволяет вам объявлять объекты в любой функции программы и отображает класс на
вкладке Class View (Представление классов), как только программа будет скомпили-
рована.
Вы объявили два объекта типа СВох внутри функции main () — boxl иЬох2.
Конечно, как это было бы и с переменными базовых типов, объекты boxl иЬох2
локальны по отношению к main (). Объекты типов классов подчиняются тем же пра-
вилам видимости объявленных переменных, что и переменные базовых типов (вроде
переменной boxVolume, использованной в этом примере).
Первых три оператора присваивания устанавливают значения данных-членов
boxl. Вы определяете значения данных-членов Ьох2 в терминах данных-членов boxl
в следующих трех операторах присваивания.
Затем идет оператор, который вычисляет объем boxl как произведение его трех
данных-членов, после чего полученное значение выводится на экран. Далее выводит-
ся сумма данных-членов Ьох2, для чего применяется выражение, суммирующее дан-
ные-члены непосредственно в операторе вывода. Финальное действие программы —
вывод количества байт памяти, занятых boxl, которое возвращает операция sizeof.
Если запустить эту программу, вы должны получить такой вывод:
Объем boxl = 33696
Ьох2 имеет сумму сторон 66.5 дюймов.
Объект СВох занимает 24 байт.
Последняя строка показывает, что объект boxl занимает 24 байта памяти, что объ-
ясняется наличием 3 членов данных по 9 байт каждый. Оператор, выдающий послед-
нюю строку, с тем же успехом может быть записан так:
cout « endl // Отобразить размер ящика в памяти
« "Объект СВох занимает "
« sizeof (СВох) << " байт.";
Определение собственных типов данных 353
Здесь я использовал в качестве операнда операции sizeof имя типа в скобках
вместо имени конкретного объекта. Вы должны помнить, что это — стандартный син-
таксис операции sizeof, как это было показано в главе 4.
Этот пример демонстрирует механизм доступа к общедоступным (public) данным-
членам класса. Он также показывает, что они могут быть использованы точно так же,
как и обычные переменные. Теперь вы готовы к восприятию нового понятия — функ-
ций-членов класса.
Функции-члены класса
Функция-член класса — это функция, определение и прототип которой находятся
внутри определения класса. Она оперирует любым объектом класса, членом которого
является, и имеет доступ ко всем членам класса этого объекта.
Практическое занятие | ДобаВЛвНИв фуНКЦИИ-ЧЛвНа К КЛЭССу СВОХ
Чтобы увидеть, как вы можете обращаться к членам класса изнутри функции-чле-
на, рассмотрим пример, расширяющий класс СВох за счет включения в него функ-
ции-члена, которая вычисляет объем объекта СВох.
// Ех7_03.срр
// Вычисление объема ящика с помощью функции-члена
#include <iostream>
using std::cout;
using std::endl;
class СВох // Определение класса в глобальном контексте
{
public:
double m—Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
// Функция для вычисления объема ящика
double Volume ()
{
return m_Length*m_Width*m__Height ;
}
};
int main ()
{
СВох boxl; // Объявление boxl типа СВох
СВох Ьох2; // Объявление Ьох2 типа СВох
double boxVolume = 0.0; / / Сохранить объем ящика
boxl.m_Height =18.0; // Определить значения
boxl .m_Length =78.0; // членов
boxl.m__Width = 24.0; // объекта boxl
box2. m_Height = boxl ,m_Height - 10; // Определить члены box2
box2. m_Length = boxl.m_Length/2.0; // в терминах boxl
box2.m_Width = 0.25*boxl .m__Length;
boxVolume = boxl .Volume (); // Вычислить объем boxl
cout « endl
« "Объем boxl = " « boxVolume;
cout « endl
« "Объем box2 = "
« box2 .Volume () ;
354 Глава 7
cout « endl
« "Объект СВох занимает "
« sizeof boxl « " байт.";
cout « endl;
return 0;
}
Описание полученных результатов
Новый код, добавленный к определению класса СВох, выделен полужирным. В
нем — только определение функции Volume (), которая является функцией-членом клас-
са. Она также имеет некоторый атрибут доступа, подобно данным-членам, а именно —
public. Это потому, что каждый член класса, следующий за атрибутом доступа, имеет
этот атрибут до тех пор, пока в определении класса не встретится другой. Функция
Volume () возвращает объем объекта СВох как значение типа double. Выражение в
операторе return — это просто произведение трех данных-членов класса.
Нет необходимости квалифицировать имена членов класса при обращении к ним из функ-
ций-членов. При выполнении функции неквалифицированные имена членов автоматически
ссылаются на члены текущего объекта.
Функция-член Volume () используется в выделенных операторах main () после ини-
циализации членов данных (как в первом примере). Использование одного и того же
имени переменной в main () не вызывает конфликтов или каких-то других проблем.
Вы можете вызывать функции-члены определенного объекта, указывая имя объекта,
точку, а за ней — имя функции-члена. Как уже отмечалось, функция автоматически об-
ращается к данным-членам объекта, для которого она вызвана, поэтому первое при-
менение Volume () вычисляет объем boxl. Использование только имени члена всегда
будет ссылаться на объект, для которого функция-член была вызвана.
Функция-член вызывается второй раз непосредственно в операторе вывода, что-
бы вычислить объем Ьох2. Если вы выполните этот пример, то получите следующий
вывод:
Объем boxl = 33696
Объем Ьох2 = 6084
Объект СВох занимает 24 байта.
Обратите внимание, что объект СВох по-прежнему занимает в памяти столько
же байт, сколько и ранее. Добавление функции-члена к классу не повлияло на раз-
мер объектов. Очевидно, что функция-член должна находиться где-то в памяти, но
существует лишь одна ее копия, независимо от того, сколько объектов вы создаете, и
память, занятая функцией, не считается при выполнении операции sizeof, опреде-
ляющей количество байт памяти, занятых объектом.
Имена данных-членов класса в функции-члене автоматически ссылаются на дан-
ные-члены конкретного объекта, используемого для вызова функции, и функция мо-
жет быть вызвана только с определенным объектом класса. В данном случае это дела-
ется через операцию прямого обращения к члену, следующего за именем объекта.
Если вы попробуете вызвать функциючлен, не указывая имени объекта, программа компи-
лироваться не будет.
Определение собственных типов данных 355
Расположение определения функции-члена
Определение функции-члена должно быть помещено внутрь определения класса.
Если вы хотите разместить его вне определения класса, то внутри класса потребует-
ся указать ее прототип. Если переписать предыдущий класс с определением функции
вне класса, он будет выглядеть следующим образом:
class СВох // Определение класса в глобальном контексте
public:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
double Volume(void); // Прототип функции-члена
Но теперь вы должны написать определение функции, однако поскольку оно появ-
ляется вне определения класса, должен существовать какой-то способ сообщить ком-
пилятору, что эта функция относится к классу СВох. Это делается за счет снабжения
имени функции префиксом — именем класса и разделением их операцией разреше-
ния контекста, обозначаемой двумя двоеточиями (::). Определение функции теперь
будет выглядеть так:
// Функция для вычисления объема ящика
double СВох::Volume()
{
return m_Length*m_Width*m_Height;
Она обеспечит тот же вывод, что и в предыдущем примере, однако это не будет
в точности той же программой. Во втором случае все вызовы функции трактуются
способом, который вам уже известен. Однако когда вы определяете функцию внутри
определения класса, как в Ех7_03. срр, то компилятор неявно трактует ее как встро-
енную (inline) функцию.
Встроенные функции
Со встроенными функциями компилятор пытается расширить код телом функции
в месте ее вызова. Это позволяет избежать накладных расходов, связанных с вызовом
функции, и потому повышает скорость выполнения вашего кода. На рис. 7.5 можно
видеть этот механизм.
Разумеется, компилятор гарантирует, что расширение вызова телом встроенной функции
не вызовет никаких проблем с именами переменных или областью видимости.
Компилятор не всегда может вставить код встроенной функции в место ее вызова
(например, в случае рекурсивных функций или функций, для которых вы получаете
адрес), но в основном это работает. Лучше всего использовать это для очень корот-
ких, простых функций вроде нашей функции Volume () в классе СВох, потому что та-
кая функция выполняется быстрее, а встраивание ее кода не увеличивает существен-
но размера исполняемого модуля.
При определении функции вне определения класса компилятор трактует функцию
как нормальную, и ее вызов работает обычным образом; однако, также можно сооб-
щить компилятору, что если возможно, то вы бы предпочли, чтобы функция тракто-
валась как встроенная. Это делается простым добавлением ключевого слова inline
356 Глава 7
выглядеть следующим образом:
// Функция для вычисления объема ящика
inline double СВох::Volume()
return m_Length*m_Width*m_Height;
При таком определении функции программа будет точно такой же, как и рань-
ше. Но так можно поместить определение функции-члена за пределами определения
класса, если вы этого хотите, и, тем не менее, воспользоваться преимуществами в
скорости выполнения, которые дает встраивание.
Вы можете применить ключевое слово inline к обычным функциям вашей
программы, которые не имеют отношения к классам, и получить тот же эффект.
Помните, однако, что лучше всего это работает с короткими простыми функциями.
Теперь разберемся с тем, что происходит, когда вы объявляете объект класса.
Функция в классе объявлена
как встроенная
int main(void)
{
inline void function()
Компилятор заменяет
вызовы встроенной функции
телом ее кода, соответствующим
образом исправленным, чтобы
избежать проблем с именами
переменных или областью
видимости
{тело}
{тело}
Рис. 7.5. Механизм встроенных функций
Конструкторы классов
В предыдущем примере программы вы объявили объекты СВох, boxl и Ьох2, а за-
тем старательно прошли по всем данным-членам каждого объекта, чтобы присвоить
им начальные значения. Это неудовлетворительно с нескольких точек зрения. Прежде
всего, очень легко пропустить инициализацию каких-то данных-членов, в частности
в классах, у которых их намного больше, чем в нашем классе СВох. Инициализация
данных-членов некоторых объектов сложных классов может потребовать многих
страниц кода, состоящих из операторов присваивания. Конечное ограничение тако-
го подхода проявляется, когда определяются данные-члены класса, которые не имеют
атрибута public — у вас нет доступа к ним извне класса. Должен существовать другой,
лучший способ, и конечно же, он есть — это конструктор.
Что такое конструктор?
Конструктор класса — это специальная функция класса, которая вызывается при
создании нового объекта этого класса. Таким образом, она предоставляет возмож-
Определение собственных типов данных 357
ность инициализировать объекты во время их создания и гарантировать, что все дан-
ные-члены будут иметь корректные значения. Класс может иметь несколько конструк-
торов, позволяя создавать объекты различными способами.
У вас нет выбора в именовании конструкторов класса — они всегда называются по
имени класса, в котором определены. Функция СВох (), например — конструктор на-
шего класса СВох. К тому же конструктор не имеет типа возврата. Нельзя специфици-
ровать тип возврата конструктора и даже не нужно его объявлять как void. Основное
назначение конструктора класса — присваивать начальные значения элементам дан-
ных класса, и никакой тип возврата в конструкторе не требуется и не допускается.
Практическое занятие | ДобЗВЛеНИб КОНСТруКТОрв К КЛЭССу СВОХ
Давайте расширим наш класс СВох, включив в него конструктор.
// Ех7_04.срр
// Использование конструктора
#include <iostream>
using std::cout;
using std::endl;
class СВох // Определение класса в глобальном контексте
{
public:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
// Определение конструктора
СВох (double lv, double bv, double hv)
{
cout « endl « "Вызван конструктор.";
m_Length = lv; // Установить значения
m_Width = bv; // данных-членов
m_Height = hv;
}
/ / Функция для вычисления объема ящика
double Volume()
{
return m_Length* m_Width* m__Height;
}
};
int main ()
{
CBox boxl (78.0,24.0,18.0); // Объявление и инициализация boxl
СВох cigarBox(8.0,5.0,1.0); // Объявление и инициализация cigarBox
double boxVolume =0.0; // Сохранить объем ящика
boxVolume = boxl.Volume(); // Вычислить объем boxl
cout << endl
<< "Объем boxl = " « boxVolume;
cout « endl
« "Объем cigarBox = "
« cigarBox. Volume () ;
cout « endl;
return 0;
}
358 Глава 7
Описание полученных результатов
Конструктор СВох () принимает три параметра типа double, соответствующие
начальным значениям членов m_Length, m_Width и m_Height объекта СВох. Первый
оператор конструктора выводит сообщение, чтобы было видно, когда он был вызван.
Вы не будете делать это в рабочих программах, но поскольку это очень полезно для
визуализации момента вызова конструктора, так часто поступают в тестовых про-
граммах. Я буду использовать это регулярно в целях иллюстрации. Код, находящийся
в теле конструктора, очень прост. Он просто присваивает аргументы, которые пере-
даны конструктору, соответствующим данным-членам. Если необходимо, вы можете
также включить проверки на правильность аргументов — что они не отрицательны, и
в реальном контексте вы, вероятно, захотите делать это, однако наш основной инте-
рес здесь — проверка работы механизма.
Внутри main () вы объявляете объект boxl с инициализирующими значениями
последовательно для данных-членов m__Length, m_Width и m__Height. Они указаны
в скобках, следующих за именем объекта. Здесь используется нотация функций для
инициализации, которая, как было показано в главе 2, также может быть применена
для инициализации обычных значений базовых типов. Вы также объявляете второй
объект СВох по имени cigarBox, который имеет инициализирующие значения.
Объем boxl вычисляется с применением функции-члена Volume (), как и в пред-
ыдущем примере, после чего отображается на экране. Точно так же отображается
значение объема cigarBox. Вывод этого примера будет таким:
Вызван конструктор.
Вызван конструктор.
Объем boxl = 33696
Объем cigarBox = 40
Первые две строки выводят вызовы конструктора СВох () — по одному для каждо-
го объявленного объекта. Конструктор, добавленный в определение класса, вызыва-
ется автоматически при объявлении объекта СВох, поэтому оба объекта инициализи-
руются значениями, указанными в объявлении. Они передаются конструктору в виде
аргументов, в той последовательности, как записаны в объявлении. Как видите, объ-
ем boxl тот же, что и ранее, а объем cigarBox выглядит правдоподобно, как произ-
ведение его измерений, что достаточно обнадеживает.
Конструктор по умолчанию
Попробуйте модифицировать последний пример, добавив объявление Ьох2, кото-
рое было в предыдущем примере:
СВох Ьох2; // Объявление Ьох2 типа СВох
Здесь мы оставляем Ьох2 без инициализирующих значений. Когда вы попытаетесь
пересобрать эту версию программы, то получите сообщение об ошибке:
error С2512:
ябка С2512:
а
Ш
СВох’: no appropriate default constructor available
' СВох *: нет доступных конструкторов по умолчанию
Это значит, что компилятор ищет конструктор по умолчанию для box2 (также
называемый конструктором без аргументов, поскольку при вызове никаких аргумен-
тов не требует), поскольку вы не указали никаких инициализирующих значений для
членов данных. Конструктор по умолчанию — это конструктор, не требующий аргу-
ментов, и он может либо не иметь параметров в своей спецификации, либо быть та-
Определение собственных типов данных 359
ким, у которого аргументы необязательны. Но этот оператор отлично удовлетворял
компилятор в Ех7_02 . срр — почему же он не работает здесь?
Ответ состоит в том, что в предыдущем примере использовался конструктор по
умолчанию без аргументов, представленный компилятором, так как вы не определи-
ли никакого своего. Поскольку в последнем примере конструктор определен, ком-
пилятор предполагает, что вы обо всем позаботитесь сами, и не создает своего кон-
структора по умолчанию. Поэтому, если вы все же хотите использовать объявления
объектов СВох без инициализирующих значений, то должны предусмотреть конструк-
тор по умолчанию самостоятельно. Как выглядит конструктор по умолчанию? В про-
стейшем случае это просто конструктор, не принимающий никаких аргументов; он
даже не обязан что-либо делать:
СВох () // Конструктор по умолчанию
{} // Полностью свободен от кода
Вы можете посмотреть на этот конструктор в действии.
Практическое занятие
Использование конструктора по умолчанию
Добавим версию конструктора по умолчанию к последнему примеру вместе с объ-
явлением Ьох2, а также исходными присваиваниями данным-членам Ьох2. Вы долж-
ны расширить конструктор по умолчанию, чтобы было видно, когда он вызван. Вот
следующая версия программы:
// Ех7_05.срр
// Добавление и использование конструктора по умолчанию
#include ciostream >
using std::cout;
using std::endl;
class СВох // Определение класса в глобальном контексте
{
public:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
// Определение конструктора
СВох(double lv, double bv, double hv)
{
cout « endl « "Вызван конструктор.";
m_Length = lv; // Установка значений
m_Width = bv; // данных-членов
m_Height = hv;
}
// Определение конструктора по умолчанию
СВох ()
{
cout « endl « "Вызван конструктор по умолчанию.";
// Функция вычисления объема ящика
double Volume()
{
return m_Length*m_Width*m_Height;
}
360 Глава 7
int main ()
СВох boxl (78.0,24.0,18.0); // Объявление и инициализация boxl
СВох Ьох2;
СВох cigarBox(8.О, 5.0, 1.0);// Объявление и инициализация cigarBox
double boxVolume =0.0; // Сохранить объем ящика
boxVolume = boxl.Volume(); // Вычислить объем boxl
cout « endl
« ’’Объем boxl = ” « boxVolume;
box2. m_Height = boxl. m__Height - 10;
box2.m_Length = boxl. m__Length / 2.0;
box2.m Width = 0.25*boxl.m Length;
cout « endl
// Определение
// членов box2
// в терминах boxl
« "Объем box2 = "
« box2. Volume () ;
cout « endl
« "Объем cigarBox = "
« cigarBox. Volume () ;
cout « endl;
return 0;
}
Описание полученных результатов
Теперь, когда вы включили собственную версию конструктора по умолчанию, нет
никаких сообщений об ошибках от компилятора, и все работает. Программа выдает
следующий вывод:
Вызван конструктор.
Вызван конструктор по умолчанию.
Вызван конструктор.
Объем boxl = 33696
Объем Ьох2 = 6084
Объем cigarBox = 40
Все, что делает конструктор по умолчанию — это отображает сообщение. Оче-
видно, что он вызывается в точке объявления объекта Ьох2. Вы также получаете кор-
ректное значение объема всех трех объектов СВох, так что остальная часть програм-
мы работает должным образом.
Один аспект этого примера, который вы, возможно, не заметили, заключается в
том, что конструкторы можно перегружать, как вы перегружали функции в главе 6.
Последний пример включает два конструктора, отличающихся списками параметров.
Один имеет три параметра типа double, а другой вообще не имеет параметров.
Присваивание параметрам в классе
значений по умолчанию
Когда мы говорили о функциях, вы видели, как можно специфицировать значения
параметров по умолчанию в прототипе функции. То же самое можно делать с функци-
ями-членами классов, включая конструкторы. Если вы поместите определение функ-
ции-члена внутрь определения класса, то можете указать значения ее параметров по
умолчанию прямо в заголовке функции. Если же в определение класса включен толь-
ко прототип функции, то в этом прототипе должны присутствовать значения параме-
тров по умолчанию.
Определение собственных типов данных 361
Если вы решили, что значение размера объекта СВох по умолчанию должно быть
равно одиночному ящику кубической формы со всеми сторонами длиной 1, то для
этого можно следующим образом изменить последний пример:
class СВох // Определение класса в глобальном контексте
{
public:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
// Определение конструктора
СВох (double 1 v = 1.0, double bv = 1.0, double hv = 1.0)
{
cout « endl « "Вызван конструктор.";
m_Length = lv; // Установка значений
m_Width = bv; // данных-членов
m_Height = hv;
}
// Определение конструктора по умолчанию
СВох()
{
cout « endl « "Вызван конструктор по умолчанию.";
}
// Функция вычисления объема ящика
double Volume()
{
return m_Length*m_Width*m_Height;
}
};
Что произойдет, если вы проведете такое изменение в последнем примере? Вы по-
лучите от компилятора другое сообщение об ошибке. Помимо большого объема дру-
гой информации, компилятором будут выданы следующие полезные комментарии:
warning С4520: ’СВох’: multiple default constructors specified
error C2668: ’СВох::CBox’: ambiguous call to overloaded function
предупреждение C4520: 'СВох1: определено множество конструкторов по умолчанию
ошибка С2668: 'СВох::СВох': неоднозначный вызов перегруженной функции
Это значит, что компилятор не может решить, какой из двух конструкторов следу-
ет вызывать — тот, для которого указаны значения параметров по умолчанию, или же
конструктор, который не принимает никаких параметров. Это связано с тем, что объ-
явление box2 требует конструктора без параметров, а теперь оба конструктора мо-
гут быть вызваны без параметров. Очевидное решение этой проблемы заключается
в том, чтобы избавиться от конструктора, который не принимает параметров. И это
действительно выгодно. Без этого конструктора объект СВох, объявленный без явной
инициализации, автоматически инициализирует свои члены значением 1.
практическое занятие | Использование значений по умолчанию
для аргументов конструктора
Сказанное можно продемонстрировать в следующем простом примере.
362 Глава 7
// Ех7_06.срр
// Использование значений по умолчанию для аргументов конструктора
#include <iostream>
using std::cout;
using std::endl;
class СВох // Определение класса в глобальном контексте
public:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
// Определение конструктора
СВох (double lv = 1.0, double bv = 1.0, double hv = 1.0)
cout « endl « "Вызван конструктор.";
m_Length = lv; // Установить значения
m_Width = bv; // данных-членов
m_Height = hv;
// Функция для вычисления объема ящика
double Volume О
return m_Length*m_Width*m_Height;
int main ()
СВох box2; // Объявление box2 — никаких начальных значений
cout « endl
« "Объем box2 = "
« box2 .Volume ();
cout « endl;
return 0;
Описание полученных результа тов
Вы объявили одну неинициализированную переменную СВох по имени Ьох2, по-
тому что для целей демонстрации этого достаточно. Эта версия программы выдает
следующий вывод:
Вызван конструктор.
Объем Ьох2 - 1
Это доказывает, что конструктор со значениями параметров по умолчанию выпол-
няет свою работу, устанавливая значения объектов, для которых не заданы инициали-
зирующие значения.
Вы не должны предполагать на основании этого, что это единственный или даже
рекомендуемый способ реализации конструктора по умолчанию. Бывает много случа-
ев, когда не нужно присваивать значения по умолчанию подобным способом, и тогда
лучше написать отдельный конструктор по умолчанию. Бывают даже ситуации, когда
вообще не нужен конструктор по умолчанию, даже несмотря на то, что вы опреде-
ляете другой конструктор. Это должно гарантировать, что все объявленные объекты
класса будут иметь инициализирующие значения, явно специфицированные в их объ-
явлении.
Определение собственных типов данных 363
Использование списка инициализации в конструкторе
Ранее вы инициализировали члены объекта в конструкторе класса, используя яв-
ные аргументы. Однако существует и другая техника, которая основана на примене-
нии списка инициализации. Ниже она демонстрируется в альтернативной версии
конструктора класса СВох.
// Определение конструктора с использованием списка инициализации
СВох (double lv = 1.0, double bv = 1.0, double hv = 1.0) :
m_Length(Iv), m_Width(bv), m_Height(hv)
cout « endl « ’’Вызван конструктор.”;
Способ написания определения этого конструктора предполагает, что он появля-
ется в теле определения класса. Теперь значения данных-членов не устанавливаются
операциями присваивания в теле конструктора. Они специфицированы в виде ини-
циализирующих значений с применением функциональной нотации и появляются в
списке инициализации как части заголовка функции. Например, член m_Length ини-
циализируется значением lv. Это может оказаться более эффективным, нежели при-
менение присваивания, как это делалось в предыдущей версии. Если вы подставите
эту версию конструктора в предыдущий пример, вы увидите, что она будет работать
так же хорошо.
Обратите внимание, что список инициализации для конструктора отделен от спи-
ска параметров двоеточием, и каждый инициализатор отделен запятой. Эта техника
инициализации параметров в конструкторе важна, потому что, как вы увидите позже,
это не единственный способ установки значений данных-членов определенного типа
объекта. Библиотека MFC также в большой степени полагается на применение техни-
ки списков инициализации.
Приватные члены класса
Наличие конструктора, который устанавливает значения данных-членов объекта
класса, но при этом открывает возможность любой части программы вмешаться во
внутреннее содержимое объекта, по определению противоречиво. В рамках анало-
гии представим, что после того, как вам сделал операцию такой блестящий хирург,
как доктор Килдер (Dr. Kildare), чья квалификация была отточена годами практики,
вы вдруг позволяете местному водопроводчику или каменщику копаться в ваших вну-
тренностях — вряд ли это разумно. Таким образом, необходима какая-то защита для
ваших данных-членов класса.
Необходимую защиту можно обеспечить, используя ключевое слово private при
определении членов класса.
Члены класса, специфицированные как private, в общем случае могут быть до-
ступны только функциям-членам этого же класса. Существует одно исключение, но
мы поговорим о нем позже. Обычные функции не имеют прямого доступа к private
членам класса. Это показано на рис. 7.6.
Наличие возможности спецификации членов класса как private также позволяет
отделить интерфейс класса от его внутренней реализации. Интерфейс класса состоит
из общедоступных (public) членов и общедоступных функций-членов в частности,
поскольку они могут обеспечить непрямой доступ ко всем членам класса, включая
private. Сохраняя внутренности класса приватными (private), вы можете позднее
364 Глава 7
модифицировать их, например, для увеличения производительности, без необходи-
мости модификации кода, который использует этот класс через его общедоступный
интерфейс. Чтобы сохранить данные и функции-члены класса защищенными от
внешнего вмешательства, хорошей практикой является объявление тех из них, кото-
рые не должны быть представлены внешнему миру, как private. Объявляйте public
только то, что действительно нужно для внешнего использования вашего класса.
Рис. 7.6. Общедоступные и приватные члены класса
Практическое занятие | ПрИВЭТНЫв ДЭННЫе-ЧЛвНЫ
Теперь вы можете переписать класс СВох так, чтобы его данные-члены были
private.
// Ех7_07.срр
// Класс с приватными членами
#include <iostream>
using std::cout;
using std::endl;
class СВох // Определение класса в глобальном контексте
{
public:
I/ Определение конструктора с использованием списка инициализации
СВох (double lv = 1.0, double bv = 1.0, double hv = 1.0) :
m_Length (lv), m_Width(bv), m_Height(hv)
Определение собственных типов данных 365
cout « endl « "Вызван конструктор.";
// Функция для вычисления объема ящика
double Volume()
return m_Length*m_Width*m_Height;
private:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
int main()
СВох match(2.2, 1.1, 0.5); // Объявление ящика match (спичечного коробка)
СВох Ьох2; // Объявление Ьох2 — без инициализирующих значений
cout « endl
« "Объем match = "
« match.Volume ();
// Раскомментировать следующую строку, чтобы получить ошибку
// box2 .m_Length = 4.0;
cout « endl
« "Объем box2 = "
« box2.Volume();
cout « endl;
return 0;
Описание полученных результатов
Определение класса СВох теперь состоит из двух разделов. Первый — public — со-
держит конструктор и функцию-член Volume (). Второй раздел специфицирован
как private и содержит данные-члены. Теперь они могут быть доступны только для
функций-членов класса. Не нужно модифицировать никакие функции-члены — они
по-прежнему могут обращаться ко всем членам класса. Но если вы удалите коммента-
рий в main () с оператора, который присваивает значение члену m Length объекта
Ьох2, то получите ошибку компиляции, которая сообщит, что член данных не досту-
пен. Если вы еще этого не сделали, обратите внимание на члены класса СВох в Class
View (Представление классов). Там вы увидите рядом с каждым членом пиктограмму,
указывающую его доступность; маленький замочек на пиктограмме означает, что член
является приватным (private).
Важно помнить, что теперь использование конструктора или функции-члена — это
единственный способ получить значение члена данных private объекта. Вы должны
удостовериться, что все способы, которыми вы пожелаете устанавливать или моди-
фицировать члены данных private класса, представлены через функции-члены.
В раздел private класса можно также помещать функции. В этом случае они мо-
гут вызываться только из других функций-членов того же класса. Если вы поместите
функцию Volume () в раздел private, то получите сообщение компилятора об ошиб-
ке в операторе, который пытается вызвать эту функцию внутри функции main ().
Если в раздел private поместить конструктор, вы не сможете объявить ни одного
объекта типа этого класса.
366 Глава 7
Предыдущий пример генерирует следующий вывод:
Вызван конструктор.
Вызван конструктор.
Объем match = 1.21
Объем Ьох2 = 1
Это демонстрирует, что класс по-прежнему работает удовлетворительно, хотя его
данные-члены определены с атрибутом доступа private. Основное отличие в том,
что теперь они полностью защищены от неавторизованного доступа и модифика-
ции.
Если вы не укажете иначе, атрибутом доступа по умолчанию, применяемым ко всем членам
класса, будет pri vate. Таким образом, вы можете поместить все приватные члены в нача-
ло определения класса и позволить им по умолчанию быть приватными, пропустив ключевое
слово доступа. Однако лучше потрудиться и явно определить состояние атрибута доступа в
каждом случае, дабы не возникало никаких сомнений относительно ваших намерений.
Конечно, вы не обязаны объявлять все данные-члены как private. Если приложе-
ние, использующее ваш класс, того требует, можно иметь некоторые члены опреде-
ленные как private, а другие — public. Все зависит от того, что вы хотите сделать.
Если нет веских причин делать члены класс public, то лучше объявить их private,
поскольку это сделает класс более безопасным. Обычные функции не будут иметь до-
ступа ни к одному из членов private вашего класса.
Доступ к приватным членам класса
Если подумать, объявление членов данных класса как private довольно-таки экс-
тремально. Это очень хорошо защищает их от неавторизованных модификаций, но
нет причин всегда держать их значение в секрете. Что вам нужно, так это “Закон о
свободе информации” для приватных членов.
Вам не придется писать письмо депутатам — этот закон уже к вашим услугам. Все,
что потребуется сделать — это написать функцию-член, которая будет возвращать зна-
чение члена данных. Взгляните на следующую функцию-член класса СВох:
inline double СВох::GetLength()
return m Length;
Только для того, чтобы показать, как это будет выглядеть, определение этой функ-
ции вынесено за пределы определения класса. Я специфицировал ее как inline, по-
скольку это дает преимущество в скорости без слишком большого увеличения размера
кода. Предполагая, что объявление этой функции находится в разделе public класса,
вы можете применять ее в операторах вроде следующего:
int len = box2.GetLength();
// Получить член данных, описывающий длину
Все, что необходимо сделать — написать подобную функцию для каждого члена
данных, который вы хотите сделать доступным для чтения внешнему миру, и их зна-
чения будут доступны без нарушения безопасности класса. Конечно, если вы помести-
те определения этих функций внутрь определения класса, они будут встроенными по
умолчанию.
Определение собственных типов данных 367
Дружественные функции класса
Бывают случаи, когда по той или иной причине вы хотите некоторым функциям,
не являющимся членами класса, разрешить доступ ко всем членам класса, то есть
определить разновидность элитной группы со специальными привилегиями. Такие
функции называются дружественными функциями класса и определяются с помо-
щью ключевого слова friend. Вы можете включить либо только прототип друже-
ственной функции в определение класса, либо все определение функции целиком.
Функции, являющиеся друзьями класса и определенные внутри определения класса,
также по умолчанию являются встроенными.
Дружественные функции не являются членами класса, и потому атрибуты доступа к ним
не применимы. Это просто обычные глобальные функции со специальными привилегиями.
Предположим, что вы хотите реализовать дружественную функцию в классе СВох
для вычисления площади поверхности объекта СВох.
практическое занятие | Использование дружественной функции
для вычисления площади поверхности
Посмотреть, как работает дружественная функция, можно в следующем примере.
// Ех7_08.срр
// Создание дружественной функции класса
#include <iostream>
using std::cout;
using std::endl;
class СВох // Определение класса в глобальном контексте
{
public:
// Определение конструктора
СВох (double lv = 1.0, double bv = 1.0, double hv = 1.0)
{
cout « endl « "Вызван конструктор.";
m_Length = lv; // Установить значения
m_Width = bv; // данных-членов
m_Height = hv;
}
// Функция для вычисления объема ящика
double Volume()
{
return m__Length*m_Width*m_Height;
}
private:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
// Дружественная функция
friend double BoxSurface (СВох аВох) ;
};
// Дружественная функция для вычисления площади поверхности объекта СВох
double BoxSurface (СВох аВох)
{
368 Глава 7
return 2. О* (aBox.m_Length*aBox.m_Width Н
аВох. m_Length*aBox. m_Height
аВох. m_Height*aBox. m_Width) ;
int main()
CBox match (2.2, 1.1/ 0.5); // Объявление ящика match
СВох Ьох2; // Объявление box2 — без инициализирующих значений
cout « endl
« "Объем match = ”
« match.Volume ();
cout « endl
« "Площадь поверхности match = "
« BoxSurf ace (match) ;
cout « endl
« "Объем box2 = "
« box2. Volume () ;
cout « endl
« "Площадь поверхности box2 = "
« BoxSurfасе(Ьох2);
cout « endl;
return 0;
}
Описание полученных результатов
Вы объявляете функцию BoxSurface () как друга класса СВох, написав ее прото-
тип с ключевым словом friend в начале. Поскольку сама функция BoxSurface () яв-
ляется глобальной, неважно, куда именно вы поместите объявление friend внутри
объявления класса, но будет хорошей идеей придерживаться определенного стиля
в размещении этого объявления. Как видите, я предпочел поместить ее после всех
public- и private-членов класса. Помните, что дружественная функция не является
членом класса, поэтому атрибуты доступа к ней не относятся.
Определение функции следует за классом. Обратите внимание, что вы специфици-
руете доступ к данным-членам объекта внутри определения BoxSurface (), используя
объект СВох, переданный функции в качестве параметра. Поскольку дружественная
функция не является членом класса, она не может ссылаться на данные-члены класса
просто по именам; они должны быть квалифицированы именем объекта — точно так
же, как в обычной функции, за исключением того, конечно, что обычная функция не
может получить доступ к приватным членам класса. Дружественная функция — это то
же самое, что и обычная функция, но с тем отличием, что ей разрешен доступ ко всем
членам класса или классов, другом которых она является, безо всяких ограничений.
Этот пример дает следующий вывод:
Вызван конструктор.
Вызван конструктор.
Объем match = 1.21
Площадь поверхности match = 8.14
Объем Ьох2 = 1
Площадь поверхности Ьох2 = 6
Это именно то, чего следовало ожидать. Дружественная функция вычисляет пло-
щадь поверхности объектов СВох по значениям приватных членов.
Определение собственных типов данных 369
Размещение определения дружественной функции внутри класса
Можно комбинировать определение функции с ее объявлением как друга класса
СВох внутри определения класса, и код будет выполняться, как и ранее. Определение
функции в классе должно выглядеть следующим образом:
friend double BoxSurface(СВох аВох)
{
return 2.О*(aBox.m_Length*aBox.m_Width +
аВох.m_Length*aBox.m_Height +
аВох.m_Height*aBox.m_Width);
}
Однако такой вариант имеет ряд недостатков, касающийся читабельности кода.
Хотя функция также будет иметь глобальный контекст, это может быть не очевидным
читателю кода, поскольку функция скрыта в теле определения класса.
Конструктор копирования по умолчанию
Предположим, что вы объявили и инициализировали объект boxl типа СВох в
следующем операторе:
СВох boxl(78.0, 24.0, 18.0);
Теперь вы хотите создать другой объект СВох, идентичный первому. Было бы не-
плохо инициализировать второй объект СВох первым — boxl. Давайте попробуем.
Практическое занятие | КОПИРОВаНИе ИНфОрМЗЦИИ МвЖДУ
экземплярами
Рассмотрим следующий пример, который демонстрирует копирование.
// Ех7_09.срр
// Инициализация объекта другим объектом того же класса
#include <iostream>
using std::cout;
using std::endl;
class СВох // Определение класса в глобальном контексте
{
public:
// Определение конструктора
СВох (double lv = 1.0, double bv = 1.0, double hv = 1.0)
{
cout « endl « ’’Вызван конструктор.”;
m_Length = lv; // Установить значения
m_Width = bv; // данных-членов
m_Height = hv;
}
// Функция для вычисления объема ящика
double Volume()
{
return m_Length*m_Width*m_Height;
}
private:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
370 Глава 7
double m_Height; // Высота ящика в дюймах
int main ()
СВох boxl (78.0, 24.0, 18.0);
СВох Ьох2 = boxl; // Инициализировать Ьох2 объектом boxl
cout « endl
« "объем boxl = " « boxl .Volume ()
« endl
« "объем box2 = " « box2 .Volume ();
cout « endl;
return 0;
Этот пример выдаст следующий результат:
Вызван конструктор.
Объем boxl = 33696
Объем Ьох2 = 33696
Описание полученных результатов
Ясно, что программа работает так, как нужно — оба ящика имеют одинаковый объ-
ем. Однако, как видно из вывода, наш конструктор был вызван только один раз — для
создания объекта boxl. Внимание, вопрос: а как был создан Ьох2? Механизм подобен
тому, с которым мы сталкивались, когда не было определено ни одного конструктора,
и компилятор создал конструктор по умолчанию, чтобы обеспечить возможность соз-
дания объекта. В данном случае компилятор генерирует версию по умолчанию того,
что называется конструктором копирования.
Конструктор копирования делает именно то, что нам здесь нужно — он создает
объект класса, инициализируя его существующим объектом того же класса. Версия
по умолчанию конструктора копирования создает новый объект путем копирования
существующего, член за членом.
Этого достаточно для таких простых классов, как СВох, но для многих других клас-
сов, которые, например, содержат в себе указатели или массивы в качестве членов,
это не будет работать правильно. В самом деле, с такими классами конструктор копи-
рования по умолчанию может создать серьезные ошибки в программах. В этих слу-
чаях вы должны создавать свой собственный конструктор копирования. Это требует
специального подхода, который будет подробно рассмотрен до конца настоящей гла-
вы, и еще раз — в следующей главе.
Указатель this
В классе СВох вы написали функцию Volume () в терминах имен членов класса вну-
три определения класса. Конечно, каждый объект типа СВох, созданный вами, содер-
жит эти члены, так что в функции должен быть механизм обращения к конкретному
объекту, для которого вызвана данная функция.
Когда любой член функции выполняется, он автоматически получает скрытый
указатель по имени this, указывающий на объект, использованный с вызовом функ-
ции. Таким образом, когда во время выполнения функции Volume () осуществляется
обращение к члену m Length, то на самом деле идет обращение к this->m_Length,
что представляет собой полностью специфицированную ссылку на член используе-
Определение собственных типов данных 371
мого объекта. Компилятор берет на себя заботу о добавлении необходимого имени
указателя this к именам членов в функции.
При необходимости вы можете использовать указатель this явно внутри функ-
ции-члена. Так, например, вы можете вернуть указатель на текущий объект.
Практическое занятие | ЯВН06 ИСП0ЛЬ30ВЭНИе this
Можно добавить в класс СВох общедоступную функцию, которая будет сравнивать
объемы двух объектов СВох.
// Ех7_10.срр
// Использование указателя this
#include <iostream>
using std::cout;
using std::endl;
class СВох // Определение класса в глобальном контексте
{
public:
// Определение конструктора
СВох (double lv = 1.0, double bv = 1.0, double hv = 1.0)
{
cout « endl « "Вызван конструктор.”;
m_Length = lv; // Установить значения
m_Width = bv; // данных-членов
m_Height = hv;
}
// Функция для вычисления объема ящика
double Volume()
{
return m_Length*m_Width*m_Height;
}
// Функция сравнения объемов двух ящиков; возвращает true (1) ,
// если первый больше второго, и false (0) - в противном случае
int Compare (СВох хВох)
{
return this->Volume () > хВох. Volume () ;
}
private:
double m_Length; / / Длина ящика в дюймах
double m_Width; 11 Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
};
int main ()
{
СВох match(2.2, 1.1, 0.5); // Объявление ящика match
СВох cigar(8.0, 5.0,1.0); // Объявление ящика cigar
if(cigar.Compare(match))
cout « endl
« "match меньше, чем cigar”;
else
cout « endl
« "match равен или больше cigar";
cout << endl;
return 0;
}
372 Глава 7
Описание полученных результатов
Функция-член Compare () возвращает true, если фиксированный текущий объект
СВох в вызове функции имеет больший объем, чем объект СВох, указанный в аргумен-
те, и false, если наоборот. В операторах return фиксированный текущий объект
выражается указателем this, использованным с операцией непрямого доступа к чле-
ну (->), которую вы уже видели ранее в настоящей главе.
Запомните, что вы используете операцию прямого обращения к члену, когда обращаетесь к
членам через объекты, и операцию непрямого доступа к члену — когда обращаетесь к члену
через указатель на объект, this— это указатель, поэтому с ним применяется операция ->.
Операция -> работает с указателями на объекты классов точно так же, как вы виде-
ли в примерах со структурами. Здесь использование указателя this демонстрирует, что
он существует и работает, хотя в данном случае совсем не обязательно применять его
явно. Если вы измените оператор return в функции Compare () следующим образом:
return Volume() > хВох.Volume ();
то обнаружите, что программа работает так же хорошо. Любые ссылки на “не укра-
шенные” имена члена автоматически трактуются как члены объекта, на который ука-
зывает this.
Вы используете функцию Compare () в main (), чтобы проверить отношение объ-
емов объектов match и cigar. Ниже показан вывод этой программы.
Вызван конструктор.
Вызван конструктор,
match меньше, чем cigar
Это подтверждает, что объект cigar больше, чем объект match.
То, что функция Compare () определена как член класса, несущественно. С тем же
успехом вы могли написать ее как обычную функцию с двумя объектами СВох в ка-
честве аргументов. Но обратите внимание, что в отношении функции Volume () это
не так, потому что ей нужен доступ к приватным (private) данным-членам класса.
Конечно, если вы реализуете функцию Compare () как обычную функцию, она не бу-
дет иметь доступа к указателю this, но, тем не менее, останется очень простой:
// Сравнение двух объектов СВох — версия обычной функции
int Compare (СВох Bl, СВох В2)
return Bl.Volume () > B2.Volume();
Эта функция принимает оба объекта в качестве аргументов и возвращает true,
если объем первого больше, чем объем второго. Вы должны использовать эту функ-
цию для выполнения того же действия, что и в предыдущем примере, следующим об-
разом:
if(Compare(cigar, match))
cout « endl
« "match меньше, чем cigar";
else
cout « endl
« "match равен или больше, чем cigar";
Это выглядит немного лучше и легче читается, чем оригинальная версия; однако
существует намного лучший способ сделать то же самое, и о нем вы узнаете в следую-
щей главе.
Определение собственных типов данных 373
const-объекты класса
Функция Volume (), определенная вами для класса СВох, не изменяет объект, с ко-
торым вызывается, как и функция getHeight (), которая возвращает значение члена
m—Height. Аналогично, функция Compare () из предыдущего примера не изменяет
объекты класса вообще. На первый взгляд это может показаться не слишком интерес-
ным и совершенно не важным фактором, но на самом деле это довольно-таки важно.
Давайте подумаем об этом.
Без сомнений, иногда вам понадобится создавать объекты, значения которых фик-
сированы, такие как pi или отношение inchesPerFoot (количество дюймов в футе),
которые вы можете объявить как const double. Предположим, что вы хотите опре-
делить объект СВох как const (например, потому, что это очень важный ящик стан-
дартного размера). Вы можете определить его с помощью следующего оператора:
const СВох standard(3.О, 5.0, 8.0);
Теперь, когда вы определили стандартный ящик размером 3x5x8, вы не хотели бы
его испортить. В частности, вы не хотите позволить изменять значения, хранимые в
данных-членах. Как обеспечить их постоянство?
Вы уже можете это сделать. Если объявить объект класса как const, то компилятор
не позволит вызвать ни одну функцию-член, которая может изменить его. Убедиться
в этом очень легко, изменив объявление объекта cigar из предыдущего примера:
const СВох cigar(8.0, 5.0,1.0); // Объявление ящика cigar
Если вы попытаетесь скомпилировать программу с этим изменением, то потерпи-
те неудачу — будет выдано следующее сообщение об ошибке:
error С2662: 'compare' : cannot convert 'this' pointer from 'const class CBox’
to 'class CBox &' Conversion loses qualifiers
ошибка C2662: 'compare' : не удается преобразовать указатель 'this' из
'const class CBox' в 'class CBox &' Преобразование приведет к потере квалификаторов
Это сообщение относится к оператору if, который вызывает функцию-член
Compare () для cigar. Объект, который вы объявляете как const, будет всегда иметь
константный указатель this, так что компилятор не позволит вызвать ни одной
функции-члена, которая не предполагает, что переданный ей this является const.
Значит, нужно найти способ, как сделать указатель this в функции-члене const.
const-функция-член класса
Чтобы сделать константным указатель this в функции-члене, ее следует объ-
явить как const внутри определения класса. Посмотрим, как это сделать с функцией
Compare () класса СВох. Определение класса должно быть модифицировано следую-
щим образом:
class СВох // Определение класса в глобальном контексте
public:
// Определение конструктора
СВох (double lv = 1.0, double bv = 1.0, double hv = 1.0)
cout « endl « "Вызван конструктор.";
m_Length = lv; // Установить значения
m Width = bv; // данных-членов
374 Глава 7
m_Height - hv;
I/ Функция для вычисления объема ящика
double Volume()
return m_Length*m_Width*m_Height;
// Функция сравнения объемов двух ящиков; возвращает true (1),
// если первый больше второго, и false (0) - в противном случае
int Compare(СВох хВох) const
return this->Volume() > хВох.Volume();
private:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
Чтобы указать, что функция-член является константной, вы просто добавляе-
те ключевое слово const к ее заголовку. Обратите внимание, что это можно делать
только с функциями-членами класса, но не с обычными глобальными функциями.
Поэтому объявление функции как const имеет смысл только в том случае, если эта
функция является членом класса. В результате указатель this в ней также становится
const, что в свою очередь означает, что вы не можете записывать члены данных в
левой части операторов присваивания внутри определения этой функции; это будет
отмечено компилятором как ошибка. Константная функция-член не может вызывать
неконстантную функцию-член того же класса, поскольку это потенциально может мо-
дифицировать объект.
Когда вы объявляете объект как const, то вызываемая с ним функция-член также
должна быть объявлена как const, иначе программа не скомпилируется.
Определения функций-членов вне класса
Когда определение константной функции-члена появляется вне класса, заголовок
этого определения должен включать дополнительное ключевое слово const — так же,
как это делается с объявлением внутри класса. Фактически, вы всегда должны объ-
являть как const все функции-члены, не изменяющие объект класса, к которому они
относятся. Имея все это в виду, класс СВох можно определить следующим образом:
class СВох // Определение класса в глобальном контексте
public:
// Конструктор
СВох (double lv = 1.0, double bv = 1.0, double hv = 1.0);
double Volume() const; // Вычисление объема ящика
int Compare(СВох хВох) const; // Сравнение двух ящиков
private:
double m_Length;
double m_Width;
double m_Height;
// Длина ящика в дюймах
/ / Ширина ящика в дюймах
// Высота ящика в дюймах
Определение собственных типов данных 375
Это предполагает, что все функции-члены определены отдельно, включая кон-
структор. Оба члена — Volume () и Compare () — объявлены как const. Функция
Volume () теперь определена вне класса:
double СВох:‘.Volume () const
{
return m_Length*m_Width*m_Height;
}
Ниже показано определение функции Compare ().
int СВох::Compare(СВох хВох) const
{
return this->Volume() > хВох.Volume О;
}
Как видите, модификатор const появляется в обоих определениях. Если его опу-
стить, код компилироваться не будет. Функция с модификатором const отличается
от функции без него, даже несмотря на то, что их имена и параметры в точности
совпадают. На самом деле вы можете иметь в классе и константную и неконстантную
версии функции, и зачастую это очень полезно.
Для класса, объявленного как показано выше, конструктор также должен быть
объявлен отдельно, а именно:
СВох::СВох(double lv, double bv, double hv) :
m_Length(lv), m_Width(bv), m_Height(hv)
{
cout « endl « "Вызван конструктор.";
J
Массивы объектов класса
Массив объектов класса объявляется точно так же, как обычный массив, элементы
которого относятся к одному из встроенных типов. Каждый элемент в массиве объ-
ектов класса требует вызова для него конструктора по умолчанию.
Практическое занятие МЭССИВЫ ОбЪвКТОВ КЛЭССЭ
Мы можем воспользоваться определением класса СВох из последнего примера, из-
менив его добавлением специфичного конструктора по умолчанию:
// Ех7__11.срр
II Использование массива объектов класса
#include <iostream>
using std::cout;
using std::endl;
class СВох // Определение конструктора в глобальном контексте
{
public:
// Определение конструктора
СВох(double lv, double bv = 1.0, double hv = 1.0)
{
cout « endl « "Вызван конструктор. ";
m_Length = lv; // Установить значения
m_Width = bv; // данных-членов
m__Height = hv;
}
У1Ъ Глава 7
СВох () // Конструктор по умолчанию
{
cout « endl
« ’’Вызван конструктор по умолчанию,
m Length = m Width = m Height = 1.0;
// Функция вычисления объема ящика
double Volume () const
return m_Length*m_Width*m_Height;
private:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
int main()
СВох boxes [5]; // Массив объявленных объектов СВох
СВох cigar(8.0, 5.0, 1.0); // Объявление ящика cigar
cout « endl
« "Объем boxes [3] - " « boxes [3] .Volume ()
« endl
« "Объем cigar = " « cigar. Volume ();
cout « endl;
return 0;
}
Эта программа сгенерирует следующий вывод:
Вызван конструктор по умолчанию.
Вызван конструктор по умолчанию.
Вызван конструктор по умолчанию.
Вызван конструктор по умолчанию.
Вызван конструктор по умолчанию.
Вызван конструктор.
Объем boxes[3] = 1
Объем cigar = 40
Описание полученных результатов
Здесь мы модифицировали конструктор, принимающий аргументы так, что только
два их них могут иметь значения по умолчанию, и добавили конструктор по умолча-
нию, инициализирующий данные-члены значением 1 после вывода сообщения о том,
что он был вызван. Теперь вы можете видеть, какой из конструкторов и когда был вы-
зван. Конструкторы теперь имеют достаточно отличающиеся списки параметров, так
что компилятор не может их спутать.
Из вывода можно видеть, что конструктор по умолчанию вызывается пять раз — по
одному для каждого элемента массива boxes. Другой конструктор был вызван доя соз-
дания объекта cigar. Отсюда видно также, что инициализация в конструкторе по умол-
чанию работает удовлетворительно, поскольку объемы элементов массива равны 1.
Определение собственных типов данных 377
Статические члены класса
И данные-члены, и функции-члены класса могут быть объявлены как static (ста-
тические) . Поскольку их контекст — определение класса, эффект от этого будет не-
сколько большим, чем эффект от ключевого слова static вне класса, так что давайте
рассмотрим для начала статические данные-члены.
Статические данные-члены класса
Когда вы объявляете данные-члены класса как static, это дает тот эффект, что
они создаются в единственном экземпляре и разделяются всеми объектами класса.
Каждый объект получает собственные копии обычных данных-членов класса, но для
всего класса имеется только по одному экземпляру каждого статического члена, не-
зависимо от того, сколько объектов этого класса было определено. Сказанное иллю-
стрируется на рис. 7.7.
Определение класса
class СВох
public:
static int objectCount;
private:
double m_Length;
double m_Width;
double m_Height;
object2
mLength
m_Width
m_Height
• • • A
objects
Одна копия каждого статического члена данных
разделяется между всеми объектами типа класса
objectCount
Рис» 7.7. Статические данные-члены класса
378 Глава 7
Одним из применений статических данных-членов является подсчет количества
существующих объектов данного класса. Вы можете поместить статические данные-
члены в общедоступный (public) раздел класса СВох, добавив следующий оператор к
предыдущему определению класса:
static int objectcount; // Счетчик существующих объектов
Теперь возникает проблема: как инициализировать статический член данных?
Вы не можете инициализировать его в определении класса — ведь это только “про-
ект” объекта, и инициализирующие значения в нем не разрешены. Вы не должны
инициализировать его в конструкторе, поскольку желаете увеличивать его значение
при каждом вызове конструктора, чтобы подсчитывать количество созданных объек-
тов. Вы не можете инициализировать его в другой функции-члене, поскольку она ас-
социирована с объектом, а инициализация нужна до создания любого объекта. Ответ
заключается в том, чтобы инициализировать статический член данных вне определе-
ния класса с помощью следующего оператора:
int СВох::objectcount = 0; // Инициализация статического члена объекта СВох
Обратите внимание, что ключевое слово static здесь отсутствует; однако вы должны ква-
лифицировать имя члена с использованием имени класса и операции разрешения контекста,
чтобы компилятор понял, что вы ссылаетесь на статический член класса. Иначе вы бы про-
сто создали глобальную переменную, не имеющую отношения к классу.
Практическое занятие) ПОДСЧвТ ЭКЗвМПЛЯрОВ
Давайте добавим к последнему примеру статический член данных и возможность
подсчета объектов.
// Ех7_12.срр
// Использование статического члена данных в классе
#include <iostream>
using std::cout;
using std::endl;
class СВох // Определение класса в глобальном контексте
{
public:
static int objectcount; // Счетчик существующих объектов
// Определение конструктора
СВох (double lv, double bv - 1.0, double hv = 1.0)
{
cout « endl « "Вызван конструктор.";
m_Length = lv; // Установка значений
m_Width = bv; // данных-членов
m_Height = hv;
objectCount++;
}
CBox() // Конструктор по умолчанию
{
cout « endl
« "Вызван конструктор no умолчанию.";
m_Length = m_Width = m_Height =1.0;
objectCount++;
}
Определение собственных типов данных 379
1
// Функция для вычисления объема ящика
double Volume () const
return m_Length*m_Width*m_Height ;
}
private:
double m__Length;
double mJWidth;
double m_Height;
// Длина ящика в дюймах
// Ширина ящика в дюймах
// Высота ящика в дюймах
int СВох: :objectcount = 0; // Инициализация статического члена класса СВох
int main()
СВох boxes [5]; // Объявление массива объектов СВох
СВох cigar(8.0, 5.0, 1.0) ; // ящажа cigar
cout « endl « endl
« "Количество объектов (через класс) = "
« СВох::objectcount;
cout « endl
« "Количество объектов (через объект) = "
« boxes[2].objectcount;
cout « endl;
return 0;
}
Этот пример генерирует следующий вывод:
Вызван конструктор по умолчанию.
Вызван конструктор по умолчанию.
Вызван конструктор по умолчанию.
Вызван конструктор по умолчанию.
Вызван конструктор по умолчанию.
Вызван конструктор.
Количество объектов (через класс) = 6
Количество объектов (через объект) = 6
Описание полученных результатов
Этот код показывает, что не важно, как вы обращаетесь к статическому члену
Objectcounter (через сам класс или через любой объект этого класса). Значение бу-
дет одним и тем же — равным количеству объектов класса, которые были созданы.
Очевидно, что шесть объектов — это пять элементов массива boxes плюс объект
cigar. Интересно отметить, что статические члены класса существуют, даже если ни-
каких членов класса нет. И это очевидно, поскольку вы инициализируете статический
член Obj ectCounter до того, как будет объявлен любой объект класса.
Статические данные-члены автоматически создаются при запуске программы и инициа-
лизируются 0, если только вы не инициализируете их каким-то другим значением. Поэтому
вы должны инициализировать статические данные-члены класса только в том случае, если
хотите, чтобы они имели начальное значение, отличное от 0.
Статические функции-члены класса
Объявляя функцию-член класса как static, вы делаете ее независимой от любо-
го конкретного объекта данного класса. Обращение к членам класса из статической
функции должно выполняться с использованием квалифицированных имен (как вы
380 Глава 7
должны это делать с обычными глобальными функциями, обращающимися к обще-
доступным данным-членам). Статическая функция-член обладает тем преимуществом,
что она существует и может быть вызвана даже тогда, когда еще не существует ни
одного объекта данного класса.
В этом случае в ней могут использоваться только статические данные-члены, по-
скольку на этот момент только они и существуют. Таким образом, вы можете вызвать
статическую функцию-член для проверки статических данных-членов, даже если не
знаете, существуют ли объекты данного класса, и если существуют — то сколько.
Конечно, после того, как объекты определены, статическая функция-член может
обращаться как к приватным (private), так и к общедоступным (public) членам объ-
ектов класса. Статическая функция может иметь следующий прототип:
static void Afunction(int n);
Статическая функция может быть вызвана с конкретным объектом в операторе
вроде следующего:
аВох.Afunction(10);
где аВох — объект класса. Та же функция также может быть вызвана без ссылки на
объект. В этом случае оператор примет следующую форму:
СВох::Afunction(10) ;
где СВох — имя класса. Использование имени класса и операции разрешения контек-
ста необходимо, чтобы сообщить компилятору, к какому классу относится функция
Afunction ().
Указатели и ссылки на объекты классов
Использование указателей, и в частности, ссылок на объекты классов — очень важ-
но в объектно-ориентированном программировании, в частности, для спецификации
параметров функций. Объекты классов могут включать в себя значительные объемы
данных, поэтому использование механизма передачи по значению параметров-объ-
ектов в функции может оказаться очень дорогостоящим по затратам времени и не-
эффективным, поскольку при этом должно выполняться копирование каждого объ-
екта-аргумента. Существует также техника, предусматривающая применение ссылок,
которая незаменима при некоторых операциях с классами. Как вы увидите, невоз-
можно написать конструктор копирования без использования параметра-ссылки.
Указатели на объекты класса
Вы объявляете указатель на объект класса точно так же, как и любой другой указа-
тель. Например, указатель на объект класса СВох объявляется следующим образом:
СВох* рВох = 0; // Объявление указателя на СВох
Теперь вы можете использовать его для хранения адреса объекта СВох путем при-
сваивания, применяя операцию получения адреса:
рВох = &cigar; // Сохранить адрес объекта СВох cigar в рВох
Как видите, при использовании указателя this в определении функции-члена
Compare () вы можете вызвать функцию, применяя указатель на объект. Например,
функцию Volume () для указателя рВох можно вызвать с помощью такого оператора:
cout « pBox->Volume(); //Отобразить объем объекта, на который указывает рВох
Определение собственных типов данных 381
Опять-таки, здесь применяется операция непрямого обращения к члену. Это — типичная но-
тация, используемая большинством программистов для операций подобного типа, поэтому
с настоящего момента я буду применять ее повсеместно.
Практическое занятие) УКЭЗЭТеЛИ НЭ КЛЭССЫ
Попробуем испытать операцию непрямого обращения к члену. Для этого обратим-
ся к примеру Ех7_10. срр, но немного изменим его.
// Ех7_13.срр
// Испытание операции непрямого доступа к члену класса
#include <iostream>
using std::cout;
using std::endl;
class СВох 11 Определение класса в глобальном контексте
{
public:
// Определение конструктора
СВох (double lv = 1.0, double bv = 1.0, double hv = 1.0)
{
cout « endl « "Вызван конструктор.";
m_Length = lv; // Установить значения
m_Width = bv; // данных-членов
m_Height = hv;
}
// Функция для вычисления объема ящика
double Volume() const
{
return m_Length*m_Width*m_Height;
}
// Функция сравнения объемов двух ящиков; возвращает true (1),
// если первый больше второго, и false (0) - в противном случае
int Compare (СВох* рВох) const
{
return this->Volume() > pBox->Volume();
}
private:
double m_Length; / / Длина ящика в дюймах
double m_Width; // Ширина ящика в дайнах
double m_Height; // Высота ящика в дюймах
};
int main ()
{
СВох boxes [5]; // Объявление массива объектов СВох
СВох match (2.2, 1.1, 0.5); // Объявление ящика match
СВох cigar(8.0, 5.0, 1.0); // Объявление ящика cigar
СВох* рВ1 = &cigar; // Инициализация указателя адресом объекта cigar
СВох* рВ2 = 0; // Указатель на СВох, инициализированный null
cout « endl
« "Адрес cigar равен " « рВ1 // Отображение адреса
« endl
« "Объем cigar равен "
« pBl->Volume() ; 11 Объем объекта, на который установлен указатель
рВ2 = Snatch;
382 Глава 7
if (pB2->Coinpare (pBl)) // Сравнение черев указатель
cout « endl
« "match большеf чем cigar";
else
cout « endl
"match меньше или равен cigar";
pBl = boxes; // Установка адреса массива
boxes [2] = match; // Установка З-го элемента равным match
cout « endl // Доступ через указатель
« "Объем boxes [2] равен " « (рВ1 + 2) ->Volume();
cout << endl;
return 0;
Если вы запустите этот пример, то получите примерно такой вывод:
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Адрес cigar равен 0012FE20
Объем cigar равен 40
match меньше или равен cigar
Объем boxes[2] равен 1.21
Конечно, значение адреса объекта cigar на вашей машине может отличаться.
Описание полученных результатов
Единственное изменение в этом определении класса не слишком существенно.
Вы только модифицировали функцию Compare (), чтобы она принимала указатель на
объект СВох в качестве аргумента, и теперь мы знаем о том, что функция-член объяв-
лена как const, поскольку она не изменяет объект. Функция main () только испыты-
вает указатели на объекты СВох различными, довольно произвольными способами.
Внутри функции main () объявлены два указателя на объекты СВох после объяв-
ления массива boxes, а также объекты СВох с именами cigar и match. Первый, рВ1,
инициализируется адресом объекта cigar, а второй, рВ2, инициализируется NULL.
Использование этих указателей осуществляется в точности таким способом, каким вы
применяете указатели на базовые типы. Тот факт, что указатель установлен на объект
типа, который вы определили самостоятельно, не имеет значения.
Вы используете рВ 1 с операцией непрямого доступа к члену для генерации объ-
ема объекта, на который он указывает, и результат отображается на экране. Затем
вы присваиваете адрес match указателю рВ2 и используете оба указателя при вызове
функции сравнения. Поскольку аргументом функции Compare () является указатель
на объект СВох, функция применяет операцию непрямого выбора члена для вызова
функции Volume () для этого объекта.
Чтобы продемонстрировать возможность использования арифметики адресов над
указателем рВ1 в случае применения его для выбора функции-члена, вы устанавливае-
те в рВ1 адрес первого элемента массива boxes элементов типа СВох. В данном случае
выбирается третий элемент массива и вычисляется его объем. Это то же самое, что
и объем match.
Определение собственных типов данных
383
В выводе программы можно видеть, что выполняется семь вызовов конструктора
объектов СВох: пять для элементов массива boxes и по одному для каждого из объ-
ектов cigar и match.
В целом почти нет разницы между использованием указателей на объекты класса
и использованием указателей на базовый тип, такой как double.
Ссылки на объекты класса
Практическая польза ссылок особенно проявляется, когда они используются с
классами. Как и в случае указателей, почти нет разницы между способом объявления
и использования ссылок на объекты класса и способом объявления и использования
ссылок на переменные базовых типов. Чтобы объявить ссылку на объект cigar, на-
пример, вы должны написать следующее:
СВох& rcigar = cigar; // Определение ссылки на объект cigar
Чтобы применить ссылку для вычисления объема объекта cigar, вы должны про-
сто использовать имя ссылки вместо имени объекта:
cout « rcigar.Volume(); // Вывод объема cigar через ссылку
Как вы должны помнить, ссылка действует в качестве псевдонима объекта, на ко-
торый она ссылается, поэтому ее применение в точности совпадает с применением
исходного имени объекта.
Реализация конструктора копирования
Важность ссылок проявляется в контексте аргументов и возвращаемых значений
функций — в частности, функций-членов класса. Давайте вернемся к вопросу о кон-
структоре копирования. Пока оставим в стороне вопрос о том, когда следует писать
собственный конструктор копирования и сосредоточимся на вопросе о том, как вы
можете его написать. Я использую класс СВох просто для того, чтобы сделать реше-
ние более конкретным.
Конструктор копирования — это конструктор, создающий объект за счет инициа-
II
лизации его существующим объектом того же класса. Поэтому он должен принимать
объект класса в качестве аргумента. Вы можете рассмотреть написание прототипа
примерно так:
СВох(СВох initB);
Теперь посмотрим, что произойдет, когда конструктор будет вызван. Если вы на-
пишете следующее объявление:
СВох myBox = cigar;
то компилятор сгенерирует вызов конструктора копирования:
СВох::СВох(cigar) ;
Выглядит так, словно никаких проблем нет, пока вы не поймете, что аргумент пе-
редается по значению. Поэтому перед тем, как объект cigar может быть передан,
компилятор должен будет создать его копию. Таким образом, он вызовет конструктор
копирования, чтобы создать аргумент для вызова конструктора копирования. К со-
жалению, поскольку аргумент передается по значению, этот вызов также потребует
копирования аргумента, так что конструктор копирования будет вызываться снова и
снова. То есть вы получите бесконечный цикл вызовов конструктора копирования.
384 Глава 7
Решение, которого, как я полагаю, вы ожидаете, заключается в использовании
константного параметра-ссылки. Значит, прототип конструктора копирования можно
написать следующим образом:
СВох(const СВох& initB);
Теперь аргумент конструктора копирования не нуждается в копировании. Он ис-
пользуется для инициализации ссылочного параметра, так что никакого копирова-
ния не происходит. Как вы должны помнить из дискуссии о ссылках, если параметр
функции — ссылка, то никакого копирования аргумента при ее вызове не происходит.
Функция обращается к переменной-аргументу непосредственно. Квалификатор const
гарантирует, что аргумент не может быть модифицирован в функции.
Это еще одно важное применение квалификатора const. Вы всегда должны объявлять ссы-
лочный параметр функции как const, если только функция не модифицирует его.
Конструктор копирования можно реализовать так:
СВох::СВох(const СВох& initB)
m_Length = initB.m_Length;
m_Width = initB.m_Width;
m_Height = initB.m_Height;
Такое определение конструктора копирования предполагает, что оно расположе-
но вне определения класса. Таким образом, имя конструктора квалифицировано име-
нем класса с использованием операции разрешения контекста. Каждый член данных
создаваемого объекта инициализируется соответствующим членом переданного в ар-
гументе объекта. Конечно, с тем же успехом вы можете использовать список инициа-
лизации, чтобы установить значения объекта.
Этот случай не может служить примером, когда вы обязаны написать конструктор
копирования. Как вы уже видели, конструктор копирования по умолчанию отлично
работает с объектами СВох. В следующей главе я объясню для чего и когда вы должны
писать собственный конструктор копирования.
Программирование на C++/CLI
Язык программирования C++/CLI имеет свои собственные типы struct и class.
Фактически C++/CLI позволяет определять по два разных вида пользовательских
структур и классов, которые имеют разные характеристики: value struct (структу-
ры типов значений) и value class (классы типов значений), а также ref struct
(структуры ссылочные) и ref class (классы ссылочные). Каждая из комбинаций
двух слов — value struct, ref struct, value class и ref class — является ключе-
вым словом и отличается от ключевых слов struct и class; слова value и ref сами
по себе не являются ключевыми. Как и в “родном” C++, единственное отличие между
классами и структурами в C++/CLI заключается в том, что члены структур по умол-
чанию общедоступны (public), в то время как члены классов по умолчанию приват-
ные (private). Одна существенная разница между классами значений (и структурами
значений), с одной стороны, и классами ссылочными (и ссылочными структурами), с
другой, состоит в том, что переменные типов значений содержат свои собственные
данные, тогда как переменные типов ссылок должны быть дескрипторами, а потому
они содержат адрес.
Определение собственных типов данных
385
Обратите внимание, что функции-члены классов C++/CLI не могут быть объявле-
ны как const. Другое отличие от родного C++ заключается в том, что указатель this
в нестатических функциях-членах класса значений типа Т является внутренним ука-
зателем типа interior_ptr<T>, в то время как указатель this в ссылочном классе
Т — это дескриптор типа ТА. Следует иметь это в виду при возврате указателя this из
функций C++/CLI или при сохранении его в локальной переменной. Существуют три
других ограничения, которые касаются и классов значений, и ссылочных классов.
□ Класс значений или ссылочный класс не может содержать поля, являющиеся
массивами “родного” C++ или типами классов “родного” C++.
□ Дружественные функции не разрешены.
□ Класс значений или ссылочный класс не может иметь членов, представляющих
собой битовые поля.
Вы уже знаете из главы 4, что имена фундаментальных типов, такие как int или
double, являются сокращениями для типов классов значений в программах CLR.
Когда вы объявляете элемент данных типа класса значений, память для него выде-
ляется в стеке, однако вы можете создавать объекты классов значений и в куче, при-
меняя операцию gcnew — в этом случае переменная, используемая для обращения к
объекту класса значений, должна быть дескриптором. Например:
double pi = 3.142;
intA lucky = gcnew
doubleA two = 2.0;
// pi сохраняется в стеке
(7); // lucky — дескриптор, и 7 сохраняется в куче
// two — дескриптор, и 2.0 сохраняется в куче
Вы можете использовать любую из этих переменных в арифметических выраже-
ниях, но следует применять операцию * для разыменования дескрипторов, чтобы об-
ратиться к значению, например:
Console::WriteLine(L"2pi = {0}"r *two*pi);
Обратите внимание, что вы можете записать произведение как pi**two и полу-
чить правильный результат, но лучше в таких случаях использовать скобки и писать
pi* (*two), что сделает код яснее.
Определение типов классов значений
Я не буду объяснять типы value struct отдельно от value class, поскольку
разница между ними заключается лишь в том, что члены value struct по умолча-
нию являются public, в то время как члены value class — по умолчанию private.
Класс значений задуман как относительно простой тип класса, обеспечивающий воз-
можность определять новые примитивные типы, которые могут быть использованы
аналогично фундаментальным типам; однако, вы не должны делать этого в полной
мере до тех пор, пока не изучите тему перегрузки операции в следующей главе.
Переменная типа класса значений создается в стеке и хранит свое значение непо-
средственно, но как вы уже видели, вы также можете применять отслеживаемый де-
скриптор, чтобы ссылаться на тип класса значений, хранящийся в куче CLR.
Рассмотрим пример определения простого класса значений.
// Класс, представляющий рост
value class Height
{
private:
// Записывает рост в футах и дюймах
int feet;
int inches;
386 Глава 7
public:
// Создает рост в дюймах
Height(int ins)
{
feet = ins/12;
inches = ins%12;
// Создает рост в футах и дюймах
Height(int ft, int ins) : feet(ft), inches(ins){}
Это определяет тип класса значений по имени Height. У него есть два приватных
поля — оба типа int, которые хранят значение роста в футах и дюймах. В классе пред-
усмотрены два конструктора — один для создания объекта Height из числа дюймов,
переданных в аргументе, а второй — для создания объекта Height на основании спе-
цификации футов и дюймов. Последний должен проверять, что передано число дюй-
мов меньше 12, но это я оставляю для вашей самостоятельной проработки. Чтобы
создать переменную типа Height, вы можете записать:
Height tall = Height (7, 8); // Рост 7 футов 8 дюймов
Этот оператор создает переменную tall, которая содержит объект Height, пред-
ставляющий 7 футов и 8 дюймов; этот объект создается вызовом конструктора с дву-
мя параметрами.
Height baseHeight;
Этот оператор создает переменную baseHeight, которая будет автоматически
инициализирована нулевым ростом. Класс Height не имеет конструктора без аргу-
ментов, и поскольку это класс значений, вы не можете определить его самостоятель-
но в определении класса. Поэтому в класс значений будет автоматически включен
конструктор без аргументов, который инициализирует все значения полей эквива-
лентом нуля, а поля-дескрипторы — nullpt г, и вы не можете заменить этот неявный
конструктор собственной версией. Именно этот конструктор по умолчанию и будет
применяться для создания значения baseHeight.
Существует еще пара других ограничений относительно того, что может содер-
жать в себе класс значений.
□ Вы не должны включать конструктор копирования в определение класса значе-
ний.
□ Вы не можете переопределить операцию присваивания в классе значений (пе-
реопределение операций в классе мы обсудим в главе 8).
Объекты классов значений всегда копируются простым копированием полей, и
присваивание одного объекта такого класса другому выполняется так же. Классы зна-
чений предназначены для представления простых объектов, содержащих в себе не-
большой объем данных, поэтому для представления объектов, не удовлетворяющих
таким характеристикам, или для которых ограничения классов значений создают
проблемы, вы должны использовать типы ref class.
Возьмем на пробу класс Height.
Определение собственных типов данных 387
Практическое занятие | ОпрвДвЛвНИв И ИСПОЛЬЗОВЭНИв ТИПЭ КЛЭССЭ
значений
Ниже приведен код для испытания класса значения Height.
// Ех7_14.срр : main project file.
// Определение и использование типа класса значений
#include "stdafx.h”
using namespace System;
// Класс, представляющий рост
value class Height
{
private:
// Записывает рост в футах и дюймах
int feet;
int inches;
public:
// Создает рост в дюймах
Height(int ins)
{
feet = ins/12;
inches = ins%12;
}
// Создает рост в футах и дюймах
Height(int ft, int ins) : feet(ft), inches(ins){}
};
int main(array<System::String A> Aargs)
{
Height myHeight = Height (6,3) ;
HeightA yourHeight = Height (70) ;
Height hisHeight = *yourHeight;
Console: :WriteLine(Ь"Мой рост есть {0}", myHeight) ;
Console: :WriteLine(Ь"Твой рост ест {0}", yourHeight) ;
Console: :WriteLine(L”Ero рост есть {0}", hisHeight);
return 0;
}
Выполнение этой программы дает в результате следующий вывод:
Мой рост есть Height
Твой рост есть Height
Его рост есть Height
Press any key to continue . . .
Описание полученных результатов
Да, вывод довольно монотонный и возможно, менее информативный, чем мы мог-
ли надеяться, но давайте вернемся к этому несколько позже. В функции main () вы
создаете три переменных с помощью следующих операторов:
Height myHeight = Height{6,3);
HeightA yourHeight = Height(70);
Height hisHeight = *yourHeight;
Первая переменная имеет тип Height, поэтому объект, представляющий рост
в 6 футов и 3 дюйма, размещается в стеке. Вторая переменная — дескриптор типа
HeightА, поэтому объект, представляющий рост в 5 футов и 10 дюймов, создается
388 Глава 7
в куче CLR. Третья переменная — еще одна стековая переменная, являющаяся копи-
ей объекта, на который ссылается yourHeight. Поскольку yourHeight — дескрип-
тор, его нужно разыменовать, чтобы присвоить переменной hisHeight, в результа-
те чего hisHeight содержит дубликат объекта, на который ссылается yourHeight.
Переменные типа класса значений содержат уникальные объекты, поэтому две таких
переменных не могут ссылаться на один и тот же объект; присваивание одной пере-
менной типа класса значений другой такой переменной всегда подразумевает копи-
рование. Конечно, несколько дескрипторов могут ссылаться на единственный объ-
ект и присваивать значение одного дескриптора другому, просто копируя адрес (или
nullpt г), так что оба объекта ссылаются на один и тот же объект.
Вывод выполняется тремя вызовами функции Console: :WrireLine ().К сожале-
нию, при этом не выводятся конкретные значения объектов, а просто имя класса. Как
это исправить? Вообще говоря, было бы слишком оптимистично ожидать, что будут
выведены значения — в конце концов, откуда компилятор может знать, как их следует
представить? Объект Height содержит два значения — какое из них нужно показать?
Класс должен иметь способ представления значения в этом контексте.
Функция класса ToString ()
Каждый класс C++/CLI, который вы определяете, включает функцию ToString () —
откуда она берется, я объясню в следующей главе, когда речь пойдет о наследова-
нии. Эта функция возвращает дескриптор строки, представляющей объект класса.
Компилятор предполагает вызов функции ToString () для объекта всякий раз, когда
определяет, что ожидается строковое представление объекта, но при необходимости
вы можете вызвать ее и явно. Например, можно записать так:
double pi = 3.142;
Console::WriteLine(pi.ToString());
На экран будет выведено значение pi в виде строки, а вызываемая здесь функция
ToString () определена в классе System::Double. Конечно, тот же результат вы по-
лучите и без явного вызова ToString ().
Версия по умолчанию функции ToString () , которую вы получаете в классе
Height, просто выводит имя класса, потому что нет способа узнать заранее, какое зна-
чение должно быть возвращено в виде строки для объекта вашего типа класса. Чтобы
получить соответствующее значение для вывода функцией Console:: WriteLine ()
в предыдущем примере, вы должны добавить собственную функцию ToString () в
класс Height, представляющий значение объекта так, как вы хотите.
Вот как будет выглядеть класс с функцией ToString ():
// Класс, представляющий рост
value class Height
private:
/ / Записывает рост в футах и дюймах
int feet;
int inches;
public:
// Создает рост в дюймах
Height(int ins)
feet = ins/12;
inches = ins%12;
Определение собственных типов данных 389
// Создает рост в футах и дюймах
Height(int ft, int ins) : feet(ft), inches (ins){}
// Создает строку, представляющую объект
virtual String^ ToStringO override
return feet + L" футов " + inches + L" дюймов”;
Комбинация ключевого слова virtual перед типом возврата ToString () и ключе-
вого слова override, за которым следует список параметров функции, указывает, что
эта версия функции ToString () переопределяет версию, представленную в классе
по умолчанию. Подробнее об этом вы узнаете в главе 8. Наша новая версия функции
ToStringO теперь выводит строку, выражающую рост в футах и дюймах. Если вы
добавите эту функцию в определение класса из предыдущего примера, то получите
следующий вывод после компиляции и выполнения программы:
Мой рост 6 футов 3 дюймов
Твой рост 5 футов 10 дюймов
Его рост 5 футов 10 дюймов
Press any key to continue . . .
Это больше похоже на то, что вы ожидали получить. Вы можете заметить в этом
выводе, что функция WriteLine () вполне успешно справляется с объектом из кучи
CLR, на который ссылается дескриптор yourHeight, так же, как и с объектами
myHeight и hisHeight, которые созданы в стеке.
Литеральные поля
Множитель 12, использованный для преобразования из футов в дюймы и наобо-
рот, немного беспокоит. Это пример так называемого “магического числа” — когда че-
ловек, читающий код, должен сам догадываться о его назначении и происхождении.
В данном случае достаточно очевидно, что такое 12, но в большинстве случаев смысл
числовых констант, участвующих в вычислениях, не столь ясен. В C++/CLI имеется
специальное средство — литеральные поля, предназначенные для представления
числовых констант в классе, — которое позволяет решить эту проблему. Ниже пока-
зано, как можно исключить “магические числа” из кода в конструкторе с одним аргу-
ментом класса Height.
value class Height
private:
11 Записывает рост в футах и дюймах
int feet;
int inches;
literal int inchesPerFoot = 12;
public:
// Создает рост в дюймах
Height(int ins)
{
feet = ins / inchesPerFoot;
inches = ins % inchesPerFoot;
11 Создает рост в футах и дюймах
Height(int ft, int ins) : feet(ft), inches(ins){}
390 Глава 7
// Создает строку, представляющую объект
virtual StringA ToStringO override
return feet + L” футов ”+ inches + L" дюймов”;
Теперь конструктор использует имя inches Per Foot вместо 12, так что не возника-
ет никаких сомнений относительно того, что происходит.
Вы можете определить значение литерального поля через другие литеральные
поля, если только имена полей, через которые выражается данное поле, определены
раньше. Например:
value class Height
I/ Прочий код...
literal int inchesPerFoot = 12;
literal double millimetersPerlnch = 25.4;
literal double millimetersPerFoot = inchesPerFoot*millimetersPer!nch;
11 Прочий код...
Здесь литеральное поле millimetersPerFoot определено как произведе-
ние двух других литеральных полей. Если вы переместите определение поля
millimetersPerFoot, так что оно будет предшествовать одному или обоим другим
полям, код компилироваться не будет.
Определение типов ссылочных классов
Ссылочный класс по своим возможностям совместим с классом “родного” C++ и не
имеет ограничений, которые присущи классу значений. В отличие от класса “родно-
го” C++, однако, ссылочный класс не имеет конструктора копирования по умолчанию
или операции присваивания по умолчанию. Если ваш класс должен поддерживать лю-
бую из этих операций, вы должны явно добавить соответствующую функцию для ее
реализации. Как это сделать — будет показано в следующей главе.
Ссылочный класс определяется с использованием ключевого слова ref class-
слова в этом сочетании, разделенные одним или более пробелами, представляют еди-
ное ключевое поле. Рассмотрим класс СВох из примера Ех7_07, переопределенный
как ссылочный класс.
ref class Box
public:
// Конструктор без аргументов, устанавливающий значения полей по умолчанию
Вох(): Length(1.0), Width(1.0), Height(1.0)
Console::WriteLine(Ь”Вызван конструктор без аргументов.”);
// Определение конструктора, использующего список инициализации
Box(double lv, double bv, double hv) :
Length(lv), Width(bv), Height(hv)
Console::WriteLine(1”Вызван конструктор.”);
Определение собственных типов данных 391
// Функция для вычисления объема ящика
double Volume()
{
return Length*Width*Height;
}
private:
double Length; // Длина ящика в дюймах
double Width; // Ширина ящика в дюймах
double Height; // Высота ящика в дюймах
Прежде всего, обратите внимание, что я исключил префикс С из имени класса,
и префикс т_ из имен полей, поскольку эта нотация не рекомендована для классов
C++/CLL Вы не можете специфицировать значения по умолчанию для параметров
функций и конструктора в классах C++/CLI, поэтому должны добавить в класс Box
конструктор без аргументов. Этот конструктор просто инициализирует три приват-
ных поля значениями 1.0.
практическое занятие | Использование типа ссылочного класса
Ниже приведен пример, использующий класс Box, который вы видели в предыду-
щем примере.
// Ех7_15.срр : main project file.
// Использование типа ссылочного класса Box
#include "stdafx.h"
using namespace System;
ref class Box
{
public:
// Конструктор без аргументов, устанавливающий значения полей по
умолчанию
Вох(): Length(1.0), Width(1.0), Height(l.O)
{
Console::WriteLine(Ь"Вызван конструктор без аргументов.");
}
// Опре деление конструктора, использующего список инициализации
Box (double lv, double bv, double hv) :
Length (lv), Width (bv) , Height (hv)
{
Console::WriteLine(Ь"Вызван конструктор. ") ;
}
// Функция для вычисления объема ящика
double Volume ()
{
return Length*Width*Height;
}
private:
double Length; // Длина ящика в дюймах
double Width; 11 Ширина ящика в дюймах
double Height; // Высота ящика в дюймах
};
int main(array<System::String л> Aargs)
{
BoxA аВох; 11 Дескриптор типа BoxA
BoxA newBox = gcnew Box (10, 15, 20);
392 Глава 7
аВох = gcnew Box; // Инициализировать Box по умолчанию
Console: :WriteLine(L"06*beM ящика по умолчанию: {0}", aBox->Volume());
Console:: WriteLine (L" Объем нового ящика: {0}", newBox->Volume ());
return 0;
Вывод этого примера будет таким:
Вызван конструктор.
Вызван конструктор без аргументов.
Объем ящика по умолчанию: 1
Объем нового ящика: 3000
Press any key to continue . . .
Описание полученных результатов
Первый оператор в main () создает дескриптор объекта Box.
ВохЛ аВох;
/ / Дескриптор типа Box
Этим оператором не создается никакой объект, создается только отслеживаемый
дескриптор — аВох. Переменная аВох инициализируется по умолчанию nullpt г, по-
этому пока она ни на что не указывает. В отличие от этого, переменная класса типа
значения всегда содержит объект.
Следующий оператор создает дескриптор нового объекта Box.
ВохА newBox = gcnew Box (10, 15, 20);
Конструктор, принимающий три аргумента, вызывается для создания в куче объ-
екта Box и его адрес сохраняется в дескрипторе newBox. Как вы знаете, объекты ти-
пов ссылочных классов всегда создаются в куче CLR и всегда доступны только через
дескриптор.
Вы создаете объект Box, вызывая конструктор без аргументов, и сохраняете его
адрес в аВох.
аВох = gcnew Box; // Инициализировать Box по умолчанию
Все поля этого объекта — Length, Width и Height установлены в 1.0.
И, наконец, выводятся объемы двух только что созданных объектов Box:
Console::WriteLine(L"Default box volume is {0}", aBox->Volume());
Console::WriteLine(L"New box volume is {0}”, newBox->Volume());
Поскольку аВох и newBox — дескрипторы, вы используете операцию ->, чтобы вы-
звать функцию Volume () для объектов, на которые они указывают.
Свойства классов
Свойство (property) — это член либо ссылочного класса, либо класса значения
к которому вы обращаетесь, как к полю, но которое, однако, полем не является.
Главное отличие между свойством и полем состоит в том, что имя поля ссылается на
место в памяти, в котором хранится элемент данных, в то время как имя свойства
вызывает функцию. Свойство имеет функции доступа get () и set () — соответствен-
но для получения и установки значения, так что когда вы используете имя свойства,
чтобы прочесть его значение, то за сценой вызывается функция get () для свойства,
а когда вы используете имя свойства в левой части оператора присваивания, то вы-
зывается функция s е t (). Если свойство представляет только определение функции
get (), оно называется свойством только для
я, потому что функция set () не-
Определение собственных типов данных 393
доступна для установки значения свойства. Свойство может иметь только функцию
set (), в этом случае оно будет свойством только для записи.
Класс может содержать два разных вида свойств: скалярные свойства и индек-
сированные свойства. Скалярные свойства представляют единственное значение
доступное через имя свойства, а индексированные свойства — это наборы значений,
к которым вы обращаетесь, указывая индекс между квадратными скобками, которые
следуют за именем свойства. Класс String имеет скалярное свойство Length, пред-
ставляющее количество символов в строке, и для объекта String типа str вы обра-
щаетесь к свойству Length с помощью выражения str->Length, потому что str — де-
скриптор. Конечно, чтобы обратиться к свойству МуРгор объекта класса значения,
хранящемся в переменной val, нужно использовать выражение val.МуРгор — как
при доступе к полю. Свойство Length строки — пример свойства только для чтения,
поскольку для него не определена функция set () — вы не можете установить длину
строки, поскольку объект String не изменяем. Класс String также предоставляет
доступ к индивидуальным символам строки в виде индексированного свойства. Для
строкового дескриптора str вы можете обратиться к третьему индексированному
свойству с помощью выражения str [2], которое соответствует третьему символу
строки.
Свойства могут быть ассоциированы с конкретным объектом — в этом случае они
описываются как свойства экземпляра. Свойство Length объекта String — пример
свойства экземпляра. Вы можете также специфицировать свойство как static — в
этом случае свойство ассоциировано с классом в целом, и его значение является об-
щим для всех объектов типа класса. Давайте рассмотрим свойства немного подроб-
нее.
Определение скалярных свойств
Скалярное свойство имеет единственное значение, и вы определяете скалярное
свойство в классе с использованием ключевого слова property. Функция get () для
скалярного свойства должна иметь тип возврата, совпадающий с типом свойства, а
функция set () должна иметь параметр того же типа, что и свойство. Рассмотрим
пример свойства класса значений Height, который видели ранее.
value class Height
private:
/1 Записывает рост в футах и дюймах
int feet;
int inches;
literal int inchesPerFoot =12;
literal double inchesTdMeters = 2.54/100;
public:
// Создает рост из значения в дюймах
Height(int ins)
feet = ins / inchesPerFoot;
inches = ins % inchesPerFoot;
/1 Создает рост из футов и дюймов
Height(int ft, int ins) : feet(ft), inches(ins){}
// Рост в метрах в качестве свойства
property double meters
394 Глава 7
// Возвращает значение свойства
double get ()
return inchesToMeters*(fееt*inchesPerFoot+inches);
// Создает строковое представление объекта
virtual String74 ToStringO override
return feet + L" футов "+ inches + L
Класс Height теперь содержит свойство по имени meters. Определение функ-
ции get для свойства находится между фигурными скобками, следующими за именем
свойства. Если будет добавлена функция set () для этого свойства, она также должна
быть помещена здесь. Обратите внимание, что за скобками, закрывающими функции
get () и set (), не следует точка с запятой. Функция get () свойства meters исполь-
зует новый литеральный член класса inchesToMeters, чтобы преобразовать рост из
дюймов в метры. Обращение к свойству meters типа Height обеспечивает доступ к
росту в метрах. Вот как можно это сделать:
Height ht = Height(6, 8); // Рост 6 футов 8 дюймов
Console::WriteLine (L’’Poct равен {0} метра", ht->meters);
Второй оператор выводит значение ht в метрах, используя выражение ht->meters.
Вы не обязаны определять функции свойства get () и set () как встроенные; их
можно определить вне определения класса в файле . срр. Например, вы можете спе-
цифицировать свойство meters класса Height следующим образом:
value class Height
// Код прежний. . .
public:
// Код прежний. ..
// Рост в
property
метрах
double meters
; // Возвращает значение свойства
double
// Здесь вы можете определить функцию set()...
Код прежний...
Функция get () свойства meters теперь объявлена, но не определена в классе
Height, поэтому определение должно быть представлено вне определения класса.
В определении функции get () в файле Height. срр имя функции должно быть ква-
лифицировано именем класса и именем свойства, так что определение выглядит, как
показано ниже.
Height::meters::get()
return inchesToMeters*(feet*inchesPerFoot+inches);
Определение собственных типов данных 395
Квалификатор Height указывает, что определение функции принадлежит классу
Height, а квалификатор meters говорит о том, что функция относится к свойству
meters этого класса.
Конечно, вы можете определить свойства и для ссылочного класса. Вот пример:
ref class Weight
private:
public:
property int pounds
void set(int value) {
lbs = value; }
property int ounces
oz = value; }
Здесь свойства pounds и ounces используются для предоставления доступа к зна-
чениям приватных полей lbs и oz. Вы можете установить значения свойств объекта
Weight и затем обращаться к ним примерно так:
WeightА wt = gcnew Weight;
wt->pounds = 162;
wt->ounces = 12;
Console::WriteLine (L’’Bec равен {0} фунтов {1} унций.", wt->pounds, wt->ounces);
Переменная объекта ref class всегда является дескриптором, поэтому вы должны
использовать операцию - > для доступа к свойствам объекта типа ссылочного класса.
Тривиальные скалярные свойства
Вы можете определить скалярное свойство класса без предоставления определе-
ний функций get() nset() —в этом случае оно называется тривиальным скалярным
свойством. Чтобы специфицировать тривиальное скалярное свойство, вы просто опу-
скаете фигурные скобки, содержащие определения функций get() и set(), и завер-
шаете объявление свойства точкой с запятой. Рассмотрим пример класса значения с
тривиальными скалярными свойствами:
value class Point
property int x;
property int y;
virtual String'
return L"
// Тривиальное свойство
// Тривиальное свойство
ToString() override
)"; // Возвращает ” (x,y)
Определения функций get () и set () применяются автоматически к каждому три-
виальному скалярному свойству и возвращают значение свойства, а также устанавли-
396 Глава 7
вают его по аргументу типа, специфицированного для свойства. Приватное простран-
ство выделяется для размещения значения свойства “за сценой”.
Давайте посмотрим, как работают скалярные свойства.
Практическое занятие | ИСПОЛЬЗОВЭНИв СКЭЛЯрНЫХ СВОЙСТВ
В примере используются три класса — два класса значений и один ссылочный.
// Ех7_16.срр : main project file.
// Использование скалярных свойств
#include "stdafx.h"
using namespace System;
// Класс, определяющий рост человека
value class Height
{
private:
// Записывает рост в футах и дюймах
int feet;
int inches;
literal int inchesPerFoot = 12;
literal double inchesToMeters = 2.54/100;
public:
// Создает рост из значения в дюймах
Height(int ins)
{
feet = ins / inchesPerFoot;
inches = ins % inchesPerFoot;
}
// Создает рост из значения в футах и дюймах
Height(int ft, int ins) : feet(ft), inches(ins){}
// Рост в метрах
property double meters 11 Скалярное свойство
{
// Возвращает значение свойства
double get()
{
return inchesToMeters*(feet*inchesPerFoot+inches);
}
// Здесь можно определить функцию set() свойства...
}
// Создает строковое представление объекта
virtual String74 ToStringO override
{
return feet + L" футов "+ inches + L" дюймов";
}
};
// Определение класса веса человека
value class Weight
{
private:
int lbs;
int oz;
literal int ouncesPerPound = 16;
literal double IbsToKg = 1.0/2.2;
Определение собственных типов данных 397
public:
Weight(int pounds, int ounces)
lbs = pounds;
oz = ounces;
property int pounds // Скалярное свойство
int get() { return lbs; }
void set (int value) { lbs = value; }
property int ounces // Скалярное свойство
int get() { return oz; }
void set (int value) { oz = value; }
property double kilograms // Скалярное свойство
double get() { return IbsToKg*(lbs + oz/ouncesPerPound); }
virtual String* ToStringO override
{ return lbs + L" фунтов " + oz + L" унций"; }
// Класс, определяющий персону
ref class Person
private:
Height ht;
Weight wt;
public:
property String* Name; // Тривиальное скалярное свойство
Person(String* name, Height h, Weight w) : ht(h), wt (w)
Name = name;
Height getHeightO { return ht; }
Weight getWeightO{ return wt; }
int main(array<System::String *> *args)
Weight hisWeight = Weight(185, 7);
Height hisHeight = Height(6, 3);
Person* him = gcnew Person(L"Fred", hisHeight, hisWeight);
Weight herWeight - Weight(105, 3);
Height herHeight = Height(5, 2);
Person* her = gcnew Person(L"Freda", herHeight, herWeight);
Console::WriteLine(L"Ee зовут {0}", her->Name);
Console::WriteLine(L"Ee вес {0:F2} килограмм.",
her->getWeight().kilograms);
Console::WriteLine(L"Ee рост {0}, что равно {1:F2} метра.",
her->getHeight(),her->getHeight().meters);
Console::WriteLine(L"Ero зовут {0}", him->Name);
Console::WriteLine(L"Ero вес {0}.", him->getWeight());
Console::WriteLine(L"Ero рост {0}, что равно {1:F2} метра.",
him->getHeight(),him->getHeight().meters);
return 0;
398 Глава 7
Этот пример генерирует следующий вывод:
Ее зовут Freda
Ее вес 47.73 килограмм.
Ее рост 5 футов 2 дюймов, что равно 1.57 метра.
Его зовут Fred
Его вес 185 фунтов 7 унций.
Его рост 6 футов 3 дюймов, что равно 1.91 метра.
Press any key to continue . . .
Описание полученных результатов
Два класса значений Height и Weight определяют рост и вес персоны. Класс
Person включает поля типов Height и Weight для хранения роста и веса человека,
а его имя хранится в свойстве Name, которое является тривиальным свойством, по-
скольку никаких явных функций get () и set () для него не определено.
Первые два оператора в main () определяют объекты Height и Weight, а следую-
щий оператор затем определяет him:
Weight hisWeight = Weight(185, 7);
Height hisHeight = Height(6, 3);
Person74 him = gcnew Person(L”Fred”, hisHeight, hisWeight);
Height и Weight — классы значений, так что переменные этих типов хранят свои
значения непосредственно. Person — ссылочный класс, поэтому him — дескриптор.
Первый аргумент конструктора класса Person — строковый литерал, так что компи-
лятор создает из него объект String и дескриптор этого объекта передается в виде
аргумента. Второй и третий аргументы — объекты классов значений, которые созда-
ны в первых двух операторах. Конечно, в качестве аргументов передаются их копии,
поскольку существует механизм передачи по значению аргументов функций. Внутри
конструктора класса Person оператор присваивания устанавливает значение параме-
тра Name, а значения двух полей, ht и wt, устанавливаются в списке инициализации.
Единственный способ установить свойство — через неявный вызов функции set ();
свойство не может быть инициализировано в списке инициализации конструктора.
Аналогичная последовательность из трех операторов, применяемая для him, ис-
пользуется и для her. Имея два объекта Person, созданных в куче, вы сначала выво-
дите информацию о her с помощью следующих операторов:
Console::WriteLine(L"Ee зовут {0}”, her->Name);
Console::WriteLine(L”Ee вес {0:F2} килограмм.”,
her->getWeight().kilograms);
Console::WriteLine(L”Ee рост {0} что равно {1:F2} метра.",
her->getHeight(),her->getHeight().meters);
В первом операторе выполняется обращение к свойству Name объекта, на который
ссылается дескриптор her, в выражении her->Name; результат его выполнения — де-
скриптор строки, которую возвращает функция get () этого свойства, так что его
тип — String74.
Во втором операторе происходит обращение к свойству kilograms поля wt объекта,
на который ссылается дескриптор her, в выражении her->getWeight () . kilograms.
Часть her->getWeight () этого выражения возвращает копию поля wt, и она исполь-
зуется для доступа к свойству kilograms; таким образом, значение, возвращенное
функцией get () свойства kilograms, становится значением второго аргумента функ-
ции WriteLine().
Определение собственных типов данных
399
В третьем операторе вывода второй аргумент — это результат выражения
her->getHeight (), которое возвращает копию ht. Чтобы получить его значение в
форме, подходящей для вывода, компилятор подключает вызов функции ToString ()
этого объекта, так что выражение эквивалентно her->getHeight () . ToString (),
и фактически вы можете записывать его так при желании. Третий аргумент функ-
ции WriteLine () — свойство meters объекта Height, возвращенного функцией
getHeight () объекта her типа Person.
Последние три оператора выводят информацию об объекте him способом, анало-
гичным выводу объекта her. В этом случае вес формируется неявным вызовом функ-
ции ToString () поля wt объекта him.
Определение индексируемых свойств
Индексируемые свойства — это набор значений свойств в классе, доступ к которым
осуществляется с указанием индекса между квадратными скобками — подобно доступу
к элементам массива. Вы уже использовали индексированные свойства для строк, по-
скольку класс String позволяет обращаться к символам строки, как к индексируемым
свойствам. Как вы видели, если str — дескриптор объекта String, то выражение
str [4] обращается к пятому индексированному значению свойства, что соответству-
ет пятому символу строки. Свойство, к которому вы обращаетесь, помещая индекс в
квадратные скобки, следующие за именем переменной, которая ссылается на объект,
является примером индексируемого свойства по умолчанию. Индексируемое свой-
ство, имеющее имя, называется именованным индексируемым свойством.
Рассмотрим пример класса, включающего индексируемое свойство по умолчанию.
ref class Name
private:
array<String*>* Names; // Сохраняет имена, как элементы массива
public:
Name(...array<String*>* names) Names(names) {}
// Индексируемое свойство, возвращающее любое имя
property String* default[int]
// Извлечь значение индексируемого свойства
String* get(int index)
if(index >= Names->Length)
throw gcnew Exception(Ь"Индекс вне диапазона");
return Names[index];
Идея класса Name состоит в хранении имен персоны в виде массива индивиду-
альных имен. Конструктор принимает произвольное количество аргументов типа
String74, которые затем сохраняются в поле Names, так что объект Name может на-
капливать любое количество имен.
Индексированное свойство здесь является индексируемым свойством по умол-
чанию, потому что его имя специфицировано ключевым словом default. Если вы
применяете обычное имя в этой позиции спецификации свойства, то такое свойство
будет именованным индексируемым свойством. Квадратные скобки, следующие за
ключевым словом default, указывают на то, что это действительно индексируемое
свойство, и тип, который они в себе заключают (в данном случае int), является ти-
400 Глава 7
пом значений индекса, который должен применяться для извлечения значений свой-
ства. Тип индекса не обязательно должен быть числовым типом, и вы можете иметь
более одного параметра индекса для доступа к значениями индексируемого свойства.
Для индексируемого свойства, доступ к которому осуществляется по единствен-
ному индексу, функция get () должна иметь параметр, специфицирующий индекс
того же типа, который указан между квадратными скобками, следующими за именем
свойства. Функция set () для такого индексируемого свойства должна иметь два пара-
метра: первый параметр — индекс, а второй — новое значение, которое должно быть
установлено для свойства, соответствующее первому параметру.
Посмотрим, как работают индексируемые свойства на практике.
Практическое занятие
Использование индексируемого свойства
по умолчанию
Ниже приведен пример, использующий слегка расширенную версию класса Name.
// Ех7_17.срр : главный файл проекта.
// Определение и использование индексируемых свойств по умолчанию
#include "stdafx.h"
using namespace System;
ref class Name
{
private:
array<StringA>A Names;
public:
Name(...array<StringA>A names) : Names(names) {}
// Скалярное свойство, указывающее количество имен
property int NameCount
{
int get() {return Names->Length; }
}
// Индексируемое свойство, возвращающее имена
property StringA default[int]
{
StringA get(int index)
{
if(index >= Names->Length)
throw gcnew Exception(Ь"Индекс вне диапазона");
return Names[index];
}
}
};
int main(array<System::String A> Aargs)
{
NameA myName = gcnew Name(L"Ebenezer"r L"Isaiah", L"Ezra", L"Inigo",
L"Whelkwhistle");
// Список имен
for (int i = 0 ; i < myName->NameCount ; i++)
Console::WriteLine (I/’Имя {0} : {1}", i+1, myName [i]);
return 0;
Определение собственных типов данных 401
Этот пример выдаст на экран следующее:
Имя 1 : Ebenezer
Имя 2 : Isaiah
Имя 3 : Ezra
Имя 4 : Inigo
Имя 5 : Whelkwhistle
Press any key to continue . . .
Описание полученных результатов
Класс Name в этом примере почти такой же, как и в предыдущем разделе, но с до-
полнительным скалярным свойством NameCount, возвращающим количество имен в
объекте Name. В функции main () вы сначала создаете объект Name с пятью именами:
NameA myName = gcnew Name (L’’Ebenezer", L"Isaiah", L”Ezra", L"Inigo",
L"Whelkwhistle");
Список параметров для конструктора класса Name начинается с многоточия, по-
этому он принимает любое количество аргументов. Аргументы, которые передаются
при его вызове, будут сохранены в элементах массива names, так что инициализация
поля Names массивом names заставляет Names обращаться к массиву names. В пред-
ыдущем операторе конструктору передаются пять аргументов, так что поле Names в
объекте, на который ссылается myName, является массивом из пяти элементов.
Вы обращаетесь к свойствам myName в цикле for для перечисления имен, содержа-
щихся в объекте:
for (int i = 0 ; i < myName->NameCount ; i++)
Console::WriteLine(Ь"Имя {0} : {1}”, i+1, myNamefi]);
Свойство NameCount применяется для управления циклом for. Без этого свойства
вы не можете знать, сколько имен должны быть выведены. Внутри цикла последний
аргумент функции WriteLine () обращается к i-тому индексируемому свойству. Как ви-
дите, обращение к индексируемому свойству по умолчанию предполагает просто при-
менение значения индекса в квадратных скобках после имени переменной myName.
Вывод демонстрирует, что индексируемое свойство работает, как ожидалось.
Индексируемое свойство доступно только по чтению, потому что класс Name вклю-
чает только функцию get () для этого свойства. Чтобы позволить изменять свойства,
вы должны добавить определение функции s е t () к индексируемому свойству по
умолчанию, как показано ниже:
ref class Name
11 Прежний код...
// Индексируемое свойство, возвращающее имена
property StringA default[int]
StringA get(int index)
if(index >= Names->Length)
throw gcnew Exception(Ь"Индекс вне диапазона");
return Names[index];
void set(int index, String* name)
{
if(index
Names->Length)
throw gcnew Exception(L"Индекс вне диапазона") ;
402 Глава 7
Names [index] = name
Теперь вы можете использовать способность устанавливать индексируемое свой-
ство, добавив оператор в main (), который установит значение последнего индекси-
руемого свойства:
Name74 myName = gcnew Name (L’'Ebenezer’’, L"Isaiah’’, L’’Ezra", L’’Inigo",
L’’Whelkwhistle");
myName[myName->NameCount - 1] = L’’Oberwurst’’; // Изменить последнее
// индексируемое свойство
// Список имен
for (int i = 0 ; i < myName->NameCount ; i++)
Console:: WriteLine (Ь’’Имя {0} : {1} ’’, i+1, myName [i]) ;
С этим дополнением вы увидите в выводе новой версии программы, что послед-
нее имя на самом деле будет обновлено оператором, присваивающим новое значение
свойству в позиции индекса myName->NameCount-l.
Вы можете также попробовать добавить в класс именованное индексируемое свой-
ство:
ref class Name
// Прежний код...
// Индексируемое свойство, возвращающее инициалы
property wchar_t Initials[int]
wchar t get(int index)
if(index >= Names->Length)
throw gcnew Exception (Ь’’Индекс вне диапазона’’);
return Names[index][0];
Новое индексируемое свойство имеет имя Initials, поскольку его функция — воз-
врат инициалов имени, специфицированного индексом. Вы определяете именован-
ное индексируемое свойство почти так же, как и индексируемое свойство по умолча-
нию, но с указанием имени, заменяющим ключевое слово default.
Если перекомпилировать программу и снова запустить ее, то можно будет увидеть
следующий вывод:
Имя 1 : Ebenezer
Имя 2 : Isaiah
Имя 3 : Ezra
Имя 4 : Inigo
Имя 4 is Oberwurst
Инициалы: Е.I.Е.I.О.
Press any key to continue . . .
Инициалы получаются обращением к именованному индексируемому свойству в
цикле for, и вывод показывает, что они работают так, как и ожидалось.
Определение собственных типов данных 403
Более сложные индексируемые свойства
Как упоминалось ранее, индексируемые свойства могут быть определены так, что
для доступа к значению понадобится более одного индекса, к тому же индексы не обя-
зательно должны быть числовыми. Рассмотрим пример класса с таким индексируе-
мым свойством.
enum class Day{Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
11 Класс, определяющий магазин
ref class Shop
public:
property StringA Opening[Day, String^] // Время открытия
StringA get(Day day, StringA AmOrPm)
switch(day)
case Day::Saturday: // Время открытия в субботу:
if(AmOrPm == L"am")
return L"9:00"; // 9 утра
else
return L’’14:30"; // 2:30 дня
break;
case Day::Sunday: // Время открытия в воскресенье:
return L"closed"; // Весь день закрыт
break;
default:
if (AmOrPm == L'*am") //Время открытия с понедельника по пятницу:
return L"9:30"; // 9:30 утра
else
return L”14:00”; //2 часа дня
break;
Класс, представляющий магазин, имеет индексированное свойство, указывающее
время открытия. Первый индекс — перечислимое значение типа Day, которое иден-
тифицирует день недели, а второй индекс — дескриптор строки, указывающей нуж-
ное время открытия — утром или после обеда. Вы можете вывести свойство Opening
объекта Shop следующим образом:
ShopA shop = gcnew Shop;
Console: .‘WriteLine (shop->Opening[Day::Saturday, L”pm”]);
Первый оператор создает объект Shop, а второй отображает время открытия ма-
газина в субботу утром. Как видите, вы просто помещаете два индексных значения
свойства между квадратными скобками и разделяете их запятыми. Второй оператор
выведет строку “14:30”. Если сможете придумать, зачем это может понадобиться, то
также можете определить в классе индексируемые свойства с тремя и более индек-
сами.
404 Глава 7
Статические свойства
Статические свойства подобны статическим членам класса в том, что они опреде-
лены для класса в целом, и существуют в единственном экземпляре для всех объектов
этого типа класса. Вы определяете статическое свойство, добавляя ключевое слово
static к определению свойства. Вот как можно определить статическое свойство в
классе Length, который вы видели раньше:
value class Length
// Прежний код...
public:
static property StringA Units
StringA get() { return Ь”футы и дюймы"; }
Это простое свойство возвращает единицы измерения, принятые в классе, в виде
строки. Вы обращаетесь к статическому свойству, квалифицируя его имя именем клас-
са — точно так же, как любой другой статический член класса:
Console::WriteLine(Е"Единицы измерения класса: {0}.", Length::Units);
Статические свойства класса существуют независимо от наличия объектов типа
этого класса. Конечно, если у вас определен объект класса, вы можете обратиться
к статическому свойству с использованием имени переменной. Например, если вы
определили объект типа Length по имени len, то можете вывести значение статиче-
ского свойства Units с помощью следующего оператора:
Console::WriteLine(L" Единицы измерения класса: {0}.", len.Units);
Для доступа к статическому свойству в ссылочном классе через дескриптор объ-
екта этого типа следует использовать операцию ->.
Зарезервированные имена свойств
Хотя свойства отличаются от полей, значения свойств также должны где-то со-
храняться, и место их хранения как-то должно быть идентифицировано. Внутренне
свойства имеют имена, созданные для мест хранения, которые им необходимы, и эти
имена зарезервированы в классе, к которым принадлежат свойства, так что вы не
должны использовать их для других целей.
Если вы определяете в классе скалярное или именованное индексируемое свой-
ство по имени NAME, то имена get NAME и set NAME резервируются в классе, так что
вы не должны использовать их для других целей. Оба имени зарезервированы неза-
висимо от того, определены ли функции get () и set () для свойства. Когда вы опре-
делите в классе индексированное свойство по умолчанию, то резервируются имена
get_Item и set_Item. Вероятность создания резервированных имен с символами
подчеркивания — достаточно веская причина избегать применения этого символа в
ваших собственных именах в программе на C++/CLI.
Поля initonly
Литеральные поля — удобный способ представления констант в классе, но они
имеют ограничение: их значения должны быть определены на момент компиляции
программы. C++/CLI также предлагает в классах поля initonly, представляющие со-
Определение собственных типов данных
405
бой константы, которые можно инициализировать в конструкторе. Рассмотрим при-
мер поля initonly в скелетной версии класса Length:
value class Length
private:
int feet;
int inches;
public:
initonly int inchesPerFoot; // поле initonly
// Конструктор
Length (int ft, int ins) :
feet(ft), inches(ins), // Инициализация полей
inchesPerFoot(12) // Инициализация поля initonly
Здесь поле initonly названо inchesPerFoot и инициализировано в списке ини-
циализации конструктора. Это пример нестатического поля initonly, и каждый объ-
ект будет иметь его собственную копию, подобно обычным полям — feet и inches.
Конечно, большая разница между полями initonly и обычными полями заключает-
ся в том, что вы не можете изменять значение поля initonly; после того, как оно
инициализировано, его значение зафиксировано навсегда. Обратите внимание, что
вы не обязаны специфицировать начальное значение нестатического поля initonly,
когда вы объявляете его; это подразумевает, что вы должны инициализировать все не-
статические initonly поля в конструкторе.
Не обязательно инициализировать initonly поля в списке инициализации кон-
структора. Это можно сделать и в его теле:
Length (int ft, int ins) :
feet (ft), inches(ins), // Инициализировать поля
inchesPerFoot =12; // Инициализировать поле initonly
Теперь поле инициализировано в теле конструктора. Обычно вы будете принимать
значение для нестатического поля initonly в качестве аргумента конструктора, а не
использовать для его инициализации явный литерал, как мы поступили в последнем
примере, поскольку назначение этих полей — быть специфичными по отношению к
экземпляру. Если значение известно, когда вы пишете код, то лучше использовать ли-
теральное поле.
Поле initonly можно объявить в классе как static — в этом случае оно разделяет-
ся между всеми экземплярами класса, и если это поле общедоступное (public), то оно
доступно по квалифицированному имени поля с именем класса. Поле inchesPerFoot
имеет смысл объявить как статическое in it only-поле — в самом деле, его значение
не должно варьироваться от одного объекта к другому. Ниже показана новая версия
класса Length со статическим полем initonly.
value class Length
private:
int feet;
int inches;
public:
initonly static int inchesPerFoot = 12; // Статическое поле initonly
406 Глава 7
// Конструктор
Length(int ft, int ins) :
feet(ft), inches(ins)
// Инициализация полей
Теперь поле inchesPerFoot статическое, и его значение специфицировано в объ-
явлении, а не в списке инициализации конструктора. На самом деле, вам не позво-
лено устанавливать значения статических полей любого вида в конструкторе. Если
хорошо подумать, то смысл этого очевиден, поскольку статические поля разделяются
между всеми объектами класса, и потому установка значений этих полей при каждом
вызове конструктора привела бы к конфликту с их нотацией.
Похоже, мы вернулись к ситуации, когда поля initonly могут быть инициализи-
рованы только во время компиляции, где в любом случае это лучше сделать литераль-
ными полями; однако существует и другой способ инициализации статических полей
initonly во время выполнения, а именно — в статическом конструкторе.
Статические конструкторы
Статический конструктор — это конструктор, который вы объявляете с исполь-
зованием ключевого поля static и применяете для инициализации статических по-
лей — обычных и initonly. Статический конструктор не принимает параметров и
не может иметь списка инициализации. Он всегда private, независимо от того, по-
местите ли вы его в раздел public определения класса. Вы можете определить ста-
тический конструктор для классов значений и для ссылочных классов. Вы не може-
те вызвать статический конструктор напрямую — он будет вызван автоматически до
первого выполнения нормального конструктора. Любые статические поля, которые
имеют начальные значения, специфицированные в их объявлениях, будут инициали-
зированы до выполнения статического конструктора. Вот как вы можете инициали-
зировать поле initonly в классе Length с помощью статического конструктора:
value class Length
private:
int feet;
int inches;
// Статический конструктор
static Length () { inchesPerFoot = 12; }
public:
initonly static int inchesPerFoot = 12; // Статическое поле initonly
11 Конструктор
Length(int ft, int ins) :
feet(ft), inches(ins)
// Инициализация полей
Этот пример использования статического конструктора не имеет определенных
преимуществ перед явной инициализацией inchesPerFoot, но имейте в виду, что
большая разница состоит в том, что инициализация теперь происходит во время вы-
полнения, и значение может быть получено из внешнего источника.
Определение собственных типов данных 407
Резюме
Теперь вы понимаете базовые идеи, положенные в основу классов C++. В осталь-
ной части книги вы увидите все новые и новые примеры применения классов. Ниже
перечислены ключевые моменты, которые вы должны усвоить после прочтения на-
стоящей главы.
□ Класс представляет собой средство определения собственных типов данных.
Классы могут отражать любые типы объектов, необходимых для решения ва-
ших конкретных проблем.
□ Класс может содержать данные-члены и функции-члены. Функции-члены клас-
са всегда имеют свободный доступ к данным-членам того же класса. Данные-
члены в классах C++/CLI называются полями.
□ Объекты класса создаются и инициализируются специальными функциями —
конструкторами. Они вызываются автоматически в точке объявления объек-
тов. Конструкторы могут быть перегруженными, чтобы представлять разные
способы инициализации объекта.
□ Классы в программах на C++/CLI могут быть классами значений или ссылоч-
ными классами.
□ Переменные типов классов значений хранят данные непосредственно, в то
время как переменные, ссылающие на объекты ссылочных классов, всегда яв-
ляются дескрипторами.
□ Классы C++/CLI могут иметь статические конструкторы для инициализации
статических членов класса.
□ Члены класса могут быть специфицированы как public; в этом случае они сво-
бодно доступны для любой функции программы. Альтернативно их можно спе-
цифицировать как private — тогда они могут быть доступны только функциям-
членам или дружественным функциям класса.
□ Члены класса могут быть определены как static. Существует только один эк-
земпляр каждого статического члена класса, и он разделен между всеми экзем-
плярами класса, независимо от того, сколько объектов этого класса создано.
□ Каждый нестатический объект класса содержит указатель this, указывающий
на текущий объект, с которым вызвана функция.
□ В нестатических функциях-членах классов типа значений this является вну-
тренним указателем, в то время как в ссылочных классах это дескриптор.
□ Функция-член, объявленная как const, имеет константный указатель this, и
потому не может модифицировать данные-члены объекта класса, с которым
она вызвана. Она также не может вызывать другие функции-члены, не объяв-
ленные как const.
□ Вы можете вызывать только константные функции-члены для объектов класса,
объявленных как const.
□ Функции-члены классов значений и ссылочных классов не могут быть объявле-
ны как const.
□ Использование ссылок на объекты класса в качестве аргументов при вызове
функции позволяет избежать существенных накладных расходов, связанных с
передачей сложных объектов в функцию.
408 Глава 7
□ Конструктор копирования, который представляет собой конструктор объекта,
инициализируемого другим существующим объектом того же класса, должен
принимать параметр, специфицированный как константная ссылка.
□ Вы не можете определить конструктор копирования для класса значений, по-
тому что копирование объектов классов значений всегда выполняется копиро-
ванием членов, одного за другим.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Определите структуру Sample, содержащую два целочисленных элемента дан-
ных. Напишите программу, которая объявит два объекта типа Sample с имена-
ми а и Ь. Установите значения для элементов данных, относящихся к а, и затем
убедитесь, что вы можете копировать значения Ь простым присваиванием.
2. Добавьте в структуру Sample из предыдущего упражнения член типа char* по
имени sPtr. Когда будете заполнять данными а, динамически создайте буфер
символов, инициализированный строкой “Hello World!”, и установите на него
указатель а. sptr. Скопируйте а в Ь. Что случится, когда вы измените содержи-
мое символьного буфера, на который указывает а. sptr, а затем выведете содер-
жимое строки, на которую указывает b. sptr? Объясните, что произошло? Как
это исправить?
3. Создайте функцию, принимающую в качестве аргумента указатель на объ-
ект типа Sample и выводящую значения всех членов переданного ей объекта.
Протестируйте эту функцию, расширив программу, написанную в предыдущем
упражнении.
4. Определите класс CRecord с двумя приватными данными-членами, которые
хранят имя до 14 символов длиной и целочисленный элемент. Определите
функцию-член getRecord() класса CRecord, которая установит значения дан-
ных-членов, читая ввод с клавиатуры, и функцию-член putRecord (), выводя-
щую значения данных-членов. Реализуйте функцию getRecord () так, чтобы
вызывающая ее программа могла обнаружить, когда вводится нулевой числовой
элемент. Протестируйте класс CRecord с помощью функции main (), которая
читает и выводит объекты CRecord до тех пор, пока не будет введен нулевой
числовой элемент.
5. Напишите класс по имени СТгасе, который можно будет использовать для ото-
бражения во время выполнения входа в блок кода и выхода из него, представ-
ляя вывод вроде следующего:
function 1 fl1 entry
’if1 block entry
1 if1 block exit
function ’fl’ exit
6. Подумайте о способе управления автоматическим отступом в предыдущем при-
мере, чтобы вывод выглядел так:
function ’fl’ entry
' if ’ block entry
’ if ’ block exit
function ’fl’ exit
Определение собственных типов данных 409
7. Определите класс, представляющий стек магазинного типа (push-down stack)
целых чисел. Стек — это список элементов, который позволяет добавление (“за-
талкивание”) или удаление (“выталкивание”) элементов только из одного конца
и работает по принципу “последний вошел — первый вышел”. Например, если
стек содержит элементы [10 4 16 20], то функция pop () должна вернуть 10,
после чего стек будет содержать [4 16 20]; последующий вызов push (13) дол-
жен привести к следующему содержимому стека: [13 4 16 20]. Вы не должны
получить элемент, не находящийся в вершине стека, предварительно не достав
все, что находятся после него. Ваш класс должен реализовать функции push ()
и pop () плюс функцию print (), чтобы можно было проверить содержимое
стека. Организуйте для начала внутреннее хранение элементов в виде массива.
Напишите тестовую программу для проверки корректности всех операций ва-
шего класса.
8. Что случится с вашим решением из предыдущего упражнения, если вы попытае-
тесь вытолкнуть функцией pop () больше элементов, чем было помещено в него
вызовом push () ? Что произойдет, если попытаться сохранить больше элемен-
тов, чем отведено места? Можете ли вы предложить устойчивый способ пере-
хвата таких ситуаций? Иногда может понадобиться увидеть число, находящееся
в вершине стека, не извлекая его оттуда; реализуйте функцию реек (), чтобы
делать это.
9. Повторите пример Ех7_04, но в виде консольной программы CLR, использую-
щей ссылочные классы.
8
Дополнительные
сведения о классах
В этой главе вы расширите свои знания о классах, узнав о том, как можно заста-
вить объекты классов работать подобно базовым типам C++. Мы поговорим о пере-
численных ниже моментах.
□ Что такое деструкторы классов и когда они необходимы.
□ Как реализовать деструктор класса.
□ Как разместить члены данных класса “родного” C++ в области свободного хра-
нилища и как удалить, когда необходимость в них отпадает.
□ Когда необходимо писать конструктор копирования класса.
□ Что такое объединения и как они могут использоваться.
□ Как заставить объекты ваших классов работать с операциями C++, такими как
+ или *.
□ Что такое шаблоны классов, как их определять и каким образом использовать.
□ Как перегружать операции в классах C++/CLI.
Деструкторы классов
Хотя заголовок этого раздела ссылается на деструкторы, он в такой же мере по-
священ и динамическому распределению памяти. Когда выделяется память в свобод-
ном хранилище для членов класса, вы обязаны использовать деструктор в дополне-
ние к конструктору и, как увидите позднее в этой главе, применение динамического
распределения членов класса также требует написания собственного конструктора
копирования.
412 Глава 8
Что такое деструктор?
Деструктор (destructor) — это функция, которая уничтожает объект, когда необхо-
димость в нем отпадает или когда он выходит из области видимости. Разрушение объ-
екта включает освобождение памяти, занятой данными-членами объекта (за исключе-
нием статических членов, которые продолжают существовать, даже когда не остается
ни одного экземпляра класса). Деструктор класса — это функция-член с именем, совпа-
дающим с именем класса, но предваренным символом “тильда” (~). Деструктор клас-
са не возвращает значения и не принимает параметров. Для класса СВох прототип
деструктора класса выглядит следующим образом:
~СВох(); // Прототип деструктора класса
Поскольку деструктор не имеет параметров, в классе может существовать лишь
один деструктор.
Специфицировать возвращаемое значение или параметры деструктора — ошибка!
Деструктор по умолчанию
Все объекты, которые вы использовали до настоящего момента, уничтожались ав-
томатически с помощью деструктора по умолчанию. Деструктор класса по умолча-
нию всегда генерируется компилятором автоматически, если только вы не определя-
ете собственного деструктора для этого класса. Деструктор по умолчанию не удаляет
объекты или члены объекта, которые были размещены в свободном хранилище опе-
рацией new. Если место для членов класса было выделено в конструкторе динами-
чески, вы должны определить собственный деструктор, который явно воспользуется
операцией delete, чтобы освободить память, выделенную в конструкторе операцией
new — так же, как вы это делаете с обычными переменными. Для правильного написа-
ния деструкторов нужна практика, так что давайте попробуем.
Практическое занятие | Простой деСТруКТОр
Чтобы получить представление о том, когда вызывается деструктор класса, можно
включить деструктор в класс СВох. Ниже приведен пример, включающий класс СВох
с деструктором.
// Ех8_01.срр
// Класс с явным деструктором
tfinclude <iostream>
using std::cout;
using std::endl;
class СВох // Определение класса в глобальном контексте
{
public:
// Определение деструктора
~СВох()
{
cout << ’’Вызван деструктор.” << endl;
}
// Определение конструктора
СВох (double lv = 1.0, double wv = 1.0, double hv = 1.0) :
m_Length(lv), m_Width(wv), m_Height(hv)
{
cout « endl « "Вызван конструктор.’’;
}
Дополнительные сведения о классах 413
// Функция для вычисления объема ящика
double Volume() const
return m_Length*m_Width*m_Height;
// Функция сравнения двух ящиков, возвращающая true,
// если первый больше второго, и false - в противном случае
int compare(СВох* рВох) const
return this->Volume() > pBox->Volume();
private:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
// Функция для демонстрации деструкторы класса СВох в действии
int main()
СВох boxes[5]; // Объявление массива объектов СВох
СВох cigar(8.0, 5.0, 1.0); // Объявление сигарного ящика
СВох match(2.2, 1.1, 0.5); // Объявление спичечного коробка
СВох* рВ1 = &cigar; // Инициализация указателя на объект cigar
СВох* рВ2 =0; // Указатель СВох, инициализированный значением null
cout « endl
« "Объем cigar равен "
« pBl->Volume(); // Объем объекта, на который установлен указатель
рВ2 = boxes; // Указатель — адрес массива
boxes [2] = match; // Установить 3-й элемент в match
cout « endl
« "Объем boxes [2] равен "
« (рВ2 + 2)->Volume(); // Теперь обратиться через указатель
cout « endl;
return 0;
Описание полученных результатов
Единственное, что делает деструктор класса СВох — отображает сообщение о том,
что он вызван.
Вывод этой программы будет таким:
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Объем cigar равен 40
Объем boxes[2] равен 1.21
Вызван деструктор.
Вызван деструктор.
Вызван деструктор.
Вызван деструктор.
Вызван деструктор.
Вызван деструктор.
Вызван деструктор.
414 Глава 8
Вы получили по одному вызову деструктора в конце программы для каждого из
созданных объектов. Каждому вызову конструктора соответствует свой вызов деструк-
тора. Вы не должны вызывать деструктор явно. Когда объект класса выходит из обла-
сти видимости (контекста), компилятор автоматически помещает в эту точку вызов
деструктора. В нашем примере вызов деструктора случается после того, как main ()
завершает выполнение, поэтому вполне возможно, что ошибка в деструкторе вызовет
крах программы после благополучного завершения main ().
деструкторы и динамическое распределение памяти
Нередко возникает необходимость в динамическом выделении памяти для членов
класса. Вы можете воспользоваться операцией new в конструкторе, чтобы выделить
место в памяти для члена объекта. В таком случае ответственность за освобождение
этого места в памяти, когда объект уже не нужен, должна быть возложена на подхо-
дящий деструктор. Давайте для начала определим простой класс, в котором сможем
это сделать.
Предположим, что нужно определить класс, в котором каждый объект содержит
некоторое сообщение, например, текстовую строку. Класс должен использовать па-
мять наиболее эффективным образом, поэтому вместо определения члена как масси-
ва char, достаточно большого, чтобы уместилась строка максимальной длины, какая
может потребоваться, мы будем распределять память для сообщения в области сво-
бодного хранилища в'о время создания объекта. Ниже показано определение класса.
// Листинг 08_01
class CMessage
private:
char* pmessage; // Указатель на объект — текстовую строку
public:
// Функция для вывода сообщения
void Showlt() const
cout « endl « pmessage;
// Определение конструктора
CMessage(const char* text = "Сообщение по умолчанию")
pmessage = new char[strlen(text) +1); // Выделить место для текста
strcpy(pmessage, text); // Копировать text в новую память
-CMessage(); // Прототип деструктора
Этот класс имеет только один член данных — pmessage, который является указа-
телем на текстовую строку. Он определен в разделе private класса, так что не может
быть доступен извне класса.
В разделе public находится функция Showlt (), которая выводит объект CMessage
на экран. Есть также определение конструктора и прототип деструктора класса,
~CMessage (), к которому вы вскоре вернемся.
Конструктор класса принимает строку в качестве аргумента, но если не передано
ничего, использует строку по умолчанию, специфицированную для этого параметра.
Конструктор получает длину строки, переданной в аргументе, исключая завершаю-
щий NULL, используя для этого библиотечную функцию strlen (). Чтобы конструк-
тор мог вызывать библиотечную функцию, в файле должен присутствовать оператор
Дополнительные сведения о классах 415
#include с заголовочным файлом <cstring>. Конструктор определяет количество
байт памяти, необходимых для размещения строки в области свободного хранилища,
добавляя 1 байт к значению, возвращенному функцией strlen ().
Конечно, если выделение памяти не удастся, будет возбуждено исключение, кото-
рое прервет выполнение программы. Если вы хотите обработать такой сбой и обе-
спечить более элегантное завершение программы, то должны будете перехватить
исключение внутри кода конструктора (информацию об обработке условия нехватки
памяти ищите в главе 6).
Получив память для строки, выделенную операцией new, вы используете би-
блиотечную функцию strcpy (), которая также объявлена в заголовочном файле
<cstring>, чтобы скопировать переданную в аргументе конструктора строку в об-
ласть памяти, выделенную для нее. Функция strcpy () копирует строку, специфици-
рованную вторым аргументом-указателем, по адресу, содержащемуся в первом аргу-
менте-указателе.
Теперь вы должны написать деструктор класса, чтобы он освобождал память, вы-
деленную для текста сообщения. Если вы не представите деструктора класса, то не
будет никакой возможности освободить память, выделенную объекту. Если вы исполь-
зуете этот класс в таком виде, как он есть в программе, в которой создается большое
количество объектов CMessage, то память свободного хранилища будет постепен-
но “съедаться” — до тех пор, пока программа не завершится аварийно. И это легко
может произойти в ситуациях, когда совсем не очевидно, что подобное возможно.
Например, если вы создаете временный объект CMessage в функции, вызываемой в
программе многократно, то можете ожидать, что объект уничтожается при возврате
из функции. Конечно, вы правы — сами указатели уничтожаются, но память из свобод-
ного хранилища, на которую они указывали, остается занятой. Поэтому при каждом
вызове функции будет занято все больше и больше памяти свободного хранилища от-
брошенными объектами CMessage.
Код деструктора класса CMessage выглядит следующим образом:
// Листинг 08—02
// Деструктор для освобождения памяти, выделенной new
CMessage::~CMessage()
{
cout « "Вызван деструктор." // Просто чтобы видеть, что происходит
<< endl;
delete [] pmessage; // Освобождением памяти, на которую установлен указатель
}
Поскольку деструктор определен вне определения класса, вы должны квалифи-
цировать имя деструктора именем класса, CMessage. Все, что делает деструктор —
отображает сообщение, чтобы было видно, что происходит, и вызывает операцию
delete для освобождения памяти, на которую указывает член класса pmessage.
Обратите внимание на добавление квадратных скобок рядом с delete, поскольку уда-
ляется массив (типа char).
практическое занятие | Использование класса CMessage
Вы можете испытать класс CMessage в небольшом примере.
// Ех8_02.срр
// Использование деструктора для освобождения памяти
#include <iostream> // Для потокового ввода-вывода
#include <cstring> // Для strlen() и strcpy()
416 Глава 8
using std::cout;
using std::endl;
// Сюда поместить определение класса CMessage (Листинг 08_01)
// Сюда поместить определение деструктора (Листинг 08_02)
int main()
// Объявление объекта
CMessage motto("Большое видится на расстоянии.");
// Динамический объект
CMessage* рМ = new CMessage("Кот, который смотрел на королеву.");
motto.Showit(); // отобразить 1-е сообщение
pM->ShowIt (); // отобразить 2-е сообщение
cout « endl;
// delete рМ; // Вручную освободить объект, выделенный new
return 0;
Не забудьте заменить комментарии в коде определениями класса CMessage и де-
структора из предыдущего раздела; без них пример не скомпилируется.
Описание полученных результатов
В начале main () в обычной манере объявляется, определяется и инициализиру-
ется объект motto класса CMessage. В следующем объявлении определен указатель
на объект CMessage — рМ, и выделяется память для объекта CMessage, на который
он указывает, с помощью операции new. Вызов new активизирует конструктор класса
CMessage, в котором снова вызывается new, чтобы выделить память для текста со-
общения, на которую указывает член класса pmessage. Если вы соберете и запустите
этот пример, он выдаст такой вывод:
Большое видится на расстоянии.
Кот, который смотрел на королеву.
Вызван деструктор.
Как видим, деструктор вызван лишь однажды, несмотря на то, что вы создали два
объекта CMessage. Я уже говорил о том, что компилятор не отвечает за объекты, соз-
данные в свободном хранилище. Компилятор вставил вызов деструктора для объекта
motto, потому что это нормальный автоматический объект, даже несмотря на то, что
память для его члена была выделена конструктором в свободном хранилище. Объект,
на который указывает рМ, не таков. Вы выделили память для объекта в свободном хра-
нилище, поэтому должны использовать delete для удаления его. Поэтому необходи-
мо убрать комментарий со следующего оператора, находящегося перед оператором
return в main():
// delete рМ; // Вручную освободить объект, выделенный new
Теперь, запустив код, вы увидите такой вывод:
Большое видится на расстоянии.
Кот, который смотрел на королеву.
Вызван деструктор.
Вызван деструктор.
Здесь добавился дополнительный вызов деструктора. И это несколько неожи-
данно. Ясно, что delete имеет дело только с памятью, выделенной new в функции
main (). То есть освобождает память, на которую указывает рМ. Но поскольку указа-
тель рМ установлен на объект CMessage (для которого определен деструктор), delete
Дополнительные сведения о классах 417
также вызывает ваш деструктор, чтобы обеспечить освобождение память, занятую
членами объекта. Поэтому когда вы используете delete для объекта, динамически
созданного с помощью new, объекта перед освобождением памяти, занятой объектом,
всегда вызывается деструктор.
Реализация конструктора копирования
Когда вы динамически распределяете пространство для членов класса, за этим
присматривают демоны свободного хранилища. Для класса CMessage конструктор
копирования вопиюще неадекватен. Предположим, что вы напишете следующие опе-
раторы:
CMessage mottol("Практика программирования на Visual C++.");
CMessage motto2(mottol); // Вызывается конструктор копирования по умолчанию
Эффект от конструктора копирования по умолчанию состоит в том, что копи-
руется адрес, хранящийся в указателе-члене класса, из mottol Bmotto2, поскольку
процесс копирования, реализованный конструктором копирования по умолчанию,
включает простое копирование значений, хранящихся в данных-членах исходного
объекта, в новый объект. Следовательно, одна текстовая строка будет совместно ис-
пользоваться двумя объектами, как показано на рис. 8.1.
CMessage mottol (“Практика программирования на Visual C++”);
CMessage motto2 (mottol); // Вызов конструктора копирования по умолчанию
Рис. 8.1. Совместное использование текстовой строки двумя объектами
418 Глава 8
Если строка изменяется в любом из двух объектов, она одновременно изменяется
и в другом, потому что оба объекта разделяют одну и ту же строку. Если mottol уни-
чтожается, то указатель в motto2 указывает на область памяти, которая была осво-
бождена, и может быть использована где-то еще, так что хаос гарантирован. Конечно,
та же проблема возникает, если удаляется motto2; mottol будет в этом случае содер-
жать член, указывающий на несуществующую строку.
Решение заключается в создании конструктора копирования, который заменит
версию по умолчанию. Вы можете реализовать его в разделе класса public, как по-
казано ниже:
CMessage(const CMessage& initM) // Определение конструктора копирования
// Выделить место для текста
pmessage = new char[ strlen(initM.pmessage) + 1 ];
/ / Копировать текст в новую память
strcpy(pmessaget initM.pmessage);
)
Вспомним, что было сказано в предыдущей главе: для того, чтобы избежать беско-
нечного цикла вызовов конструкторов копирования, параметр должен быть специфи-
цирован как const-ссылка. Конструктор копирования сначала выделяет достаточную
память, чтобы уместилась строка из объекта initM, сохраняя адрес в члене данных
нового объекта, а затем копирует текстовую строку из инициализирующего объекта.
Теперь новый объект идентичен, но достаточно независим от старого.
Но не думайте, что вы в безопасности и не должны беспокоиться о конструкторе
копирования только потому, что не инициализируете один объект CMessage другим.
В свободном хранилище таится еще один монстр, который может нанести удар, когда
вы менее всего этого ожидаете. Рассмотрим следующие операторы:
CMessage thought (’’Шила в мешке не утаишь.’’);
DisplayMessage(thought); // Вызов функции для вывода сообщения
Функция DisplayMessage () определена следующим образом:
void DisplayMessage(CMessage localMsg)
{
cout « endl « ’’Сообщение: ”
« localMsg.Showlt();
return;
Выглядит просто, не правда ли? Что здесь может быть не так? Катастрофическая
ошибка — вот что! В действительности то, что делает эта функция, совершенно неу-
местно. Проблема заключена в параметре. Параметр — объект CMessage, поэтому при
вызове аргумент передается по значению. При конструкторе копирования по умолча-
нию последовательность событий перечислена ниже.
1. Объект thought создается с местом для сообщения ’’Шила в мешке не утаишь. ”,
выделенным в свободном хранилище.
2. Функция DisplayMessage () вызывается и, поскольку аргумент передан по зна-
чению, копия localMsg создается конструктором копирования по умолчанию.
Теперь указатель в копии указывает на ту же строку в свободном хранилище,
что и в оригинальном объекте.
Дополнительные сведения о классах 419
3. В конце функции локальный объект выходит из области видимости, поэтому
вызывается деструктор класса CMessage. Он удаляет локальный объект (копию),
освобождая память, на которую указывает pmessage.
4. При возврате из функции DisplayMessage (), указатель на исходный объект,
thought, все еще указывает на область памяти, которая уже была освобожде-
на. В следующий раз, когда вы попытаетесь использовать оригинальный объект
(или даже если вы этого не сделаете, все равно рано или поздно его придется
удалить), программа начнет вести себя мистическим образом.
Любой вызов функции, которая принимает по значению объект класса, имеющего
динамически определенный член, приведет к проблемам. Поэтому, чтобы избежать
этого, вы должны на 100% придерживаться следующего золотого правила.
Если имеется динамическое выделение памяти для члена класса “родного ” C++, всегда реали-
зуйте конструктор копирования.
Разделение памяти между переменными
В качестве реликта с тех времен, когда 64 Кбайт считались достаточно большим
объемом памяти, в C++ сохранилась возможность хранения более чем одной пере-
менной в одной и той же памяти (но очевидно, что не в одно и то же время). Это
называется union (объединение), и существует четыре основных способа его исполь-
зования.
□ Можно сделать так, чтобы переменная А занимала в одной точке программы
блок памяти, который позднее отдавался другой переменной В другого типа,
поскольку необходимость в А отпала. Я не советую так поступать. Не стоит ри-
сковать ошибками, к которым приведет такая организация. Того же эффекта
можно достичь динамическим выделением памяти.
□ Можно столкнуться с ситуацией в программе, когда требуется большой массив
данных, но вы заранее не знаете, какого типа данные придется хранить — это
будет зависеть от вводимых данных. Я также рекомендую не использовать объ-
единения в этом случае, поскольку того же результата можно достичь, исполь-
зуя несколько указателей разного типа и, опять-таки, выделяя память динами-
чески.
□ Третье возможное применение объединения — когда вы хотите интерпрети-
ровать одни и те же данные двумя или более разными способами. Это может
случиться, когда есть переменная типа long, а вы хотите трактовать ее как две
переменных типа short. Windows иногда упаковывает два значения short в
единственный параметр типа long с целью передачи в функцию. Другой слу-
чай — когда вы хотите трактовать блок памяти, содержащий числовые данные,
как строку байт, или наоборот.
□ Вы можете использовать объединение в качестве средства передачи объекта
или значения данных, когда заранее не известно, какого типа эти данные будут.
Тогда можно применить объединение, которое способно хранить любое значе-
ние из диапазона возможных типов.
420 Глава 8
Определение объединений
Объединение определяется ключевым словом union. Легче всего пояснить это на
примере:
union shareLD // Разделение памяти между long и double
double dval;
long Ival;
Здесь определен тип объединения shareLD, который позволяет переменным ти-
пов long и double занимать одну и ту же память. Имя типа объединения обычно на-
зывается именем дескриптора (tag name). Этот оператор подобен объявлению клас-
са — в том смысле, что вы на самом деле еще не определяете экземпляр объединения,
поэтому не имеете никакой переменной в этой точке. После того, как тип объедине-
ния определен, можно объявлять экземпляры объединения в объявлении, например:
shareLD myUnion;
Здесь определяется экземпляр типа объединения shareLD, определенного ранее.
Вы можете также сразу объявить экземпляр этого объединения myUnion, включив его
в оператор, определяющий тип объединения:
union shareLD // Разделение памяти между long и double
double dval;
long Ival;
} myUnion;
Для обращения к члену объединения используется операция прямого выбора чле-
на (точка) с именем экземпляра объединения — точно так же, как осуществляется об-
ращение к членам класса. Вот как можно присвоить значение 100 переменной Ival
типа long в объединении myUnion:
myUnion.Ival = 100; // Использование члена объединения
Если позднее в программе использовать подобный оператор для инициализации
переменной dval типа double, то значение Ival будет переписано. Основная про-
блема в применении объединений для хранения значений разного типа в одной и той
же памяти состоит в том, что необходим какой-то способ определения того, какое из
значений членов объединения является текущим. Обычно это достигается добавлени-
ем другой переменной, служащей индикатором типа хранимого значения.
Объединение не ограничено двумя членами. При желании можно разделить одну
и ту же память между несколькими переменными. Объем памяти, занятой объедине-
нием, определяется его самым большим членом. Например, предположим, вы опре-
делили объединение:
union shareDLF
double dval;
long Ival;
float fval;
} uinst = {1.5};
Экземпляр shareDLF занимает 8 байт, как показано на рис. 8.2.
В этом примере определен экземпляр объединения uinst вместе с именем де-
скриптора объединения. Экземпляр инициализирован значением 1.5.
Дополнительные сведения о классах 421
8 байт -
Ival
fval
dval
Puc. 8.2. Объединение shareDLF
Вы можете инициализировать первый член объединения при объявлении экземпляра.
Анонимные объединения
Можно объявить объединение без имени типа — в этом случае экземпляр объеди-
нения объявляется автоматически. Например, предположим, что определено следую-
щее объединение:
union
char* pval;
double dval;
long Ival;
Этот оператор определяет неименованное объединение и экземпляр этого объ-
единения. Впоследствии вы можете обращаться к переменным, которые оно содер-
жит, просто по именам, под которыми они указаны в определении объединения:
pval, dval и Ival. Это может оказаться более удобным, чем обычное объединение
с именем типа, но следует быть осторожным, чтобы не спутать члены объединения с
обычными переменными. Члены объединения все равно разделяют одну и ту же па-
мять. В качестве иллюстрации работы анонимного объединения, для использования
члена double вы можете написать следующий оператор:
dval =99.5; // Использование члена анонимного объединения
Как видите, ничто не указывает на то, что переменная dval является членом объ-
единения. Если вам нужно использовать анонимные объединения, вы можете при-
менить соглашение об именовании, чтобы сделать более очевидной принадлежность
переменных объединению, и таким образом предохранить ваш код от неправильного
понимания.
Объединения в классах и структурах
Вы можете включить экземпляр объединения в класс или структуру. Если вы на-
мерены сохранять разные типы значений в разное время, это обычно влечет за со-
бой необходимость поддержки члена данных класса для индикации типа значения,
422 Глава 8
хранимого в объединении на данный момент. Поэтому применение объединений как
членов класса или структуры обычно не дает особых преимуществ.
Перегрузка операций
Перегрузка операций — очень важное средство, поскольку позволяет заставить
стандартные операции C++, такие как +, -, * и так далее, работать с объектами ваших
собственных типов данных. Это позволяет написать функцию, которая переопреде-
ляет некоторую операцию, чтобы она выполняла определенное действие, будучи ис-
пользованной с объектами класса. Например, вы можете переопределить операцию
>, чтобы, будучи примененной к объектам класса СВох, который вы видели выше, она
возвращала true, если первый аргумент СВох имеет больший объем, чем второй.
Перегрузка операций не позволяет вводить новые операции, как не позволяет из-
менять приоритеты операций, так что ваша перегруженная версия операции будет
иметь тот же приоритет в последовательности вычисления выражений, что и ори-
гинальная базовая операция. Таблицу приоритетов операций можно найти в главе 2
этой книги, а также в библиотеке MSDN.
Хотя вы не можете перегрузить все операции, ограничения не слишком суровы.
Ниже перечислены операции, которые не могут быть перегружены:
Операция разрешения контекста ::
Условная операция ?:
Операция прямого выбора члена
Операция размера объекта sizeof
Разыменующий указатель на член класса . *
Во всем остальном ваши руки развязаны. Понятно, что хорошей идеей будет обе-
спечить разумную согласованность ваших собственных версий стандартных операций
с их обычным применением, или, по крайней мере, интуитивно обоснованную их
функциональность. Было бы не слишком разумно написать перегруженную операцию
+ для класса, выполняющую нечто, эквивалентное умножению объектов этого клас-
са. Лучший способ понимания работы перегруженных операций состоит в разборе
примера, поэтому давайте реализуем то, о чем я упомянул выше — операцию > — для
класса СВох.
Реализация перегруженной операции
Чтобы реализовать перегруженную операцию для класса, необходимо написать
специальную функцию. Предполагая, что она будет членом класса СВох, объявление
функции для перегрузки операции > внутри определения класса выглядит следующим
образом:
class СВох
public:
bool operator>(СВох& аВох) const; // перегруженная операция ’больше чем’
// остальная часть определения класса...
Слово operator здесь является ключевым. Комбинированное с символом опера-
ции или ее именем, в данном случае,
оно определяет функцию операции. Таким
образом, в данном случае именем функции будет operator> (). Вы можете написать
Дополнительные сведения о классах 423
1
функцию операции с пробелом или без, между ключевым словом operator и симво-
лом самой операции, до тех пор, пока нет неоднозначности. Неоднозначность воз-
никает с операциями, обладающими именами вместо символов, такими как new или
delete. Если вы напишете operatornew или operatordelete без пробела, это будут
легальные имена обычных функций, поэтому в таких случаях необходимо оставлять
пробелы между словом operator и самим именем операции. Обратите внимание, что
вы объявляете функцию operator> () как const, потому что она не модифицирует
данные-члены класса.
С функцией operator> () правый операнд перегружаемой операции определяется
как ее параметр. Левый операнд определяется неявно указатели this. Поэтому, если
у вас есть следующий оператор i f:
if (boxl > box2)
cout « endl « "boxl большеt чем box2”;
выражение между скобками в i f вызовет функцию операции и эквивалентно следую-
щему:
boxl.operator>(box2);
Соответствие между объектами СВох в выражении и параметрами функции опера-
ции проиллюстрировано на рис. 8.3.
if( boxl > Ьох2 )
Аргумент функции
bool CBox::operator>(const СВох& аВох) const
Объект, на который______
указывает this |
return (this->volume()) > (aBox.VolumeO);
Рис. 8.3. Соответствие между объектами СВох в выражении
и параметрами функции операции
Посмотрим, как устроен код функции operator> ():
// Функция операции ’больше чем’, сравнивающая
// объемы объектов СВох.
bool СВох::operator>(const СВох& аВох) const
return this->Volume () > aBox.VolumeO;
Здесь используется параметр-ссылка, дабы избежать излишнего копирования при
вызове функции. Поскольку функция не меняет объекта, для которого она вызывает-
ся, можно объявить ее как const. Если этого не сделать, вы вообще не сможете ис-
пользовать операцию для сравнения константных объектов СВох.
В выражении return используется функция-член Volume () для вычисления объ-
ема объекта СВох, на который указывает this, и производится сравнение результа-
та с объемом объекта СВох с помощью базовой операции >. Базовая операция > воз-
424 Глава 8
вращает значение типа int (не bool) и потому возвращается 1, если объект СВох, на
который указывает this, имеет больший объем, чем объект аВох, переданный в ар-
гументе-ссылке, и 0 — в противном случае. Значение, полученное в результате сравне-
ния, будет автоматически преобразовано в тип возврата функции операции, то есть
в bool.
Практическое занятие | ПереГруЗКЭ ОПбрЭЦИИ
Чтобы испытать функцию opereator> (), рассмотрим пример.
// Ех8_03.срр
// Испытание перегруженной операции 'больше чем'
#include <iostream> // Для потокового ввода-вывода
using std::cout;
using std::endl;
class CBox // Определение класса в глобальном контексте
{
public:
// Определение конструктора
СВох (double lv = 1.0Л double wv = 1.0, double hv = 1.0) :
m_Length(lv), m_Width(wv), m_Height(hv)
{
cout « endl « "Вызван конструктор.";
}
// Функция для вычисления объема ящика
double Volume () const
{
return m_Length*m_Width*m_Height;
}
bool operator>(const CBox& aBox) const; //Перегруженная операция 'больше чем'
// Определение деструктора
-СВох ()
{
cout « "Вызван деструктор." « endl;
}
private:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
};
// Функция операции 'больше чем',
// сравнивающая объемы объектов СВох.
bool СВох: :operator>(const СВох& аВох) const
{
return this->Volume() > аВох.Volume() ;
}
int main ()
{
CBox small Box (4.0, 2.0, 1.0);
CBox mediumBox( 10.0, 4.0, 2.0);
CBox bigBox(30.0, 20.0, 40.0);
if (mediumBox > smallBox)
cout « endl
« "mediumBox больше, чем smallBox";
Дополнительные сведения о классах 425
if (mediumBox > bigBox)
cout « endl
« "mediumBox больше, чем bigBox";
else
cout « endl
cout « endl;
return 0;
Описание полученных результатов
Прототип operator> () функции операции появляется в разделе public объявле-
ния класса. Поскольку определение функции находится вне определения класса, она
не является по умолчанию inline. Это довольно-таки произвольно. Точно так же вы
могли бы поместить ее определение вместо прототипа внутри определения класса.
Тогда вам не пришлось бы квалифицировать имя функции префиксом СВох::. Как вы
помните, это необходимо, когда функция-член определена вне определения класса,
чтобы сообщить компилятору, что данная функция является членом класса СВох.
Функция main () включает два оператора if, использующих операцию > с экзем-
плярами класса. Это автоматически вызывает перегруженную операцию. Если вы хо-
тите получить подтверждение этому, можете добавить оператор вывода в функцию
операции. Вывод этого примера должен быть таким:
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
mediumBox больше, чем smallBox
mediumBox не больше, чем bigBox
Вызван деструктор.
Вызван деструктор.
Вызван деструктор.
Этот вывод демонстрирует, что оператор i f работает правильно с нашей функци-
ей операции, а потому выражает решение проблем СВох непосредственно в терминах
объектов СВох, что весьма реалистично.
Реализация полной поддержки операции
Однако при такой реализации функции operator> () остается множество вещей,
которых вы делать не можете. Спецификация решения проблемы в терминах объек-
тов СВох может включать операторы вроде следующего:
if (аВох > 20.0)
// Что-то делать.. .
Наша функция с этим не справится. Если вы попытаетесь использовать выраже-
ние сравнения объекта СВох с числовым значением, то получите сообщение об ошиб-
ке. Чтобы поддерживать такую возможность, понадобится написать другую версию
функции operator> () как перегруженную функцию.
Запрограммировать поддержку выражения, приведенного выше, достаточно лег-
ко. Объявление функции-члена внутри класса должно быть таким:
// Сравнение объекта СВох с константой
bool operatcr>(const doubles value) const;
426 Глава 8
Это может появиться в определении класса, и правый операнд операции > будет
соответствовать параметру функции. Объект СВох, являющийся левым операндом,
передается в виде неявного указателя this.
Реализация этой перегруженной операции также проста. Тело функции включает
только один оператор:
// Функция для сравнения объекта СВох с константой
bool СВох::operators (const doubles value) const
return this->Volume() > value;
Что может быть проще, не так ли? Однако у вас по-прежнему остается проблема с
применением операции > с объектом СВох. Вы можете пожелать написать оператор
вроде следующего:
if (20.0 > аВох)
// что-то делать...
Можно возразить, что это можно сделать, реализуя функцию operators (), кото-
рая принимает правый аргумент типа double, и переписать последний оператор, что-
бы в нем использовалась именно эта функция, что совершенно верно. В самом деле,
реализация операции <, вероятно, в любом случае будет необходимой для сравнения
объектов СВох, но реализация поддержки типа объекта не должна ограничивать спо-
собы использования объектов в выражении. Применение объектов должно быть на-
сколько возможно естественным. Проблема в том, как это сделать.
Функция-член операции всегда представляет левый аргумент как указатель this.
Поскольку в данном случае левый аргумент имеет тип double, вы не можете реали-
зовать это как функцию-член. Это оставляет вам лишь два выбора: обычная функция
или дружественная функция (friend). Поскольку вам не нужен доступ к private-
членам класса, то и нет необходимости в дружественной функции, поэтому вы може-
те реализовать перегруженную операцию > с левым операндом типа double в виде
обычной функции. Прототип, помещенный вне определения класса, конечно, не бу-
дучи членом, должен выглядеть так:
bool operator>(const doubles value, const CBoxS aBox);
А вот и реализация:
/ / Функция сравнения константы с объектом СВох
bool operator>(const doubles value, const CBoxS aBox)
return value > aBox.VolumeO;
Как вы уже видели, обычная функция (да и дружественная функция) обращается
к членам объекта, используя операцию обращения к члену и имя объекта. Конечно,
обычная функция имеет доступ только к общедоступным членам. Функция-член
Volume () является public, поэтому с ее использованием здесь проблем нет.
Если бы класс не имел общедоступной функции Volume (), вы могли бы либо
воспользоваться дружественной функцией, обращающейся к приватным (private)
членам класса непосредственно, либо написать набор функций-членов, которые воз-
вращают значения приватных данных-членов и применить их в обычной функции,
чтобы реализовать сравнение.
Дополнительные сведения о классах 427
Практическое занятие | ПОЛНЭЯ ПбрвГруЗКа ОПерЭЦИИ >
А теперь соберем все в один пример, чтобы продемонстрировать, как это работает.
// Ех8_04.срр
// Реализация полностью перегруженной операции "больше чем"
#include <iostream> // Для потокового ввода-вывода
using std::cout;
using std::endl;
class СВох // Определение класса в глобальном контексте
{
public:
I/ Определение конструктора
СВох(double lv = 1.0, double wv = 1.0, double hv = 1.0) :
m_Length(lv), m_Width(wv), m_Height(hv)
{
cout « endl « ’’Вызван конструктор.’’;
}
// Функция для вычисления объема ящика
double Volume() const
{
return m_Length*m_Width*m_Height;
}
// Функция операции 'больше чем',
// сравнивающая объемы объектов СВох.
bool operator> (const CBoxS аВох) const
{
return this->Volume () > aBox. Volume ();
}
11 Ъуахззухя. сравнения объекта СВох с константой
bool operator>(const doubles value) const
{
return this->Volume() > value;
}
// Определение деструктора
-СВох ()
{ cout « "Вызван деструктор." « endl;}
private:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
};
int operator>(const doubles value, const CBoxs aBox); // Прототип функции
int main()
{
CBox smallBox(4.0, 2.0, 1.0);
CBox mediumBox(10.0, 4.0, 2.0);
if(mediumBox > smallBox)
cout « endl
« "mediumBox больше, чем smallBox";
if (mediumBox > 50.0)
cout « endl
« "Вместимость mediumBox больше 50";
else
cout « endl
« "Вместимость mediumBox не больше 50";
428 Глава 8
smallBox)
« "Вместимость smallBox меньше 10";
else
« "Вместимость smallBox не меньше 10
cout « endl;
return 0;
// Функция для сравнения константы с объектом СВох
int operator>(const doubles value, const CBoxS aBox)
return value > aBox. Volume ();
Описание полученных результатов
Обратите внимание на позицию прототипа версии обычной функции operator> ().
Она должна следовать за определением класса, поскольку ссылается на объект СВох
в списке параметров. Если вы поместите его перед определением класса, пример не
будет компилироваться.
Существует способ поместить его в начале файла программы, следом за операто-
ром #include: для этого нужно применить незавершенное объявление класса. Оно
должно предшествовать прототипу и выглядеть так:
class СВох; // Незавершенное объявление класса
int operator>(const doubles value, CBoxS aBox); // Прототип функции
Незавершенное объявление идентифицирует СВох для компилятора как имя клас-
са, и этого достаточно, чтобы компилятор смог правильно обработать прототип
функции, потому что он знает, что СВох — пользовательский тип, который будет спе-
цифицирован позднее.
Этот механизм также важен в тех случаях, когда у вас есть
;ва класса, каждый из
которых имеет указатель на объект другого класса в качестве члена. Каждый из них
требует предварительного объявления второго. Эту тупиковую ситуацию можно раз-
решить, используя незавершенные объявления класса.
Ниже показан вывод этого примера.
Вызван конструктор.
Вызван конструктор.
mediumBox больше, чем smallBox
Вместимость mediumBox больше 50
Вместимость smallBox меньше 10
Вызван деструктор.
Вызван деструктор.
После сообщений конструктора об объявлении объектов smallBox и mediumBox
имеем три строки вывода от операторов if, каждый из которых работает так, как и
можно было ожидать. Первый из них вызывает функцию операции, которая является
членом класса и работает с двумя объектами СВох. Второй вызывает функцию-член,
имеющую параметр типа double. Выражение в третьем операторе if вызывает функ-
цию операции, реализованную в виде обычной функции.
В данном случае обе функции операций, которые являются членами класса, мож-
но было бы также сделать обычными функциями, поскольку им необходим доступ
только к функции-члену Volume (), которая объявлена как public.
Дополнительные сведения о классах 429
Любая операция сравнения может быть реализована почти так, как было показано здесь.
Они могут отличаться лишь в незначительных деталях, однако общий подход к реализации
будет точно таким же.
Перегрузка операции присваивания
Если вы не представляете в своем классе перегруженной функции операции при-
сваивания, компилятор использует ее вариант по умолчанию. Версия по умолчанию
просто выполняет процесс копирования член за членом, подобно тому, как это дела-
конструктор копирования; однако не путайте конструктор копирования по умолча-
нию с операцией присваивания по умолчанию. Конструктор копирования по умолча-
нию вызывается при объявлении объекта класса, инициализируемого существующим
объектом того же класса, или при передаче такого объекта в функцию по значению.
С другой стороны, операция присваивания по умолчанию вызывается, когда левая
и правая части операции присваивания являются объектами одного и того же типа
класса.
Для класса СВох операция присваивания по умолчанию работает без проблем, но
для любого класса, в котором память для членов выделяется динамически, необходи-
мо тщательно исследовать требования, которые к нему предъявляются. Существует
потенциальная угроза породить хаос в программе, если вы оставите без внимания
операцию присваивания в таком классе.
Вернемся на минутку к классу CMessage, который мы использовали, когда гово-
рили о конструкторах копирования. Вспомните, что в нем был член pmessage —
указатель на строку. Теперь представьте эффект, который может иметь операция
присваивания по умолчанию. Предположим, что имеются два экземпляра этого клас-
са— mottol и motto2. Можно попытаться установить значения членов motto2 равны-
ми членам mottol, используя для этого операцию присваивания по умолчанию, как
показано ниже:
motto2 = mottol; // Использовать операцию присваивания по умолчанию
Эффект от применения операции присваивания по умолчанию для этого класса бу-
дет точно таким же, как от использования конструктора копирования: случится беда!
Поскольку каждый объект имеет в себе указатель на одну и ту же строку, то строка,
изменяемая в одном объекте, меняется в обоих. Кроме того, существует проблема,
связанная с тем, что когда один из экземпляров класса разрушается, его деструктор
освобождает память, использованную для строки, и другой объект остается с указате-
лем на память, которая уже может быть задействована для чего-то другого.
Поэтому необходимо, чтобы операция присваивания копировала текст в область
памяти, которой владеет целевой объект.
Решение проблемы
Вы можете исправить это, применив собственную функцию операции присваива-
ния, которая может быть определена в рамках определения класса:
// Перегруженная операция присваивания для объектов CMessage
CMessage& operator=(const CMessage& aMess)
// Освободить память 1-го операнда
delete[] pmessage;
pmessage = new chart strlen(aMess.pmessage) + 1];
430 Глава 8
// Скопировать строку 2-го операнда в 1-й
strcpy(this->pmessage, aMess.pmessage);
// Возвратить ссылку на 1-й операнд
return *this;
Присваивание может выглядеть очень простым, однако есть пара тонкостей, ко-
торые требуют дополнительного исследования. Обратите внимание, что вы возвра-
щаете ссылку из функции операции присваивания. Не сразу можно понять, почему
так сделано (в конце концов, функция выполняет полную операцию присваивания,
и объект справа копируется в объект слева). На первый взгляд может показаться, что
вы не должны ничего возвращать, однако следует тщательно подумать о возможных
случаях применения этой операции.
Существует вероятность, что вам понадобится использовать результат операции
присваивания в правой части выражения. Рассмотрим следующий оператор:
mottol = motto2 = motto3;
Поскольку операция присваивания имеет ассоциативность справа налево, сначала
выполняется присваивание motto3 объекту motto2, что транслируется в такой опе-
ратор:
mottol = (motto2 .operator55 (motto3));
Результат этого вызова функции операции расположен справа от знака равенства,
поэтому данный оператор в конце принимает следующий вид:
mottol .operator55 (motto2 .operator55 (motto3));
Чтобы это работало, вы, безусловно, должны что-то возвращать. Вызов функции
operator= () между скобками должен возвращать объект, который может быть ис-
пользован в качестве аргумента для вызова функции operator= (). В этом случае дол-
жен подойти тип возврата либо CMessage, либо CMessage&, поэтому ссылка в такой
ситуации не обязательна, но, по крайней мере, вы должны вернуть объект CMessage.
Рассмотрим следующий пример:
(mottol = motto2) = motto3;
Это совершенно легитимный код (скобки служат для того, чтобы обеспечить вы-
полнение в первую очередь левого присваивания). Пример транслируется в такой
оператор:
(mottol.operator55(motto2)) = motto3;
Если выразить оставшуюся операцию присваивания как явный вызов перегружен-
ной функции, в конечном итоге получаем:
(mottol.operator= (motto2)) .operator55 (motto3);
Теперь у нас ситуация, когда возвращенный из функции operator= () объект ис-
пользуется для вызова функции operator=(). Если возвращаемый тип — просто
CMessage, это не допустимо, поскольку в действительности возвращается времен-
ная копия оригинального объекта, и компилятор не разрешает вызов функции-члена
с временным объектом. Другими словами, возвращаемое значение, когда его тип
CMessage, не является lvalue. Единственный способ обеспечить возможность ком-
пиляции и корректной работы вещей подобного рода — возвращать ссылку, которая
является lvalue, так что единственно возможным типом возврата для полноценной
реализации операции присваивания будет CMessage&.
Дополнительные сведения о классах 431
Обратите внимание, что “родной” C++ не накладывает никаких ограничений на
допустимые параметры или типы возврата операции присваивания, но имеет смысл
объявить операцию так, как описано выше, если вы хотите, чтобы ваша функция опе-
рации присваивания поддерживала нормальное использование присваивания C++.
Вторая тонкость, которую следует иметь в виду — это то, что каждый объект уже
имеет распределенную память для строки, так что первое, что должна делать функ-
ция операции — это освобождать память, выделенную для первого объекта, и повтор-
но выделить достаточно памяти, чтобы принять строку, принадлежащую второму
объекту. После того, как это будет сделано, строка из второго объекта может быть
скопирована в новую память, теперь принадлежащую первому объекту.
Однако в этой функции операции все еще содержится дефект. Что, если вы напи-
шете оператор вроде такого:
mottol = mottol;
Очевидно, что вы не станете делать подобную глупость непосредственно, но она мо-
жет быть легко скрыта за указателем, например, как показано в следующем операторе:
Mottol = *pMess;
Если указатель pMess указывает на mottol, вы, по сути, выполняете предыдущий
оператор присваивания. В этом случае функция операции, как она есть, должна очи-
стить память в mottol, выделить ее заново на базе длины строки, которая уже была
удалена, и попытаться скопировать старую память, которая к этому моменту уже по-
вреждена. Исправить это можно, проверив левый операнд на идентичность правому
в самом начале функции, так что теперь определение функции operator^ () станет
таким:
// Перегруженная операция присваивания для объектов CMessage
CMessage& operator=(const CMessage& aMess)
{
if(this == &aMess) // Проверить адреса; если совпадают,
return *this; // вернуть первый операнд
// Освободить память 1-го операнда
delete[] pmessage;
pmessage = new chart strlen(aMess.pmessage) +1];
// Копировать 2-й операнд в 1-й
strcpy(this->pmessage, aMess.pmessage);
// Вернуть ссылку на 1-й операнд
return *this;
}
Этот код предполагает, что определение функции находится внутри объявления
класса.
Практическое занятие | ПврвГруЗКв ОПерЭЦИИ ПрИСВЭИВЭНИЯ
Соберем все это в работающий пример. Одновременно добавим в класс функцию
под названием Reset (). Она будет просто сбрасывать сообщение в строку звездочек.
// Ех8_05.срр
// Окончательный вариант перегруженной операции присваивания
#include <iostream>
#include <cstring>
432 Глава 8
using std::cout;
using std::endl;
class CMessage
private:
char* pmessage; // Указатель на объект — текстовую строку
public:
// Функция для отображения сообщений
void Showit() const
cout « endl « pmessage;
// Функция для сброса сообщений в *
void Reset ()
char*
while(*temp)
* (temp++) = • * •;
// Перегруженная операция присваивания для объектов CMessage
CMessage& operators (const CMessage& aMess)
{
if (this = &aMess) // Проверить адреса, если совпадают,
return *this; // вернуть 1-й операнд
// Освободить память
для 1-го операнда
delete[] pmessage;
pmessage
= new char[
// Копировать 2-й операнд в 1-й
strcpy(this->pmessage, aMess.pmessage);
// Вернуть ссылку на 1-й операнд
return *this;
}
// Определение конструктора
CMessage(const char* text = "Сообщение по умолчанию”)
pmessage = new chart strlen(text) +1 ]; // Выделить место для текста
strcpy(pmessage, text); // Скопировать текст в новую память
// Деструктор для освобождения памяти, выделенной new
~CMessage()
cout « "Вызван деструктор.” // Просто, чтобы видеть, что происходит
« endl;
delete [] pmessage; // Освободить память, присвоенную указателю
int main ()
{
CMessage mottol ("Дьявол заботится о себе сам");
CMessage motto2;
cout « "motto2 содержит
motto2. Showit ();
cout « endl;
Дополнительные сведения о классах 433
otto2 = mottol; // Использование новой операции присваивания
cout « "motto2 содержит - ";
motto2. Showlt ();
cout « endl;
mottol. Reset (); // Установка mottol a * не затрагивает motto2
cout « "mottol теперь содержит - ";
mottol.Showlt() ;
cout « endl;
cout « "motto2 по-прежнему содержит - ";
motto2.Showlt();
cout « endl;
return 0;
Из вывода этой программы видно, что все работает в точности так, как требуется,
без связи между сообщениями в двух объектах, за исключением того случая, когда вы
явно установите их эквивалентными друг другу:
motto2 содержит -
Сообщение по умолчанию
motto2 содержит -
Дьявол заботится о себе сам
mottol теперь содержит -
motto2 по-прежнему содержит -
Дьявол заботится о себе сам
Вызван деструктор.
Вызван деструктор.
На основании всего этого сформулируем еще одно золотое правило.
Всегда реализуйте операцию присваивания, если динамически выделяете память для дан-
ных-членов класса.
Но когда реализована операция присваивания, что случится с такими операция-
ми, как +=? Они не будут работать, пока вы их не реализуете. Для каждой из форм
ор=, которые вы хотите использовать с объектами своих классов, потребуется напи-
сать другую функцию операции.
Перегрузка операции сложения
Давайте взглянем на операцию сложения для нашего класса СВох. Это интересно,
потому что включает создание и возврат нового объекта. Новый объект представляет
собой сумму (как бы вы ни определили ее смысл) двух объектов СВох, являющихся ее
операндами.
Итак, как же мы хотим понимать сумму двух ящиков? Существует относительно
немного легитимных возможностей, но мы ограничимся простейшим вариантом.
Определим сумму двух объектов СВох как объект СВох, который достаточно велик,
чтобы вместить два других, поставленных один на другой. Вы можете сделать это, до-
бавив в новый объект член m_Length, значение которого будет равно m_Length боль-
шего из двух складываемых объектов. Точно так же определим член m_Width. Член
m_Height результирующего объекта будет вычисляться как сумма членов m_Height
двух объектов, так что результирующий объект СВох может содержать два других объ-
екта СВох. Это не обязательно самое оптимальное решение, но для наших целей его
434 Глава 8
вполне достаточно. Изменив конструктор, мы также определим, что член m Length
объекта СВох всегда больше или равен члену m_Width.
Нашу версию операции сложения ящиков легче объяснить графически, и это по-
казано на рис. 8.4.
Рис. 8.4. Графическая трактовка операции сложения для класса СВох
Поскольку необходимо обращаться к членам объекта непосредственно, сделаем
operator + () функцией-членом. Объявление функции-члена внутри определения
класса будет выглядеть так:
СВох operator*(const СВох& аВох) const; // Функция сложения двух объектов СВох
Дополнительные сведения о классах 435
Мы определяем параметр как ссылку, дабы избежать ненужного копирования пра-
вого аргумента при вызове функции, и делаем его константной ссылкой, поскольку
функция не должна модифицировать аргумент. Если не определить параметр как кон-
стантную ссылку, то компилятор не позволит передавать в функцию const-объект,
так что невозможно будет правому операнду операции + быть объектом const СВох.
Кроме того, сама функция определена как const, так как она не изменяет объект, для
которого вызывается. Если так не сделать, левый операнд операции + не сможет быть
объектом const СВох.
Определение функции operator* () теперь принимает следующий вид:
// Функция сложения двух объектов СВох
СВох СВох::operator*(const СВох& aBox) const
{
// Новый объект имеет максимальную длину и ширину, а также сумму высот
return СВох (m__Length > aBox .m_Length ? m_Length:aBox.m_Length,
m_Width > aBox.m_Width ? m_Width:aBox.m_Width,
m_Height + aBox.m_Height);
}
Локальный объект CBox конструируется из текущего объекта (*this) и объекта,
переданного в качестве аргумента — аВох. Напомним, что процесс возврата создает
временную копию локального объекта и передает обратно вызывающей функции его,
а не локальный объект, который исчезает при возврате из функции.
практическое занятие | Испытание новой операции сложения
Работу перегруженной операции сложения для класса СВох можно испытать в при-
веденном ниже примере.
// Ех8__06.срр
// Сложение объектов СВох
#include <iostream> // Для потокового ввода-вывода
using std::cout;
using std::endl;
class CBox // Определение класса в глобальном контексте
{
public:
// Определение конструктора
СВох (double lv = 1.0, double wv = 1.0, double hv = 1.0) : m_Height(hv)
{
m_Length = lv > wv? lv: wv; // Убедиться, что
mJWidth = wv < lv? wv: lv; // length >= width
}
// Функция вычисления объема ящика
double Volume() const
{
return m_Length*m_Width*m_Height;
}
/ / Функция операции ’больше чем ’,
// сравнивающая объемы объектов СВох.
int СВох::operator>(const СВох& aBox) const
{
return this->Volume() > aBox.Volume();
}
436 Глава 8
// Функция сравнения объекта CBox с константой
int operator>(const doubles value) const
return Volume() > value;
// Функция сложения двух объектов СВох
СВох operator*(const СВох& aBox) const
// Новый объект имеет максимальную длину и ширину, а также сумму высот
return СВох(m_Length > aBox.m__Length? m_Leng th: aBox. m__Leng th,
mJWidth > aBox. m_Width? m_Width: aBox. m__Width,
m Height * aBox.m Height);
void ShowBox () const
private:
double
double
double
m_Length;
m_Width;
m_Height;
// Длина ящика в дюймах
// Ширина ящика в
// Высота ящика в
дюймах
дюймах
operator>(const doubles value, const
CBoxS aBox); // Прототип функции
CBox smallBox(4.0, 2.0, 1.0);
CBox mediumBox(10.0, 4.0, 2.0);
CBox aBox;
CBox bBox;
aBox = smallBox + mediumBox;
cout « "Размеры aBox:
aBox.ShowBox();
bBox = aBox + smallBox * mediumBox;
cout « " Размеры bBox: ";
bBox.ShowBox();
return 0;
I / Функция сравнения константы с объектом СВох
int operator>(const doubles value, const CBoxS aBox)
return value > aBox.Volume();
Ниже в настоящей главе мы еще обратимся к определению класса СВох.
Описание полученных результатов
В этом примере я немного изменил члены класса СВох. Деструктор исключен, по-
скольку он в этом классе не нужен, а конструктор модифицирован, чтобы гаранти-
ровать, что член m_Length не будет меньше, чем m_Width. Уверенность в том, что
длина ящика всегда не меньше ширины, несколько облегчает операцию сложения.
Я также добавил функцию ShowBox () для вывода размеров объекта СВох. Благодаря
ополнительные сведения о классах
437
ей, мы сможем проверить, что наша перегруженная операция сложения работает, как
ожидалось.
Ниже показан вывод этой программы.
Размеры аВох: 10 4 3
Размеры ЬВох: 10 4 6
Это выглядит согласованно с принятой нами идеей сложения объектов СВох и, как
видите, функция также работает в примере с множественными операциями сложе-
ния. Для вычисления ЬВох перегруженная операция сложения вызывается дважды.
Вы также можете реализовать сложение для этого класса в виде дружественной
функции. Вот ее прототип:
friend СВох operator*(const CBoxS аВох, const СВох& ЬВох);
Процесс получения результата был бы почти таким же, за исключением необхо-
димости использовать операцию прямого обращения к членам для получения членов
обоих аргументов функции. Это будет работать почти так же хорошо, как и первая
версия функции операции.
Перегрузка операций инкремента и декремента
Я кратко представлю механизм перегрузки операций инкремента и декремента в
классе, поскольку они обладают некоторыми особыми характеристиками, которые
отличают их от других унарных операций. Нужно каким-то образом учесть тот факт,
что операции ++ и — могут иметь префиксную и постфиксную формы, и эффект от
них отличается. В “родном” C++ перегруженные операции для префиксной и пост-
фиксной форм инкремента и декремента отличаются. Вот, например, как они могут
быть определены в классе Length:
class Length
private:
double len;
public:
Lengths operator** ();
const Length operator**(int);
Lengths operator—();
const Length operator—(int);
// остальная часть класса.. .
// Значение длины для класса
// Префиксная операция инкремента
// Постфиксная операция инкремента
// Префиксная операция декремента
// Постфиксная операция инкремента
Этот простой класс предполагает, что длина хранится как значение типа double.
В реальности вы можете сделать этот класс несколько сложнее, однако он служит
только для иллюстрации перегрузки операций инкремента и декремента.
Первое, чем отличаются перегруженные формы операций — это списками пара-
метров; префиксная форма не принимает параметров, а постфиксная принимает
параметр типа int. Параметр в функции постфиксной операции служит только для
того, чтобы отличать ее от префиксной формы, и никак не используется в реализа-
ции функции.
Операции префиксного инкремента и декремента обрабатывают операнд перед
тем, как его значение будет использовано в выражении, так что вы просто возвра-
щаете ссылку на текущий объект после того, как он будет увеличен или уменьшен.
С префиксной формой операнд увеличивается (или уменьшается) после того, как его
текущее значение используется в выражении. Это достигается созданием копии те-
438 Глава 8
кущего объекта перед выполнением самой операции и возвратом этой копии после
модификации объекта.
Шаблоны классов
Как было сказано в главе 6, вы можете определять шаблоны функций, которые
автоматически генерируют различные вариации функций в зависимости от типа при-
нятого аргумента или типа возвращаемого значения. В C++ включен подобный меха-
низм и для классов. Шаблон класса сам по себе не является классом. Это некоторая
разновидность “рецепта” для компилятора, по которому он генерирует код класса.
Как видно на рис. 8.5, он подобен шаблону функции — вы определяете класс, который
хотите генерировать, специфицируя выбор типа параметра (в данном случае Т), ко-
торый указывается между угловыми скобками в объявлении шаблона. Это заставляет
компилятор генерировать код определенного класса, который мы называем экзем-
пляром шаблона класса. Процесс создания класса из шаблона называется реализаци-
ей шаблона.
Соответствующее определение класса генерируется, когда вы создаете экземпляр
объекта шаблона класса для конкретного параметра типа, так что вы можете генери-
ровать любое количество классов из одного-единственного шаблона. Чтобы получить
представление о том, как это работает на практике, рассмотрим соответствующий
пример.
class CExample
int m_Value;
• •
T — параметр, для которого вы применяете
значение, являющееся типом.
Каждое отдельное значение аргумента типа,
которое вы примените, создает новый класс.
Экземпляры
класса
Рис, 8,5, Реализация шаблона класса
Дополнительные сведения о классах 439
Определение шаблона класса
Для иллюстрации определения и использования шаблона класса рассмотрим про-
стой пример. Мы не станем чересчур усложнять его, слишком заботясь об ошибках,
которые могут возникнуть. Предположим, что вы хотите определять классы, кото-
рые могут хранить количество выборок данных некоторого вида, и каждый класс
предоставляет функцию Мах () для определения максимальной выборки. Эта функ-
ция подобна той, что вы видели при обсуждении о шаблонах из главы 6. Вы можете
определить шаблон класса, который генерирует класс CSample для хранения выборок
любого типа, который хотите.
template <class Т>
class CSamples
public:
// Определение конструктора — принимает массив выборок
CSamples(const Т values[], int count)
m_Free = count < 100? count:100; // He превышать размер массива
for (int i = 0; i < m_Free; i++)
m_Values[i] = values[i]; // Сохранить счетчик выборок
// Конструктор, принимающий единственную выборку
CSamples(const Т& value)
m_Values[0] = value;
m_ Free = 1;
// Конструктор по умолчанию
CSamples(){ m_Free = 0 }
// Сохранить выборку
// Следующий свободный элемент
// Нечего хранить, так что первая свободна
// Функция для добавления выборки
bool Add(const Т& value)
bool OK = m Free < 100; // Индикация свободного места
if(OK)
m_Values [m_Free++] = value; //OK равно true, так что сохраняем значение,
return OK;
}
// Функция для получения максимальной выборки
Т Мах() const
// Установить первую выборку или 0 в качестве максимума
Т theMax = m_Free ? m_Values[0] : 0;
for(int i = 1; i < m_Free; i++) // Проверить все выборки
if(m_Values[i] > theMax)
theMax = m_Values(iJ; // Сохранять любое большее значение
return theMax;
}
private:
T m_Values(100]; // Массив для хранения выборок
int m Free; // Индекс свободного места в m Values
440 Глава 8
Чтобы указать, что вы определяете шаблон, а не обычный класс, вставляется клю-
чевое слово template и параметр типа Т между угловыми скобками непосредственно
перед ключевым словом class и именем класса CSamples. По сути, это тот же син-
таксис, который применялся для определения шаблона функции в главе 6. Параметр
Т — это переменная типа, которая заменяется специфическим типом, когда вы объяв-
ляете объект класса. Всякий раз, когда параметр Т появляется в определении класса,
он заменяется типом, который вы специфицируете в объявлении объекта; это созда-
ет определение класса, соответствующего этому типу. Вы можете задавать любой тип
(базовый или тип класса), но он, конечно, должен иметь смысл в контексте шаблона
класса. Любой тип класса, используемый для реализации класса из шаблона, должен
иметь определенными все операции, которые применяются функциями-членами это-
го шаблона с объектами этого типа. Например, если ваш класс не имеет реализован-
ной функции operator> (), он не работает с шаблоном класса CSample. Вообще вы
можете специфицировать множество параметров в шаблоне класса, если это необхо-
димо. Я вернусь к этой возможности немного позже.
Возвращаясь к примеру, обратим внимание на то, что тип массива, в котором со-
храняются выборки данных, указан как Т. Поэтому этот массив будет массивом любо-
го типа, который вы зададите вместо Т при объявлении объекта CSample. Как видим,
тип Т также используется в двух конструкторах класса, а также в функциях Add () и
Мах (). В каждом из этих мест Т заменяется реальным типом при создании объекта
класса с использованием шаблона.
Конструктор поддерживает создание пустого объекта, объекта с единственной вы-
боркой и объекта, инициализированного массивом выборок. Функция Add () всегда
позволяет добавлять к объекту новые выборки по одной. Вы также можете перегру-
зить эту функцию для добавления сразу массива выборок. Шаблон класса включает не-
которые элементарные меры предосторожности, чтобы предотвратить переполнение
массива m Values в функции Add () и конструкторе, принимающем массив выборок.
Как уже упоминалось, теоретически вы можете создавать объекты классов
CSample, которые обрабатывают данные любых типов: типа int, типа double и
любого типа класса, определенного вами. На практике это не значит, что они будут
компилироваться и работать так, как вы ожидаете. Все зависит от того, что делает
определение шаблона, и обычно шаблон работает только для определенного диапа-
зона типов. Например, функция Мах () неявно предполагает, что для любого обраба-
тываемого типа доступна операция >. Если это не так, программа не скомпилируется.
Понятно, что вы всегда будете определять шаблон, который работает только с не-
которыми типами, а с другими — не работает, однако нет способа ограничить типы,
применимые с конкретным шаблоном.
Шаблонная функция-член
Может возникнуть необходимость в размещении определения шаблонной функ-
ции-члена класса за пределами определения шаблона класса. Синтаксис, применяе-
мый для этого, не столь очевиден, так что давайте посмотрим, как это сделать. Вы
помещаете объявление функции в определение шаблона класса обычным способом,
например:
template <class Т>
class CSamples
// Остальная часть определения шаблона...
// Остальная часть определения шаблона...
Здесь объявляется функция Мах () как член шаблона класса, однако без своего
определения. Теперь вы должны создать отдельный шаблон функции для определе-
ния такой функции-члена. Для этого вы должны использовать имя шаблона класса
плюс параметры в угловых скобках, чтобы идентифицировать шаблон класса, к кото-
рому относится шаблон функции:
template<class Т>
Т CSamples<T>::Мах() const
Т theMax = m_Values[0]; // Установить первую выборку в качестве максимума
for (int i = 1; i < m_Free; i++) // Проверить все выборки
if(m_Values[i] > theMax)
theMax = m_Values[i]; // Сохранить максимальную выборку
return theMax;
Вы видели синтаксис шаблона функции в главе 6. Поскольку этот шаблон функции
предназначен для члена шаблона класса с параметром Т, определение шаблона функ-
ции должно иметь те же параметры, что и определение шаблона класса. В данном
случае он один — Т, но вообще их может быть несколько. Если шаблон класса прини-
мает два или более параметров, точно так же должна быть определена каждая из его
функций-членов.
Обратите внимание на то, как должно указываться имя параметра Т наряду с име-
нем класса перед операцией разрешения контекста. Это необходимо — параметры
важны для идентификации класса, к которому относится функция, произведенная от
шаблона. Типом будет Scamples<T> с любым типом, назначенным вместо Т, когда вы
создаете экземпляр шаблона класса. Ваш тип включается в шаблон класса для генера-
ции определения класса и в шаблон функции — для генерации определения функции
Мах () для класса. Каждый класс, полученный от шаблона класса, должен иметь соб-
ственное определение функции Мах ().
Определение конструктора или деструктора вне шаблона класса выполняется ана-
логично. Вы можете написать определение конструктора, который принимает массив
выборок, следующим образом:
template<class Т>
CSamples<T>::CSamples(Т values[], int count)
m_Free = count < 100? count: 100; // He превышать размер массива
for (int i = 0; i < m_Free; i++)
m_Values[i] = values[i]; // Сохранить номер счетчика выборок
Класс, к которому относится конструктор, специфицирован в шаблоне таким же
образом, как и для обычной функции-члена. Обратите внимание, что имя конструк-
тора не требует спецификации параметра (это просто CSamples, но он должен быть
квалифицирован именем типа шаблона класса — CSamples<T>). Параметр с шаблоном
класса вы используете только перед операцией разрешения контекста.
Создание объектов из шаблона класса
Когда вы применяете функцию, определенную шаблоном функции, компилятор
способен сгенерировать функцию на основании используемых типов аргументов.
Параметр типа для шаблона функции неявно определен специфическим применени-
ем этой функции. Шаблоны классов в этом отношении немного отличаются. Чтобы
442 Глава 8
создать объект на основе шаблона класса, вы всегда должны специфицировать в объ-
явлении параметр типа следом за именем класса.
Например, чтобы объявить объект С Sample so для работы с выборками типа
double, потребуется написать объявление так:
CSamples<double> myData(10.0);
Это определяет объект типа CSamples<double>, который может хранить выборки
типа double, и объект создается с одной выборкой, сохраненной в нем, со значением
10.0.
[Практическое занятие) ШабЛОНЫ КЛЭССОВ
Вы можете создать объект из шаблона CSampleso, который будет хранить объек-
ты СВох. Это работает, потому что в классе СВох реализована функция operator> ()
для перегрузки операции “больше чем”. Вы можете испытать шаблон класса с функ-
цией main () в следующем примере.
// Ех8_07.срр
// Использование шаблона класса
#include <iostream>
using std::cout;
using std::endl;
// Сюда поместить определение класса СВох из Ех8_06.срр...
// Определение шаблона класса CSamples
template <class Т> class CSamples
{
public:
// Конструкторы
CSamples(const T values[], int count);
CSamples(const T& value);
CSamples(){ m_Free =0; }
bool Add(const T& value); // Вставить значение
T Max() const; // Вычислить максимум
private:
Т m_Values[100]; // Массив
int m_Free; // Индекс свободного места в m_Values
};
// Определение шаблонного конструктора, принимающего массив выборок
template<class Т> CSamples<T>::CSamples(const Т values[], int count)
{
m_Free = count < 100? count:100; // He превышать размер массива
for (int i = 0; i < m_Free; i++)
m_Values[i] = values [i]; // Сохранить номер счетчика выборок
}
// Конструктор, принимающий единственную выборку
template<class Т> CSamples<T>::CSamples(const Т& value)
{
m_Values[0] = value; // Сохранить выборку
m_Free =1; // Следующий свободный элемент
}
// Функция для добавления выборки
template<class Т> bool CSamples<T>::Add(const T& value)
{
Дополнительные сведения о классах
443
bool ОК = m_Free <100; // Индикатор свободного пространства
if(ОК)
m_Values[m_Free++] = value; // ОК равно true, поэтому сохранить значение
return OK;
// Функция для получения максимальной выборки
template<class Т> Т CSamples<T>::Мах() const
Т theMax ® m__Free ? m_Values[0] : 0; // Установить первую выборку или 0
в качестве максимума
for (int i = 1; i < m_Free; i++) // Проверить все выборки
if(m_Values[i] > theMax)
theMax = m_Values[i]; // Сохранять любое большее значение
return theMax;
int main()
CBox boxes[] = { // Создать массив объектов
CBox (8.0, 5.0, 2.0), // Инициализировать ящики...
СВох(5.0, 4.0, 6.0),
СВох(4.0, 3.0, 3.0)
// Создать объект CSamples для хранения объектов СВох
CSamples<CBox> myBoxes(boxes, sizeof boxes / sizeof CBox);
CBox maxBox = myBoxes.Max (); // Получить самый большой ящик
cout « endl // и вывести его объем
« "Объем самого большого ящика: "
« maxBox.Volume()
« endl;
return 0;
Комментарий нужно будет заменить определением класса СВох из листинга
Ех8_06. срр, приведенного ранее в этой главе. Вам не нужно беспокоиться о функции
operators (), поддерживающей сравнение объекта СВох со значением типа double,
поскольку этот пример в нем не нуждается. За исключением конструктора по умол-
чанию, все функции-члены шаблона определены отдельными шаблонами функций,
только чтобы показать вам полный пример того, как это делается.
В main () вы создаете массив из трех объектов СВох и затем используете его для
инициализации объекта CSamples, который может хранить объекты СВох. Объяв-
ление объекта CSamples в основном такое же, как оно могло быть для обычного клас-
са, но с добавлением параметра типа в угловых скобках, следующего за именем ша-
блона класса.
Эта программа генерирует следующий вывод:
Объем самого большого ящика: 120
Обратите внимание, что когда вы создаете экземпляр шаблона класса, из этого не
следует, что экземпляры шаблонов функций для функций-членов также будут созда-
ны. Компилятор создает экземпляры шаблонов только для функций-членов, которые
действительно вызываются в программе. Фактически ваши шаблоны функций могут
даже содержать ошибки в коде, и до тех пор, пока вы не вызываете функцию-член,
которую генерирует шаблон, компилятор не станет жаловаться. Вы можете протести-
ровать это на примере. Попробуйте внести несколько ошибок в шаблон функции-чле-
444 Глава 8
*
на Add (). Программа по-прежнему будет компилироваться и запускаться, потому что
она не вызывает функцию Add ().
Вы можете попробовать модифицировать пример и возможно, увидите, что слу-
чится, когда вы реализуете классы, используя шаблон с другими типами.
Наверное, вас удивит то, что произойдет, если добавить несколько выходных параметров в
конструкторы класса. Выяснится, что конструктор СВох будет вызван 103 раза! Посмот-
рите, что происходит в функции ma in (). Сначала вы создаете массив из 3 объектов СВох —
отсюда 3 вызова. Затем вы создаете объект CSampl es для их хранения, но объект CSampl es
содержит массив из 100 переменных типа СВох, значит, вы вызываете конструктор по
умолчанию 100 раз — по одному для каждого элемента массива. Конечно, объект тахВох
будет создан конструктором копирования по умолчанию, созданным компилятором.
Шаблоны классов с множественными параметрами
Применение множественных параметров типа в шаблоне класса — естественное
продолжение представленного выше примера с единственным параметром. Можно
использовать каждый из параметров типа всякий раз, когда они понадобятся в опре-
делении шаблона. Например, вы можете определить шаблон класса с двумя параме-
трами типа:
template<class Tlr class Т2>
class CExampleClass
// Данные-члены шаблона
private:
Т1 m_Valuel;
Т2 m_Value2;
// Остальная часть определения шаблона...
Типы двух членов класса, показанных здесь, определяются типами, которые при-
меняются в качестве параметров при создании экземпляра объекта.
Параметры в шаблоне класса не ограничены типами. Можно также использовать
параметры, которые требуют подстановки констант или константных выражений в
определении класса. В нашем шаблоне CSamples мы произвольно определили массив
m_Values со 100 элементами. Вы можете, однако, позволить пользователю шаблона
выбрать размер массива при создании экземпляра объекта, определив шаблон следу-
ющим образом:
template <class Т, int Size> class CSamples
private:
T m_Values [Size]; // Массив для хранения выборок
int m Free; // Индекс свободного места в m Values
public:
// Определение конструктора, принимающего массив выборок
CSamples(const Т values[], int count)
mJFree = count < Size ? count:Size;// He превышать размер массива
for (int i = 0; i < m_Free; i++)
m Values[i] = values[i]; // Сохранить номер счетчика выборок
Дополнительные сведения о классах
445
// Конструктор, принимающий одну выборку
CSamples(const Т& value)
m_Values[0] = value; // Сохранить выборку
m_Free =1; // Следующий свободный элемент
// Конструктор по умолчанию
CSamples()
m_Free =0; // Ничего не сохранено, поэтому свободен первый
// Функция для добавления выборки
int Add(const Т& value)
if(OK)
Size;
// Признак наличия свободного места
m_Values[m_Free++] = value;// OK равно true, поэтому сохранить значение
return OK;
// Функция для получения максимального значения
Т Мах() const
// Установить первую
Т theMax = m_Free ? m_Values[0] : 0;
for (int i = 1; i < m_Free; i++) // Проверить все выборки
if(m_Values[i] > theMax)
theMax = m_Values[i]; // Сохранять любое большее значение
return theMax;
Значение, указанное как Size при создании объекта, заменяет параметр по всему
определению шаблона. Теперь вы можете объявить объект CSamples из предыдущего
примера следующим образом:
CSamples<CBox, 3> MyBoxes(boxes, sizeof boxes/sizeof СВох);
Поскольку вы можете применить любое константное выражение для параметра
Size, можно записать и так:
CSamplescCBox, sizeof boxes/sizeof СВох>
MyBoxes(boxes, sizeof boxes/sizeof CBox);
Это пример не лучшего использования шаблона (оригинальная версия была гораз-
до удачнее). Последствие объявления Size параметром шаблона состоит в том, что
экземпляры шаблона, которые хранят одинаковые типы объектов, но с разными зна-
чениями параметра размера, представляют собой совершенно различные классы и не
могут быть смешаны. Например, объект типа CSamples<double, 10> не может быть
использован в выражении с объектом типа CSamples<double, 20>.
Следует соблюдать осторожность с выражениями, которые включают операции
сравнения при реализации шаблонов. Взгляните на этот оператор:
CSamples<aType, х > у ? 10 : 20 > МуТуре(); // Не верно!
Он не скомпилируется, потому что операция > перед у в выражении интерпрети-
руется как правая угловая скобка. Вместо этого следует записать этот оператор следу-
ющим образом:
CSamples<aType, (х > у ? 10 : 20) > МуТуреО; // ОК
446 Глава 8
Скобки гарантируют, что выражение для второго аргумента шаблона не будет спу-
тано с угловой скобкой.
Использование классов
Я рассказал о большинстве базовых аспектов определения классов в “родном” C++,
поэтому теперь имеет смысл разобрать пример использования класса для решения
проблемы. Придется ограничиться простой проблемой, дабы сохранить размер на-
стоящей книги в разумных пределах, потому рассмотрим проблемы, для решения ко-
торых можно использовать расширенную версию класса СВох.
Понятие интерфейса класса
Реализация расширенного класса СВох должна включать понятие интерфейса
класса. Сейчас мы попробуем разработать набор инструментов для любого, кто по-
желает работать с объектами СВох — нам понадобится собрать множество функций,
предоставляющих интерфейс к миру ящиков. Поскольку интерфейс — это лишь спо-
соб работы с объектами СВох, он должен быть определен так, чтобы адекватно по-
крывать все, что может понадобиться делать с объектом СВох, и быть реализованным
так, чтобы насколько возможно, предотвращать неправильное понимание и непред-
намеренные ошибки.
Первый вопрос, который вы должны рассмотреть при проектировании клас-
са — это природа проблемы, которую вы намерены решить, и отсюда определить вид
функциональности, которую вы хотите представить в интерфейсе класса.
Определение проблемы
Принципиальное назначение ящика — содержать объекты того или иного типа,
что можно обозначить одним словом: упаковка. Попытаемся представить класс, ко-
торый вообще избавляет от проблем упаковки, и посмотрим, как он может исполь-
зоваться. Предположим, что всегда можно упаковать объекты СВох в другой объект
СВох, потому что, если вы хотите упаковать конфеты в коробку, то всегда можно
представить каждую конфету как идеализированный объект СВох. Основные опера-
ции, которые вы можете пожелать представить в классе СВох, перечислены ниже.
□ Вычисление объема СВох. Это фундаментальная характеристика объекта СВох,
и ее реализация у вас уже имеется.
□ Сравнение объемов двух объектов СВох для определения того, который из них
больше. Вероятно, вы должны поддерживать полный набор операций сравне-
ния для объектов СВох. Версия операции > у вас уже есть.
□ Сравнение объема объекта СВох с указанным значением и наоборот. Реализация
этой операции > также уже имеется, но функции, поддерживающие другие опе-
рации сравнения, придется реализовать дополнительно.
□ Сложение двух объектов СВох для создания нового объекта СВох, который бу-
дет содержать оба исходных. Таким образом, результат будет иметь, как мини-
мум, сумму их объемов, но может быть и больше. У вас уже есть версия этого,
которая перегружает операцию +.
Дополнительные сведения о классах 447
Умножение объекта СВох на целое (и наоборот) для создания объекта СВох,
который будет содержать указанное количество исходных объектов. Это позво-
лит эффективно моделировать коробку, в которую пакуется группа ящиков.
□ Определение, сколько объектов СВох заданного размера можно упаковать в
другой объект СВох заданного размера. Это, по сути, деление, поэтому вы мог-
ли бы реализовать его, перегрузив операцию /.
Определение объема свободного пространства, остающегося в объекте СВох
после помещения в него максимального количества объектов СВох заданного
размера.
На этом я лучше остановлюсь! Несомненно, могут существовать и другие функ-
ции — очень удобные, но для экономии места мы договоримся, что приведенного
перечня достаточно, помимо таких служебных средств, как, например, доступ к от:
дельным размерам (длине, ширине, высоте).
Реализация класса СВох
Вам следует подумать о степени защиты от ошибок, которую вы хотите встроить
в класс СВох. Базовый класс, который мы определили для иллюстрации различных
аспектов классов, является начальной точкой, но вы должны также рассмотреть неко-
торые моменты более глубоко. В предложенном дизайне конструктор — слабое место,
поскольку он не гарантирует правильности размерностей СВох, поэтому, возможно,
первое, что вам потребуется сделать — это гарантировать, что вы всегда получите
корректные объекты. Чтобы это сделать, можно переопределить базовый класс, как
показано ниже.
class СВох // Определение класса на глобальном уровне
public:
// Определение конструктора
СВох (double lv = 1.0, double wv = 1.0, double hv = 1.0)
lv = lv <= 0? 1.0: lv; // Обеспечить положительные
wv = wv <= 0? 1.0: wv; // размеры
hv = hv <= 0? 1.0: hv; // объекта
m_Length = lv > wv? lv: wv; // Гарантировать, что
m_Width = wv < lv? wv: lv; // length >= width
m_Height = hv;
// Функция для вычисления объема ящика
double Volume() const
return m_Length*m_Width*m_Height;
// Функция, представляющая длину ящика
double GetLengthO const { return m_Length; }
// Функция, представляющая ширину ящика
double GetWidthO const { return m_Width; }
// Функция, представляющая высоту ящика
double GetHeightO const { return m_Height; }
private:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах
double m_Height; // Высота ящика в дюймах
448 Глава 8
Теперь конструктор безопасен, потому что любая размерность, которую пользова-
тель класса попытается указать, как отрицательное число или ноль, автоматически в
конструкторе заменяется единицей. Вы можете также выдать в этом случае сообще-
ние, поскольку это, очевидно, ошибочная ситуация, и произвольная и молчаливая
установка размера в 1 может оказаться не лучшим решением.
Конструктор копирования по умолчанию удовлетворителен для нашего класса,
поскольку здесь нет никакого динамического выделения памяти для членов данных,
и операция присваивания также будет работать, как надо. Деструктор по умолчанию
также в этом случае работает отлично, и нет необходимости переопределять его.
Возможно, теперь следует рассмотреть, что понадобится для поддержки сравнения
объектов нашего класса.
Сравнение объектов СВох
Вы должны включить поддержку операций >, >=. ==, < и <=, чтобы они работали с
обоими операндами объектов СВох, а также между объектом СВох и значением типа
double. Это можно реализовать в виде обычных глобальных функций, поскольку не
обязательно, чтобы они были функциями-членами. Вы можете написать функции
сравнения двух объектов СВох в терминах функций, сравнивающих объем объекта
СВох со значением типа double, поэтому начнем с последних. Повторим функцию
operator> (), которую уже вы видели ранее:
// Функция проверки того, больше ли константа объекта СВох
int operator>(const doubles value, const CBoxS aBox)
return value > aBox.Volume();
Теперь вы можете написать функцию ope г at or < () аналогичным образом:
// Функция проверки того, меньше ли константа объекта СВох
int operator<(const doubles value, const CBoxS aBox)
return value < aBox.Volume();
Вы можете закодировать реализацию тех же операций с аргументами в противо-
положном порядке в терминах двух предыдущих функций:
// Функция проверки того, больше ли объект СВох, чем константа
int operator>(const CBoxS aBox, const doubles value)
{ return value < aBox; }
// Функция проверки того, меньше ли объект СВох, чем константа
int operator<(const CBoxS aBox, const doubles value)
{ return value > aBox; }
Вы просто используете соответствующие перегруженные функции операций, ко-
торый написали ранее, но с перестановкой аргументов.
Функции, реализующие операции >= и <= — такие же, как первые две, но с заме-
ной операции < на <=, а > — на >=; на этой стадии их реализовать совсем не сложно.
Функция operator== () также очень похожа:
// Функция для проверки равенства константы объему объекта СВох
int operator==(const doubles value, const CBoxS aBox)
return value == aBox.Volume();
Дополнительные сведения о классах
449
/ / Функция проверки равенства объема объекта СВох константе
int operator==(const СВох& аВох, const doubles value)
return value == aBox;
Теперь у вас есть полный набор операций сравнения объектов СВох. Имейте в
виду, что это также работает с выражениями — до тех пор, пока результаты выраже-
ний являются объектами требуемого типа, так что их можно комбинировать с приме-
нением перегруженных операций.
Комбинирование объектов СВох
Теперь пришла очередь перегрузки операций +, *, / и %. Приступим к ним по по-
рядку. Операция сложения уже реализована в Ех8_06. срр и имеет такой прототип:
СВох operator+(const СВох& аВох); // Функция сложения двух объектов СВох
Хотя исходная реализация этого не идеальна, воспользуемся ею, дабы не услож-
нять класс. Лучшая версия должна была бы проверять, не имеют ли операнды какие-
то грани с одинаковой размерностью и если так, складывать вдоль этих граней, но ко-
дирование такой логики было бы несколько громоздким. Конечно, для практического
приложения лучше было бы разработать операцию сложения позднее и подставить
вместо имеющейся версии, чтобы любые программы, написанные с использованием
оригинальной версии, продолжали работать без изменений. Отделение интерфейса
от класса — ключ к хорошему программированию на C++.
Обратите внимание, что я оставил без внимания операцию вычитания. Это благо-
разумное упущение, чтобы избежать сложностей, которыми чревата ее реализация.
Если вы действительно хотите ее реализовать и считаете, что это будет полезной
идеей, можете попробовать, но вам придется решить, что делать, если результат бу-
дет иметь отрицательное значение. Если вы допустите такую концепцию, то должны
будете решить, какие размерности ящика могут быть отрицательными, и как должен
вести себя ящик в последующих операциях.
Операция умножения очень проста. Она представляет процесс создания ящика,
который содержит п ящиков, где п — множитель. Простейшее решение могло бы за-
ключаться в том, чтобы взять m_Length или m_Width объекта, который нужно упако-
вать и просто умножить его на п, чтобы получить новый объект СВох. Вы можете сде-
лать его чуть более интеллектуальным, проверяя, является ли множитель четным, и,
если так, сложить ящики бок о бок, удваивая значение m_Width и умножая m_Length
на половину п. Этот механизм проиллюстрирован на рис. 8.6.
Конечно, вам не нужно проверять, что больше — длина или ширина нового объ-
екта, поскольку конструктор будет делать это автоматически. Вы можете написать
версию функции operator* () как функцию-член с левым операндом в виде объекта
СВох:
// Операция умножения СВох - this*n
СВох operator* (int n) const
if (n % 2)
return CBox(m_Length, m_Width, n*m_Height);
else
// n нечетное
return CBox(m_Length, 2.0*m_Width, (n/2)*m_Height); // n четное
450 Глава 8
Умножение СВох: п нечетное
: 3*аВох
Умножение СВох: п четное
: 6*аВох
Рис, 8.6. Графическая трактовка операции умножения дм класса СВох
Здесь вы используете операцию % для определения того, является ли п нечетным.
Если п нечетное, то значение п%2 равно 1 и оператор if дает true. Если же п четное,
то п%2 равно 0 и весь оператор дает false.
Теперь вы можете использовать только что написанную функцию в реализации
версии с левым операндом как целым. Это можно оформить в виде обычно функ-
ции — не члена:
// Операция умножения СВох - п*аВох
СВох operator*(int n, const CBox& аВох)
return aBox*n;
Дополнительные сведения о классах 451
Данная версия операции умножения просто обращает порядок операндов, чтобы
непосредственно использовать предыдущую версию функции. Это завершает набор
арифметических операций для объектов СВох, которые вы определили. И, наконец,
рассмотрим две функции аналитических операций — operator/ () и operator% ().
Анализ объектов СВох
Как я уже говорил, операция деления определяет, сколько объектов СВох, иден-
тичных тому, который указан в правом операнде, могут быть помещены в объект
СВох, специфицированный в левом операнде. Чтобы не слишком усложнять, предпо-
ложим, что все объекты СВох упаковываются вверх — то есть, по вертикальному из-
мерению. Также предположим, что они все они выравниваются по длине. Без этих
предположений все было бы несколько сложнее.
Проблема состоит в том, чтобы определить, сколько объектов правого операнда
могут быть помещены в один слой, и затем решить, сколько слоев мы можем иметь
внутри СВох левого операнда.
Вы можете закодировать это в виде функции-члена примерно так:
int operator/(const СВох& aBox)
int tel = 0;// Временное значение числа в горизонтальном плане
//в одном направлении
int tc2 = 0;// Временное значение для числа в другом направлении
tel = static_cast<int>((m_Length / aBox.m_Length))*
static_cast<int>((m_Width / aBox.m_Width)); // заполнить так
tc2 = static_cast<int>((m_Length / aBox.m_Width))*
static_cast<int>((m_Width / aBox.m_Length)); //и этак
// Вернуть наилучшее наполнение
return static_cast<int>((m_Height/aBox.m_Height)*(tcl>tc2 ? tel : tc2));
Сначала эта функция определяет, сколько объектов СВох правого операнда могут
поместиться в слое с выравниванием их длин по длине СВох левого операнда. Это
число сохраняется в tel. Затем можно вычислить, сколько их может поместиться в
слое с выравниванием их длин по ширине СВох левого операнда. И, наконец, макси-
мальное из двух полученных значений умножается на количество слоев, в которые
можно уложить ящики, и это значение возвращается. Описанный процесс проиллю-
стрирован на рис. 8.7.
Мы рассматриваем две возможности: размещение ЬВох в аВох, когда длинные сто-
роны первого выравниваются по длине второго, и когда длины ЬВох выравниваются
по ширине аВох.
Другая функция аналитической операции — operator% () — предназначена для
получения свободного объема в упакованном аВох; она более проста, поскольку вы
можете использовать уже написанную операцию для ее реализации. Вы можете на-
писать ее в виде обычной глобальной функции, поскольку здесь не нужен доступ к
приватным (private) членам класса.
// Операция вычисления свободного объема в упакованном ящике
double operator%(const СВох& аВох, const СВох& ЬВох)
return аВох.Volume() - ((аВох/bBox)*ЬВох.Volume());
452 Глава 8
Н = 1
W = 2
В этой конфигурации может
быть сохранено 12
Рис. 8.7. Графическая трактовка операции деления для класса СВох
Это вычисление реализуется очень легко с помощью существующих функций клас-
са. Результат — объем большего ящика, аВох, минус объем всех ЬВох, которые могут
быть помещены в него. Количество объектов ЬВох, упакованных в аВох, дает выра-
жение аВох/ЬВох, которое использует ранее перегруженную операцию деления. Вы
умножаете его на объем объектов ЬВох, чтобы получить объем, который нужно вы-
честь из общего объема большего ящика аВох.
На этом интерфейс класса завершен. Ясно, что для реального использования клас-
са может понадобиться намного больше функций, однако в качестве интересной ра-
ботающей модели, демонстрирующей проектирование класса, предназначенного для
решения определенного рода проблем, этого вполне достаточно. Теперь можно дви-
нуться дальше и попытаться применить ее для решения реальной проблемы.
Практическое занятие
Многофайловый проект, использующий
класс СВох
Прежде чем вы сможете действительно начать написание кода для использования
класса СВох и его перегруженных операций, сначала понадобится собрать определе-
ние класса в единое целое. Мы применим несколько другой подход, отличающийся
ополнительные сведения о классах
453
от того, что вы видели ранее — составим проект из нескольких файлов. Начнем с ис-
пользования средств, предоставляемых Visual Studio C++ 2005 для создания и сопро-
вождения кода наших классов. Это значит, что у вас будет меньше работы, но это так-
же значит, что местоположение кода слегка изменится.
Начнем с создания нового проекта консольного приложения Win32 по имени
Ех8_08, отметив опцию Empty project (Пустой проект). После выбора вкладки Class
View (Представление классов) вы увидите окно, показанное на рис. 8.8.
Class View ▼ Д- X
<Search>
- Ех8JO®
^Solution Explorer Class View Resource View
Puc. 8.8. Вкладка Class View (Пред-
ставление классов) проекта Ex8__08
Здесь показано представление всех классов проекта, но, конечно, на данный мо-
мент ни одного еще нет. Хотя ни один класс пока не определен, среда Visual C++ 2005
уже кое-что включила сюда. Вы можете использовать Visual C++ 2005 для создания ске-
лета нашего класса СВох, а также файлов, которые к нему относятся. Щелкните пра-
вой кнопкой мыши на Ех8_08 в Class View и выберите Add^Class (Добавить^Класс)
из всплывающего меню. Вы можете затем выбрать C++ из категорий классов в левой
панели диалогового окна Add Class (Добавить класс) и шаблон C++ Class (Класс C++)
в правой панели, после чего нажать клавишу <Enter>. После этого можете ввести имя
класса, который хотите создать — СВох — в диалоговом окне Generic C++ Class Wizard
(Мастер создания обобщенных классов C++), показанном на рис. 8.9.
Имя файла, высвеченное в диалоговом окне — Box. срр — используется для раз-
Здесь будет находиться исполняемый код класса. При желании вы можете изме-
нить имя файла, но Box. срр кажется вполне подходящим именем для этого случая.
Определение класса будет сохранено в файле под именем Box. h. Это стандартный
способ структурирования программы. Код, состоящий из определений класса, сохра-
няется в файлах с расширением . h, а код определения его функций — в файлах с рас-
ширением . срр. Обычно каждое определение класса попадает в свой собственный
файл . h, а реализация каждого класса — в собственный файл . срр.
454 Глава 8
Generic C++ Class Wizard - Ex8_08
^^нааан||а|иннна||^^|||нннвнавнн|нванннан||нв||нд^нн|нн|||а|аввннннвин^на|ва|аннн||н
Welcome to the Generic C+ + Class Wiza rd
Class name:
CBox
Base dass:
.h file:
Eox.h
Access:
public
Box. cpp
| | Virtual destructor
| | Inline
Finish
Puc. 8.9. Мастер Generic C++ Class Wizard (Мастер создания обобщенных классов C++)
Когда вы щелкаете на кнопке Finish (Готово) в диалоговом окне, происходят два
события.
Создается файл Box. h, в котором содержится скелет определения класса СВох.
Он включает конструктор без аргументов и деструктор.
2. Создается файл Box. срр, в котором содержится скелет определения реализа-
ции функций класса, включая тела конструктора и деструктора — разумеется,
пустые.
Панель редактора, отображающая код, должна выглядеть так, как показано на рис.
8.10. Если она еще не отображена, щелкните правой кнопкой на СВох в Class View, и
она появится.
Рис. 8.10. Панель редактора с кодом класса СВох
Как видите, над панелью, содержащей листинг кода класса, расположены два эле-
мента управления. Левый отображает текущее имя класса — СВох, а щелчок на кнопке
Дополнительные сведения о классах 455
справа от имени класса отобразит список всех классов проекта. В общем случае, вы
можете использовать этот элемент управления для переключения на другой класс,
выбирая его из списка, но в данной ситуации определен только один класс. Элемент
управления справа имеет отношение к членам текущего класса, определенным в фай-
ле . срр, и щелчок на его кнопке отобразит члены класса. Выбор члена из списка де-
лает видимым его код в панели, находящейся ниже.
Начнем разработку класса СВох на основе того, что нам автоматически предостав-
ляет Visual C++.
Определение класса СВох
Если вы щелкнете на + слева от Ех8_08 в Class View, дерево раскроется, и вы уви-
дите, что класс СВох определен в проекте. В этом дереве отображаются все классы
проекта. Вы можете просмотреть исходный код определения класса, выполнив двой-
ной щелчок на имени класса в дереве или используя элемент управления над пане-
лью, отображающей код, как было описано в предыдущем разделе.
Сгенерированное определение класса СВох начинается с директивы препроцессора:
#pragma once
Эффект от этого заключается в предотвращении открытия и включения в исхо-
дный код этого файла компилятором более одного раза. Обычно определение класса
включается в несколько файлов проекта, поскольку каждый файл, ссылающийся на
имя определенного класса, нуждается в доступе к его определению. В некоторых слу-
чаях заголовочный файл сам может содержать директивы #include для других заго-
ловочных файлов. Это может привести к тому, что содержимое одного заголовочного
файла появится в исходном коде более одного раза. Наличие более одного опреде-
ления класса при сборке не допускается и будет помечено как ошибка. Директива
#pragma once в начале каждого заголовочного файла гарантирует, что этого не про-
изойдет.
Обратите внимание, что #pragma once — это специфичная для Microsoft директи-
ва, которая может не поддерживаться другими средами разработки. Если вы разраба-
тываете код, который, предположительно, должен компилироваться в разных средах,
вы можете использовать следующую форму директивы заголовочного файла, чтобы
достичь того же эффекта:
/ / Заголовочный файл Box. h
#ifndef ВОХ_Н
#define ВОХ_Н
// Код, который не должен включаться более одного раза -
/ / такой как определение класса СВох
#endif
Важные строки здесь выделены полужирным и соответствуют директивам, под-
держиваемым компиляторами ISO/ASNI C++. Строки, следующие за директивой
#ifndef и до директивы #endif, включаются в сборку, если символ ВОХ_Н не опреде-
лен. Строка, следующая за #ifndef, определяет этот символ ВОХ_Н, гарантируя таким
образом, что код этого заголовочного файла не будет включен вторично. То есть, это
дает тот же эффект, что и помещение директивы #pragma once в начало заголовоч-
ного файла. Ясно, что директива #pragma once проще и менее громоздка, поэтому
лучше применять ее, если вы рассчитываете использовать свой код в среде разработ-
ки Visual C++ 2005. Иногда вы можете встретить комбинацию #ifndef/#endif, за-
писанную так:
456 Глава 8
#if !defined BOX_H
#def ine BOX_H
11 Код, который не должен включаться более одного раза -
// такой, как определение класса СВох
#endif
Файл Box. срр, сгенерированный мастером Class Wizard, содержит следующий
код:
#include "Box.h”
СВох::СВох(void)
СВох::~СВох(void)
Первая строка — это директива препроцессора #include, эффект от которой —
включение содержимого файла Box.h (определение класса) в данный файл Box.срр.
Это необходимо потому, что код Box. срр ссылается на имя класса СВох и определе-
ние класса должно быть доступным, чтобы имя СВох осмысленно воспринималось
компилятором.
Добавление данных* членов
Теперь вы можете добавить приватные данные-члены m_Length, m_Width и
m_Height. Для этого щелкните правой кнопкой мыши в Class View и выберите из кон-
текстного меню пункт Add^Add Variable (Добавить1^Добавить переменную). Затем
вы можете указать имя, тип и доступ для первого члена, который хотите добавить к
классу, в диалоговом окне Add Member Variable Wizard (Мастер добавления членов-
переменных).
Способ спецификации нового члена данных в диалоговом окне достаточно оче-
виден. Если вы указываете нижний предел значений члена данных, то должны также
указать и верхний предел. Когда вы укажете эти пределы, определение конструктора
в файле . срр будет модифицировано добавлением значения по умолчанию, равного
нижнему пределу. При желании вы можете добавить комментарий в нижнем поле вво-
да. После щелчка на кнопке ОК переменная добавляется к определению класса вместе
с комментарием, если вы его задали. Этот процесс потребуется повторить для двух
других данных-членов класса — m_Width и m_Height. Определение класса в Box.h за-
тем изменяется, и будет выглядеть так:
tpragma once
class СВох
public:
СВох(void);
public:
~CBox(void);
private:
// Длина ящика в дюймах
double m_Length;
// Ширина ящика в дюймах
double m_Width;
// Высота ящика в дюймах
double m_Height;
Дополнительные сведения о классах 457
Конечно, при желании вы можете вводить объявления этих членов вручную, не-
посредственно в коде. Но всегда есть выбор — использование автоматизации, пред-
ставленной IDE-средой.
Вы также можете вручную удалить любую часть, сгенерированную автоматически,
но не забудьте при этом внести соответствующие изменения в файлы . h и . срр.
Хорошей идеей будет сохранение всех файлов при каждом ручном изменении, по-
скольку это вызовет обновление информации в Class View.
Если вы заглянете в файл Box. срр, то увидите, что мастер также добавил список
инициализации в определение конструктора для данных-членов, которые вы добави-
ли, причем каждая переменная инициализируется нулем. Далее вы можете модифи-
цировать конструктор, чтобы он делал то, что необходимо.
Определение конструктора
Вам нужно изменить объявление конструктора без аргументов, сгенерированного
в определении класса, чтобы он принимал аргументы со значениями по умолчанию,
поэтому сделайте так:
СВох(double lv = 1.0, double wv = 1.0, double hv = 1.0);
Теперь вы готовы к реализации. Откройте файл Box. срр, если он еще не открыт,
и модифицируйте определение конструктора следующим образом:
СВох::СВох(double lv, double wv,
lv = lv <- 0.0 ? 1.0 : lv;
wv = wv <= 0.0 ? 1.0 : wv;
hv = hv <= 0.0 ? 1.0 : hv;
m_Length = lv>wv ? lv : wv;
m_Width = wv<lv ? wv : lv;
m Height = hv;
double hv)
// Обеспечить положительные
// размеры
/I объекта
// Обеспечить, чтобы
// length >= width
Вспомните, что инициализаторы параметров функций-членов должны появлять-
ся только в объявлении члена, находящемся в определении класса, а не в определе-
нии самой функции. Если вы поместите их в определение функции, ваш код не будет
компилироваться. Вы уже видели этот код, поэтому я не стану на нем останавли-
ваться. Будет хорошей идеей сохранить файл в этой точке, щелкнув на кнопке Save
(Сохранить) в панели инструментов. Возьмите за правило сохранять редактируемый
файл перед тем, как переключаетесь на что-то другое. Если вам опять понадобится
редактировать конструктор, вы сможете легко это сделать, выполнив двойной щел-
чок на нем в нижней панели на вкладке Class View или выбрав его из правого выпада-
ющего меню над панелью с кодом.
Также вы можете напрямую обратиться к определению функции-члена в фай-
ле . срр или к ее объявлению в файле . h непосредственно правым щелчком на его
имени в панели Class View и выбором соответствующей позиции в появившемся кон-
текстном меню.
Добавление функций- членов
Теперь вам нужно добавить все функции, которые вы видели ранее в классе СВох.
Ранее вы уже определяли несколько функций-членов внутри определения класса, так
что эти функции по умолчанию были встроенными (inline). Можно достичь одного и
того же результата, вводя код этих функций в определение класса вручную или же с по-
мощью мастера Add Member Function Wizard (Мастер добавления функций-членов).
458 Глава 8
Вы можете подумать, что можно определить каждую встроенную функцию в фай-
ле . срр с добавлением к ее определению ключевого слова inline, однако проблема в
том, что встроенные функции на самом деле не являются “настоящими” функциями.
Поскольку код тела каждой функции должен вставляться непосредственно в точку их
вызова, определение функции должно быть доступно, когда компилируется файл, со-
держащий эти вызовы. Если этого не будет, вы получите ошибки компоновки и ваша
программа не запустится. Если вы хотите, чтобы функции-члены были встроенными,
то вы должны включить определения функций в заголовочный файл . h класса. Они
могут быть определены либо внутри определения класса, либо немедленно вслед за
ним, в файле . h. Вы должны поместить все глобальные встроенные функции, ко-
торые вам нужны, в файл .
файл . срр, который использует их.
Чтобы добавить функцию GetHeight () как встроенную, щелкните правой кноп-
кой мыши на СВох во вкладке Class View и выберите Add*=>Add Function (Добавить1^
Добавить функцию) из контекстного меню. Затем вы можете ввести данные, опреде-
ляющие функцию в диалоге, как показано на рис. 8.11.
h и включить с помощью #include этот файл в каждый
Рис. 8.11. Создание в мастере Add Member Function Wizard (Мастер добав-
ления функций-членов) встроенной функции
Вы можете указать в качестве типа возврата double, выбирая его в выпадающем
списке, но точно также можете ввести его вручную. Очевидно, что тип, которого нет
в списке, придется вводить с клавиатуры. Отметка флажка Inline (Встроенная) обеспе-
чивает создание Get Height () в виде встроенной функции. Обратите внимание, что
среди других опций для объявления функции имеются флажки Static (Статическая),
Virtual (Виртуальная) и Pure (Чистая). Как вы знаете, статическая (static) функ-
ция-член существует независимо от объектов класса. Что касается виртуальных
(virtual) и чистых (pure) функций, то с ними мы познакомимся в главе 9. Функция
GetHeight () не имеет параметров, так что ничего более добавлять не нужно. Щелчок
Дополнительные сведения о классах 459
на кнопке ОК добавит определение функции к определению класса в Box. h. Если вы
повторите этот процесс для функций-членов GetWidth (), GetLength () и Volume (),
то определение класса СВох в Box. h будет выглядеть следующим образом:
#pragma once
class СВох
дюймах
public:
СВох (double lv - 1.0, double wv = 1.0, double hv = 1.0);
~CBox(void);
private:
// Длина ящика в дюймах
double m_Length;
// Ширина ящика в
double m_Width;
11 Высота ящика в
double m_Height;
public:
double GetHeight(void)
дюймах
return 0;
double GetWidth(void)
return 0;
double CBox::GetLength(void)
return 0;
11 Вычисление объема ящика
double Volume(void)
return 0;
В определение класса добавлен дополнительный раздел public, который содер-
жит определения встроенных функций. Вы должны модифицировать каждое из опре-
делений, чтобы обеспечить возврат правильного значения и объявлять функции как
const. Например, код для GetHeight () должен быть изменен так:
double GetHeight (void) const
return m Height;
Вы можете изменить определение функций GetWidth () и GetLength () анало-
гичным образом. Определение функции Volume () должно выглядеть, как показано
ниже:
double Volume (void) const
return m Length*m_Width*m_Height;
Вы можете также ввести другие невстроенные функции-члены непосредствен-
но в панели редактора, содержащей код, но для этого также можно использовать
мастер Add Member Function Wizard, что более удобно. Как и ранее, щелкните пра-
460 Глава 8
вой кнопкой на СВох во вкладке Class View и выберите пункт Add^Add Function
(Добавить1^ Добавить функцию) из контекстного меню. Затем в появившемся диало-
говом окне мастера вы можете ввести детали первой функции, которую хотите до-
бавить (рис. 8.12).
Puc. 8.12. Создание в мастере Add Member Function Wizard (Мастер добав-
ления функцийчленов) обычной функции
Здесь я определил функцию operator+ () как public с типом возврата СВох.
Тип параметра и его имя также должны быть введены в соответствующих полях.
Потребуется щелкнуть на кнопке Add (добавить) для регистрации параметра, а толь-
ко затем щелкать на кнопке Finish (Готово). Это также обновит сигнатуру функции,
показанную в нижней части диалогового окна Add Member Function Wizard. Затем
можете ввести детали другого параметра, если их больше одного, и снова щелкнуть
на кнопке Add, чтобы добавить его. Введенные в этом диалоговом окне комментарии
мастер поместит в файлы Box. h и Box. срр. Когда вы щелкаете на кнопке Finish, объ-
явление функции добавляется к определению класса в Box. h, а скелет определения
функции — в файл Box. срр. Функция должна быть объявлена как const, поэтому вы
должны добавить это ключевое поле к объявлению функции operators- () внутри
определения класса и к определению функции в Box. срр. Вы должны также добавить
код в тело функции, примерно так:
СВох СВох:: operator (const СВох& аВох) const
аВох.m_Length ? m_Length : aBox. m_Length,
I Width : aBox.m Width,
return СВох (m_Length
m__Width > аВох.mJWidth ?
m Height + aBox.m Height
11
Дополнительные сведения о классах 461
Вы должны повторить этот процесс для функций operator* () и operator / (),
которые видели ранее. По завершении определение класса в Box. h будет выглядеть
так, как показано ниже.
#pragma once
class СВох
public:
СВох(double lv = 1.0, double wv = 1.0, double hv - 1.0);
~CBox(void);
private:
11 Длина ящика в дюймах
double m_Length;
// Ширина ящика в дюймах
double m_Width;
// Высота ящика в дюймах
double m_Height;
public:
double GetHeight(void) const
return m_Height;
public:
double GetWidth(void) const
return m__Width;
public:
double GetLength(void) const
return m__Length;
public:
double Volume(void) const
return m_Length*m_Width*m_Height;
public:
// Перегруженная операция сложения
CBox operator*(const CBox& aBox) const;
public:
// Умножение ящика на целое число
СВох operator*(int n) const;
publi c:
// Деление одного ящика на другой
int operator/(const CBox& aBox) const;
Вы можете редактировать или упорядочивать код любым способом по своему же-
ланию (конечно, при условии его корректности). Я добавил несколько пустых строк,
чтобы сделать код чуть более читаемым.
Содержимое файла Box. срр в конечном итоге будет выглядеть примерно так:
#include ".\box.h"
СВох::СВох (double lv, double wv, double hv)
lv = lv <= 0.0 ? 1.0 : lv; 11 Обеспечить положительные
wv = wv<=0.0?1.0 : wv; // размеры
hv = hv <= 0.0 ? 1.0 : hv; // объекта
462 Глава 8
m_Length = lv>wv ? lv : wv; // Гарантировать, что
mJWidth = wv<lv ? wv : lv; // length >= width
m_Height = hv;
CBox::~CBox(void)
// Перегруженная операция сложения
CBox CBox::operator+(const CBoxS aBox) const
11 Новый объект имеет наибольшую длину и ширину из двух,
//и сумму двух высот
return СВох (m_Length > аВох. m__Leng th ? m_Length : аВох. m_Length,
m_Width > aBox.m__Width ? m_Width : aBox.mJWidth,
m_Height + aBox. m__Height);
/ / Умножение ящика на целое число
СВох СВох::operator*(int n) const
if(n%2)
return CBox (ni_Length, mJHidth, n*m__Height); // n нечетное
else
return CBox (m_Length, 2.0 *m__Width, (n/2) *m_Height); // n четное
// Деление одного ящика на другой
int СВох::operator/(const CBoxS аВох) const
// Временное значение числа в горизонтальном плане в одном направлении
int tel = 0;
// Временное значение для числа в
другом направлении
int tc2 = 0;
= static_cast<int>((m_Length
static_cast<int>((m_Width /
= static_cast<int> ((m__Length
static__cast<int> ((m_Width I
//Вернуть наилучшее наполнение
tel
tc2
/ aBox.m_Length)) *
aBox.m__Width)) ;
/ aBox.m_Width)) *
aBox.m Length));
// заполнить так
// и этак
return static__cast<int>( (m_Height/аВох.m_Height) * (tcl>tc2 ? tel : tc2));
Полужирным выделены строки, модифицированные или добавленные вручную.
Очень короткие функции, в частности те, что просто возвращают значение члена
данных, содержат свои определения внутри определения класса, поэтому они явля-
ются встроенными. Если заглянуть во вкладку Class View, щелкнув на ней и затем на
+ рядом с именем класса СВох, вы увидите, что все члены класса показаны в нижней
панели.
На этом класс СВох завершен, но еще потребуется определить глобальные функ-
ции, реализующие операции сравнения объема объекта СВох с числовым значением.
Добавление глобальных функций
Вам нужно создать файл . срр, который будет содержать определения глобальных
функций, поддерживающих операции объектов СВох. Этот файл также должен быть
частью проекта. Щелкните на вкладке Solution Explorer (Проводник решений) для
его отображения (у вас в данный момент открыта вкладка Class View) и щелкните
правой кнопкой на папке Source Files (Исходные файлы). Выберите пункт Add^New
Дополнительные сведения о классах 463
Item (Добавить1^ Новый элемент) из контекстного меню для отображения диалогово-
го окна. Выберите категорию Code (Код) и шаблон C++ File (.Срр) (Файл C++ (.срр)) в
правой панели диалогового окна и введите в качестве имени файла BoxOperators.
После этого можете ввести следующий код в панели редактора:
// BoxOperators.срр
// Операции с СВох, которым не нужен доступ к приватным членам
#include "Box.h”
// Функция проверки константы на предмет > объекта СВох
bool operator>(const doubles value, const CBoxS aBox)
{ return value > aBox.Volume(); }
// Функция проверки константы на предмет < объекта СВох
bool operator<(const doubles value, const CBoxS aBox)
{ return value < aBox.Volume(); }
// Функция проверки объекта CBox на предмет > константы
bool operator>(const CBoxS aBox, const doubles value)
{ return value < aBox; }
// Функция проверки объекта CBox на предмет < константы
bool operator<( const CBoxS aBox, const doubles value)
{ return value > aBox; }
I/ Функция проверки константы на предмет >= объекту СВох
bool operator>=(const doubles value, const CBoxS aBox)
{ return value >= aBox.Volume(); }
// Функция проверки константы на предмет <= объекту СВох
bool operator<=(const doubles value, const CBoxS aBox)
{ return value <= aBox.Volume(); }
I / Функция проверки объекта CBox на предмет >= константе
bool operator>=( const CBoxS aBox, const doubles value)
{ return value <= aBox; }
/ / Функция проверки объекта CBox на предмет <= константе
bool operator<=( const CBoxS aBox, const doubles value)
{ return value >= aBox; }
I/ Функция проверки константы на предмет равенства объекту СВох
bool operator==(const doubles value, const CBoxS aBox)
{ return value == aBox.Volume(); }
// Функция проверки объекта CBox на предмет равенства константе
bool operator==(const CBoxS aBox, const doubles value)
{ return value == aBox; }
// Операция умножения CBox - n*aBox
CBox operator*(int n, const CBoxS aBox)
{ return aBox * n; )
// Операция для возврата свободного объема в упакованном СВох
double operator%( const CBoxS aBox, const CBoxS ЬВох)
{ return aBox.Volume() - (aBox / ЬВох) * ЬВох.Volume(); }
Здесь нужна директива #include для Box.h, поскольку функции ссылаются на
класс СВох. Сохраните файл. Когда вы покончите с этим, можете перейти на вклад-
ку Class View. Теперь она будет содержать папку Global Functions and Variables
(Глобальные функции и переменные), в которой находятся все только что добавлен-
ные функции.
464 Глава 8
Определения всех этих функций вы уже видели ранее в настоящей главе, поэтому
мы не будем еще раз обсуждать их реализацию. Если вы захотите использовать любую
из этих функций в другом файле . срр, нужно будет обеспечить объявление всех этих
функций, которые предполагаете применять, чтобы компилятор узнал их. Этого мож-
но достичь, поместив набор объявлений в заголовочный файл. Переключитесь еще раз
на панель Solution Explorer и щелкните правой кнопкой мыши на имени папки Header
Files (Заголовочные файлы). Выберите Add^New Item (Добавить^Новый элемент) из
контекстного меню, чтобы отобразить диалоговое окно, но на этот раз установите в
качестве категории Code (Код), шаблона — Header File (.h) (Заголовочный файл (.h)), а
имени — BoxOperators. После щелчка на кнопке Add (Добавить) к проекту будет добав-
лен пустой заголовочный файл, и вы сможете ввести следующий код в окне редактора:
// BoxOperators.h — Объявления глобальных операций с ящиками
#pragma once
bool operator>(const doubles value, const CBoxS aBox);
bool operator<(const doubles value, const CBoxS aBox);
bool operator>(const CBoxS aBox, const doubles value);
bool operator<(const CBoxS aBox, const doubles value);
bool operator>=(const doubles value, const CBoxS aBox);
bool operator<=(const doubles value, const CBoxS aBox);
bool operator>=(const CBoxS aBox, const doubles value);
bool operator<=(const CBoxS aBox, const doubles value);
bool operator==(const doubles value, const CBoxS aBox);
bool operator==(const CBoxS aBox, const doubles value);
CBox operator*(int n, const CBox aBox);
double operator%(const CBoxS aBox, const CBoxS bBox);
Директива flpragma once обеспечивает однократное включение содержимо-
го файла в процессе сборки. Вы только должны добавить директиву #include для
BoxOperators .h в каждый исходный файл, где используется любая из этих функций.
Теперь вы готовы к тому, чтобы начать применение этих функций наряду с клас-
сом СВох для решения специфических проблем из мира ящиков.
Использование класса СВох
Предположим, что вы пакуете конфеты. Конфеты имеют большой размер, — на-
стоящие зуболомы: в обертке имеют 1,5 дюйма в длину, 1 дюйм в ширину и 1 дюйм в
высоту. У вас есть стандартная коробка для конфет размером 4,5 дюйма на 7 дюймов
и на 2 дюйма, и вы хотите знать, сколько конфет уместится в эту коробку, чтобы уста-
новить на нее цену. У вас есть также стандартный картонный ящик 2 фута и 6 дюймов
в длину, 18 дюймов в ширину и 18 дюймов в глубину, и вы хотите знать, сколько ко-
робок конфет может в него поместиться и сколько пустого места останется, когда вы
его наполните.
И если стандартная коробка конфет окажется не лучшим решением, вам также
нужно будет знать, какого размера специальные коробки подойдут в этом случае. Вы
знаете, что можно установить хорошую цену на коробки длиной от 3 до 7 дюймов,
шириной от 3 до 5 дюймов и высотой от 1 до 2,5 дюймов, причем каждый размер
может изменяться с шагом в полдюйма. Вы также знаете, что вам нужно иметь как
минимум 30 конфет в коробке, потому что это минимальное количество, потребляе-
мое вашим крупнейшим покупателем за один присест. К тому же коробка конфет не
должна иметь пустого пространства, поскольку иначе могут последовать жалобы от
покупателей, которые подумают, что их обманули. Более того, в идеале вы хотели бы
наполнить стандартный ящик полностью, чтобы в нем ничего не тарахтело при тра-
ополнительные сведения о классах
465
спортировке. Вы не хотите быть слишком строги, иначе упаковка будет сильно затруд-
нена, поэтому будем считать, что вы не тратите пространство впустую, если упаковы-
ваемый ящик имеет свободного места меньше, чем объем одной коробки конфет.
При наличии класса СВох проблема становится почти тривиальной и ее решение
представлено в следующей функции main (). Добавьте к проекту новый исходный
файл C++ по имени Ех8_08. срр через контекстное меню, которое вы получите пра-
вым щелчком на Source Files в панели Solution Explorer, как делали это ранее. Затем
введите следующий код:
// Ех8_08.срр
// Пример решения проблемы упаковки
#include <iostream>
#include "Box.h"
#include "BoxOperators.h"
using std::cout;
using std::endl;
int main ()
CBox candy(1.5, 1.0, 1.0); // Определение конфеты
CBox candyBox(7.0, 4.5, 2.0); // Определение коробки конфет
CBox carton(30.0, 18.0, 18.0); // Определение картонного ящика
// Вычислить количество конфет в коробке
int numCandies = candyBox/candy;
// Вычислить количество коробок в ящике
int numCboxes = carton/candyBox;
// Вычислить пустое пространство
double space = carton%candyBox;
cout « endl
« "В одной коробке ’’ « numCandies
« ’’ конфет"
« endl
« "В стандартный ящик поместится " « numCboxes
« ’’ коробок конфет " « endl « " и еще останется "
« space « ’’ кубических дюймов пустого места.";
cout « endl
« endl « "Анализ применения специальных коробок (без пустого места)’’;
// Попробовать весь диапазон размеров коробок конфет
for (double length = 3.0 ; length <= 7.5 ; length += 0.5)
for (double width = 3.0 ; width <= 5.0 ; width += 0.5)
for (double height = 1.0 ; height <= 2.5 ; height += 0.5)
//На каждом шаге цикла создавать новую коробку
СВох tryBox(length, width, height);
if(carton%tryBox < tryBox.Volume() &&
tryBox % candy == 0.0 && tryBox/candy >= 30)
cout « endl « endl
« "Пробная коробка L = ’’ « tryBox.GetLength ()
« ’’ W = ’’ « tryBox. GetWidth ()
« ’’ H = ’’ « tryBox.GetHeight ()
« endl
« "Пробная коробка содержит ’’ « tryBox / candy « ’’ конфет"
«’’ив ящик поместится ’’ « carton / tryBox
« " таких коробок. ’’;
cout « endl;
return 0;
466 Глава 8
Посмотрим, как структурирована программа. Вы разделили ее на множество
файлов, что общепринято при написании программ на C++. Вы можете увидеть их
все, если взглянете на вкладку Solution Explorer, которая выглядит, как показано на
I Solution Explorer - Solution 'Ex8_08' (1 project) ▼ Д X
J
Solution 'Ех8_08' (1 project)
ЕхВ_08
s Header Files
BoxOperators.h
-u Resource Files
мй Source Files
BoxOperators.cpp
j Solution Explorer class Vie jgl Resource View
Puc. 8.13. Вкладка Solution Explorer проекта Ex8 08
Файл Ex8_08. срр содержит функцию main () и директиву #include для файла
BoxOperators. h, содержащего прототипы функций из BoxOperators. срр (не явля-
ющихся членами класса). Он также содержит директиву #include для определения
класса СВох в Box. h. Консольная программа C++ обычно делится на множество фай-
лов, каждый из которых попадает в одну из трех перечисленных ниже категорий.
1. Файлы .h, содержащие команды #include библиотек, глобальные константы и
переменные, определения класса и прототипы функций — другими словами, все,
за исключением исполняемого кода. Они также содержат определения встроен-
ных функций. Когда программа имеет несколько определений классов, обычно
они помещаются в отдельные файлы . h.
2. Файлы . срр, содержащие исполняемый код программы плюс команды #include
для всех определений, необходимых исполняемому коду.
3. Еще один файл . срр, содержащий функцию main ().
Код нашей функции ma i п () не требует особых пояснений — он почти напрямую
выражает определение проблемы в ее терминах, поскольку операции интерфейса
класса выполняют проблемно-ориентированные действия над объектами СВох.
Решение об использовании стандартных коробок, принятое в операторах объяв-
ления, также вычисляет ответы, которые нам нужны, как значения инициализации.
Затем эти значения выводятся с некоторыми поясняющими комментариями.
Вторая часть проблемы решается использованием трех вложенных циклов for,
выполняющих итерации в пределах допустимых диапазонов m_Length, m_Width и
m_Height, так что оцениваются все возможные комбинации. Вы можете вывести их
все, но поскольку из 200 комбинаций вас могут интересовать лишь несколько, у вас
Дополнительные сведения о классах 467
есть оператор if, который идентифицирует те варианты, которые действительно мо-
гут вас заинтересовать.
Выражение if истинно только тогда, когда в ящике не расходуется место впустую,
и текущая пробная коробка также не расходует места впустую, и она содержит мини-
мум 30 конфет.
Ниже показан вывод этой программы.
В одной коробке 42 конфет
В стандартный ящик поместится 144 коробки конфет
и еще останется 648 кубических дюймов пустого места.
Анализ применения специальных коробок
(без пустого места)
Пробная коробка L = 5W=4.5H = 2
Пробная коробка содержит 30 конфет, и в ящик поместится 216 таких коробок.
Пробная коробка L=5W=4.5H = 2
Пробная коробка содержит 30 конфет, и в ящик поместится 216 таких коробок.
Пробная коробка L=6W = 4.5H = 2
Пробная коробка содержит 36 конфет, и в ящик поместится 180 таких коробок.
Пробная коробка L=6W=5H = 2
Пробная коробка содержит 40 конфет, и в ящик поместится 162 таких коробок.
Пробная коробка L = 7.5W=3H = 2
Пробная коробка содержит 30 конфет, и в ящик поместится 216 таких коробок.
Вам приходится дублировать решение из-за того факта, что во вложенном ци-
кле вы оцениваете коробки в 5 дюймов длиной и 4,5 шириной, а также 4,5 дюймов
длиной и 5 шириной. Поскольку конструктор класса СВох гарантирует, что длина не
меньше ширины, эти два случая идентичны. Вы можете добавить некоторую допол-
нительную логику, дабы избежать подобного дублирования, но вряд оно стоит таких
усилий. Можете рассматривать это как самостоятельное упражнение.
Организация кода программы
В последнем примере мы распределили код между несколькими файлами. Это не
только общепринятая практика для приложений C++, но также для всего программи-
рования под Windows вообще. Общий объем кода, составляющий даже простейшую
программу, должен быть разделен на отдельно обрабатываемые фрагменты.
Как говорилось в предыдущем разделе, существует два основных типа исходных
файлов программы на C++ — файлы .h и файлы . срр (рис. 8.14).
Существует исполняемый код, которому соответствуют определения функций, со-
ставляющих программу. Также есть определения различного рода, которые необхо-
димы для корректной компиляции исполняемого кода. К ним относятся глобальные
константы и переменные, типы данных, включающие классы, структуры и объедине-
ния, а также прототипы функций. Исполняемый код сохраняется в файлах с расши-
рением .срр, а определения хранятся в файлах с расширением . h.
Время от времени вам может понадобиться использовать код из существую-
щих файлов в новом проекте. В этом случае вы должны только добавить к про-
екту файлы . срр, что можно сделать, используя пункт меню Projects Add Existing
Item (Проект^ Добавить существующий элемент) или щелкнув правой кнопкой
мыши либо на папке Source Files (Исходные файлы), либо на папке Header Files
(Заголовочные файлы) во вкладке Solution Explorer и выбрав из контекстного меню
пункт Add^Existing Item (Добавить^Существующий элемент). Вам не обязательно до-
468 Глава 8
бавлять к проекту файлы .h, хотя вы можете сделать это, если хотите, чтобы они
появились в панели Solution Explorer немедленно. Код из файлов . h добавляется в на-
чало файлов . срр, которые требуют их через специфицированные вами директивы
♦include. Вам необходимы директивы #include для заголовков файлов с прототи-
пами функций стандартной библиотеки и других стандартных определений, а также
для ваших собственных заголовочных файлов. Visual C++ 2005 автоматически отсле-
живает все эти файлы и позволяет видеть их на вкладке Solution Explorer. Как показа-
но в последнем примере, во вкладке Class View вы также можете видеть определения
классов и глобальные константы и переменные.
В программе для Windows есть и другие виды определений для спецификации та-
ких вещей, как меню и кнопки панели инструментов. Они сохраняются в файлах с
расширениями вроде .геи. ico. Так же, как и файлы . h, их не обязательно явно
добавлять к проекту, поскольку они создаются и отслеживаются автоматически Visual
C++ 2005, когда в этом возникает необходимость.
Определения функций
Определение класса
Определения функций
Определение класса
Определения функций
Глобальные константы
Определение main()
Глобальные константы
Исходные файлы
с расширением .срр
Исходные файлы
с расширением .h
Рис. 8.14. Исходные файлы программы на C++
ополнительные сведения о классах
469
Именование программных файлов
Как я уже говорил, для классов любой сложности обычно принято хранить опреде-
ления в файле . h с именем, основанным на имени класса, а реализацию его функций-
членов, определенных вне определения класса — в файлах . срр с теми же именами.
На основании этого определение класса СВох размещено в файле Box. h, а его реали-
зация — в Box. срр. Мы не следовали этому соглашению в ранних примерах настоя-
щей главы, поскольку те примеры были очень короткими, и было проще производить
имя примера от номера главы и следующего за ним номера примера в главе. Но в про-
граммах любого размера лучше структурировать код упомянутым в начале образом,
поэтому отныне будет хорошей идеей взять за привычку создавать отдельные файлы
. h и . срр для размещения кода программ.
Сегментирование программ C++ в файлах . h и . срр — очень удобный подход,
поскольку он облегчает нахождение определения или реализации любого класса, в
частности, если вы имеете дело со средой разработки, которая не имеет набора ин-
струментов, предлагаемого Visual C++. До тех пор, пока вы знаете имя класса, вы мо-
жете непосредственно обратиться к нужному файлу. Однако это не жесткое правило.
Иногда удобно группировать определения набора тесно связанных классов вместе в
одном файле и также собирать вместе их реализации; однако, как бы вы не решили
структурировать ваши файлы, панель Class View (Представление классов) все равно
будет отображать индивидуальные классы, а также всех членов каждого класса, как
показано на рис. 8.15.
Class View ▼ J X
!.s Macros and Constants
CBox:
v ~CBox(void]
;v CBox(double lv = 1.000000, double wv - 1.000000, double hv = 1.000000]
М» GetHeight(void]
v GetLengthfvoid)
=5* GetWidth(void)
Volume(void) const
operator *(int n) const
=7> operator /(const CBox &aBox] const
I.=£, operator +(const CBox &aBox) const
' m_Height
4' m_Le ngth
l * m_Width
Solution Explorer GjClass View |iWr-source View
Puc. 8.15. Панель Class View (Представление
классов) для проекта Ех8__08
Я изменил размер панели Class View, чтобы были видны все элементы проекта.
Здесь вы можете видеть детали классов и глобальные сущности из последнего при-
мера. Как уже упоминалось, двойной щелчок на любом элементе в дереве переносит
непосредственно в соответствующую точку исходного кода.
470 Глава 8
Программирование на C++/CLI
Хотя вы можете определить деструктор в ссылочном классе точно так же, как вы
делаете это для классов “родного” C++, в большинстве случаев это не нужно; тем не
менее, позднее в этой главе я вернусь к теме деструкторов ссылочных классов. Вы
также можете вызывать delete для дескриптора ссылочного класса, но опять-таки,
обычно это не нужно, поскольку сборщик мусора удаляет ненужные объекты автома-
тически.
Классы C++/CLI поддерживают перегрузку операций, но с некоторыми отличи-
ями, о которых вам следует знать. Прежде всего, рассмотрим некоторые базовые
отличия между перегрузкой операций классов C++/CLI и родного C++. О паре отли-
чий вы уже слышали. Возможно, вы вспомните о том, что нельзя перегружать опе-
рацию присваивания одного объекта класса значений другому объекту того же типа,
поскольку присваивание почленным копированием для них уже определено, и вы не
можете этого изменить. Я также упоминал, что в отличие от родных классов, ссылоч-
ные классы не имеют операции присваивания по умолчанию (если вы хотите, чтобы
операция присваивания работала с объектами ваших ссылочных классов, то должны
для этого реализовать соответствующую функцию). Другое отличие от классов род-
ного C++ заключается в том, что функции, которые реализуют перегруженные опера-
ции в классах C++/CLI, могут быть как статическими членами класса, так и членами
экземпляров. Это значит, что при реализации бинарных операций в классах C++/CLI
у вас есть выбор — реализовать их статическими функциями-членами с двумя пара-
метрами в дополнение к возможности, которую вы видели в контексте родного C++,
где функции операций являются функциями экземпляров с одним параметром, или
функциями, не являющимися членами, с двумя параметрами. Аналогично, в C++/CLI
у вас имеется дополнительная возможность реализовать префиксную унарную опера-
цию как статическую функцию-член без параметров. И, наконец, хотя в “родном” C++
вы можете перегрузить операцию new, вы не можете перегрузить операцию gcnew в
классе C++/CLI.
Перегрузка операций в классах значений
Определим класс, представляющий длину в футах и дюймах, и используем его в
качестве базы для демонстрации того, как может быть реализована перегрузка опе-
раций для классов значений. Операция сложения — хорошее начало, поэтому рассмо-
трим определение класса Length, оснащенного функцией операции сложения.
value class Length
private:
int feet; // Компонент футов
int inches; // Компонент дюймов
public:
static initonly int inchesPerFoot = 12;
// Конструктор
Length(int ft, int ins) : feet(ft), inches(ins){ }
/ / Длина в виде строки
virtual String^ ToStringO override
{ return feet+L" футов " + inches + L” дюймов"; }
Дополнительные сведения о классах 471
// Операция сложения
Length operator*(Length len)
int inchTotal = inches+len.inches+inchesPerFoot*(feet+len.feet) ;
return Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot);
Константа inchesPerFoot статическая, поэтому она непосредственно доступна
для статических и нестатических функций-членов класса. Объявление inchesPerFoot
как intonly означает, что она не может быть модифицирована, поэтому она может
быть общедоступным членом класса. Имеется переопределение функции ToString ()
для класса, так что вы можете выводить объекты Length в командной строке с ис-
пользованием функции Console: :WriteLine (). Реализация функции operator* ()
очень проста. Эта функция возвращает новый объект Length, представленный
комбинацией компонентов feet и inches для текущего объекта и параметра len.
Вычисление выполняется комбинацией двух длин в дюймах с последующим вычисле-
нием аргументов для конструктора класса Length с целью создания нового объекта из
значения для комбинированного значения в дюймах.
Следующий фрагмент кода тестирует работу новой функции операции сложения.
Length lenl = Length (6, 9);
Length 1еп2 = Length (7, 8);
Console::WriteLine(L”{0} плюс {1} равно {2}", lenl, len2, lenl+len2);
Последний аргумент функции WriteLine () — сумма двух объектов Length, так что
это вызывает функцию operator* (). В результате получается новый объект Length,
для которого компилятор вызывает функцию ToString (), так что последний опера-
тор на самом деле выглядит так:
Console::WriteLine(L”{0} плюс {1} равно {2}”, lenl, 1еп2,
lenl.operator*(1еп2).ToString());
Конечно, вы можете определить функцию operator* () как статический член
класса Length:
static Length operator*(Length lenl, Length len2)
int inchTotal = lenl.inches*len2.inches+inchesPerFoot*(lenl.feet+len2.feet);
return Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot);
Параметры — два объекта Length, которые нужно сложить вместе, чтобы породить
новый объект Length. Поскольку это статический член класса, функция operator* ()
полностью вправе обращаться к приватным членам — feet и inches обоих объектов
Length, переданных в качестве аргументов. Дружественные функции в классах C++/
CLI не допускаются, а внешние функции не должны иметь доступа к приватным чле-
нам класса, поэтому у вас не остается других возможностей выбора при реализации
операции сложения.
Поскольку мы не работаем с площадями, умножение объектов Length имеет смысл
только как умножение длины на числовое значение. Вы можете реализовать это как
статический член класса, но давайте определим эту функцию вне класса. Класс вы-
глядит примерно так:
472 Глава 8
value class Length
{
private:
int feet;
int inches;
public:
static initonly int inchesPerFoot = 12;
// Конструктор
Length (int ft, int ins) : feet (ft)-, inches (ins) { }
// Длина в виде строки
virtual StringA ToStringO override
{ return feet+L" футов •' + inches + L" дюймов"; }
// Операция сложения
Length operator+(Length len)
{
int inchTotal = inches+len.inches+inchesPerFoot*(feet+len.feet);
return Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot);
static Length operator*(double x, Length len); // Предумножение на
// значение double
static Length operator*(Length len, double x); // Постумножение на
// значение double
Новые объявления функций в классе представляют перегруженную функцию
операции * для пред- и постумножения объекта Length на значение типа double.
Определение функции operator* () вне класса для предумножения выглядит следу-
ющим образом:
Length Length::operator *(double x, Length len)
int ins = safe_cast<int>(x*len.inches +x*len.feet*inchesPerFoot);
return Length(ins/12, ins %12);
Версия постумножения теперь может быть определена так:
Length Length::operator *(Length len, double x)
{ return operator*(x, len); }
Здесь просто вызывается версия предумножения с перестановкой аргументов. Вы
можете протестировать эти функции в следующем фрагменте кода:
double factor = 2.5;
Console::WriteLine(L"{0} умножить на {1} равно {2}", factor, 1еп2, factor*len2);
Console::WriteLine(L"{!) умножить на {0} равно {2}", factor, len2, len2*factor);
Обе строки вывода из этого фрагмента кода должны отображать один и тот же
результат умножения (19 футов 2 дюйма). Аргумент выражения factor*1еп2 эквива-
лентен следующему:
Length::operator*(factor, 1еп2).ToStringO
Результатом вызова статической функции operator* () является новый объект
Length, и функция ToString () вызывается с ним для формирования аргумента для
функции WriteLine (). Выражение 1еп2*factor аналогично, но вызывает функцию
operator* () с параметрами в противоположном порядке. Хотя функция operator* ()
Дополнительные сведения о классах 473
и была написана, чтобы иметь дело с множителем типа double, она также работает
и с целыми. Компилятор автоматически приводит целочисленное значение к типу
double, когда вы используете его в таком выражении, как 12* (1еп1+1еп2).
Теперь мы еще глубже раскроем перегруженные операции класса Length, рассмо-
трев их применение в работающем примере.
Практическое занятие
Класс значения с перегруженными
операциями
В этом примере реализуется перегрузка операций сложения, умножения и деле-
ния для класса Length.
// Ех8_09.срр : главный файл проекта.
// Перегруженные операции в классе значений Length
#include "stdafx.h"
using namespace System;
value class Length
{
private:
int feet;
int inches;
public:
static initonly int inchesPerFoot = 12;
// Конструктор
Length(int ft, int ins) : feet (ft), inches(ins){ }
// Длина в виде строки
virtual StringA ToString () override
{ return feet+L" feet ” + inches + L” inches”; }
// Операция сложения
Length operator+(Length len)
{
int inchTotal = inches+len.inches+inchesPerFoot*(feet+len.feet);
return Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot);
}
// Операция деления
static Length operator/(Length len, double x)
{
int ins - safe_cast<int>((len.feet*inchesPerFoot + len.inches)/x);
return Length(ins/inchesPerFoot, ins%inchesPerFoot);
)
static Length operator*(double x, Length len); // Предумножение на
// значение double
static Length operator*(Length len, double x); // Постумножение на
// значение double
};
Length Length::operator *(double x, Length len)
{
int ins = safe_cast<int>(x*len.inches +x*len.feet*inchesPerFoot);
return Length(ins/inchesPerFoot, .ins%inchesPerFoot);
}
474 Глава 8
Length Length::operator *(Length len, double x)
{ return operator*(x, len); }
int main(array<System::String A> Aargs)
Length lenl = Length(6, 9);
Length len2 = Length(7, 8);
double factor = 2.5;
Console::WriteLine(L"{0} плюс {1} равно {2}”, lenl, len2, lenl+len2);
Console::WriteLine(L"{0} умножить на {1} равно {2}", factor, len2, factor*len2);
Console::WriteLine(L“{1} умножить на {0} равно {2}", factor, len2, len2*factor);
Console::WriteLine(L”CyMMa {0} и {1} деленная на {2} равна {3}",
lenl, 1еп2, factor, (lenl+len2)/factor);
return 0;
Ниже показано, как выглядит вывод этого примера.
6 футов 9 дюймов плюс 7 футов 8 дюймов равно 14 футов 5 дюймов
2.5 умножить на 7 футов 8 дюймов равно 19 футов 2 дюймов
7 футов 8 дюймов умножить на 2.5 равно 19 футов 2 дюймов
Сумма 6 футов 9 дюймов и 7 футов 8 дюймов деленная на 2.5 равна 5 футов 9 дюймов
Press any key to continue . . .
Описание полученных результа тов
Новая функция перегруженной операции в классе Length предназначена для деле-
ния, и позволяет делить значение Length на значение типа double. Деление double
значения на Length не имеет очевидного смысла, потому эту версию реализовывать
незачем. Функция operator / () реализована как другой статический член класса, и
ее определение, в отличие от функций operator* (), появляется внутри тела опреде-
ления класса. Обычно вы будете определять все такие функции внутри определения
класса.
Конечно, вы можете определить функцию operator/ () как нестатический член
класса:
Length operator/(double х)
int ins = safe_cast<int>((feet*inchesPerFoot + inches)/x);
return Length(ins/inchesPerFoot, ins%inchesPerFoot);
Теперь она имеет один аргумент — правый операнд операции деления. Левый опе-
ранд— текущий объект, на который ссылается указатель this (в данном случае не-
явно).
Операция выполняется в четырех операторах вывода. Только последнее должно
быть для вас новым — в нем комбинируется перегруженная операция + для объек-
тов Length с перегруженной операцией /. Последний аргумент функции Console: ;
WriteLine () — это четвертый выходной оператор (lenl+len2) /factor, что эквива-
лентно выражению:
Length::operator/(lenl.operator+(1еп2), factor) .ToStringO
Первый аргумент функции operator/ () — объект Length, возвращенный функ-
цией operatort (), а второй аргумент — переменная factor, которая является де-
лителем. Функция ToString () для объекта Length, возвращенная operator () / ,
вызывается для генерации строкового аргумента для функции вывода Console: :
WriteLine().
Может случиться, что вы захотите иметь возможность делить один объект Length
на другой и получать в результате значение типа int. Это может пригодиться, напри-
мер, чтобы найти, сколько 17-дюймовых досок можно вырезать из куска бруса 12 фу-
тов и 6 дюймов длиной. Вы можете реализовать это достаточно легко примерно так:
static int operator/(Length lenl, Length len2)
return
(lenl.feet*inchesPerFoot + lenl.inches) / ( len2.feet*inchesPerFoot + len2.inches);
Это просто вернет результат деления первой длины в дюймах на вторую длину в
дюймах.
Чтобы завершить набор, можно добавить функцию для перегрузки операции %, что-
бы узнать величину остатка от деления. Ее можно реализовать следующим образом:
static Length operator%(Length lenl, Length len2)
int ins = (lenl.feet*inchesPerFoot + lenl.inches)%
(len2.feet*inchesPerFoot + len2.inches);
return Length(ins/inchesPerFoot, ins%inchesPerFoot);
Здесь вычисляется остаток в дюймах после деления lenl на len2 и возвращается
новый объект Length.
Имея все эти операции, вы действительно можете использовать объекты Length в
арифметических выражениях. Вы можете записывать операторы вроде следующих:
Length lenl = Length(2,6); //
Length 1еп2 = Length(3,5); //
Length len3 = Length(14,6); //
Length total = 12* (lenl + len2 + len3)
2 фута 6 дюймов
3 фута 5 дюймов
14 футов 6 дюймов
+ (Ien3/Length(1,7))*1еп2;
Значение total будет составлять 275 футов и 9 дюймов. Последний оператор ис-
пользует операцию присваивания, которая определена для каждого класса значения —
так же, как функции operator* (), operator* () и operator/ () в классе Length.
Перегрузка операций — не только мощное средство, но и очень простое, не правда ли?
Перегрузка операций инкремента и декремента
Перегрузка инкремента и декремента в C++/CLI проще, чем в “родном” C++. Пока
вы реализуете функцию операции как статический член класса, одна и та же функция
будет служить и префиксной, и постфиксной формами операций. Вот как вы можете
реализовать операцию инкремента в классе Length:
class Length
public:
// Ранее представленный код...
// Перегруженная функция операции инкремента — инкремент на 1 дюйм
static Length oper a tor++(Length len)
{
++len.inches;
len. feet += len. inches/len. inchesPerFoot;
len.inches %= len.inchesPerFoot;
return len;
476 Глава 8
Эта реализация функции operator++ () увеличивает длину на 1 дюйм. С помощью
приведенного ниже кода функция тестируется.
Length len = Length (1, 11); // 1 фут 11 дюймов
Console::WriteLine(len++) ;
Console::WriteLine(++len);
Выполнение показанного фрагмента кода выдаст следующее:
1 фут 11 дюймов
2 фут 1 дюймов
Таким образом, и префиксная и постфиксная операции инкремента работают так,
как должны, используя единственную функцию класса Length. Это происходит благо-
даря тому, что компилятор способен определить, как используется операнд в окружа-
ющем выражении — перед выполнением инкремента или после него — и компилирует
код надлежащим образом.
Перегрузка операций в ссылочных классах
Перегрузка операций в ссылочных классах, по сути, такая же, как перегрузка опе-
раций в классах значений. Главное отличие в том, что параметры и возвращаемые
значения обычно являются дескрипторами. Давайте теперь посмотрим, как будет вы-
глядеть класс Length, реализованный как ссылочный, а затем сравним две версии.
практическое занятие | Перегруженные операции в ссылочном
классе
В этом примере определяется Length как ссылочный класс с тем же набором пере-
груженных операций, что и предыдущая версия класса значений.
// Ех8_10.срр : главный файл проекта.
// Определение и использование перегруженных операций
#include "stdafx.h”
using namespace System;
ref class Length
{
private:
int feet;
int inches;
public:
static initonly int inchesPerFoot = 12;
// Конструктор
Length(int ft, int ins) : feet(ft), inches(ins){ }
// Длина в виде строки
virtual String74 ToStringO override
{ return feet+L" feet " + inches + L" inches"; }
// Перегруженная операция сложения
Length74 operator+(Length74 len)
{
int inchTotal = inches+len->inches+inchesPerFoot*(feet+len->feet) ;
return gcnew Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot);
}
Дополнительные сведения о классах 477
// Перегруженная операция деления — правый операнд типа double
static Length* operator/(Length* len, double x)
int ins = safe_cast<int>((len->feet*inchesPerFoot + len->inches)/x);
return gcnew Length(ins/inchesPerFoot, ins%inchesPerFoot);
// Перегруженная операция деления — оба операнда типа Length
static int operator/(Length* lenl, Length* len2)
return (lenl->feet*inchesPerFoot + lenl->inches)/
(len2->feet*inchesPerFoot + len2-->inches);
}
11 Перегруженная операция остатка от деления
static Length* operator%(Length* lenl, Length* len2)
int ins ® (lenl->feet*inchesPerFoot + lenl->inches)%
(len2->feet*inchesPerFoot + len2->inches);
return gcnew Length(ins/inchesPerFoot, ins%inchesPerFoot);
}
static Length* operator* (double x, Length* len); // Умножение - справа double
static Length* operator*(Length* len, double x); // Умножение - слева double
// Префиксная и постфиксная операции инкремента
static Length* operator++(Length* len)
++len->inches;
len->feet += len->inches/len->inchesPerFoot;
len->inches %= len->inchesPerFoot;
return len;
// Реализация операции умножения — правый операнд double
Length* Length::operator*(double x, Length* len)
int ins = safe_cast<int>(x*len->inches +x*len->feet*inchesPerFoot);
return gcnew Length(ins/inchesPerFoot, ins%inchesPerFoot);
// Реализация операции умножения — левый операнд double
Length* Length::operator*(Length* len, double x)
{ return operator*(x, len); }
int main(array<System::String *> *args)
Length* lenl = gcnew Length(2,6); // 2 фута 6 дюймов
Length* len2 = gcnew Length(3,5); //3 фута 5 дюймов
Length* len3 = gcnew Length(14,6); 11 14 футов 6 дюймов
// Использование операций +, * и /
Length* total = 12* (Ienl+len2+len3) + (len3/gcnew Length(1,7))*1еп2;
Console::WriteLine(total);
// Использование операции остатка от деления
Console::WriteLine(
L"{0} можно разделить на {1} частей {2} длиной с остатком {3}."
1епЗ, len3/lenl, lenl, len3%lenl);
Length* 1еп4 = gcnew Length (1, 11); // 1 foot 11 inches
478 Глава 8
// Использование префиксной и постфиксной операций
Console::WriteLine(1еп4++); // Использование постфиксной операции инкремента
Console:: WriteLine (++1еп4); / / Использование префиксной операции инкремента
return 0;
Ниже показан вывод этого примера.
275 футов 9 дюймов
14 футов 6 дюймов можно разделить на 5 частей 2 футов 6 дюймов длиной
с остатком 2 футов 0 дюймов.
2 футов 0
дюймов
2 футов 1 дюймов
Press any key to continue
Описание полученных результатов
Основные отличия от варианта с классом значений заключаются в типе параме-
тров и возвращаемых значений функций перегруженных операций, как следствие —
использовании операции ->, а также в том, что объекты типа Length теперь создают-
ся в куче CLR с применением ключевого слова gcnew. Помимо этих изменений, код
в основном тот же самый, и функции операций работают так же эффективно, как и в
предыдущем примере.
Резюме
В этой главе вы изучили основы определения классов, создания и использования
объектов классов. Вы также узнали о том, как перегруженные операции классов могут
быть применены к объектам классов.
Ниже перечислены ключевые моменты, о которых вы узнали в этой главе и кото-
рые следует иметь в виду.
□ Объекты создаются с помощью функций, называемых конструкторами.
Основное назначение конструкторов — устанавливать значения данных-членов
(полей) в объекте класса.
□ Классы C++/CLI могут также иметь статический конструктор, который ини-
циализирует статические поля класса.
□ Объекты уничтожаются с помощью функций, называемых деструкторами. В
“родном” C++ важно определять деструктор для уничтожения объектов, кото-
рые содержат члены, распределяемые в куче, поскольку деструктор по умолча-
нию этого не делает.
□ Если вы не определяете в классе “родного” C++ конструктор копирования, то
компилятор просто применяет конструктор копирования по умолчанию. Этот
конструктор копирования по умолчанию не обрабатывает корректно объекты
классов, в которых есть данные-члены, распределенные в свободном хранили-
сь Когда вы определяете собственный конструктор копирования в классе “родно-
го” C++, то должны использовать параметр-ссылку.
□ Вы не должны определять конструктор копирования в классах значений; копии
объектов классов значений всегда создаются путем копирования полей.
□ Для ссылочных классов не создается конструкторов копирования по умолча-
нию, хотя при необходимости вы можете определить собственные.
Дополнительные сведения о классах 479
Если вы не определяете операцию присваивания для своего класса “родного”
C++, то компилятор применяет версию по умолчанию. Как и с конструктором
копирования, операция присваивания по умолчанию не работает корректно с
классами, в которых есть данные-члены, распределенные в свободном храни-
лище
□ Вы не должны определять операцию присваивания в классе значений.
Присваивание объектов класса значений всегда выполняется путем копирова-
ния полей.
□ Операция присваивания по умолчанию не предоставляется в ссылочных клас-
сах, но в случае необходимости вы можете определить собственную функцию
операции присваивания.
□ Важно, чтобы вы всегда представляли деструктор, конструктор копирования и
операцию присваивания для классов “родного” C++, который включают члены,
распределенные с помощью new.
□ Объединение — это механизм, позволяющий двум или более переменным зани-
мать одно и то же место в памяти.
□ Классы C++/CLI могут содержать литеральные поля, которые определяют
константы внутри класса. Они также могут содержать поля initonly (только
инициализируемые поля), которые не могут быть модифицированы после ини-
циализации.
□ Большинство базовых операций могут быть перегружены, чтобы представить
действия, специфичные для объектов класса. Вы должны реализовывать в сво-
их классах функции операций, которые согласованы с нормальной интерпрета-
цией базовых операций.
□ Шаблон класса — это образец, который вы можете применять для создания
классов с одинаковой структурой, но поддерживающих разные типы данных.
□ Вы можете определять шаблоны классов, которые имеют множество параме-
тров, включая параметры, которые могут быть константными значениями, а не
типами.
□ Вы должны помещать определения для своих программ в файлы . h, а испол-
няемый код — определения функций — в файлы . срр. Вы можете затем встраи-
вать файлы .h в свои файлы . срр с помощью директив #include.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Определите класс “родного” C++, представляющий приблизительные целые, та-
кие как “около 40”. Это целые, значения которых могут быть рассмотрены как
целое или приблизительное, поэтому класс должен иметь в качестве данных-
членов значение и флаг “приблизительности”. Состояние этого флага влияет
на арифметические операции, так что “2 * около 40” должно давать “около 80”.
Состояние переменных должно быть переключаемым между “приблизительно”
и “точно”.
Представьте один или более конструкторов для такого класса. Перегрузите
операцию +, чтобы такие числа можно было использовать в арифметических
480 Глава 8
операциях. Хотите ли вы сделать операцию + глобальной функцией или функ-
цией-членом? Нужна ли операция присваивания? Предложите функцию-член
Print (), чтобы можно было распечатывать такие числа, используя ведущий
символ “Е” для указания на то, что установлен флаг “приблизительности”.
Напишите программу для тестирования операций вашего класса, особо обра-
щая внимание на правильность операций с флагом приблизительности.
2. Реализуйте простой строковый класс в “родном” C++, который содержит char*
мающий аргумент типа const char*, и реализуйте конструктор копирования,
операцию присваивания, а также функцию-деструктор. Убедитесь, что класс ра-
ботает. Легче всего для этого воспользоваться строковыми функциями из заго-
ловочного файла <cstring>.
3. Какие еще другие конструкторы можно применить в вашем строковом классе?
Составьте список и закодируйте их.
4. (Усложненное.) Правильно ли ваш класс обрабатывает подобные ситуации?
string si;
si = s2;
Если нет, как его можно модифицировать?
5. (Усложненное.) Перегрузите операции + и += вашего класса для выполнения
конкатенации строк.
6. Измените пример стека из упражнения 7 предыдущей главы, чтобы можно
было размещать стек заданного размера, который передается в конструкторе.
Что еще потребуется добавить? Протектируйте работу полученного нового
класса.
7. Определите ссылочный класс Box с той же функциональностью, что и класс
СВох в примере Ех8_08. срр, и реализуйте пример в виде программы CLR.
9
Наследование классов
и виртуальные функции
В этой главе мы обратимся к теме, лежащей в сердце объектно-ориентированного
программирования — наследованию классов. Если говорить просто, то наследова-
ние представляет собой средство, с помощью которого вы можете определить новый
класс в терминах уже существующего. Это — фундаментальная концепция в програм-
мировании на C++, поэтому важно, чтобы вы поняли, как работает наследование.
В этой главе вы изучите следующие вопросы.
□ Как наследование укладывается в идею объектно-ориентированного програм-
мирования.
□ Определение нового класса в терминах существующего.
□ Использование ключевого слова protected для определения новой специфи-
кации доступа к членам классов.
□ Как один класс может быть дружественным для другого класса.
□ Виртуальные функции и их использование.
□ Чистые виртуальные функции.
□ Абстрактные классы.
□ Виртуальные деструкторы и их использование.
Базовые идеи объектно-ориентированного
программирования
Как вы уже видели, класс — это тип данных, которые вы определяете для удовлет-
ворения требований вашего приложения. Классы в объектно-ориентированном про-
482 Глава 9
граммировании (ООП) также определяют объекты, с которыми имеет дело ваша про-
грамма. Вы программируете решение проблемы в терминах объектов, специфичных
для этой проблемы, используя операции для непосредственной работы с этими объ-
ектами. Вы можете определить класс для представления некоторой абстракции, та-
кой как комплексное число, представляющее собой математическую концепцию, или
грузовик, который, несомненно, является физической сущностью (особенно если вы
едете на одном из них по дороге). Поэтому, будучи типом данных, класс также может
быть определением набора объектов реального мира определенного рода — по край-
ней мере, в той степени, которая необходима для решения данной проблемы.
Вы можете думать о классе как об определении характеристик конкретной груп-
пы вещей, которые специфицированы общим набором параметров и разделяют об-
щий набор операций, выполняемых над ними. Операции, которые вы применяете
к объектам данного класса, определяются его интерфейсом, который соответствует
функциям, находящимся в разделе public определения класса. Класс СВох, который
мы использовали в предыдущей главе — это хороший пример; он определяет ящик в
терминах размеров плюс набор общедоступных функций, которые можно применять
к объектам СВох для решения проблемы.
Конечно, в реальном мире существует великое разнообразие всяческих ящиков:
картонные коробки, упаковки для мониторов, конфетные коробки, коробки для кру-
пы и многие-многие другие, с которыми вам, определенно, приходилось сталкиваться.
Вы можете различать ящики по содержимому, которое может в них храниться, мате-
риалу, из которого они сделаны, и по множеству других параметров. Но даже несмо-
тря на то, что существует много разнообразных видов ящиков, все они разделяют не-
который общий набор характеристик — некую сущность “ящичности ”, если можно так
выразиться. Поэтому вы можете визуализировать все виды ящиков в их отношениях
друг к другу, невзирая на то, что они обладают многими отличительными особенно-
стями. Вы можете определить некоторый конкретный вид ящиков как обладающий
общими характеристиками всех ящиков — возможно, длиной, шириной и высотой.
Затем вы можете добавить некоторые дополнительные характеристики к базовому
типу ящика, чтобы отличать его от остальных. Вы можете также обнаружить, что су-
ществуют некоторые вещи, которые вы можете совершать со своим специфическим
видом ящиков, но которые нельзя сделать ни с какими другими ящиками.
Также возможно, что некоторые объекты могут быть результатом комбинации
определенных видов ящиков с другими типами объектов: например, коробка конфет
или ящик пива. Чтобы описать все это, вы можете определить один вид ящиков как
общий, обладающий базовыми характеристиками, присущими абсолютно всем ящи-
кам, а затем специфицировать другую разновидность ящиков как уточняющую специ-
ализацию первой. На рис. 9.1 показан пример такого рода отношений, которые могут
быть определены между разными видами ящиков.
Ящики становятся все более специализированными по мере движения вниз по ди-
аграмме, а стрелки на ней направлены от данного типа ящика к другому, на котором
он основан. На рис. 9.1 определены три разных типа ящиков, основанных на общем
для всех базовом типе СВох. На нем также определен ящик пива (CBeerCrate) как
специальная разновидность ящиков, предназначенных для хранения бутылок.
Это хороший способ приближения к описанию реального мира с использовани-
ем классов C++, благодаря способности языка определять классы, которые связаны
между собой. Коробка конфет может рассматриваться как ящик, обладающий всеми
характеристиками базового ящика, плюс несколько собственных характеристик, ко-
торые. идентифицируют то, что делает ее специальной. Давайте посмотрим, как все
это работает на практике.
Наследование классов и виртуальные функции 483
Рис. 9.1. Отношение между ящиками
Наследование в классах
Когда вы определяете один класс на базе другого, то такой класс называется про-
изводным. Производный класс автоматически получает данные-члены того класса,
который использован в качестве базового при его определении, и, с некоторыми
ограничениями, также функции-члены этого базового класса. Говорят, что класс на-
следует данные-члены и функции-члены класса, на котором он базируется.
Единственными членами базового класса, которые не наследуются производным
классом, является деструктор, конструкторы и любые функции-члены, перегружающие
операцию присваивания. Все прочие функции-члены, вместе со всеми данными-члена-
ми базового класса, наследуются производным классом. Конечно, причина того, что
некоторые базовые члены не наследуются, состоит в том, что производный класс всег-
да имеет свои собственные конструкторы и деструктор. Если у базового класса есть
операция присваивания, производный класс представляет ее собственную версию.
Когда я говорю, что эти функции не наследуются, то имею в виду, что они не суще-
ствуют как члены объекта производного класса. Однако они по-прежнему существуют
как часть объекта, относящаяся к базовому классу, и вскоре вы в этом убедитесь.
Что такое базовый класс?
Базовый класс — это любой класс, который вы используете в качестве основы для
определения другого класса. Например, если вы определяете класс В непосредствен-
но в терминах класса А, то об А говорят, что это прямой базовый класс класса В.
На рис. 9.1 класс CCrate — прямой базовый класс для CBeerCrate. Когда такой класс,
как CBeerCrate, определяется в терминах другого класса CCrate, то о CBeerCrate
говорят, что он унаследован от CCrate. Поскольку CCrate сам определен в терминах
484 Глава 9
класса СВох, то о СВох можно сказать, что для CBeerCrate он является непрямым ба-
зовым классом. Очень скоро вы увидите, как это выражается в определении класса
в программе. На рис. 9.2 показан способ наследования членов базового класса в про-
изводном классе.
Базовый класс
Наследо-
ванные
члены
Производный класс
данные-члены
Функции-члены
Конструкторы
деструктор-------------
Перегруженная операция =
Другие перегруженные операции
/Нет
/Нет
/Нет
Данные-члены
Функции-члены
Другие перегруженные операции
Собственные данные-члены
Собственные функции-члены
Собственные конструкторы
Собственный деструктор
Рис. 9.2. Наследование членов базового класса в производном классе
Тот факт, что функции-члены унаследованы, еще не означает, что вы не захотите
заменить их в производном классе новыми версиями, и, конечно же, вы можете сде-
лать это при необходимости.
Наследование классов от базового класса
Вернемся к исходному классу СВох с данными-членами public, которые вы видели
// Заголовочный файл Box.h в проекте Ех9__01
#pragma once
class СВох
public:
double m_Length;
double m_Width;
double m_Height;
CBox(double lv = 1.0, double wv = 1.0, double hv = 1.0) :
m_Length(lv), m_Width(wv), m_Height(hv){}
Создайте новое пустое консольное приложение WIN32 по имени Ех9_01 и сохра-
ните этот код в новом заголовочном файле Box.h. Директива #pragma once гаран-
тирует, что определение класса СВох появится в сборке только один раз. В классе
есть конструктор, так что вы можете инициализировать объекты при их объявлении.
Предположим, что вам понадобится другой класс объектов — CCandyBox, который во
всем похож на СВох, но также имеет еще один член данных — указатель на текстовую
строку, идентифицирующую содержимое коробки.
Вы можете определить CCandyBox как производный класс, имеющий СВох в каче-
стве базового, как показано ниже:
Наследование классов и виртуальные функции 485
// Заголовочный файл CandyBox.h в проекте Ех9_01
#pragma once
#include "Box.h"
class CCandyBox: CBox
{
public:
char* m_Contents;
CCandyBox(char* str = "Candy") // Конструктор
{
m_Contents = new chart strlen (str) + 1 ];
strcpy_s(m_Contents, strlen(str) + 1, str);
}
~CCandyBox() // Деструктор
{ delete[] m_Contents; };
};
Добавьте этот заголовочный файл в проект Ех9_01. Вам понадобится директива
#include для заголовочного файла Box.h, поскольку вы ссылаетесь в этом коде на
класс СВох. Если эту директиву опустить, компилятору не будет известен СВох, так
что код компилироваться не будет. Имя базового класса СВох появляется после имени
производного класса CCandyBox и отделяется от него двоеточием. Во всех остальных
отношениях он выглядит как нормальное определение класса. Вы добавили новый
член m_Contents, и, поскольку это указатель на строку, нужно, чтобы конструктор
инициализировал ее, а деструктор освобождал память, занятую этой строкой. Вы так-
же должны предусмотреть в конструкторе значение по умолчанию для строки, описы-
вающей содержимое объекта CCandyBox. Объекты типа класса CCandyBox содержат
все члены базового класса СВох плюс дополнительный член данных m_Contents.
Обратите внимание на использование функции strcpy s (), которую вы впервые
встречали в главе 6. Она принимает три аргумента: место назначения операции копи-
рования, длина принимающего буфера и строка-источник. Если оба массива будут ста-
тическими — то есть, не размещенными в куче — вы можете опустить второй аргумент
и задать только указатели на источник и исходную строку. Это возможно благодаря
тому, что функция strcpy s () также доступна как функция-шаблон, определяющая
длину строки назначения автоматически. Поэтому, работая со статическими строка-
ми, вы можете вызывать эту функцию только с двумя аргументами.
Практическое занятие
Использование производного класса
Теперь посмотрим, как работает производный класс, на соответствующем при-
мере. Добавьте приведенный ниже код к проекту Ех9_01 в виде исходного файла
Ех9_01.срр.
// Ех9_01.срр
// Использование производного класса
#include <iostream> // Для потокового ввода-вывода
#include <cstring> // Для strlen() и strcpyO
#include "CandyBox.h" // Для CBox и CCandyBox
using std::cout;
using std::endl;
int main()
{
CBox myBox(4.0, 3.0, 2.0); // Создать объект CBox
CCandyBox myCandyBox;
CCandyBox myMintBox ("Wafer Thin Mints’’); // Создать объект CCandyBox
486 Глава 9
cout « endl
« "myBox занимает ’’ « sizeof myBox // Показать, сколько памяти
« " байт" « endl // потребовалось объекту
« "myCandyBox занимает ’’ « sizeof myCandyBox
« ’’ байт" « endl
<< "myMintBox занимает " « sizeof myMintBox
<< "байт";
cout « endl
« "длина myBox равна " « myBox.m_Length;
myBox.m_Length = 10.0;
// myCandyBox.m_Length =10.0; // удалите комментарий — получите ошибку
cout « endl;
return 0;
Описание полученных результатов
Здесь вы видите директиву #include для заголовочного файла CandyBox.h, и по-
скольку вы знаете, что она содержит директиву #include для файла Box. h, включать
этот файл повторно не нужно. Вы можете поместить директиву # in elude для файла
Box. h, но при этом директива #pragma once в файле Box. h предотвратит повторное
включение его содержимого. Это важно, потому что каждый класс может быть опре-
делен лишь однажды; два определения класса в коде приводят к ошибке.
После объявления объекта СВох и двух объектов CCandyBox выводится количество
байт памяти, занятых каждым объектом. Посмотрим, что получится на выходе:
myBox занимает 24 байт
myCandyBox занимает 32 байт
myMintBox занимает 32 байт
длина myBox равна 4
Первая строка показывает то, чего можно было ожидать на основании сказанно-
го в предыдущей главе. Объект СВох включает три члена данных типа double, каж-
дый из которых имеет размер 8 байт, что в сумме составляет 24 байта. Оба объекта
CCandyBox имеют одинаковые размеры — 32 байта. Длина строки не влияет на размер
объекта, поскольку память для размещения строки выделяется динамически в свобод-
ном хранилище. 32 байта получаются из 24 байт, которые занимают три члена типа
double, унаследованные от базового класса СВох, плюс 4 байта для члена-указателя
m Contents, что дает в сумме 28 байт. Откуда же берутся еще 4 байта? Это объясня-
ется тем, что компилятор выравнивает члены класса по адресам, кратным четырем
байтам. Вы можете убедиться в этом, добавив к классу CCandyBox дополнительный
член, скажем, типа int. После этого размер объекта класса по-прежнему составит 32
байта.
В этой программе также выводится значение члена m_Length объекта myBox типа
СВох. Даже несмотря на то, что нет никаких проблем с доступом к этому члену в объ-
екте СВох, если вы удалите комментарий со следующего оператора в main ():
// myCandyBox.m_Length =10.0; // удалите комментарий — получите ошибку
то программа перестанет компилироваться. Компилятор выдаст следующее сообще-
ние:
error С2247: ’СВох::m_Length’ not accessible because ’CCandyBox* uses ’private’ to
inherit from ’CBox’
ошибка C2247: ' CBox::m_Length' недоступен, поскольку 'CCandyBox' использует
'private' для наследования от 'CBox'
Наследование классов и виртуальные функции 487
Это ясно указывает на то, что член m_Length из базового класса недоступен, по-
тому что m_Length в производном классе стал private. Это потому, что для базового
класса по умолчанию устанавливается спецификатор доступа private, когда вы опре-
деляете производный класс. Это все равно, как если бы первая строка определения
класса-наследника выглядела бы следующим образом:
class CCandyBox: private СВох
Всегда необходимо указывать спецификатор доступа к базовому классу, который
определяет состояние унаследованных членов в производном классе. Если вы опусти-
те спецификатор доступа к базовому классу, то компилятор подразумевает private.
Если изменить определение класса CCandyBox в CandyBox. h следующим образом:
class CCandyBox: public СВох
public:
char* m_Contents;
CCandyBox(char* str = "Candy") // Конструктор
m_Contents = new chart strlen (str) + 1 ];
strcpy_s(m_Contents, strlen(str) + 1, str);
-CCandyBox() // Деструктор
{ delete[] m Contents; };
то член m_Length будет унаследован в производном классе как public и станет до-
ступным функции main (). Со спецификатором доступа public к базовому классу
все унаследованные члены, изначально специфицированные в базовом классе как
public, будут иметь тот же уровень доступа в классе-наследнике.
Управление доступом при наследовании
Вопрос о доступе к унаследованным членам в производном классе требует более
тщательного изучения. Рассмотрим состояние private-членов базового класса в про-
изводном классе.
Существует веская причина для выбора в предыдущем примере версии класса
СВох с данными-членами public, а не более безопасной поздней версии с данными-
членами private. Причина в том, что хотя private-члены базового класса являются
также членами производного класса, они остаются private по отношению к базово-
му классу, поэтому функции-члены, добавленные к производному классу, не могут по-
лучить к ним доступ. Они могут быть доступны в производном классе через функции-
члены базового класса, которые не находятся в разделе private базового класса. Это
очень легко продемонстрировать, сделав все данные-члены класса СВох приватными
(private) и добавив в производный класс CCandyBox функцию Volume (), так что
определения классов станут такими:
// Версии классов, которые не компилируются
class СВох
public:
СВох (double lv » 1.0, double wv = 1.0, double hv = 1.0) :
m_Length(lv), m^Width(wv), m_Height(hv){}
private:
double m Length;
488 Глава 9
double m_Width;
double m_Height;
};
class CCandyBox: public CBox
{
public:
char* m_Contents;
// Функция вычисления объема объекта CCandyBox
double Volume() const // Ошибка — член не доступен
{ return m_Length*m_Width*m_Height; }
CCandyBox(char* str = "Candy") // Конструктор
{
m_Contents = new chart strlen (str) + 1 ];
strcpy_s(m_Contents, strlen(str) + 1, str);
}
~CCandyBox() // Деструктор
{ deleted m_Contents; }
};
Программа, использующая эти классы, компилироваться не будет. Функция
Volume () в классе CCandyBox пытается обратиться к private-членам базового клас-
са, что недопустимо.
Практическое занятие | ОбраЩвНИв К ПрИВЭТНЫМ ЧЛвНЭМ
базового класса
Однако совершенно законно использовать функцию Volume () в базовом классе,
так что если вы перенесете определение функции Volume () в раздел public базо-
вого класса СВох, то программа не только скомпилируется, но вы также сможете вы-
звать эту функцию для получения объема объекта CCandyBox. Создайте новый проект
WIN32 по имени Ех9_02 со следующим содержимым Box.h:
// Box.h в Ех9_02
#pragma once
class СВох
{
public:
СВох(double lv = 1.0, double wv = 1.0, double hv = 1.0) :
//Функция для вычисления объема объекта СВох
double Volume () const
{ return m_Length*m__Width*m_Height; }
private:
double m_Length;
double m_Width;
double m_Height;
};
Заголовочный файл CandyBox. h в проекте должен содержать приведенный ниже
код.
// Заголовочный файл CandyBox.h в проекте Ех9_02
#pragma once
#include "Box.h"
class CCandyBox: public CBox
{
public:
Наследование классов и виртуальные функции 489
1
char* m_Contents;
CCandyBox(char* str = "Candy") // Конструктор
m_Contents = new char[ strlen (str) + 1 ];
strcpy_s(m_Contents, strlen(str) + 1, str);
~CCandyBox() // Деструктор
{ delete[] m_Contents; };
А вот файл Ex9_02. срр в этом проекте:
// Ех9_02.срр
// Использование функции, унаследованной от базового класса
#include <iostream> // Для потокового ввода-вывода
#include <cstring> // Для strlen() и strcpy()
#include "CandyBox.h" // Для CBox и CCandyBox
using std::cout;
using std::endl;
int main ()
CBox myBox(4.0,3.0,2.0); // Создать объект CBox
CCandyBox myCandyBox;
CCandyBox myMintBox("Wafer Thin Mints"); // Создать объект CCandyBox
cout « endl
« "myBox занимает " « sizeof myBox // Показать объем памяти,
« " байт" « endl // занятой объектами
« "myCandyBox занимает " « sizeof myCandyBox
« " байт" « endl
« "myMintBox занимает " « sizeof myMintBox
« " байт";
cout « endl
« " Объем myMintBox равен " « myMintBox. Volume (); / /Получить объем
//объекта CCandyBox
cout « endl;
return 0;
Этот пример выдает следующий вывод:
myBox занимает 24 байт
myCandyBox занимает 32 байт
myMintBox занимает 32 байт
Объем myMintBox равен 1
Описание полученных результатов
Интересен дополнительный вывод в последней строке. Он показывает значение,
порожденное функцией Volume (), которая теперь находится в разделе public базо-
вого класса. Внутри производного класса она работает с членами производного клас-
са, которые унаследованы от базового. Это полноценный член производного класса,
поэтому она может свободно использоваться с объектами производного класса.
Значение объема объекта производного класса равно 1, потому что при создании
объекта CCandyBox конструктор по умолчанию СВох () был вызван первым, чтобы
создать часть объекта, относящуюся к базовому классу, и он установил значения раз-
меров СВох по умолчанию равными 1.
490 Глава 9
Работа конструктора в производном классе
Хотя я говорил, что конструкторы базового класса не наследуются в производном
классе, все же они существуют в базовом классе и используются для создания той ча-
сти объекта производного класса, которая относится к базовому классу. Это объясня-
ется тем, что создание этой части объекта производного класса — действительно за-
дача конструктора базового класса, а не конструктора производного класса. В конце
концов, вы видели, что приватные члены базового класса недоступны в объекте про-
изводного класса, даже несмотря на то, что они унаследованы, поэтому ответствен-
ность за них лежит на конструкторах базового класса.
В последнем примере конструктор базового класса по умолчанию вызывается ав-
томатически, чтобы создать часть объекта производного класса, которая относиться
к базовому классу. Вы можете вызвать определенный конструктор базового класса из
конструктора производного класса. Это позволит инициализировать данные-члены
базового класса конструктором, отличным от конструктора по умолчанию, или в са-
мом деле выбрать определенный конструктор базового класса, в зависимости от дан-
ных, переданных конструктору производного класса.
Практическое занятие I п,
I—L—_______________I Вызов конструкторов
Вы можете увидеть все это в действии в модифицированной версии предыдущего
примера. Чтобы сделать этот класс удобным в применении, вы должны предоставить
конструктор для производного класса, который позволит специфицировать размеры
объекта. Для этого вы можете добавить дополнительный конструктор в производный
класс и вызывать в нем явно конструктор базового класса, чтобы установить значе-
ния данных-членов, которые унаследованы от базового класса.
Ниже показано содержимое файла Box .h из проекта Ех9 03.
И Box.h в Ех9_03
tfpragma once
#include <iostream>
using std: :cout;
using std: :endl;
class CBox
{
public:
// Конструктор базового класса
CBox (double lv = 1.0, double wv = 1.0, double hv = 1.0) :
m_Length (lv), m__Width (wv), m_Height (hv)
{ cout « endl « "Вызван конструктор CBox"; }
//Функция для вычисления объема объекта СВох
double Volume() const
{ return m_Length*m_Width*m_Height; }
private:
double m_Length;
double m_Width;
double m_Height;
};
А вот содержимое заголовочного файлы CandyBox. h.
// CandyBox.h в Ex9_03
#pragma once
#include <iostreaxn>
#include "Box.h"
Наследование классов и виртуальные функции
491
using std: :cout;
using std: :endl;
class CCandyBox: public CBox
public:
char* m_Contents;
// Конструктор для установки размеров и содержимого
//с явным вызовом конструктора СВох
CCandyBox(double lv, double wv, double hv, char* str = "Candy")
:CBox(lv, wv, hv)
{
cout « endl «"Вызван конструктор2 CCandyBox";
m_Contents = new chart strlen (str) + 1 ] ;
strcpy_s (mjContents, strlen(str) +1, str);
// Конструктор для установки только содержимого,
// автоматически вызывает конструктор СВох по умолчанию
CCandyBox(char* str = "Candy")
cout « endl « "Вызван конструктор 1 CCandyBox";
m_Contents = new chart strlen(str) + 1 ];
strcpy__s (m_Contents, strlen (str) + 1, str);
}
-CCandyBox() / / Деструктор
{ delete[] m_Contents; }
Директива #include для заголовка <iostream> и два объявления using здесь не
являются абсолютно необходимыми, поскольку Box. h содержит тот же код, тем не
менее, не вредно его повторить еще раз. Помещение этих операторов здесь также
означает, что если вы удалите этот код из Box.h, поскольку он больше не нужен, то
CandyBox. h все равно будет компилироваться.
Ниже показано содержимое файла Ех 9_0 3. срр.
// Ех9__03.срр
// Вызов конструктора базового класса из конструктора производного класса
#include <iostream> // Для потокового ввода-вывода
#include <cstring> // Для strlen() и strcpyO
#include "CandyBox.h" // Для CBox и CCandyBox
using std::cout;
using std::endl;
int main()
CBox myBox(4.0, 3.0, 2.0);
CCandyBox myCandyBox;
CCandyBox myMintBox(1.0, 2.0, 3.0,
"Wafer Thin Mints");
cout « endl
« "myBox занимает " « sizeof myBox // Показать, сколько памяти
« " байт" « endl // занимают объекты
« "myCandyBox занимает " « sizeof myCandyBox
« " байт" « endl
« "myMintBox занимает " « sizeof myMintBox
« " байт";
cout « endl
« "Объем myMintBox равен " // Получить объем
« myMintBox.Volume(); // объекта CCandyBox
cout « endl;
return 0;
492 Глава 9
Описание полученных результатов
Вместе с добавлением дополнительного конструктора к производному классу, мы
добавили операторы вывода в каждый конструктор, чтобы наблюдать, когда каждый
из них вызывается. Явный вызов конструктора класса СВох появляется после двоето-
чия в заголовке функции конструктора производного класса. Вы должны заметить,
что нотация та же самая, как и использованная ранее для инициализации членов в
конструкторе:
// Вызов конструктора базового класса
CCandyBox(double lv, double wv, double hv, char* str== "Candy") :
CBox(lv, wv, hv)
Это отлично согласовано с тем, что сделано здесь, потому что вы, по сути, ини-
циализируете подобъект СВох объекта производного класса. В первом случае вы
явно вызывали конструкторы по умолчанию для double-членов m_Length, m_Width и
m_Height в списке инициализации. Во втором случае вызывается конструктор СВох.
Это запускает выбранный вами специфический конструктор СВох перед выполнени-
ем конструктора CCandyBox.
Если вы скомпилируете и запустите этот пример, он выдаст следующий вывод:
Вызван конструктор СВох
Вызван конструктор СВох
Вызван конструктор! CCandyBox
Вызван конструктор СВох
Вызван конструктор2 CCandyBox
myBox занимает 24 байт
myCandyBox занимает 32 байт
myMintBox занимает 32 байт
Объем myMintBox равен 6
Вызовы конструкторов объясняются в табл. 9.1.
Таблица 9.1. Вызовы конструкторов в примере Ех9_03
Экранный вывод Конструируемый объект
Вызван конструктор СВох
МуВох
Вызван конструктор СВох
MyCandyBox
Вызван конструктор 1 CCandyBox
MyCandyBox
Вызван конструктор СВох
MyMintBox
Вызван конструктор2 CCandyBox
MyMintBox
Первая строка вывода сформирована вызовом конструктора СВох, происходящим
в момент объявления объекта ту В ох типа СВох. Вторая строка вывода получается в
результате автоматического вызова конструктора базового класса, происходящего
при объявлении объекта myCandyBox типа CCandyBox.
Обратите внимание, что конструктор базового класса всегда вызывается перед конструкто-
ром производного класса.
Наследование классов и виртуальные функции 493
Следующая строка получается от вызова конструктора производного класса, вы-
званного для объекта MyCandyBox. Этот конструктор вызывается потому, что объект
не инициализирован. Четвертая строка вывода происходит от явной идентификации
конструктора класса СВох, вызываемого из нашего нового конструктора для объектов
CCandyBox. Значения аргументов, специфицирующие размеры объекта CCandyBox,
передаются конструктору базового класса. Далее идет вывод от самого нового кон-
структора производного класса, так что конструкторы опять же вызываются в после-
довательности — сначала для базового класса, затем для производного.
Из сказанного должно быть ясно, что когда вызывается конструктор производно-
го класса, то конструктор базового класса всегда вызывается для построения той ча-
сти объекта производного класса, которая относится к базовому классу. Если вы не
специфицируете используемый конструктор базового класса, то компилятор вызовет
автоматически конструктор базового класса по умолчанию.
Последняя строка табл. 9.1 показывает, что инициализация базовой части объек-
та myMintBox работает так, как должна, и приватные члены инициализируются кон-
структором класса СВох.
Наличие private-членов базового класса, доступных только функциям-членам
базового класса, не всегда удобно. Должно существовать много случаев, когда может
понадобиться иметь private-члены базового класса, которые могут быть доступны в
производном классе. И, как можно было ожидать, C++ предоставляет такую возмож-
ность.
Объявление членов класса как protected
В дополнение к спецификаторам доступа public и private, члены класса мож-
но также объявлять как protected (защищенные). Внутри класса ключевое слово
protected обеспечивает тот же эффект, что и слово private: члены класса с этой
спецификацией могут быть доступны только функциям-членам этого класса и его дру-
жественным функциям (а также функциям-членам класса, объявленным как friend
для данного класса — дружественные классы рассматриваются далее в настоящей гла-
ве). Используя ключевое слово protected, вы можете переопределить класс СВох
следующим образом:
// Box.h в Ех9 04
#pragma once
^include <iostream>
using std::cout;
using std::endl;
class CBox
public:
// Конструктор базового класса
CBox (double lv = 1.0, double wv = 1.0, double hv = 1.0) :
m_Length(lv), m_Width(wv), m_Height(hv)
{ cout « endl « "Вызван конструктор СВох"; }
// Деструктор СВох
СВох()
{ cout « "Вызван деструктор СВох" « endl; }
protected:
double in_Length;
double m_Widtre-
double m Height;
494 Глава 9
Теперь данные-члены по-прежнему являются приватными, и доступ к ним закрыт
для обычных глобальных функций, но при этом они доступны функциям-членам про-
изводных классов.
Практическое занятие ИСПОЛЬЗОВЭНИб ЗЭЩИЩеННЫХ ЧЛвНОВ
Вы можете попробовать поработать с данными-членами protected, применив
показанную ниже версию класса СВох в качестве базовой для новой версии класса
CCandyBox, которая обращается к членам базового класса через его собственную
функцию Volume ().
// CandyBox.h в Ех9_04
#pragma once
#include "Box.h"
tfinclude <iostream>
using std::cout;
using std::endl;
class CCandyBox: public CBox
{
public:
char* m_Contents;
// Функция производного класса, вычисляющая объем
double Volume () const
{ return m_Length*m_Width*m_Height; }
11 Конструктор, устанавливающий размеры и содержимое,
//с явным вызовом конструктора СВох
CCandyBox(double lv, double wv, double hv, char* str = "Candy")
:CBox(lv, wv, hv) // Конструктор
{
cout « endl «"Вызван конструктор2 CCandyBox";
m_Contents = new chart strlen (str) + 1 ];
strcpy_s(m_Contents, strlen(str) + 1, str);
}
// Конструктор, устанавливающий только содержимое,
// автоматически вызывающий конструктор СВох по умолчанию
CCandyBox(char* str = "Candy") // Конструктор
{
cout « endl « "Вызван конструктор! CCandyBox";
m_Contents = new chart strlen (str) + 1 ];
strcpy_s(m_Contents, strlen(str) + 1, str);
}
~CCandyBox() //Деструктор
{
cout « "Вызван деструктор CCandyBox" « endl;
delete[] m_Contents;
}
};
Код main () в Ex9_04 . срр показан ниже.
// Ex9_04.cpp
// Использование спецификатора доступа protected
#include <iostream> // Для потокового ввода-вывода
#include <cstring> // Для strlen() и strcpyO
#include "CandyBox.h” // Для CBox и CCandyBox
using std::cout;
using std::endl;
Наследование классов и виртуальные функции 495
I
{
CCandyBox myCandyBox;
CCandyBox myToffeeBox (2, 3, 4, "Stickjaw Toffee");
cout « endl
« " объем myCandyBox равен " « myCandyBox. Volume ()
« endl
« "объем myToffeeBox равен " « myToffeeBox.Volume ();
t « endl « myToffeeBox.m Length;//уберите комментарии — получите ошибку
cout « endl;
return 0;
Описание полученных результатов
В этом примере вычисляются объемы двух объектов CCandyBox путем вызова
функции Volume () — члена производного класса. Для вычисления результата эта
функция обращается к унаследованным членам m__Length, m_Width и m__Height. В ба-
зовом классе эти члены объявлены как protected и остаются таковыми в произво-
дном классе. Программа генерирует следующий вывод:
Вызван конструктор СВох
Вызван конструктор! CCandyBox
Вызван конструктор СВох
Вызван конструктор2 CCandyBox
Объем myCandyBox равен 1
Объем myToffeeBox равен 24
Вызван деструктор CCandyBox
Вызван деструктор СВох
Вызван деструктор CCandyBox
Вызван деструктор СВох
Вывод показывает, что объем вычисляется правильно для обоих объектов CCandyBox.
Первый объект имеет размеры по умолчанию, полученные вызовом конструктора по
умолчанию СВох, поэтому его объем равен 1, а второй объект получает размеры, ука-
занные при его объявлении.
В выводе также показана последовательность вызовов конструкторов и деструк-
торов, и вы можете видеть, как каждый объект производного класса уничтожается за
два шага.
Деструкторы в объектах производных классов вызываются в порядке, обратном вызовам
конструкторов объекта. Это — общее правило, которое действует всегда. Конструкторы вы-
зываются, начиная с конструктора базового класса, затем — производного, в то время как
деструктор производного класса вызывается первым, когда уничтожается объект, а за ним
идет вызов деструктора базового класса.
Вы можете убедиться, что protected-члены базового класса остаются protected
и в производном классе, убрав комментарий со строки, предшествующей оператору
return в функции main (). Если это сделать, компилятор выдаст следующее сообще-
ние об ошибке:
error С2248: 'm_Length’: cannot access protected member declared in class ’CBox*
ошибка C2248: 'm_Length': невозможен доступ к члену protected, объявленному
в классе 'СВох*
Отсюда ясно, что член m_Length недоступен.
496 Глава 9
Уровень доступа унаследованных членов класса
Вы знаете, что если не указан спецификатор доступа базового класса в определе-
нии производного класса, то по умолчанию принимается private. Эффект от этого
заключается в том, что унаследованные public- и protected-члены базового класса
становятся в производном классе private, и потому не доступны функциям-членам
производного класса. Фактически они остаются private по отношению к базовому
классу, независимо от того, как специфицирован базовый класс в определении про-
изводного класса.
Вы также использовали public в качестве спецификатора для базового класса.
Это оставляет в силе спецификации доступа членов базового класса в производном
классе, так что public-члены остаются public, a protected — остаются protected.
Последняя возможность — объявить базовый класс как protected. Это меняет до-
ступ унаследованных public-членов базового класса на protected в производном
классе. Унаследованные члены с уровнем доступа protected (и private) сохраня-
ют свою спецификацию доступа в производном классе. Все это резюмировано на
рис. 9.3.
class САВох: public СВох
наследуется, как
► public:
наследуется, как
► protected:
class СВох
public:
class СВВох: protected СВох
наследуется, как
protected:
protected:
наследуется, как----► р rotected:
private:
Нет доступа — вообще
class ССВох: private СВох
наследуется, как
► private:
наследуется, как
► private:
Рис. 9.3. Наследование уровней доступа
Наследование классов и виртуальные функции 497
Это может показаться несколько сложным, но вы можете свести все, что касается
унаследованных членов, к следующим трем утверждениям.
□ Члены базового класса, которые объявлены как private, никогда не доступны
в производном классе.
□ Определение базового класса как public не изменяет уровней доступа его чле-
нов в производном классе.
□ Определение базового класса как protected изменяет его public-члены на
protected в производном классе.
Возможность изменять уровень доступа унаследованных членов в производном
классе предлагает вам определенную степень гибкости, но не забывайте, что вы не
можете ослабить уровень, специфицированный в базовом классе; вы можете только
сделать доступ более ограниченным. Это предполагает, что ваш базовый класс должен
объявлять как public те члены, доступ к которым вы хотите варьировать в произво-
дных классах. Это может показаться противоречащим идее инкапсуляции данных в
классе для предотвращения неавторизованного доступа, но как вы вскоре убедитесь,
часто приходится объявлять в подобной манере базовые классы, единственное назна-
чение которых — служить базой для других классов, и которые не предназначены для
того, чтобы создавать объекты их собственного типа.
Конструктор копирования
в производном классе
Вспомните, что конструктор копирования вызывается автоматически, когда вы
объявляете объект, инициализированный объектом того же самого класса. Взгляните
на следующие операторы:
СВох myBox(2.О, 3.0, 4.0); // Вызывает конструктор
СВох соруВох(myBox); // Вызывает конструктор копирования
Первый оператор вызывает конструктор, принимающий три аргумента типа
double, а второй вызывает конструктор копирования. Если вы не предусмотрите
своего собственного конструктора копирования, то компилятор создаст его само-
стоятельно, и этот конструктор будет копировать инициализирующий объект в но-
вый — член за членом. Поэтому, дабы видеть, что происходит во время выполнения,
вы можете добавить свою собственную версию конструктора копирования в класс
СВох. Затем вы можете использовать этот класс в качестве базового для определения
класса CCandyBox.
// Box.h в Ех9_05
#pragma once
#include <iostream>
using std::cout;
using std::endl;
class CBox 11 Определение базового класса
public:
// Конструктор базового класса
СВох (double lv = 1.0, double wv = 1.0, double hv = 1.0) :
m_Length(lv), m_Width(wv), m_Height(hv)
{ cout << endl « "Вызван конструктор CBox"; }
498 Глава 9
// Конструктор копирования
СВох (const СВох& initB)
cout « endl « ’’Вызван конструктор копирования СВох”;
m_Length = ini tB. m_Length;
mJWidth = ini tB. m_Wid th ;
m. Height = initB.m Height;
/ Деструктор СВох — только чтобы отследить вызов
СВох()
{ cout « ’’Вызван деструктор CBox” « endl; }
protected:
double m_Length;
double m_Width;
double m_Height;
Вспомните также, что конструктор копирования должен иметь параметр, спе-
цифицированный как ссылка, дабы избежать бесконечного цикла вызовов, причи-
ной которого является необходимость копирования аргументов при передаче их
по значению. Когда вызывается конструктор копирования из последнего примера,
он выводит сообщение на экран, так что вы можете увидеть, когда это произойдет.
Воспользуемся снова версией класса CCandyBox из Ех9_04. срр, как показано ниже.
// CandyBox.h в Ех9_05
#pragma once
#include "Box.h"
#include <iostream>
using std::cout;
using std::endl;
class CCandyBox: public CBox
public:
char* m_Contents;
// Функция производного класса, вычисляющая объем
double Volume() const
{ return m__Length*m_Width*m_Height; }
// Конструктор, устанавливающий размеры и содержимое,
//с явным вызовом конструктора СВох
CCandyBox(double lv, double wv, double hv, char* str » "Candy")
:CBox(lv, wv, hv) // Конструктор
cout « endl «" Вызван конструктор2 CCandyBox";
m_Contents = new char[ strlen(str) + 1 ];
strcpy_s(m_Contents, strlen(str) + 1, str);
// Конструктор, устанавливающий только содержимое,
// автоматически вызывающий конструктор СВох по умолчанию
CCandyBox(char* str « "Candy") // Конструктор
cout « endl « "Вызван конструктор! CCandyBox";
m_Contents = new chart strlen(str) + 1 ];
strcpy_s (m__Contents, strlen (str) + 1, str);
~CCandyBox() // Деструктор
cout « "Вызван деструктор CCandyBox" « endl;
delete [] m Contents;
Наследование классов и виртуальные функции
499
Сюда пока не добавлен конструктор копирования, поэтому будем полагаться на
версию, сгенерированную компилятором.
Практическое занятие
Конструктор копирования в производных
классах
Вы можете испытать только что определенный конструктор копирования в следу-
ющем примере.
// Ех9__05.срр
// Использование конструктора копирования производного класса
#include <iostream> // Для потокового ввода-вывода
#include <cstring> // Для strlen() и strcpyO
#include "CandyBox.h" // Для CBox и CCandyBox
using std::cout;
using std::endl;
int main()
CCandyBox chocBox(2.0, 3.0, 4.0, ’’Chockies’’);// Объявление и инициализация
CCandyBox chocolateBox(chocBox); // Использование конструктора копирования
cout « endl
« ’’Объем chocBox равен ” « chocBox. Volume ()
« endl
« ’’Объем chocolateBox равен ” « chocolateBox.Volume()
« endl;
return 0;
Описание полученных результатов (или почему это не работает)
Если вы запустите отладочную версию этого примера, то в дополнение к ожидае-
мому выводу увидите диалоговое окно, показанное на рис. 9.4.
Щелкните на кнопке Abort (Прервать) для того, чтобы закрыть диалоговое окно
и увидеть окно консоли вывода. Вывод покажет, что генерированный компилятором
конструктор копирования для производного класса автоматически вызывает кон-
структор копирования базового класса.
Microsoft Visual C++
Debug Library
Debug Assertion Failed!
Program: ...visual c++.net\codeViapter 09\ex9j05\ex9_0 5\debugyEx9_05.exe
File: dbgdel.cpp
Line: 52
Expression: _BLOCK_Tf PE_IS_VALID(pHead->nBlodcUse)
For information on how your program can cause an assertion
failure, see the Visual C++documentation on asserts.
[Press Retry to debug the application)
.........I |
Abort J Retry Ignore
...........'1 I
Puc. 9.4. Диалоговое окно утверждения отладки
500 Глава 9
Однако, как вы, возможно, заметили, все не так, как должно быть. В этом конкрет-
ном случае сгенерированный компилятором конструктор копирования вызывает про-
блемы, потому что область памяти, на которую указывает член m Content произво-
дного класса второго объекта — та же самая, что и у первого объекта. Когда один из
объектов уничтожается (при выходе из области видимости в конце функции main ()),
он освобождает память, занятую текстом. Потом, когда уничтожается второй объект,
деструктор пытается освободить память, которая уже была освобождена вызовом де-
структора предыдущего объекта — и это является причиной сообщения об ошибке в
диалоговом окне.
Чтобы исправить это, необходимо предусмотреть конструктор копирования для
производного класса, который будет распределять дополнительную память для ново-
го объекта.
практическое занятие Решение проблемы конструктора
копирования
Вы можете сделать это, добавив в Ех9_05 следующий код конструктора копирова-
ния в раздел public производного класса CCandyBox:
// Конструктор копирования производного класса
CCandyBox(const CCandyBox& initCB)
{
cout « endl « "Вызван конструктор копирования CCandyBox";
// Получить новую память
m_Contents = new chart strlen(initCB.m_Contents) + 1 ];
// Скопировать строку
strcpy_s(m_Contents, strlen(initCB.m_Contents) + 1, initCB.m_Contents);
}
Теперь можете запустить эту новую версию последнего примера с той же функци-
ей main () и убедиться, что новый конструктор копирования работает правильно.
Описание полученных результатов
Теперь после запуска пример ведет себя лучше и выдает следующий вывод:
Вызван конструктор СВох
Вызван конструктор2 CCandyBox
Вызван конструктор СВох
Вызван конструктор копирования CCandyBox
Объем chocBox равен 24
Объем chocolateBox равен 1
Вызван деструктор CCandyBox
Вызван деструктор СВох
Вызван деструктор CCandyBox
Вызван деструктор СВох
Однако все еще есть нечто такое, что идет не так. Третья строка вывода показы-
вает, что для части СВох объекта chocolateBox вызывается конструктор по умолча-
нию вместо конструктора копирования. В результате этого объект получает размер
по умолчанию вместо размера инициализирующего объекта, поэтому объем его выда-
ется неправильно. Причина в том, что когда вы пишете конструктор для объекта про-
изводного класса, то отвечаете за правильную инициализацию членов этого объекта.
Это касается и унаследованных членов.
Наследование классов и виртуальные функции
501
Чтобы исправить это, потребуется вызвать конструктор копирования для базовой
части класса в списке инициализации конструктора копирования класса CCandyBox.
Тогда конструктор копирования будет таким:
// Конструктор копирования производного класса
CCandyBox (const CCandyBox& initCB) : CBox (initCB)
cout « endl « ’’Вызван конструктор копирования CCandyBox’’;
// Получить новую память
m_Contents = new char[ strlen(initCB.m_Contents) + 1 ];
// Скопировать строку
strcpy_s(m_Contents, strlen(initCB.m_Contents) + 1, initCB.m_Contents);
Теперь конструктор копирования класса СВох вызывается с объектом initCB. При
этом ему передается только та часть объекта, которая относится к базовому классу,
так что все работает правильно. Если вы модифицируете последний пример, добавив
вызов базового конструктора копирования, то вывод будет выглядеть следующим об-
разом:
Вызван конструктор СВох
Вызван конструктор2 CCandyBox
Вызван конструктор копирования СВох
Вызван конструктор копирования CCandyBox
Объем chocBox равен 24
Объем chocolateBox равен 24
Вызван деструктор CCandyBox
Вызван деструктор СВох
Вызван деструктор CCandyBox
Вызван деструктор СВох
Этот вывод показывает, что конструкторы и деструкторы вызываются в правиль-
ной последовательности, и конструктор копирования части СВох объекта chocolate-
Box вызывается перед вызовом конструктора копирования CCandyBox. Объем объ-
екта chocolateBox производного класса теперь равен объему инициализирующего
объекта, как и должно быть.
Итак, вы должны запомнить еще одно золотое правило.
При написании любых конструкторов производного класса вы отвечаете за инициализацию
всех членов объекта производного класса, включая все унаследованные члены.
Члены класса как друзья
В главе 7 вы видели, как функция может быть объявлена другом класса. Это дает та-
кой функции привилегии свободного доступа к любому члену класса. Конечно, нет при-
чин, которые бы не позволили дружественным функциям быть членами другого класса.
Предположим, что вы определили класс CBottle, представляющий бутылку:
class CBottle
public:
CBottle(double height, double diameter)
m_Height « height;
m_Diameter = diameter;
502 Глава 9
private:
double m_Height;
double m_Diameter;
// Высота бутылки
// Диаметр бутылки
Теперь вам нужен класс, представляющий упаковку для дюжины бутылок, который
автоматически принимает требуемые размеры, чтобы принять бутылки определенно-
го типа. Это можно определить следующим образом:
class CCarton
public:
CCarton(const CBottle& aBottle)
m_Height = aBottle.m_Height;
m_Length = 4.0*aBottle.m_Diameter;
m_Width = 3.0*aBottle.m_Diameter;
II Высота бутылки
/ / Четыре ряда . ..
// . . .по три бутылки
private:
double
double
double
m_Length;
m_Width;
m__H eight;
// Длина коробки
// Ширина коробки
// Высота коробки
Здесь конструктор устанавливает высоту коробки по высоте бутылки, а длину и
ширину — на основе диаметра бутылок, чтобы в коробку поместилось 12 штук.
Как вы уже знаете, подобное работать не будет. Данные-члены класса CBottle
имеет спецификатор доступа private, поэтому конструктор CCarton не может по-
лучить доступ к ним. Но вы знаете также, что объявление friend в классе CBottle
исправит это:
class CBottle
public:
CBottle(double height, double diameter)
m_Height = height;
m_Diameter = diameter;
private:
double m_Height; // Высота бутылки
double m_Diameter; // Диаметр бутылки
// Разрешить доступ конструктору коробки
friend CCarton: :CCarton(const CBottlefi aBottle) ;
Единственное отличие этого объявления friend от того, что было показано в гла-
ве 7, заключается в том, что вы должны поместить имя класса и операцию разрешения
контекста рядом с именем дружественной функции, чтобы идентифицировать ее.
Чтобы это правильно компилировалось, компилятор должен иметь информацию
о конструкторе класса CCarton, поэтому вы должны поместить оператор #include с
заголовочным файлом, содержащим объявление класса CCarton перед определением
класса CBottle.
Наследование классов и виртуальные функции 503
Дружественные классы
Вы также можете разрешить всем функциям-членам одного класса иметь доступ ко
всем членам другого класса, объявив его дружественным классом. Вы можете объ-
явить класс CCarton другом класса CBottle, добавив объявление friend в определе-
ние класса CBottle:
friend CCarton;
При наличии такого объявления в классе CBottle все функции-члены класса
CCarton теперь имеют свободный доступ ко всем членам класса CBottle.
Ограничения отношения дружественности классов
Отношение дружественности классов не является двухсторонним. Объявление
класса CCarton другом класса CBottle не означает, что CBottle также становится
другом CCarton. Если вы хотите, чтобы это было так, то должны добавить объявле-
ние friend для класса CBottle в класс CCarton.
Отношение дружественности классов не наследуется. Если вы определите другой
класс как производный от CBottle, то члены класса CCarton не будут иметь доступа к
его данным-членам, даже к тем, что унаследованы от CBottle.
Виртуальные функции
Давайте присмотримся к поведению унаследованных функций-членов и их отно-
шениям с функциями-членами производного класса. Вы можете добавить функцию к
классу СВох, чтобы вывести объем объекта СВох. Упрощенная версия класса станет
такой:
// Box.h в Ех9_06
tfpragma once
tfinclude <iostream>
using std::cout;
using std::endl;
class CBox 11 Базовый класс
public:
11 Функция для отображения объема объекта
void ShowVolume () const
cout << endl
« ’’Полезный объем CBox равен ” « Volume () ;
11 Функция вычисления объема объекта СВох
double Volume () const
{ return m_Length*m_Width*m_Height; }
// Конструктор
CBox (double lv = 1.0, double wv = 1.0, double hv = 1.0)
:m_Length (lv) , m_Width(wv), m__Height (hv) {}
protected:
double m_Length;
double m_Width;
double m_Height;
504 Глава 9
Теперь вы можете вывести полезный объем объекта СВох, просто вызвав функ-
цию ShowVolume () с любым объектом, который вам нужен. Конструктор устанавли-
вает значения данных-членов в списке инициализации, так что никакие операторы в
теле функции не нужны. Данные-члены остаются прежними и специфицируются как
protected, поэтому они доступны функциям-членам любого производного класса.
Предположим, что вы хотите создать производный класс для моделирования
ящиков другого вида под названием CGlassBox, чтобы хранить стеклянную посуду.
Содержимое хрупко, и поскольку для его предохранения добавляется упаковочный
материал, емкость такого ящика будет меньше, чем базового объекта СВох. Поэтому
вам понадобится другая функция Volume (), чтобы учесть это обстоятельство, и вы
добавляете ее к производному классу:
// GlassBox.h in Ех9_06
#pragma once
#include "Box.h"
class CGlassBox: public CBox // Производный класс
{
public:
// Функция для вычисления объема CGlassBox,
// резервирующая 15% на упаковку
double Volume() const
{ return 0.85*m_Length*m_Width*m_Height; }
// Конструктор
CGlassBox(double lv, double wv, double hv) : CBox(lv, wv, hv) {}
};
Вероятно, в производном классе будут и другие дополнительные члены, но для
простоты мы не будем их добавлять, а сосредоточимся на том, как работают унасле-
дованные функции. Конструктор для объектов производного класса просто вызывает
конструктор базового класса в своем списке инициализации, чтобы установить значе-
ния данных-членов. Никаких дополнительных операторов в его теле не требуется. Вы
включили новую версию функции Volume () взамен версии из базового класса. Идея
состоит в том, чтобы заставить унаследованную функцию ShowVolume () обращаться
к версии функции вычисления объема Volume () производного класса, когда вы запу-
скаете ее с объектом производного класса CGlassBox.
Практическое занятие
Использование унаследованной функции
Теперь посмотрим, как производный класс работает на практике. Вы можете про-
верить это очень просто — создав объект базового класса и объект производного
класса одного и того же размера и затем сравнив правильность вычисления объемов
в первом и втором. Функция main (), в которой это делается, будет выглядеть так:
// Ех9_06.срр
// Поведение унаследованных функций в производном классе
#include <iostream>
#include "GlassBox.h" // для CBox и CGlassBox
using std::cout;
using std::endl;
int main ()
{
CBox myBox (2.0, 3.0, 4.0); // Объявление базового ящика
CGlassBox myGlassBox(2.0, 3.0, 4.0); // Объявление производного ящика
// того же размера
Наследование классов и виртуальные функции 505
myBox. ShowVolume () ; // Отобразить объем базового ящика
myGlassBox.ShowVolume(); // Отобразить объем производного ящика
cout « endl;
return 0;
}
Описание полученных результатов
Если запустить этот пример, он выдаст следующее:
Полезный объем СВох равен 24
Полезный объем СВох равен 24
Это не только глупо, но и губительно. Программа вообще не работает так, как
вы хотите, и единственное, что интересует — почему? Очевидно, что второй вы-
зов не принимает во внимание, что он выполнен для объекта производного класса
CGlassBox. Это видно из неправильного вывода вычисленного объема. Объем объек-
та CGlassBox по определению должен быть меньше, чем объем базового класса СВох
с теми же внешними размерами.
Причина некорректного вывода в том, что вызов Volume ( ) в функции
ShowVolume () установлен компилятором раз и навсегда по версии, определенной
в базовом классе. ShowVolume () — функция базового класса, и когда компилируется
класс СВох, то вызов Volume () в нем разрешается в момент компиляции как вызов
функции Volume () базового класса; компилятор не имеет понятия ни о какой другой
функции Volume (). Это называется статическим разрешением вызова функции, по-
скольку вызов фиксирован до выполнения программы. Иногда это также называют
ранним связыванием, поскольку определенная выбранная функция привязывается к
вызову из функции ShowVolume () во время компиляции программы.
В этом примере мы с вами надеялись, что решение о том, какая функция Volume ()
будет вызвана для каждого конкретного экземпляра объекта, будет принято во время
выполнения программы. Операции подобного рода называются динамическим или
поздним связыванием. Нам хотелось, чтобы конкретная версия функции Volume (),
вызываемая из ShowVolume О , определялась типом обрабатываемого объекта, а не
была произвольно фиксированной компилятором перед выполнением программы.
Несомненно, вы не удивитесь, узнав, что C++ на самом деле позволяет вам добить-
ся этого, поскольку в противном случае вся эта дискуссия была бы ни к чему! Итак,
вы должны применить нечто, называемое виртуальной функцией.
Что такое виртуальная функция?
Виртуальная функция — это функция в базовом классе, которая объявлена с исполь-
зованием ключевого слова virtual. Если вы специфицируете функцию базового класса
как virtual, и в производном классе имеется другое определение этой функции, это
сообщит компилятору, что вам не нужна статическая компоновка этой функции. Что
вам нужно — так это чтобы выбор функции, которая должна быть вызвана в любой
заданной точке программы, был основан на типе объекта, для которого она вызыва-
ется.
[Практическое занятие) ИСПрЭВЛеНИв CGlaSSBOX
Чтобы заставить этот пример оправдать надежды, вы должны просто добавить
ключевое слово virtual к определению функции Volume () в этих двух классах.
506 Глава 9
Можете попробовать это в новом проекте Ех9_07. Вот как должно выглядеть опреде-
ление СВох:
// Box.h в Ех9__07
#pragma once
#include <iostream>
using std::cout;
using std::endl;
class CBox // Базовый класс
public:
// Функция для отображения объема объекта
void ShowVolume() const
cout « endl
« ’’Полезный объем CBox равен ’’ « Volume ();
// Функция вычисления объема объекта СВох
virtual double Volume () const
{ return m_Length*m_Width*m_Height; }
// Конструктор
CBox (double lv = 1.0, double wv = 1.0, double hv = 1.0)
:m_Length(lv), m_Width(wv), m_Height(hv) {}
protected:
double m_Length;
double m_Width;
double m_Height;
Ниже показано содержимое заголовочного файла GlassBox.h.
// GlassBox.h in Ex9_07
#pragma once
#include ’’Box.h”
class CGlassBox: public CBox // Производный класс
public:
// Функция для вычисления объема CGlassBox
//с учетом 15% на упаковку
virtual double Volume () const
{ return 0.85*m_Length*m_Width*m_Height; }
// Конструктор
CGlassBox(double lv, double wv, double hv) : CBox(lv, wv, hv) {}
Версия функции main () в файле Ex9_07. срр будет точно такой же, как и в преды-
дущем примере:
// Ех9_06.срр (то же самое, что и в Ех9_06.срр)
// Использование виртуальной функции
ftinclude <iostream>
#include ’’GlassBox.h" // для CBox и CGlassBox
using std::cout;
using std::endl;
int main()
CBox myBox(2.0, 3.0, 4.0); // Объявление базового ящика
CGlassBox myGlassBox(2.0, 3.0, 4.0); // Объявление производного ящика
// того же размера
Наследование классов и виртуальные функции
507
myBox.ShowVolume(); // Отобразить объем базового ящика
myGlassBox.ShowVolume(); // Отобразить объем производного ящика
cout « endl;
return 0;
Описание полученных результатов
Если вы запустите эту версию программы с добавлением короткого слова virtual
к определениям Volume (), она выдаст следующий вывод:
Полезный объем СВох равен 24
Полезный объем СВох равен 20.4
Вот теперь программа делает то, что вы хотели. Первый вызов функции
ShowVolume () с объектом myBox типа СВох обращается к версии Volume () из класса
СВох. Второй вызов с объектом myGlassBox типа CGlassBox активизирует версию,
определенную в производном классе.
Обратите внимание, что, несмотря на то, что вы поместили ключевое слово
virtual в определение производного класса функции Volume (), делать это не обя-
зательно. Определения базовой версии функции как virtual вполне достаточно.
Однако я все же рекомендую, чтобы вы указывали это ключевое слово для виртуаль-
ных функций в производных классах, поскольку это сделает ясным любому, кто будет
читать определение производного класса, что речь идет о виртуальных функциях, ко-
торые при выполнении выбираются динамически.
Чтобы функция вела себя как виртуальная, она должна иметь то же имя, список
параметров и тип возврата во всех производных классах, как в базовом классе, и если
в базовом классе функция объявлена как const, функция производного класса также
должна быть const. Если вы попытаетесь использовать другие типы параметров или
возврата или же объявить функцию в одном месте const, а в другом — нет, то в этом
случае механизм виртуальных функций работать не будет. Функция будет компонова-
на статически и фиксирована во время компиляции.
Работа виртуальных функций — исключительно мощный механизм. Вы, должно
быть, слышали термин полиморфизм в связи с объектно-ориентированным програм-
мированием; этот термин имеет отношение к возможностям виртуальных функций.
Иногда полиморфизм может проявляться в разных формах, таких как оборотень, или
доктор Джекилл, или в поведении политиков перед выборами и после них. Вызов
виртуальных функций приводит к разному эффекту в зависимости от вида объекта,
для которого она вызвана.
Следует отметить, что функция Vol ите () в производном классе CGlassBox на самом деле
скрывает версию этой функции из базового класса от функций производного класса. Если
вы хотите вызвать версию Volume () базового класса из функции производного класса, то
должны будете использовать операцию разрешения контекста для обращения к функции,
например, СВох:: Vol ите ().
Использование указателей на объекты классов
Применение указателей с объектами базового класса или производного класса —
важная техника. Указателю на объект базового класса можно присвоить адрес объ-
екта производного класса, равно как и объекта базового класса. Поэтому вы можете
использовать указатель типа “указатель на объект базового класса”, чтобы получить
разное поведение с виртуальными функциями, в зависимости от того, на объект ка-
508 Глава 9
кого типа установлен указатель. Чтобы лучше понять, как это работает, рассмотрим
пример.
Практическое занятие
Указатели на базовый и производный классы
Мы применим те же классы, что и в предыдущем примере, но внесем небольшое
изменение в функцию main (), так что она будет использовать указатель на объект базо-
вого класса. Создайте проект Ех9_08 с заголовочными файлами Box. h и GlassBox. h —
такими же, как в предыдущем примере. Вы можете скопировать их из проекта Ех9_07
в папку нового проекта. Добавить существующий файл к проекту достаточно легко; вы
щелкаете правой кнопкой мыши на Ех9_08 во вкладке Solution Explorer (Проводник
решений), выбираете Add^New Item (Добавить1^ Новый элемент) из всплывающего
меню, после чего выбираете заголовочный файл, который нужно добавить в проект.
Когда заголовки будут добавлены, модифицируйте Ех9_08. срр следующим образом:
// Ех9_08.срр
// Использование указателя базового класса для вызова виртуальной функции
#include <iostream>
#include "GlassBox.h" // Для CBox и CGlassBox
using std::cout;
using std::endl;
int main()
{
CBox myBox(2.0, 3.0, 4.0); // Объявление базового ящика
CGlassBox myGlassBox(2.0, 3.0, 4.0); // Объявление производного ящика
// того же размера
СВох* рВох = 0; // Объявление указателя на объекты базового класса
рВох = &шуВох; // Установить указатель на адрес базового объекта
pBox->ShowVolume() ; // Отобразить объем базового ящика
рВох = fomyGlassBox; / / Установить указатель на адрес объекта
// производного класса
pBox->ShowVolume (); // Отобразить объем производного ящика
cout « endl;
return 0;
}
Описание полученных результатов
Здесь используются те же самые классы, что и в примере Ех9_07. срр, но функция
main () изменена, чтобы использовать указатель для вызова ShowVolume (). Поскольку
применяется указатель, с ним нужно использовать операцию непрямого выбора чле-
на, ->, для вызова функции. Функция ShowVolume () вызывается дважды, и оба вы-
зова используют один и тот же указатель на объект базового класса — рВох. В первом
случае указатель содержит адрес объекта базового класса myBox, а при втором — адрес
объекта производного класса, то есть myGlassBox.
Вывод этой программы будет таким:
Полезный объем СВох равен 24
Полезный объем СВох равен 20.4
Это в точности то же самое, что выдавал и предыдущий пример, где использова-
лись явные обращения к объектам для вызова функции.
Наследование классов и виртуальные функции 509
На основании этого примера вы можете сделать вывод, что механизм виртуаль-
ных функций работает одинаково хорошо и через указатель на базовый класс; при
этом выбирается специфическая функция на основе действительного типа объекта,
на который он указывает. Это проиллюстрировано на рис. 9.5.
Это значит, что даже когда вы точно не знаете тип объекта, на который установ-
лен указатель базового класса в программе (например, когда указатель передается в
функцию в качестве аргумента), механизм виртуальных функций гарантирует вызов
правильной функции. Это чрезвычайно мощное средство, поэтому убедитесь, что вы
хорошо понимаете его. Полиморфизм — фундаментальный механизм в C++, который
вы будете использовать снова и снова.
pBox->ShowVolume()
Указатель this
установлен на рВох
void ShowVolume() const
{
cout« endl
classCBox
«"Полезный объем СВох равен" рВох, указывающий
на объект СВох
« VolumeO;
рВох, указывающий
на объект CGlassBox
virtual double Volume () const
Рис. 9.5. Механизм виртуальных функций
Использование ссылок с виртуальными функциями
Если вы определяете виртуальную функцию со ссылкой на базовый класс в каче-
стве параметра, то можете передать в нее объект производного класса в виде аргу-
мента. Когда ваша функция выполняется, соответствующая виртуальная функция для
переданного объекта выбирается автоматически. Мы можем увидеть, что происходит,
модифицировав main () из последнего примера, чтобы вызывалась функция, прини-
мающая ссылку как параметр.
Практическое занятие | ИСПОЛЬЗОВЭНИв ССЫЛОК С ВИрТуЭЛЬНЫМИ
функциями
Давайте перенесем вызов ShowVolume () в отдельную функцию и вызовем ее из
main ():
510 Глава 9
// Ех9_09.срр
// Использование ссылки для вызова виртуальной функции
#include <iostream>
#include "GlassBox.h" // Для CBox и CGlassBox
using std::cout;
using std::endl;
void Output(const CBox& aBox); // Прототип функции
int main ()
CBox myBox(2.0, 3.0, 4.0); // Объявление базового ящика
CGlassBox myGlassBox (2.0, 3.0, 4.0); // Объявление производного ящика
// того же размера
Output(myBox); // Вывод объема объекта базового класса
Output(myGlassBox); // Вывод объема объекта производного класса
cout « endl;
return 0;
void Output(const CBox& aBox)
aBox.ShowVolume();
Содержимое файлов Box.h и GlassBox.h для этого примера совпадает с тем, что
в предыдущем примере.
Описание полученных результатов
Функция main () теперь в основном состоит из двух вызовов функции Output (),
первого с аргументом — объектом базового класса, и второго с аргументом — объек-
том производного класса. Поскольку параметр представляет собой ссылку на базовый
класс, Output () может принимать объекты обоих классов в качестве аргумента, и со-
ответствующая версия виртуальной функции Volume () вызывается в зависимости от
объекта, инициализировавшего ссылку.
Программа генерирует точно тот же вывод, что и предыдущий пример, демон-
стрируя, что механизм виртуальных функций действительно работает и со ссылочны-
ми параметрами.
Неполные определения классов
В начале предыдущего примера находится объявление прототипа функции
Output (). Чтобы обработать это объявление, компилятору необходим доступ к опре-
делению класса СВох, поскольку параметр имеет тип СВох&. В данном случае опреде-
ление класса СВох доступно, потому что есть директива #include для включения фай-
ла GlassBox. h, который в свою очередь, содержит директиву #include для Box. h.
Однако, бывают ситуации, когда у вас есть такое объявление, но определение клас-
са не может быть включено обычным образом, и тогда требуется какой-то другой спо-
соб хотя бы идентифицировать, что имя СВох ссылается на тип класса. В таких слу-
чаях вы можете предоставить неполное определение класса СВох, предшествующее
прототипу функции вывода. Оператор, который обеспечивает неполное определение
класса СВох, очень прост:
class СВох;
Этот оператор просто указывает, что имя СВох ссылается на класс, который еще
не определен в этой точке, но этого достаточно, чтобы компилятор знал, что СВох —
Наследование классов и виртуальные функции 511
имя класса, и это позволяет ему обработать прототип функции Output (). Если не бу-
дет никакого указания на то, что СВох является классом, прототип приведет к генера-
ции сообщения об ошибке.
Чистые виртуальные функции
Иногда вам может понадобиться включить в базовый класс виртуальную функ-
цию — так, чтобы она была переопределена в производном классе в соответствии с
назначением этого класса, но в базовом классе для нее пока нет осмысленного опре-
деления.
Например, вы предположительно можете иметь класс CContainer, который мо-
жет служить базовым для определения класса СВох, или класса CBottle, или даже
класса CTeapot. Класс CContainer не будет иметь данных-членов, но вы можете
решить все-таки предоставить виртуальную функцию-член Volume () для любого
производного класса. Поскольку класс CContainer не имеет данных-членов, а, сле-
довательно, не имеет размеров, то и не может быть осмысленного определения функ-
ции Volume (). Однако вы можете определить класс, включающий функцию-член
Volume (), следующим образом:
// Container.h для Ех9_10
♦pragma once
♦include <iostream>
using std::cout;
using std::endl;
class CContainer // Общий базовый класс для специфических контейнеров
public:
// Функция для вычисления объема — без содержимого.
// Это определение чистой (’pure’) виртуальной функции, на что указывает ’= О'
virtual double Volume О const = 0;
// Функция для отображения объема
virtual void ShowVolume() const
cout « endl
« "Объем равен ’’ « Volume ();
Оператор для виртуальной функции Volume () определяет ее, как не имеющую
содержимого, путем размещения знака равно и нуля в заголовке. Это называется
чистой (pure) виртуальной функцией. Любой класс, унаследованный от данного,
должен либо определять функции Volume (), либо переопределять ее повторно как
чистую виртуальную. Поскольку вы объявили Volume () как const, ее реализация в
любом производном классе также должна быть const.
Вспомните, что вариации const и не-const функции с одинаковыми именами и
списками параметров являются разными функциями. Другими словами, функцию
можно перегрузить, добавив const.
Класс также включает функцию ShowVolume (), которая отображает объем объек-
тов производных классов. Поскольку она объявлена как virtual, то может быть заме-
нена в производном классе, но если нет, будет вызвана ее версия из базового класса,
которую вы видите здесь.
512 Глава 9
Абстрактные классы
Класс, содержащий чистую виртуальную функцию, называется абстрактным клас-
сом. Он называется абстрактным, потому что определить объекты класса, включаю-
щего чистую виртуальную функцию, невозможно. Такой класс существует только для
того, чтобы можно было определять классы, производные от него. Если класс, произ-
водный от абстрактного класса, также определяет чистую виртуальную функцию, он
также является абстрактным.
Из предыдущего примера класса CContainer вы не должны заключить, что аб-
страктный класс не может иметь данных-членов. Наличие чистой виртуальной
функции — это единственное условие, которое делает класс абстрактным. Иногда аб-
страктный класс может иметь более одной чистой виртуальной функции. Тогда про-
изводный от него класс должен реализовать каждую из этих функций, иначе он тоже
будет абстрактным. Если вы забудете объявить версию функции Volume () в произ-
водном классе как const, то производный класс останется абстрактным, потому что
он будет включать чистую виртуальную функцию-член Volume (), которая является
const, наряду с не-cons t-функцией Volume (), которую вы определите.
Практическое занятие) АбСТрЭКТНЫЙ КЛЭСС
Вы можете реализовать класс ССап, представляющий банки пива или колы, вместе
с оригинальным классом СВох, как наследников класса CContainer, определенного в
предыдущем разделе. Определение класса СВох как подкласса CContainer будет таким:
// Box.h для Ех9_10
#pragma once
#indude "Container.h" // Для определения CContainer
#include <iostream>
using std::cout;
using std::endl;
class CBox: public CContainer // Производный класс
{
public:
// Функция для отображения объема объекта
virtual void ShowVolume () const
{
cout << endl
« "Полезный объем CBox равен " « Volume();
}
11 Функция для вычисления объема объекта СВох
virtual double Volume() const
{ return m_Length*m_Width*m_Height; }
// Конструктор
CBox (double lv = 1.0, double wv = 1.0, double hv = 1.0)
:m_Length(lv), m_Width(wv), m_Height(hv){}
protected:
double m_Length;
double m_Width;
double m_Height;
};
Наследование классов и виртуальные функции
513
Не выделенные полужирным строки — такие же, как и в предыдущей версии клас-
са СВох. Класс СВох, по сути, тот же, что и в предыдущем примере, за исключени-
ем того, что теперь он объявлен как производный от класса CContainer. Функция
Volume () полностью определена внутри класса (как и должно быть, если этот класс
будет служить для создания объектов). Единственный альтернативный вариант заклю-
чается в спецификации ее как чистой виртуальной функции, поскольку она является
таковой в базовом классе, но тогда мы не смогли бы создавать объекты СВох.
Класс ССап вы можете определить в заголовочном файле Can. h следующим обра-
зом:
// Can.h для Ех9_10
#pragma once
#include "Container.h" // Для определения CContainer
extern const double PI; // PI определено где-то
class ССап: public CContainer
public:
// Функция вычисления объема банки
virtual double Volume() const
{ return 0.25*PI*m_Diameter*m_Diameter*m_Height; }
// Конструктор
ССап(double hv = 4.0, double dv = 2.0): m_Height(hv), m_Diameter(dv){}
protected:
double m_Height;
double m_Diameter;
В классе ССап также определена функция Volume () на основании формулы Лтс/2,
где h — высота банки, аг— радиус ее поперечного сечения. Объем вычисляется как вы-
сота, умноженная на площадь сечения. Выражение в определении функции предпола-
гает, что определена глобальная константа PI, поэтому мы имеем внешний оператор,
указывающий, что PI — глобальная переменная типа const double, которая определе-
на где-то в другом месте — в данной программе она определена в файле Ех9_10. срр.
Также обратите внимание, что мы переопределили функцию ShowVolume () в классе
СВох, но не в классе ССап. Вы увидите эффект от этого, когда мы доберемся до выво-
да программы.
Испытать эти классы можно в следующей программе, включающей функцию
main():
// Ех9_10.срр
// Использование абстрактного класса
#include "Box.h" // Для СВох и CContainer
#include "Can.h" // Для ССап (и CContainer)
#include <iostream> // Для потокового ввода-вывода
using std::cout;
using std::endl;
const double PI= 3.14159265; // Глобальное определение PI
int main(void)
// Указатель на абстрактный базовый класс,
// инициализированный адресом объекта СВох
CContainer* рС1 = new СВох (2.0, 3.0, 4.0);
// Указатель на абстрактный базовый класс,
// инициализированный адресом объекта ССап
CContainer* рС2 = new ССап (6.5, 3.0);
514 Глава 9
pCl->ShowVolume();
pC2->ShowVolume();
cout « endl;
delete pCl;
delete pC2;
return 0;
}
// Вывод объемов двух объектов
// через указатели
// Освободить память в свободном хранилище
И ....
Описание полученных результатов
В этой программе объявлены два указателя на базовый класс CContainer. Хотя
вы не можете объявлять объекты CContainer (поскольку CContainer — абстрактный
класс), все же вы можете определить указатель на CContainer и затем использовать
его для хранения адреса объекта производного класса; фактически он может служить
для хранения адреса любого объекта, тип которого является прямым или непрямым
наследником CContainer. Указателю рС1 присвоен адрес объекта СВох, созданного
в свободном хранилище с помощью операции new. Второму указателю подобным же
образом присвоен адрес объекта ССап.
Конечно, поскольку объекты производных классов созданы динамически, вы должны выпол-
нить операцию delete, чтобы очистить место в свободном хранилище, когда необходи-
мость в этих объектах отпадет.
Ниже показан вывод, генерируемый этим примером.
Полезный объем СВох равен 24
Объем равен 45.9458
Поскольку функция ShowVolume () определена в классе СВох, именно версия этой
функции из производного класса вызывается для объекта СВох. Эта функция не опре-
делена в классе ССап, поэтому для объекта ССап вызывается версия этой функции из
базового класса, унаследованная ССап. Поскольку Volume () — виртуальная функция,
реализованная в обоих производных классах (что необходимо, потому что в базовом
классе она является чистой виртуальной), ее вызов разрешается при выполнении
программы выбором версии, относящейся к классу объекта, на который установлен
указатель. Поэтому для указателя рС1 вызывается версия из класса СВох, а для указате-
ля рС 2 — версия из класса ССап. В каждом случае, таким образом, получается коррект-
ный результат.
Точно так же вы могли бы использовать только один указатель, и присвоить ему
адрес объекта ССап (после вызова функции Volume () объекта СВох). Указатель на ба-
зовый класс может содержать адрес объекта любого производного класса, даже когда
несколько различных классов унаследованы от одного и того же базового класса, и
потому вы можете иметь автоматический выбор правильной виртуальной функции
для всего диапазона производных классов. Впечатляет, не правда ли?
Непрямые базовые классы
В начале этой главы упоминалось, что базовый класс подкласса, в свою очередь,
может быть унаследован от другого, “еще более базового” класса. Маленькое расши-
рение последнего примера предлагает иллюстрацию этого, а также демонстрацию ис-
пользования виртуальной функции на втором уровне наследования.
Наследование классов и виртуальные функции 515
практическое занятие | Более одного уровня наследования
Все, что потребуется сделать — это добавить класс CGlassBox к классам, перечис-
ленным в предыдущем примере. Отношения между классами показаны на рис. 9.6.
Рис. 9.6. Отношения между классами CContainer, CCan, СВох и CGlassBox
Класс CGlassBox, как и ранее, унаследован от СВох, но мы опустили версию
ShowVolume () производного класса, чтобы продемонстрировать, что версия базо-
вого класса по-прежнему распространяется на производные классы. В показанной
здесь иерархии классов класс CContainer является непрямым базовым классом для
CGlassBox и прямым базовым — для СВох и ССап.
Заголовочный файл GlassBox.h для этого примера выглядит следующим обра-
зом.
// GlassBox.h для Ех9_11
#pragma once
#include "Box.h" // Для CBox
class CGlassBox: public CBox // Производный класс
{
public:
// Функция для вычисления объема CGlassBox
//с учетом 15% на упаковку
virtual double Volume() const
{ return 0.85*m_Length*m_Width*m_Height; }
// Конструктор
CGlassBox(double lv, double wv, double hv) : CBox(lv, wv, hv) {}
};
Заголовочные файлы Container. h, Can. h и Box. h содержат тот же код, что и в
предыдущем примере Ех9_10.
516 Глава 9
Исходный файл для нового примера с обновленной функцией main () для исполь-
зования дополнительного класса в иерархии выглядит так:
// Ех9_11.срр
// Использование абстрактного класса с несколькими уровнями наследования
#include "Box.h” // Для СВох и CContainer
#include "Can.h" // Для ССап (и CContainer)
#include "GlassBox.h" // Для CGlassBox (а также CBox и CContainer)
#include <iostream> // Для потокового ввода-вывода
using std::cout;
using std::endl;
const double PI = 3.14159265; //
Глобальное определение числа PI
int main()
// Указатель на абстрактный базовый класс, инициализированный
// адресом объекта СВох
CContainer* рС1 = new СВох (2. О, 3.0, 4.0);
ССап туСап(6.5, 3.0); // Определение объекта ССап
CGlassBox myGlassBox(2.0, 3.0, 4.0); // Определение объекта CGlassBox
pCl->ShowVolume(); // Вывод объема СВох
delete рС1; // Очистка памяти в свободном хранилище
// Инициализация адресом объекта ССап
рС1 = &шуСап; // Присвоить указателю адрес туСап
pCl->ShowVolume(); // Вывод объема ССап
рС1 - &myGlassBox; // Присвоить указателю адрес myGlassBox
pCl->ShowVolume(); // Вывод объема CGlassBox
cout « endl;
return 0;
Описание полученных результатов
Здесь мы имеем трехуровневую иерархию классов, показанную на рис. 9.6, с
CContainer в качестве абстрактного базового класса, поскольку он содержит чи-
стую виртуальную функцию Volume (). Функция main () теперь вызывает функцию
ShowVolume () три раза, используя один и тот же указатель базового класса, но при
этом указатель каждый раз содержит адреса объектов разных классов. Поскольку
ShowVolume () не определена ни в одном из производных классов, каждый раз вызы-
вается ее версия из базового класса. Отдельная ветвь от базы CContainer определяет
производный класс ССап.
Пример выдает следующий вывод:
Полезный объем СВох равен 24
Объем равен 45.9458
Полезный объем СВох равен 20.4
Вывод показывает, что в соответствии с типом объекта, каждый раз для выполне-
ния выбирается одна из трех разные версий Volume ().
Обратите внимание, что вы должны удалять объект СВох из свободного хранили-
ща перед тем, как присваивать указателю другое значение адреса. Если этого не де-
лать, вы не сможете очистить память, выделенную в свободном хранилище, потому
что адрес оригинального объекта не сохранится. Это ошибка, которую легко допу-
стить при переназначении указателей и использовании свободного хранилища.
Наследование классов и виртуальные функции 517
Виртуальные деструкторы
При работе с объектами производных классов через указатель базового класса
возникает одна проблема — в этом случае может не быть вызвана правильная версия
деструктора. Такой эффект можно наблюдать, немного модифицировав последний
пример.
Практическое занятие | ВЫЗОВ НеПрЭВИЛЬНОГО ДвСТруКТОрЭ
Чтобы отследить, какие деструкторы вызываются при уничтожении объектов,
достаточно добавить деструктор, выдающий соответствующее сообщение, в каждый
класс последнего примера. Файл Container .h для этого примера будет таким:
// Container.h для Ех9_12
#pragma once
#include <iostream>
using std::cout;
using std::endl;
class CContainer // Общий базовый класс для специфических контейнеров
{
public:
// Деструктор
CContainer ()
{ cout « "Вызван деструктор CContainer" « endl; }
// Функция для вычисления объема — без содержимого.
// Это определение чистой (’pure’) виртуальной функции, на что указывает ’=0’
virtual double Volume () const = 0;
// Функция для отображения объема
virtual void ShowVolumeO const
{
cout « endl
« "Объем равен " « Volume () ;
}
};
Ниже показано содержимое Can. h для этого примера.
// Can.h для Ех9_12
#pragma once
#include "Container.h" // Для определения CContainer
extern const double PI; // PI определено где-то
class ССап: public CContainer
{
public:
// Деструктор
~CCan()
{ cout « "Вызван деструктор ССап* " « endl; }
// Функция вычисления объема банки
virtual double Volume() const
{ return 0.25*PI*m_Diameter*m_Diameter*m_Height; }
// Конструктор
ССап(double hv = 4.0, double dv = 2.0): m_Height(hv), m_Diameter(dv){}
518 Глава 9
protected:
double m_Height;
double m_Diameter;
А вот содержимое Box. h для этого примера.
// Box.h для Ex9__12
#pragma once
// Box.h for Ex9_12
#include "Container. h" // Для определения CContainer
#include <iostream>
using std::cout;
using std::endl;
class CBox: public CContainer // Производный класс
public:
11 Деструктор
**CBox ()
{ cout « "Вызван деструктор CBox " « endl; }
// Функция для отображения объема объекта
virtual void ShowVolume() const
cout « endl
« "Полезный объем CBox равен " « Volume ();
// Функция для вычисления объема объекта СВох
virtual double Volume() const
{ return m_Length*m_Width*m_Height; }
// Конструктор
CBox (double lv = 1.0, double wv = 1.0, double hv = 1.0)
:m_Length(lv), m_Width(wv), m_Height(hv){}
protected:
double m_Length;
double m_Width;
double m_Height;
Файл заголовка GlassBox. h должен содержать такой код:
// GlassBox.h для Ex9_12
#pragma once
#include "Box.h" // Для CBox
class CGlassBox: public CBox // Производный класс
public:
// Деструктор
~CGlassBox()
{ cout « "Вызван деструктор CGlassBox " « endl; }
// Функция для вычисления объема CGlassBox с учетом 15% на упаковку
virtual double Volume() const
{ return 0.85*m_Length*m_Width*m_Height; }
// Конструктор
CGlassBox(double lv, double wv, double hv) : CBox(lv, wv, hv) {}
Наследование классов и виртуальные функции 519
И, наконец, исходный файл Ех9_12 . срр для этой программы должен выглядеть
следующим образом:
// Ех9__12.срр
// Вызовы деструкторов с производными классами,
// используя объекты через указатель на базовый класс
#include "Box.h" // Для СВох и CContainer
#include "Can.h" // Для ССап (и CContainer)
#include "GlassBox.h" // Для CGlassBox (а также CBox и CContainer)
#include <iostream> // Для потокового ввода-вывода
using std::cout;
using std::endl;
const double PI = 3.14159265; // Глобальное определение PI
int main ()
// Указатель на абстрактный базовый класс, инициализированный
// адресом объекта СВох
CContainer* рС1 = new СВох(2.0, 3.0, 4.0);
ССап туСап(6.5, 3.0);
CGlassBox myGlassBox(2.0, 3.0,
pCl->ShowVolume();
cout « endl « "Удаление СВох
delete pCl;
// Определение объекта ССап
4.0); // Определение объекта CGlassBox
// Вывод объема объекта СВох
" « endl;
//Очистка памяти в свободном хранилище
6.0); // Динамически создать CGlassBox
// .. .вывести его объем...
pCl = new CGlassBox(4.0, 5.0, 6.0); // Динамически созд
pCl->ShowVolume(); // .. .вывести его о
cout « endl « "Удаление CGlassBox" « endl;
delete pCl; // .. .и удалить его
pCl = &myCan;
pCl->ShowVolume();
pCl = &myGlassBox;
pCl->ShowVolume();
// Получить в указатель адрес myCan
// Вывести объем ССап
// Получить в указатель адрес myGlassBox
// Вывести объем CGlassBox
cout « endl;
return 0;
Описание полученных результатов
Помимо добавления в каждый класс деструкторов, выводящих сообщения о сво-
ем вызове, единственное изменение состоит в появлении нескольких новых строк
в функции main (). Эти дополнительные операторы динамически создают объект
CGlassBox, выводят его объем и удаляют его. Добавился также вывод сообщения, ука-
зывающего момент удаления динамически созданного объекта СВох. Вывод, сгенери-
рованный этим примером, показан ниже.
Полезный объем СВох равен 24
Удаление СВох
Вызван деструктор CContainer
Полезный объем СВох равен 102
Удаление CGlassBox
Вызван деструктор CContainer
Объем равен 45.9458
Полезный объем СВох равен 20.4
520 Глава 9
Вызван деструктор CGlassBox
Вызван деструктор СВох
Вызван деструктор CContainer
Вызван деструктор ССап
Вызван деструктор CContainer
Отсюда видно, что когда вы удаляете объект СВох, на который указывает рС1, вы-
зывается деструктор базового класса CContainer, но нет записи о вызове деструкто-
ра СВох. Аналогично, при удалении дополнительного объекта CGlassBox, опять-таки
вызывается деструктор базового класса CContainer, но не видно вызовов деструкто-
ров CGlassBox и СВох. Для прочих объектов вызовы корректного деструктора проис-
ходит после того, как сначала вызван конструктор производного класса, за которым
следовал вызов конструктора базового класса. Для первого объекта класса CGlassBox,
созданного в объявлении, вызываются три деструктора: сначала деструктор произво-
дного класса, за которым идет деструктор непосредственного базового класса, и, на-
конец, деструктор непрямого базового класса.
Все проблемы связаны с объектами, созданными в свободном хранилище. В обо-
их случаях вызываются неправильный деструктор. Причина в том, что компоновка
деструкторов разрешается статически, во время компиляции. Для автоматических
объектов проблем нет — компилятор знает, что они собой представляют, и может
установить вызов правильного деструктора. С объектами, созданными динамически
и доступными через указатель, все иначе. Единственная информация, которую име-
ет компилятор при выполнении операции delete — это то, что указатель имеет тип
базового класса. Действительный тип объекта, на который установлен указатель, ком-
пилятору неизвестен, потому что определяется во время выполнения программы.
Поэтому компилятор просто решает, что операция delete должна вызвать деструк-
тор базового класса. В реальном приложении это может вызвать множество проблем,
оставляя рассыпанные куски объектов в свободном хранилище, а может, и более се-
рьезные проблемы — в зависимости от природы вовлеченных объектов.
Решение просто. Нужно добиться того, чтобы вызовы разрешались динамически
во время выполнения программы. Вы можете организовать это, предусмотрев в сво-
их классах виртуальные деструкторы. Как уже говорилось при первом упоминании
виртуальных функций, достаточно объявить функцию базового класса виртуальной,
чтобы гарантировать, что все одноименные функции производных классов с тем же
списком параметров и типом возврата, также были виртуальными. Это касается и
деструкторов, как и обычных функций-членов. Вам нужно добавить ключевое слово
virtual к определению деструктора класса CContainer в файле CContainer . h, что-
бы определение класса стало таким:
class CContainer // Общий базовый класс для специфических контейнеров
public:
// Деструктор
virtual "CContainer()
{ cout « "Вызван деструктор CContainer" « endl; }
// Остальной код класса — тот же, что и ранее
Теперь деструкторы во всех производных классах автоматически станут вир-
туальными, даже несмотря на отсутствие в них явной спецификации, как таковых.
Конечно, вы можете специфицировать их как виртуальные, если хотите, чтобы код
был абсолютно прозрачен.
Если теперь запустить пример с этими модификациями, он выдаст следующее:
Наследование классов и виртуальные функции 521
Полезный объем СВох равен 24
Удаление СВох
Вызван деструктор СВох
Вызван деструктор CContainer
Полезный объем СВох равен 102
Удаление CGlassBox
Вызван деструктор CGlassBox
Вызван деструктор СВох
Вызван деструктор CContainer
Объем равен 45.9458
Полезный объем СВох равен 20.4
Вызван деструктор CGlassBox
Вызван деструктор СВох
Вызван деструктор CContainer
Вызван деструктор ССап
Вызван деструктор CContainer
Как видите, все объекты теперь уничтожаются с правильной последовательнос-
тью вызовов деструкторов. Уничтожение динамических объектов в программе сопро-
вождается той же последовательностью вызовов деструкторов, что и автоматических
объектов того же типа.
В этом месте у вас может возникнуть вопрос: может ли быть объявлен виртуаль-
ным конструктор? Ответ — нет. Виртуальными могут быть только деструкторы и дру-
гие функции-члены.
Хорошей идеей будет всегда объявлять деструкторы базовых классов виртуальными, когда
предполагается наследование. Существуют небольшие накладные расходы, которые свя-
заны с выполнением деструкторов класса, но в большинстве случаев вы их не заметите.
Использование виртуальных деструкторов гарантирует, что ваши объекты будут правиль-
но уничтожаться., а это позволит избежать потенциальной опасности краха программ,
которая вам грозит в противном случае.
Приведение между типами классов
Вы уже видели, как можно хранить адрес объекта производного класса в перемен-
ной типа указателя базового класса, так что переменная типа CContainer*, напри-
мер, может хранить адрес объекта СВох. Отсюда вопрос: можно ли привести указа-
тель типа CContainer* к типу СВох*? В самом деле, вы можете добавить операцию
dynamic cast, специально предназначенную для операций подобного рода. Вот как
это работает:
CContainer* pContainer = new CGlassBox(2.0, 3.0, 4.0);
CBox* рВох = dynamic_cast<CBox*>( pContainer);
CGlassBox* pGlassBox = dynamic_cast<CGlassBox*>( pContainer);
Первый оператор сохраняет адрес объекта CGlassBox, созданного в куче, в ука-
зателе на базовый класс типа CContainer*. Второй оператор приводит pContainer
вверх по иерархии классов к типу СВох*. Третий оператор приводит адрес
pContainer к его реальному типу CGlassBox*.
Вы можете применить также операцию dynamic cast к ссылкам, как и к указа-
телям. Разница между dynamic_cast и static__cast состоит в том, что операция
dynamic cast проверяет корректность приведения во время выполнения, в то время
как static_cast этого не делает. Если операция dynamic cast некорректна, резуль-
522 Глава 9
татом будет null. В отношении корректности операции static_cast компилятор
полагается на программиста, поэтому вы всегда должны использовать dynamic_cast
для приведения вверх и вниз по иерархии классов и проверять результат на равен-
ство null, если хотите избежать аварийного прерывания вашей программы в резуль-
тате использования нулевого указателя.
Вложенные классы
Определение одного класса можно поместить внутрь определения другого — в
этом случае вы определяете вложенны
: класс. Вложенный класс ведет себя как ста-
Г
4
тический член класса, включающего его, и является субъектом спецификаторов об-
ращения к членам — как и любой другой член класса. Если вы поместите определение
вложенного класса в раздел private класса, то к такому классу можно будет обра-
щаться только из функций-членов включающего класса. Если же вы специфицируе-
те определение вложенного класса как public, то такой класс будет доступен извне
включающего класса, но в этом случае имя вложенного класса должно быть квалифи-
цировано именем внешнего класса.
Вложенный класс имеет свободный доступ ко всем статическим членам включаю-
щего класса. Все члены экземпляров могут быть доступными через объект типа вклю-
чающего класса, указатель или ссылку на такой объект. Включающий класс может
иметь доступ только к общедоступным членам вложенного класса, но во вложенном
классе, который объявлен как private во включающем классе, члены часто объявля-
ются как public, чтобы обеспечить свободный доступ ко всему вложенному классу в
пределах включающего класса.
Вложенный класс может оказаться полезным, когда вы хотите определить тип, ко-
торый должен использоваться только внутри другого типа — тогда вложенный класс
может быть объявлен как private. Ниже показан пример.
/ / Стек для хранения объектов Box
class CStack
{
private:
// Определение элементов, хранящихся в стеке
struct CItem
СВох* рВох; // Указатель на объект в данном узле
CItem* pNext; // Указатель на следующий элемент в стеке или null
// Конструктор
CItem(СВох* рВ, CItem* pN) : рВох(рВ), pNext(pN){}
CItem* pTop; // Указатель на элемент, находящийся на верхушке
public:
// Затолкнуть объект Box в стек
void Push(СВох* рВох)
{
pTop = new CItem(рВох, pTop)///Создать элемент и поместить его на верхушку
}
// Вытолкнуть объект из стека
СВох* Pop()
if (pTop == 0) // Если стек пуст,
return 0; // вернуть null
СВох* рВох = рТор->рВох; // Получить ящик из элемента
CItem* pTemp = pTop; // Сохранить адрес верхнего элемента
Наследование классов и виртуальные функции 523
рТор = pTop->pNext; // Сделать верхним следующий элемент
delete pTemp; // Удалить старый верхний элемент из кучи
return рВох;
}
};
Класс CStack определяет стек магазинного типа для хранения объектов типа СВох.
Чтобы быть абсолютно точным, скажу, что он хранит указатели на объекты СВох, так
что объекты, на которые они указывают, находятся в юрисдикции кода, использую-
щего класс CStack. Вложенная структура С Item определяет элементы, хранящиеся
в стеке. Я решил использовать вложенную структуру, а не класс, потому что члены
структуры по умолчанию являются public. Вы можете определить С Item как класс
и затем специфицировать его члены, как public, так что к ним можно обращаться
из функций класса CStack. Стек реализован в виде набора объектов CItem, причем
каждый из них сохраняет указатель на объект СВох плюс адрес следующего элемента
CItem в стеке. Функция Push () в классе CStack заталкивает объект СВох на верхушку
стека, а функция Pop () выталкивает объект из стека обратно.
Помещение объекта в стек включает создание объекта CItem, который хранит
адрес хранимого объекта плюс адрес предыдущего элемента, который находил-
ся на верхушке до этого. Вначале при помещении первого объекта это будет null.
Выталкивание объекта из стека возвращает адрес объекта в элементе рТор. Элемент
из верхушки удаляется и верхним становится следующий, находящийся в стеке.
Давайте посмотрим, как это работает.
практическое занятие | использование вложенного класса
В этом примере используются классы CContainer, СВох и CGlassBox из приме-
ра Ех9_12, так что создайте пустой проект консольной программы WIN32 по имени
Ех9_13 и добавьте в него заголовочные файлы с определениями перечисленных клас-
сов. Затем добавьте к проекту файл Stack.h, содержащий определение класса CStack
из предыдущего раздела, а также файл Ех9_13. срр со следующим содержимым:
// Ех9_13.срр
// Использование вложенного класса для определения стека
#include ’’Box.h” // Для СВох и CContainer
tfinclude "GlassBox.h" // Для CGlassBox (а также CBox и CContainer)
#include "Stack.h" // Для класса стека с вложенной структурой Item
#include <iostream> // Для потокового ввода-вывода
using std::cout;
using std::endl;
int main()
{
CBox* pBoxes[] = { new CBox (2.0, 3.0, 4.0),
new CGlassBox (2.0, 3.0, 4.0),
new CBox(4.0, 5.0, 6.0),
new CGlassBox (4.0, 5.0, 6.0)
};
cout « "Массив ящиков, имеющих следующие объемы:’’;
for (int i = 0 ; i<4 ; i++)
pBoxesEi]->ShowVolume(); 11 Вывод объема ящика
cout « endl « endl
« "Теперь затолкаем ящики в стек. ..’’
« endl;
CStack* pStack = new CStack; // Создать стек
524 Глава 9
for (int i = 0 ; i<4 ; i++)
pStack->Push(pBoxes[i]);
cout « "Выталкивание ящиков из стека в обратном порядке:";
for (int i = 0 ; i<4 ; i++)
pStack->Pop()->ShowVolume();
cout « endl;
return 0;
Ниже показан вывод этого примера.
Массив ящиков, имеющих следующие объемы:
Полезный объем СВох равен 24
Полезный объем СВох равен 20.4
Полезный объем СВох равен 120
Полезный объем СВох равен 102
Теперь затолкаем ящики в стек...
Выталкивание ящиков из стека в обратном порядке:
Полезный объем СВох равен 102
Полезный объем СВох равен 120
Полезный объем СВох равен 20.4
Полезный объем СВох равен 24
Press any key to continue . . .
Описание полученных результатов
Мы создали массив указателей на объекты СВох, так что каждый элемент массива
может хранить адрес объекта СВох или адрес объекта любого типа, унаследованного
от СВох. Массив инициализируется адресами четырех объектов, созданных в стеке:
СВох* pBoxes [ ] = {
newCBox(2.0, 3.0,
new CGlassBox(2.0,
new CBox (4.0, 5.0,
new CGlassBox(4.0,
4.0) ,
3.0, 4.0),
6.0) ,
5.0, 6.0)
Здесь присутствуют два объекта СВох и два CGlassBox с такими же размерами, что
и у объектов СВох.
После перечисления объемов этих четырех объектов создается объект CStack, и в
цикле for в стек заталкиваются объекты:
CStack* pStack = new CStack;
for (int i = 0 ; i<4 ; i++)
pStack->Push(pBoxes[i]);
Каждый элемент массива pBoxes заталкивается в стек путем передачи этого эле-
мента в виде аргумента функции Push () объекта CStack. В результате первый эле-
мент массива оказывается на дне стека, а последний — на его верхушке.
Затем в следующем цикле объекты выталкиваются из стека:
for (int i = 0 ; i<4 ; i++)
pStack->Pop()->ShowVolume();
Функция Pop () возвращает адрес элемента, находящегося на верхушке стека, и вы
используете его для вызова функции ShowVolume () этого объекта. Поскольку послед-
ний элемент был на верхушке стека, цикл перечисляет все объемы объектов в обрат-
ном порядке. Из вывода вы можете видеть, что класс CStack в самом деле реализует
стек, используя вложенную структуру для определения элементов, хранимых в стеке.
Наследование классов и виртуальные функции
525
Программирование на C++/CLI
Все классы C++/CLI, включая классы, определенные вами, являются производны-
ми по умолчанию. Это потому, что классы значений и ссылочные классы имеют в ка-
честве базового стандартный класс System: :Object, а потому обладают всеми воз-
можностями класса System::Object. Поскольку функция ToString () определена как
виртуальная в System: :Object, вы можете переопределять ее в своих собственных
классах, и вызывать ее полиморфно, когда это требуется. Именно это мы и делали в
предыдущих главах, когда определяли в классе функцию ToString ().
Базовый класс System: :Object для всех классов типа значений также отвечает
за разрешение упаковки и распаковки значений фундаментальных типов. Этот про-
цесс означает, что значения фундаментальных типов могут вести себя как объекты,
но также могут участвовать в числовых операциях, не требуя накладных расходов,
связанных с тем, что они являются объектами. Значения фундаментальных типов
сохраняются просто как значения для обычных операций и только при необходи-
мости преобразуются в объекты, на которые можно сослаться по дескриптору типа
System: :ObjectA, когда нужно, чтобы они вели себя как объекты. Поэтому вы по-
лучаете преимущества от возможности трактовать фундаментальные значения как
объекты, когда это необходимо, не навязывая накладных расходов, связанных с их
объектной природой, когда в этом нет необходимости.
Наследование классов в C++/CLI
Хотя классы значений всегда имеют в качестве базового класса System: :Object,
вы не можете определить классы значений как производные от какого-то существу-
ющего класса. Другими словами, при определении класса значений вы не можете
специфицировать базовый класс. Это подразумевает, что полиморфизм в классах
значений ограничен функциями, определенными как виртуальные в классе System::
Obj ect. В табл. 9.2 перечислены виртуальные функции, которые все классы значений
наследуют от System: .‘Object.
Таблица 9.2. Виртуальные функции System::Object, наследуемые всеми
классами значений
Функция
String7' ToString ()
Описание
Возвращает строковое (string) представление объекта и реализована
в классе System::Object как возвращающая имя класса в виде стро-
ки. Обычно вы должны переопределять эту функцию в своих собствен-
ных классах, чтобы она возвращала строковое представление значения
объекта.
bool Equals (Object^ obj) Сравнивает текущий объект с obj и возвращает true, если они эквива-
лентны, либо false — в противном случае. Эквивалентность в данном
случае означает ссылочную эквивалентность — то есть признак того,
что объекты одинаковы. Обычно вы будете переопределять эту функ-
цию в своих собственных классах, чтобы она возвращала true, когда
текущий объект имеет то же значение, что и аргумент; другими слова-
ми, когда поля объектов эквивалентны.
int GetHashCode()
Возвращает целое число, представляющее хеш-код текущего объекта.
Хеш-коды используются в качестве ключей для сохранения объектов в
коллекции, хранящей пары “ключ-объект”. Объекты последовательно
извлекаются из такой коллекции по ключам, которые указывались при
их сохранении.
526 Глава 9
Конечно, поскольку класс System::Object также является базовым и для ссылоч-
ных классов, вы можете переопределять эти функции и в ссылочных классах тоже.
Вы можете унаследовать ссылочный класс от существующего ссылочного класса
точно так же, как определяете производный класс на “родном” C++. Давайте повтор-
но реализуем Ех9_ 12 как программу на C++/CLI, чтобы заодно продемонстрировать
вложенные классы в программе CLR. Начнем с определения класса Container.
// Container.h для Ех9_14
#pragma once
using namespace System;
/ / Абстрактный базовый класс для специфических контейнеров
ref class Container abstract
public:
// Функция для вычисления объема - без содержимого,
// определена как ’абстрактная* виртуальная функция,
// что отмечено ключевым словом ’abstract*
virtual double Volume() abstract;
// Функция для отображения объема
virtual void ShowVolume()
{
Console::WriteLine (Ъ’’Объем равен {0} , Volume ());
Первое, что необходимо отметить — это ключевое слово abstract, следующее
за именем класса. Если класс C++/CLI содержит эквивалент чистой виртуальной
функции на “родном” C++, вы должны специфицировать абстрактные функции, что
предотвратит создание объектов этого типа класса. Ключевое слово abstract так-
же появляется в конце объявления функции-члена Volume (), чтобы указать, что оно
должно быть также определено и для этого класса. Вы можете также добавить в ко-
нец объявления члена Volume () фрагмент = 0, как это делается в “родном” C++, хотя
это не обязательно.
Обе функции — и Volume (), и ShowVolume () — являются виртуальными, пото-
му они могут быть вызваны полиморфно для объектов классов, производных от
Container.
Класс Box можно определить следующим образом:
// Box.h для Ех9__14
#pragma once
#include ’’Container.h” // Для определения Container
ref class Box : Container // Производный класс
public:
// Функция для показа объема объекта
virtual void ShowVolume() override
Console::WriteLine(1"Полезный объем Box равен {0}”, Volume ());
/ / Функция для вычисления объема объекта
virtual double Volume() override
{ return m__Length*m_Width*m_Height; }
// Конструктор
Box() : m_Length(1.0), m_Width(1.0), m_Height(1.0){}
Наследование классов и виртуальные функции 527
// Конструктор
Box(double lv, double wv, double hv)
: m_Length (lv) t m_Width (wv) t m__Height (hv) {}
protected:
double m_Length;
double m_Width;
double m_Height;
Базовый класс для ссылочного класса всегда public, и ключевое слово public
подразумевается по умолчанию. Вы можете специфицировать базовый класс явно как
public, но это делать не обязательно. Базовый класс для ссылочного класса не может
быть специфицирован иначе, чем public. Поскольку вы не можете применить значе-
ния по умолчанию для параметров, как в версии этого класса на “родном” C++, прихо-
дится определять конструктор без аргументов, чтобы он инициализировал три поля
значениями 1.0. Класс Box определяет функцию Volume (), как переопределенную
для унаследованной версии базового класса. Вы всегда должны специфицировать клю-
чевое слово о ve г г i de, когда намереваетесь переопределить функцию базового класса.
Если класс Box не реализует функцию Volume (), он будет абстрактным, и вы должны
будете специфицировать это, чтобы компиляция класса выполнилась успешно.
Вот как выглядит определение класса GlassBox:
// GlassBox.h для Ех9_14
#pragma once
#include "Box.h” // Для Box
ref class GlassBox : Box // Производный класс
public:
// Функция для вычисления объема GlassBox
//с учетом 15% на упаковку
virtual double Volume () override
{ return 0.85*m_Length*m_Width*m_Height; }
// Конструктор
GlassBox(double lv, double wv, double hv) : Box(lv, wv, hv) {}
Базовым классом является Box, который public по умолчанию. Остальная часть
класса, по сути, та же самая, что и оригинал.
А вот определение класса Stack:
// Stack.h для Ех9_14
// Стек магазинного типа для хранения объектов ссылочных классов любого типа
#pragma once
ref class Stack
private:
// Определение элементов для хранения в стеке
ref struct Item
ObjectA Obj; // Дескриптор объекта в данном элементе
ItemA Next; // Дескриптор следующего элемента в стеке или nullptr
// Конструктор
Item(ObjectA obj, ItemA next): Obj(obj), Next(next){}
ItemA Top; // Дескриптор элемента, находящегося на верхушке стека
528 Глава 9
public:
// Затолкнуть объект в стек
void Push {Object74 obj)
{
Top = gcnew Item(obj, Top); // Создать новый элемент и поместить
// его на верхушку
}
// Вытолкнуть объект из стека
Object74 Pop ()
{
if (Тор == nullptr) // Если стек пуст
return nullptr; // вернуть nullptr
Object74 obj = Top->Obj; // Получить объект из элемента
Тор = Top->Next; // Поместить следующий элемент на верхушку
return obj;
}
};
Первое отличие, которое следует отметить, заключается в том, что параметры
функций и поля теперь являются дескрипторами, потому что мы имеем дело с объек-
тами ссылочного класса. Вложенная структура Item теперь хранит дескриптор типа
Object74, который позволяет объектам любого типа класса CLR сохраняться в стеке;
это значит, что и классы значений и ссылочные классы могут быть приспособлены
для этого, что представляет собой существенное усовершенствование по сравнению
с классом CStack на “родном” C++. Вам не нужно беспокоиться об удалении объектов
Item при вызове функции Pop (), потому что об этом позаботится сборщик мусора.
Подведем итоги всех отличий от “родного” C++, которые были продемонстриро-
ваны этими классами.
□ Только ссылочные классы могут быть унаследованными типами классов.
□ Базовый класс для производного ссылочного класса всегда public.
□ Функция, которая не имеет определения в ссылочном классе, является аб-
страктной и должна быть объявлена таковой с применением ключевого слова
abstract.
□ Класс, который содержит одну или более абстрактных функций, должен быть
явно специфицирован как абстрактный с помощью ключевого слова abstract,
следующего за именем класса.
□ Класс, не содержащий абстрактных функций, также может быть специфици-
рован как абстрактный; в этом случае экземпляры этого класса не могут быть
созданы.
□ Вы должны явно использовать ключевое слово override, определяя функцию,
которая переопределяет функцию, унаследованную от базового класса.
Чтобы попробовать все эти классы в действии, потребуется создать консольный
проект CLR с определением main (), поэтому давайте это и сделаем.
Практическое занятие
Использование производных ссылочных
классов
Создайте консольную программу CLR по имени Ех9_14 и добавьте классы из пред-
ыдущего раздела в проект; затем добавьте следующее содержимое в Ех9_14. срр:
Наследование классов и виртуальные функции 529
// Ех9_14.срр : главный файл проекта.
// Использование вложенного класса для определения стека
#include "stdafx.h"
#include "Box.h" // Для Box и Container
#include "GlassBox.h" // Для GlassBox (а также Box и Container)
#include "Stack.h" // Для класса стека с вложенной структурой Item
using namespace System;
int main(array<System::String A> Aargs)
array<BoxA>A boxes = { gcnew Box(2.0, 3.0, 4.0),
gcnew GlassBox(2.0, 3.0, 4.0),
gcnew Box(4.0, 5.0, 6.0),
gcnew GlassBox(4.0, 5.0, 6.0)
Console::WriteLine(Ь"Массив ящиков, имеющих следующие объемы:");
for each(BoxA box in boxes)
box->ShowVolume(); // Вывести объем ящика
Console::WriteLine(Ь"\пТеперь затолкнем ящики в стек...");
StackA stack = gcnew Stack; // Создать стек
for each(BoxA box in boxes)
stack->Push(box);
Console::WriteLine(
L"Выталкивание ящиков из стека представляет их в обратном порядке:");
ObjectA item;
while((item = stack->Pop()) !» nullptr)
safe_cast<ContainerA>(item)->ShowVolume();
Console::WriteLine(L"\nTenepb затолкнем целые числа в стек:");
for (int i = 2 ; i<=12 ; i += 2)
Console::Write(L"{0,5}",i);
stack->Push(i);
Console::WriteLine(П"\п\пВыталкивание целых чисел из стека порождает:");
while((item = stack->Pop()) !» nullptr)
Console::Write(L"{0,5}",item);
Console::WriteLine();
return 0;
Ниже показан вывод этого примера.
Массив ящиков, имеющих следующие объемы:
Полезный объем Box равен 24
Полезный объем Box равен 20.4
Полезный объем Box равен 120
Полезный объем Box равен 102
Теперь затолкнем ящики в стек...
Выталкивание ящиков из стека представляет их в обратном порядке:
Полезный объем Box равен 102
Полезный объем Box
Полезный объем Box
Полезный объев
Box
равен
равен
равен
120
20.4
24
Теперь затолкнем целые числа в стек:
2 4 6 8 10 12
Выталкивание целых чисел из стека порождает:
12 10 8 6 4 2
Press any key to continue . . .
530 Глава 9
Описание полученных результатов
Сначала в следующих строках создается массив дескрипторов:
array<BoxA>A boxes =
{ gcnew Box(2.0, 3.0,
gcnew GlassBox(2.0,
gcnew Box(4.0, 5.0,
gcnew GlassBox(4.0,
4.0),
3.0, 4.0),
6.0),
5.0, 6.0)
Поскольку Box и GlassBox — ссылочные классы, объекты создаются в куче CLR с
применением gcnew. Полученные адреса объектов инициализируют элементы масси-
ва boxes.
Затем вы можете создать объект Stack и затолкнуть в него ящики:
StackA stack e gcnew Stack; // Создать стек
for each(BoxA box in boxes)
stack->Push(box);
Параметр функции Push () имеет тип ObjectЛ, поэтому функция принимает лю-
бой тип класса в качестве аргумента. Цикл for each заталкивает каждый из элемен-
тов массива boxes в стек.
Выталкивание элементов из стека происходит в цикле while:
ObjectA item;
while((item = stack->Pop()) != nullptr)
safe_cast<ContainerA>(item)->ShowVolume();
Условие цикла сохраняет значение, возвращенное функцией Pop () объекта stack
в item и сравнивает его с nullptr. До тех пор, пока item не равно nullptr, выпол-
няются операторы, находящиеся в теле цикла while. Внутри цикла дескриптор, поме-
щенный в item, приводится к типу Container'4. Переменная item имеет тип Object'4,
а поскольку класс Object не определяет функцию ShowVolume (), вы не можете вы-
звать ее, применяя дескриптор этого типа. Чтобы вызвать функцию полиморфно,
вы должны использовать дескриптор типа базового класса, который определяет эту
функцию как виртуальный член. Приведя дескриптор к типу ContainerА, вы можете
вызвать функцию ShowVolume () полиморфно, поэтому при выполнении выбирается
версия этой функции, принадлежащая классу, к которому в действительности отно-
сится объект, на который ссылается дескриптор. В данном случае вы можете достичь
того же результата, приведя item к типу ВохА. Здесь применяется safe_cast, по-
скольку приведение осуществляется вверх по иерархии классов, а в таком случае луч-
ше применить ограниченную операцию приведения. Операция safe_cast проверяет
правильность приведения и, если преобразование не удается, генерирует исключение
типа System:: InvalidCastException. Вы можете использовать и dynamic__cast, но
в программах CLR лучше применять safe_cast.
Интерфейсные классы
Определение интерфейсного класса внешне довольно похоже на определение
ссылочного класса, но представляет довольно-таки отличающуюся концепцию.
Интерфейс — это класс, определяющий набор функций, которые должны быть реа-
лизованы другими классами, чтобы обеспечить стандартизованный способ предо-
ставления некоторой специфической функциональности. Как классы значений, так
и ссылочные классы могут реализовывать интерфейсы. Интерфейс не определяет
никакие из своих функций-членов — их определяет каждый класс, который реализует
этот интерфейс.
Наследование классов и виртуальные функции 531
Вы уже встречали интерфейс System: : IComparable в контексте обобщенных
функций, где специфицировали интерфейс IComparable в качестве ограничения.
Интерфейс IComparable определяет функцию CompareTo () для сравнения объектов,
так что все классы, которые реализуют этот интерфейс, обладают одинаковым меха-
низмом сравнения объектов. Вы специфицируете интерфейс, реализуемый классом,
точно так же, как базовый класс. Например, вот как можно было бы заставить класс
Box из предыдущего примера реализовывать интерфейс System:: IComparable:
ref class Box : Container, IComparable // Производный класс
public:
// Функция, специфицированная интерфейсом IComparable
virtual int CompareTo(Object74 obj)
{
if(Volume() < safe_cast<BoxA>(obj)->Volume())
return -1;
else if(Volume() > safe_cast<BoxA>(obj)->Volume())
return 1;
else
return 0;
// Остальная часть класса - без изменений...
Имя интерфейса следует за именем базового класса Container. Если бы не было
базового класса, здесь было бы указано только имя интерфейса. Ссылочный класс
может иметь только один базовый класс, но при этом реализовывать столько ин-
терфейсов, сколько от него требуется. Интерфейс IComparable определяет только
одну функцию, но, если нужно, в интерфейсе может быть и много функций. Класс
Box теперь определяет функцию CompareTo () с той же сигнатурой, что и указанная в
определении интерфейса IComparable. Поскольку параметр функцию CompareTo ()
имеет тип Object74, вы должны привести его к типу ВохА, прежде чем обращаться к
членам объекта Box, на который он ссылается.
Определение интерфейсных классов
Интерфейсный класс определяется с применением ключевых слов interface
class или interface struct. Независимо от того, использовано interface class
или interface struct для определения интерфейса, все члены интерфейса всегда
являются public по умолчанию, и вы не можете это изменить. Членами интерфейса
могут быть функции, включая функции операций, свойства, статические поля и со-
бытия — обо всем этом вы узнаете далее в настоящей главе. Интерфейс может также
специфицировать статический конструктор и содержать в себе определения вложен-
ных классов любого рода. Несмотря на такое потенциальное разнообразие членов,
большинство интерфейсов относительно просты. Обратите внимание, что можно на-
следовать один интерфейс от другого — почти так же, как наследовать один ссылоч-
ный класс от другого. Например:
interface class IController : ITelevison, IRecorder
// Члены IController...
Интерфейс IController включает в себя свои собственные члены, а также на-
следует члены интерфейсов ITelevision и IRecorder. В классе, который реализует
532 Глава 9
интерфейс IController, должны быть определены функции-члены из IController,
ITelevision и IRecorder.
В примере Ех9_14 вы можете использовать интерфейс вместо базового класса
Container. Вот как могло бы выглядеть определение этого интерфейса:
// IContainer.h для Ех9_15
#pragma once
interface class IContainer
double Volume (); // Функция для вычисления объема
void ShowVolume(); // Функция для отображения объема
По существующему соглашению имена интерфейсов в C++/CLI начинаются с I,
поэтому интерфейс назван IContainer. Он содержит два члена: функции Volume ()
и ShowVolume (), которые являются public, потому что все члены интерфейсов всег-
да являются public. Обе функции абстрактны, поскольку интерфейсы никогда не
включают определений функций — в самом деле, вы можете добавить ключевое слово
abstract к обеим этим функциям, хотя это не обязательно. Функции экземпляров в
определении интерфейса могут быть специфицированы как virtual и abstract, но
это делать не обязательно, поскольку они таковыми являются в любом случае.
Любой класс, который реализует интерфейс IContainer, должен реализовать обе
его функции, если только этот класс не абстрактный. Посмотрим, как выглядит класс
Box:
// Box.h для Ех9_15
tpragma once
#include "IContainer.h" // Для определения интерфейса
using namespace System;
ref class Box : IContainer
public:
/ / Функция для отображения объема объекта
virtual void ShowVolume ()
Console::WriteLine(Ь"Полезный объем Box равен {0}", VolumeO);
// Функция для вычисления объема объекта Box
virtual double VolumeO
{ return m_Length*m_Width*m_Height; }
// Конструктор
Box() : m_Length(1.0)t m_Width(1.0), m_Height(1.0){}
11 Конструктор
Box(double lv, double wv, double hv)
: m_Length(lv), m_Width(wv), m_Height(hv){}
protected:
double m_Length;
double m_Width;
double m_Height;
Имя интерфейса следует за двоеточием в первой строке определения класса —
точно так же, как если бы он был базовым классом. Конечно, здесь может также
присутствовать и базовый класс, тогда имя интерфейса должно следовать за именем
Наследование классов и виртуальные функции 533
базового класса, отделяясь от него запятой. Класс может реализовывать множество
интерфейсов, при этом их имена разделяются запятыми.
Класс Box должен реализовать обе функции-члена интерфейсного класса
IContainer, иначе он будет абстрактным классом, и должен быть объявлен тако-
вым. Определения этих функций в классе Box не сопровождаются ключевым словом
override, поскольку здесь не происходит переопределения существующего определе-
ния функции; вы реализуете их впервые.
Класс GlassBox — производный от класса Box, и потому наследует реализацию
IContainer. Определение класса GlassBox не нуждается в каких-либо изменениях,
связанных с введением в программу интерфейсного класса IContainer.
Интерфейсный класс IContainer играет в полиморфизме ту же роль, что и базо-
вый класс. Вы можете использовать дескриптор типа IContainer, чтобы сохранить
адрес объекта любого типа класса, реализующего этот интерфейс. Поэтому дескрип-
тор типа IContainer может применяться для ссылки на объекты типа Box или типа
GlassBox, чтобы получить полиморфное поведение при вызове функции-члена ин-
терфейсного класса. Давайте попробуем это.
Практическое занятие
Реализация интерфейсного класса
Создайте консольный проект CLR по имени Ех9_15 и добавьте в него заголовоч-
ные файлы IContainer.h и Box.h с содержимым, представленным в предыдущем
разделе. Вы также должны добавить в проект копии заголовочных файлов Stack.h
и GlassBox.h из Ех9_14. И, наконец, модифицируйте содержимое Ех9_15.срр сле-
дующим образом:
// Ех9_15.срр : главный файл проекта.
#include "stdafx.h"
#include "Box.h" // Для Box и IContainer
#include "GlassBox.h" // Для GlassBox (а также Box и IContainer)
#include "Stack.h" // Для класса стека с вложенной структурой Item
using namespace System;
int main(array<System::String A> Aargs)
{
array<IContainerA>A containers = { gcnew Box(2.0, 3.0, 4.0),
gcnew GlassBox (2.0, 3.0, 4.0),
gcnew Box(4.0, 5.0, 6.0),
gcnew GlassBox(4.0, 5.0, 6.0)
};
Console::WriteLine(1"Массив контейнеров, имеющих следующие объемы:");
for each(IContainerA container in containers)
container->ShowVolume(); // Вывод объема ящика
Console::WriteLine(L"\nTenepb затолкнем контейнеры в стек...");
StackA stack = gcnew Stack; // Создать стек
for each(IContainerA container in containers)
stack->Push(container);
Console::WriteLine(
L"Выталкивание контейнеров из стека представляет их в обратном порядке:");
ObjectA item;
while((item = stack->Pop ()) != nullptr)
safe_cast<IContainerA>(item)->ShowVolume();
Console::WriteLine();
return 0;
}
534 Глава 9
Этот пример выдаст следующий вывод:
Массив контейнеров, имеющих следующие объемы:
Полезный объем СВох равен 24
Полезный объем СВох равен 20.4
Полезный объем СВох равен 120
Полезный объем СВох равен 102
Теперь затолкнем контейнеры в стек...
Выталкивание контейнеров из стека представляет их в обратном порядке:
Полезный объем СВох равен 102
Полезный объем СВох равен 120
Полезный объем СВох равен 20.4
Полезный объем СВох равен 24
Press any key to continue...
Описание полученных результатов
Мы создаем массив элементов типа IContainer
и инициализируем его элементы
адресами объектов Box и GlassBox:
array<IContainerA>A containers = { gcnew Вох(2.0, 3.0, 4.0),
gcnew GlassBox(2.0, 3.0, 4.0),
gcnew Box(4.0, 5.0, 6.0),
gcnew GlassBox(4.0, 5.0, 6.0)
Классы Box и GlassBox реализуют интерфейс IContainer, поэтому можно со-
хранить адреса объектов этих типов в переменных типа дескриптора IContainer.
Преимущество этого подхода состоит в том, что можно будет вызывать функции-чле-
ны интерфейса IContainer полиморфно.
Далее в цикле for each выводятся объемы объектов Box и GlassBox:
for each(IContainer74 container in containers)
container->ShowVolume(); // Вывод объема ящика
Тело цикла показывает полиморфизм в действии; вызывается функция
ShowVolume () для специфического типа объекта, на который ссылается container,
что видно из вывода.
Элементы массива контейнеров заталкиваются в стек, по сути, точно так же, как
и в предыдущем примере. Выталкивание элементов из с^ека также аналогично преды-
дущему примеру:
ObjectA item;
while((item = stack->Pop()) ! = nullptr)
safe_cast<IContainerA>(item)->ShowVolume();
Тело цикла доказывает, что вы можете приводить дескриптор к типу интерфей-
са, используя safe cast, точно таким же образом, как приводили к типу ссылочно-
го класса. Затем этот дескриптор применяется для полиморфного вызова функции
ShowVolume ().
Использование интерфейсных классов — не только удобный способ определения
наборов функций, представляющих стандартные интерфейсы классов, но также мощ-
ный механизм для применения полиморфизма в ваших программах.
Наследование классов и виртуальные функции 535
Классы и сборки
Приложение C++/CLI всегда расположено в одной или нескольких сборках, по-
этому классы C++/CLI всегда находятся в сборке. Все классы, определенные нами
для каждого примера до настоящего момента, находились в единственной простой
сборке, представляющей исполняемую программу, но вы можете создавать сборки,
содержащие ваши собственные библиотечные классы. В C++/CLI добавлены специ*
фикаторы видимости для классов, определяющие доступность данного класса извне
сборки, в которой они находятся и которую мы будем называть родительской сбор-
кой (parent assembly). В дополнение к спецификаторам доступа к членам public,
private и protected, которые присутствуют в родном C++, в C++/CLI предусмотре-
ны дополнительные спецификаторы доступа для членов класса, определяющие, от-
куда они могут быть доступны в разных сборках.
Спецификаторы видимости классов и интерфейсов
Вы можете специфицировать видимость невложенных классов, интерфейсов или
перечислений как private или public. Общедоступный (public) класс видим извне
сборки, в которой он находится, в то время как приватный (private) класс доступен
только в пределах его родительской сборки. Классы, интерфейсы и перечислимые
классы являются private по умолчанию, и потому видимы только внутри их роди-
тельской сборки. Чтобы специфицировать класс как public, вы просто используете
ключевое слово public:
public interface class IContainer
// Детали интерфейса. . .
Интерфейс IContainer здесь видим во внешней сборке, потому что определен
как public. Если опустить ключевое слово public, то интерфейс будет по умолчанию
private, и видим только внутри его родительской сборки. При желании вы можете
явно специфицировать класс, перечисление или интерфейс как private, но это не
обязательно.
Спецификаторы доступа к членам класса и интерфейса
В C++/CLI добавлены еще три спецификатора доступа к членам класса: internal,
public protected и private protected. Эффект от них описан в комментариях
приведенного ниже определения класса.
public ref class MyClass // Класс видим вне его сборки
public:
// Члены доступны из классов внутри и вне родительской сборки
internal:
// Члены доступны из классов внутри родительской сборки
public protected:
// Члены доступны в типах, унаследованных от MyClass, вне его родительской
// сборки и в любых классах внутри родительской сборки
private protected:
// Члены доступны в типах, унаследованных от MyClass, внутри родительской сборки
Очевидно, что класс должен быть public, чтобы спецификаторы доступа к чле-
нам могли обеспечить доступ к ним извне родительской сборки. Когда спецификатор
536 Глава 9
доступа состоит из двух ключевых слов, например, private protected, то менее
ограничивающее слово применяется внутри сборки, а более ограничивающее — вне
сборки. Вы можете менять местами слова в этих парах, потому protected private
имеет тот же смысл, что и private protected.
Чтобы использовать некоторые из этих спецификаторов, вы должны создать при-
ложение, состоящее из более чем одной сборки, поэтому мы пересоздадим пример
Ех9_15 как сборку библиотеки классов плюс сборку, использующую эту библиотеку
классов.
[Практическое занятие | Создание бибЛИОТвКИ КЛЭССОВ
Чтобы создать библиотеку классов, вы можете сначала создать проект CLR по
имени Ex9_161ib, используя шаблон Class Library (Библиотека классов). Проект со-
держит заголовочный файл Ex9_161ib.h со следующим содержимым:
// Ex9_161ib.h
#pragma once
using namespace System;
namespace Ex9_161ib
{
public ref class Classi
{
// TODO: добавьте сюда свои методы класса.
};
}
Библиотека классов имеет свое собственное пространство имен и здесь оно назы-
вается по умолчанию Ех916lib. Вы можете изменить это имя на что-то более подхо-
дящее по своему желанию. Имена классов в библиотеке квалифицируются названием
пространства имен, поэтому следует использовать директиву using с наименованием
пространства имен в любом внешнем исходном файле, который обращается к любому
классу в этой библиотеке. Определения классов, которые помещаются в библиотеку,
располагаются между фигурными скобками пространства имен. Здесь есть ссылочный
класс по умолчанию, определенный в пространстве имен, но вы можете заменить его
своими собственными классами. Обратите внимание, что класс Classi отмечен как
public; все классы, которые должны быть видимы в другой сборке, должны быть спе-
цифицированы как public.
Модифицируйте содержимое Ex9_161ib.h следующим образом:
И Ex9_161ib.h
#pragma once
using namespace System;
namespace Ex9_161ib
{
// IContainer.h для Ex9_16
public interface class IContainer
{
virtual double VolumeO; // Функция для вычисления объема
virtual void ShowVolume(); // Функция отображения объема
};
// Box.h для Ех9__1б
public ref class Box : IContainer
{
Наследование классов и виртуальные функции 537
public:
/ / Функция отображения объема объекта
virtual void ShowVolume()
Console::WriteLine(Ь"Полезный объем Box равен {0}”, VolumeO);
// Функция вычисления объема объекта Box
virtual double VolumeO
{ return m_Length*m_Width*m_Height; }
// Конструктор
Box() : m_Length(1.0), m_Width(1.0), m_Height(1.0){}
// Конструктор
Box(double lv, double wv, double hv)
: m_Length(lv), m_Width(wv), m_Height(hv){}
public protected:
double m_Length;
double m_Width;
double m_Height;
// Stack.,h для Ex9_16
public ref class Stack
private:
// Определение элементов, хранимых в стеке
ref struct Item
Object74 Obj; // Дескриптор объекта данного элемента
Item74 Next; // Дескриптор следующего элемента в стеке или nullptr
// Конструктор
Item (Object74 obj, Item74 next): Obj (obj), Next (next) {}
Item74 Top; // Дескриптор элемента, находящегося на верхушке
public:
/ / Затолкнуть объект в стек
void Push (Object74 obj)
Top = gcnew Item(obj, Top); // Создать новый элемент и поместить
//на верхушку стека
}
// Вытолкнуть объект из стека
Object74 Pop ()
if (Top == nullptr) // Если стек пуст,
return nullptr; // вернуть nullptr
Object74 obj = Top->Obj; // Получить ящик из элемента
Top = Top->Next; // Сделать следующий элемент верхушкой
return obj;
Интерфейсный класс IContainer, класс Box и класс Stack теперь включены в
эту библиотеку. Изменения исходных определений выделены полужирным. Каждый
класс теперь является public, что обеспечивает доступ к нему из внешних сборок.
Поля класса Box объявлены как public protected, что означает, что они унаследо-
ваны в производном классе как protected, но являются public, пока речь идет о
классах внутри родительской сборки. В действительности вы не обращаетесь к этим
538 Глава 9
полям из других классов внутри родительской сборки, поэтому в данном случае мож-
но оставить эти поля класса Box как protected.
После успешной компиляции этого проекта сборка, содержащая библиотеку
классов, находится в файле Ex9_161ib.dll, хранящемся в подкаталоге debug ката-
лога проекта, если вы компилировали отладочную версию проекта, и в подкаталоге
release, если компилировали рабочую версию. Расширение .dll означает, что это
динамически подключаемая библиотека (dynamic link library), или DLL. Теперь нам
понадобится другой проект, который будет использовать эту библиотеку классов.
Практическое занятие | ИСПОЛЬЗОВЭНИе бибЛИОТвКИ КЛЭССОВ
Добавьте новый консольный проект CLR по имени Ех9_16 в его собственном ре-
шении, как обычно. Затем модифицируйте Ех9_16. срр следующим образом:
// Ех9_16.срр : главный файл проекта.
// Использование библиотеки классов из отдельной сборки
#include "stdafx.h"
#include "GlassBox.h"
#using <Ex9_161ib.dll>
using namespace System;
using namespace Ex9_161ib;
int main(array<System::String A> Aargs)
{
array<IContainerA>A containers = { gcnew Box(2.0, 3.0, 4.0),
gcnew GlassBox(2.0, 3.0, 4.0),
gcnew Box(4.0, 5.0, 6.0),
gcnew GlassBox(4.0, 5.0, 6.0)
Console::WriteLine(L"Массив контейнеров имеет следующие объемы:") ;
for each(IContainerА container in containers)
container->ShowVolume (); // Вывод объема ящика
Console: .‘WriteLine (L"\nTenepb затолкнем контейнеры в стек. . .") ;
StackA stack = gcnew Stack; // Создать стек
for each(IContainerA container in containers)
stack->Push(container);
Console: '.WriteLine(
L"Выталкивание контейнеров из стека представляет их в обратном порядке:");
ObjectA item;
while((item = stack->Pop()) != nullptr)
safe_cast<IContainerA>(item) ->ShowVolume () ;
Console::WriteLine();
return 0;
}
Вам также понадобится добавить заголовочный файл GlassBox.h к проекту с тем
же кодом, что и в Ех9_15, так что вы можете скопировать его в каталог этого про-
екта и затем добавить к проекты правым щелчком мыши на папке Header Files (За-
головочные файлы) во вкладке Solution Explorer (Проводник решений) и выбором
пункта Add=>Existing Item (Добавить^Существующий элемент) из контекстного меню.
Конечно, класс GlassBox унаследован от класса Box, поэтому компилятор должен
знать, где найти определение класса Box. В данном случае он находится в библиоте-
ке, созданной в предыдущем проекте, поэтому добавьте следующую директиву в заго-
ловочный файл GlassBox.h после директивы tfpragma once:
#using <Ex9_161ib.dll>
Наследование классов и виртуальные функции
539
Имя класса Box определено внутри пространства имен Ex9_161ib, поэтому также
нужно добавить оператор using следом за директивой #using:
using namespace Ex9_161ib;
Чтобы дать возможность компилятору найти библиотеку, скопируйте файл
Ex9_161ib.dll из проекта Ex9_161ib в подкаталог debug решения Ех9_16, в кото-
ром находится файл Ех9_16. ехе. Вы можете специфицировать полный путь к сборке
в директиве #using, но обычно принято помещать все библиотеки классов, использу-
емые проектом, в каталог, содержащий исполняемый файл приложения. Здесь очень
легко спутать каталоги. Файл Ex9_161ib.dll находится в подкаталоге debug каталога
решения Ex9_161ib, а не в подкаталоге debug каталога проекта Ex9_161ib. Вы копиру-
ете файл библиотеки в подкаталог debug каталога решения Ех9_16. Убедитесь, что вы
скопировали файл . dll в правильный каталог, иначе библиотека не будет найдена.
Поскольку классы во внешней сборке находятся в их собственном пространстве
имен, вы должны применить директиву using для указания имени пространства имен
Ex9_161ib. Без этого придется квалифицировать имена IContainer, Box и Stack на-
званием пространства имен, так что нужно будет писать, например, Ex9_161ib: :Вох
вместо просто Box.
Остальная часть кода в точности такая же, как и в функции main () из Ех9_15; ни-
какие изменения не нужны, поскольку теперь используются классы из внешней сбор-
ки. Если вы запустите программу, то увидите тот же вывод, что и в Ех9_15.
Функции, специфицированные как new
Вы уже видели, как следует использовать ключевое слово override для переопре-
деления функций базового класса. Но вы также можете специфицировать функцию
в производном классе как new — в этом случае она скроет функцию базового класса с
той же сигнатурой, и новая функция не будет участвовать в полиморфном поведении.
Чтобы определить функцию Volume () как new в классе NewBox, унаследованном от
Box, можете записать следующий код:
ref class NewBox : Box // Производный класс
public:
// Новая функция для вычисления объема объекта NewBox
virtual double Volume() new
{ return 0.5*m_Length*m_Width*m_Height; }
// Конструктор
NewBox(double lv, double wv, double hv) : Box(lv, wv, hv) {}
Эта версия функции скрывает версию Volume (), определенную в Box, так что ког-
да вы вызываете функцию Volume (), используя дескриптор типа NewBoxА, то вызыва-
ется новая версия. Например:
NewBoxА newBox = gcnew NewBox(2.О, 3.0,4.0);
Console::WriteLine(newBox->Volume()); // Вывод - 12
Получается результат 12, потому что новая функция Volume () скрывает поли-
морфную версию, которую класс NewBox унаследовал от Box.
Новая функция Volume () не является полиморфной, поэтому для полиморфного
вызова с использованием дескриптора типа базового класса новая версия не вызыва-
ется, например:
540 Глава 9
ВохА newBox = gcnew NewBox(2.0, 3.0,4.0);
Console::WriteLine(newBox->Volume()); // Вывод - 24
Единственная полиморфная функция Volume () в классе NewBox — та, что унасле-
дована от класса Box, поэтому именно она и вызывается в данном случае.
Делегаты и события
Событие (event) — это член класса, который позволяет объекту сигнализировать о
наступлении определенного события, и процесс сигнализации для события включает
использование делегата (delegate). Щелчок кнопкой мыши — типичный пример собы-
тия, а объект, получающий событие щелчка, должен сигнализировать о наступлении со-
бытия, вызывая одну или более функций, отвечающих за обработку события. Давайте
сначала рассмотрим делегаты, а к событиям вернемся чуть позже в этой главе.
Идея делегата очень проста — это объект, который инкапсулирует один или бо-
лее указателей на функции, имеющие заданный список параметров и тип возврата.
Таким образом, делегат представляет собой в C++/CLI средство, подобное указателю
на функцию в “родном” C++. Хотя идея делегата проста, все же детали их создания и
использования несколько запутанны, поэтому давайте сосредоточимся.
Объявление делегатов
Объявление делегата выглядит как прототип функции, которому предшествует
ключевое слово delegate, но в действительности оно определяет две вещи: ссылоч-
ное имя типа объекта делегата и список параметров и тип возврата функции, которая
может быть ассоциирована с делегатом. Тип ссылки на делегат имеет в качестве ба-
зового класс System:: Delegate, поэтому тип делегата всегда наследует члены этого
класса. Объявление делегата выглядит как прототип функции, предваренный ключе-
вым словом delegate, но в действительности оно определяет ссылочный тип делега-
та и сигнатуру функции, которая может быть ассоциирована с делегатом. Рассмотрим
пример объявления делегата:
public delegate void Handler(int value); // Объявление делегата
Это определяет тип ссылки на делегат по имени Handler, который является про-
изводным от System: : Delegate. Объект типа Handler может содержать указатели
на одну или более функций, имеющих единственный параметр int и тип возврата
void. Функции, на которые указывает делегат, могут быть функциями экземпляров
или статическими функциями.
Создание делегатов
Имя определенный тип делегата, вы можете создавать объекты делегатов этого
типа. У вас есть выбор между двумя конструкторами делегата: один принимает един-
ственный аргумент, а другой — два.
Аргумент конструктора делегата, принимающий один аргумент, должен быть ста-
тической функцией-членом класса или глобальной функцией, имеющей тип возврата
и список параметров, заданные в объявлении делегата. Предположим, вы определили
класс по имени Handlerclass следующим образом:
public ref class Handlerclass
public:
static void Funl(int m)
{ Console::WriteLine(Ь”Функция1 вызвана co значением {0} ’’, m) ; }
Наследование классов и виртуальные функции 541
static void Fun2(int m)
{ Console::WriteLine(Ь"Функция2 вызвана co значением {0}", m); }
void Fun3(int m)
{ Console::WriteLine(Ь"ФункцияЗ вызвана co значением {0}”, m+value); }
void Fun4(int m)
{ Console::WriteLine(1"Функция4 вызвана co значением {0}", m+value); } ()
Handlerclass():value(1){}
Handlerclass(int m)rvalue(m){}
protected:
int value;
Класс имеет четыре функции с параметром типа int и типом возврата void. Две
из них статические функции и две — функции экземпляра. Он также имеет два кон-
структора, включая один конструктор без аргументов. Класс не делает ничего особен-
ного, кроме выдачи сообщения на экран, позволяющего определить, какая функция
была вызвана, и для функций экземпляра — с каким объектом.
Вы можете создать делегат типа Handler следующим образом:
Handler^ handler = gcnew Handler(Handlerclass::Funl); // Объект делегата
Объект handler содержит адрес статической функции Funl из класса Handler-
Class. Если вызвать делегат, то будет вызвана функция Handlerclass:: Funl () с ар-
гументом, переданным при вызове делегата. Вызов делегата можно написать так:
handler->Invoke(90);
Это вызывает все функции в списке вызовов делегата handler. В данном случае это
будет всего одна функция — Handlerclass: : Funl (), поэтому вывод получится такой:
Функция1 вызвана со значением 90
Вы можете также вызвать делегат следующим оператором:
handler(90);
Это сокращенный вариант предыдущего оператора, который явно вызвал функ-
цию Invoke (), и это та форма вызова делегата, которую вы обычно видите.
Операция + для типов делегатов перегружена, чтобы комбинировать списки вызо-
вов для двух делегатов в новый объект делегата. Например, вы можете модифициро-
вать список вызовов делегата handler с помощью следующего оператора:
handler += gcnew Handler(Handlerclass::Fun2);
Теперь переменная handler ссылается на объект делегата, список вызовов кото-
рого включает две функции: Funl и Fun2. Однако это новый объект делегата. Список
вызовов делегата не может быть изменен, поэтому операция + работает подобно тому,
как она работает с объектами String (вы всегда при этом создаете новый объект). Вы
опять можете вызвать делегат оператором:
handler(80);
Но теперь получите другой вывод:
Функция1 вызвана со значением 80
Функция2 вызвана со значением 80
Обе функции в списке вызовов вызываются в последовательности, в которой были
добавлены к объекту делегата.
Вы можете исключить элемент списка вызовов делегата, используя операцию -=:
handler -= gcnew Handler(Handlerclass::Funl);
542 Глава 9
Это опять создает новый объект делегата, который включает только Handler-
Class : : Fun2 () в свой список вызовов, потому что эффект заключается в удалении
функции, указанной в правой части выражения (Handlerclass: :Funl), и создании
нового объекта, указывающего на оставшуюся функцию.
Обратите внимание, что список вызовов делегата должен содержать хотя бы один указа-
тель функции» Если вы удалите все указатели функций, используя операцию вычитания, то
получите в результате nullp tr.
Используя конструктор делегата, принимающий два параметра, вы передаете в
первом аргументе ссылку на объект из кучи CLR, а во втором — адрес функции экзем-
пляра типа этого объекта. Таким образом, этот конструктор создает делегат, содержа-
щий указатель на функцию экземпляра, специфицированную вторым аргументом, для
объекта, заданного первым аргументом. Вот как можно создать такой делегат:
Handlerclass'4 obj = gcnew Handlerclass;
Handler^ handler2 = gcnew Handler (obj, SHandlerClass::Fun3);
Первый оператор создает объект, а второй — делегат, указывающий на функцию
Fun3 () для объекта obj типа HandlerClass. Делегат принимает аргумент типа int,
поэтому вы можете вызвать его следующим оператором:
handler2(70);
В результате этого будет вызвана функция Fun3 () для объекта obj со значением
аргумента 7 0, так что вывод получится такой:
ФункцияЗ вызвана со значением 71
Значение, хранящееся в поле value объекта obj, равно 1, так как объект создан
с использованием конструктора по умолчанию. Оператор в теле Fun3 () прибавляет
значение поля value к аргументу функции — отсюда 71 в выводе.
Поскольку оба они одного типа, вы можете комбинировать список вызовов для
handler со списком вызовов делегата handler2:
HandlerА handler = gcnew Handler(Handlerclass::Funl); // Объект делегата
handler += gcnew Handler(HandlerClass::Fun2);
Handlerclass'4 obj « gcnew HandlerClass;
Handler74 handler2 « gcnew Handler (obj, ^HandlerClass: :Fun3);
handler += handler2;
Здесь пересоздается handler для ссылки на делегат, включающий указатели на
статические функции Funl () и Fun2 (). Затем вы можете создать новый делегат, на
который ссылается handler, содержащий эти две статические функции плюс функ-
цию экземпляра Fun3 () для объекта obj. После этого вы можете вызвать делегат с
помощью следующего оператора:
handler(50);
и получить такой вывод:
Функция1 вызвана со значением 50
Функция2 вызвана со значением 50
ФункцияЗ вызвана со значением 51
Press any key to continue . . .
Как видим, вызов делегата запускает две статических функции плюс Fun3 () с объ-
ектом obj, так что вы можете комбинировать статические и нестатические функции
в одном списке вызовов для делегата.
Наследование классов и виртуальные функции 543
Теперь давайте соберем некоторые из этих фрагментов вместе в одном примере,
дабы убедиться, что они действительно работают.
Практическое занятие) СОЗДЗНИб И ВЫЗОВ ДвЛеГЭТОВ
Этот пример — попурри из того, что вы уже видели относительно делегатов:
// Ех9_17.срр : main project file.
// Создание и вызов делегатов
#include “stdafx.h"
using namespace System;
public ref class Handlerclass
{
public:
static void Funl(int m)
{ Console::WriteLine(Ь"Функция1 вызвана со значением {0}", m); }
static void Fun2(int m) { Console::WriteLine^"Функция2 вызвана co значением {0}", m); }
void Fun3(int m) { Console::WriteLine^"ФункцияЗ вызвана co значением {0}", m+value); }
void Fun4(int m) { Console::WriteLine^"Функция4 вызвана co значением {0}", m+value); }
Handlerclass()lvalue(1){}
Handlerclass (int m):value(m){}
protected:
int value;
};
public delegate void Handler (int value); // Объявление делегата
int main(array<System::String A> Aargs)
{
HandlerA handler = gcnew Handler(Handlerclass::Funl); // Объект делегата
Console: :WriteLine (Ь"Делегат с одним указателем на статическую функцию:") ;
handler->Invoke(90);
handler += gcnew Handler(Handlerclass::Fun2);
Console::WriteLine(Ь"\пДелегат с двумя указателями на статические функции:");
handler->Invoke(80);
HandlerClassA obj - gcnew Handlerclass;
HandlerA handler2 = gcnew Handler (obj, &HandlerClass::Fun3);
handler +« handler2;
Console: .‘WriteLine (L"\nfleneraT с тремя указателями на функции:");
handler(70);
Console::WriteLine(Ь"\пСокращение списка вызовов...");
handler -= gcnew Handler(Handlerclass::Funl);
Console::WriteLine
(Е"\пДелегат с указателями на одну статическую и одну функцию экземпляра:”);
handler(60);
}
Этот пример выдаст следующий вывод:
Делегат с одним указателем на статическую функцию:
Функция1 вызвана со значением 90
Делегат с двумя указателями на статические функции:
Функция! вызвана со значением 80
Функция2 вызвана со значением 80
544 Глава 9
Делегат с тремя указателями на функции:
Функция! вызвана со значением 70
Функция? вызвана со значением 70
ФункцияЗ вызвана со значением 71
Сокращение списка вызовов...
Делегат с указателями на одну статическую и одну функцию экземпляра:
Функция? вызвана со значением 60
ФункцияЗ вызвана со значением 61
Press any key to continue . . .
Описание полученных результатов
Все операции, присутствующие в main ()
вы уже видели в предыдущем разделе.
Делегат вызывается с явным использованием функции I nvo ke (), а также с приме-
нением дескриптора делегата, за которым следует список аргументов. Как видно из
вывода, все работает так, как надо.
Хотя этот пример показывает делегат, который может содержать указатели на
функции с единственным аргументом, но вообще делегат может указывать на функ-
ции с таким количеством аргументов, какое необходимо. Например, вы могли бы объ-
явить тип делегата следующим образом:
delegate void MyHandler(double x, String* description);
Этот оператор объявляет тип делегата MyHandler, который может указывать толь-
ко на функции с типом возврата void и двумя параметрами: первый типа double, а
второй — String*.
Несвязанные делегаты
Делегаты, которые вы видели до сих пор, были примерами связанных (bound) де-
легатов. Они называются так потому, что каждый имеет фиксированный набор функ-
ция в своем списке вызовов. Но вы также можете создавать и несвязанные (unbound)
делегаты; несвязанный делегат указывает на функцию экземпляра с заданным спи-
ском параметров и типом возврата для заданного типа объектов. Такой делегат может
вызывать функцию экземпляра для любого объекта специфицированного типа. Ниже
приведен пример объявления несвязанного делегата.
public delegate void UBHandler(ThisClass*, int value);
Первый аргумент специфицирует тип указателя this, для которого делегат типа
UBHandler может вызывать функцию экземпляра; функция должна иметь один пара-
метр типа int и тип возврата void. Таким образом, делегат UBHandler может вызы-
вать только функцию для объекта типа ThisClass, но для любого объекта этого типа.
Это может показаться несколько ограниченным, но на самом деле оказывается до-
вольно удобным; вы можете использовать делегат, например, для вызова функции для
каждого элемента типа ThisClassA в массиве.
Делегат типа UBHandler можно создать следующим образом:
UBHandler* ubh = gcnew UBHandler(&ThisClass::Sum);
Аргументом конструктора служит адрес функции в классе ThisClass, которая при-
нимает нужный список параметров и имеет правильный тип возврата.
Вот определение ThisClass:
Наследование классов и виртуальные функции 545
public ref class ThisClass
{
public:
void Sum(int n, StringA str)
{ Console::WriteLine(Ъ"Результат сложения = {0}", value + n); }
void Product(int n, StringA str)
{ Console::WriteLine(Ъ"Результат умножения = {0}", value*n); }
ThisClass(double v) : value(v){}
private:
double value;
};
Функция Sum () — общедоступный член класса ThisClass, поэтому вызов делегата
ubh вызовет функцию Sum () для любого объекта этого типа класса.
Когда вызывается несвязанный делегат, то первым аргументом должен быть объ-
ект, для которого вызываются функции из списка вызовов, а все последующие аргу-
менты — это аргументы этих функций. Вот как может быть вызван делегат ubh:
ThisClassА obj = gcnew ThisClass (99.0);
ubh(obj, 5);
Первый аргумент — дескриптор объекта ThisClass, который был создан в куче
CLR с передачей аргумента 99.0 конструктору класса. Второй аргумент ubh — число
5, поэтому в результате вызывается функция Sum () с аргументом 5 для объекта, на
который ссылается obj.
Вы можете комбинировать несвязанные делегаты с помощью операции +, чтобы
создать делегат, вызывающий множество функций. Конечно, все эти функции долж-
ны быть совместимы с делегатом, поэтому для ubh они должны быть функциями эк-
земпляра класса ThisClass, принимающими единственный параметр типа int и име-
ющими тип возврата void. Ниже показан пример.
ubh += gcnew UBHandler(&ThisClass::Product);
Вывод нового делегата, на который ссылается ubh, вызовет обе функции — Sum ()
и Product () — с объектом типа ThisClass. Рассмотрим это в действии.
Практическое занятие
Использование несвязанного делегата
В этом примере используются фрагменты кода из предыдущего раздела, чтобы
продемонстрировать работу несвязанного делегата.
// Ех9—18.срр : главный файл проекта.
// Использование несвязанного делегата
#include "stdafx.h"
using namespace System;
public ref class ThisClass
{
public:
void Sum(int n, StringA str)
{ Console::WriteLine(Е"Результат сложения = {0}", value + n); }
void Product(int n, StringA str)
{ Console::WriteLine(Е"Результат умножения = {0}", value*n); }
ThisClass(double v) : value(v){}
private:
double value;
546 Глава 9
public delegate void UBHandler (ThisClass74, int value);
int main(array<System::String A> Aargs)
{
array<ThisClass74>74 things = { gcnew ThisClass(5.0),gcnew ThisClass(10.0),
gcnew ThisClass (15.0),gcnew ThisClass(20.0),
gcnew ThisClass(25.0)
UBHandler74 ubh = gcnew UBHandler (&ThisClass: :Sum); //Создать объект делегата
// Вызвать делегат с каждым элементом массива
for each (ThisClass74 thing in things)
ubh(thing, 3);
ubh += gcnew UBHandler (&ThisClass:: Product); // Добавить функцию к делегату
// Вызвать новый делегат с каждым элементом массива
for each(ThisClass74 thing in things)
ubh(thing, 2);
return 0;
Этот пример выдаст следующий вывод:
Результат
Результат
Результат
Результат
Результат
Результат
Результат
Результат
Результат
Результат
Результат
Результат
Результат
Результат
Результат
Press any
= 18
сложения
сложения
сложения
сложения
сложения
сложения
умножения =10
сложения =12
умножения =20
сложения =17
умножения =30
сложения =22
умножения =40
сложения =27
умножения =50
key to continue
Описание полученных результатов
Тип делегата UBHandler объявлен в следующем операторе:
public delegate void UBHandler (ThisClass74, int value) ;
Объекты типа делегата UBHandler являются несвязанными делегатами, который
могут вызывать функции экземпляров для объектов типа ThisClass до тех пор, пока
они имеют один аргумент типа int и тип возврата void.
Определение класса ThisClass в этом примере точно такое же, как вы видели в
предыдущем разделе. Он имеет две функции экземпляра — Sum () и Product (), — ко-
торые принимают параметр типа int и возвращают void, поэтому обе могут быть вы-
званы делегатом типа UBHandler.
Вы создаете массив дескрипторов объектов ThisClass в функции main () с помо-
щью следующего оператора:
array<ThisClass74>74
things = { gcnew ThisClass(5.0),gcnew ThisClass(10.0),
gcnew ThisClass(15.0),gcnew ThisClass(20.0),
gcnew ThisClass (25.0)
Наследование классов и виртуальные функции 547
В списке инициализации создаются пять объектов, каждый из которых инкапсу-
лирует свое значение типа double, так что при вызовах функций Sum () и Product ()
участвующий объект можно будет легко идентифицировать по выводу.
Объект делегата создается следующим оператором:
UBHandler74 ubh = gcnew UBHandler(&ThisClass::Sum); // Создать объект делегата
Вызов объекта делегата, на который ссылается ubh, вызывает функцию Sum () для
любого объекта типа ThisClass, что и делается с каждым объектом в массиве things:
for each(ThisClass74 thing in things)
ubh(thing, 3);
Цикл for each выполняет итерацию по каждому элементу массива things, так
что в теле цикла вызывается делегат с элементом массива в качестве первого аргумен-
та. Это заставляет вызвать функцию Sum () для объекта thing с аргументом 3. Таким
образом, цикл выдает пять строк вывода.
Затем вы создаете новый делегат:
ubh += gcnew UBHandler(&ThisClass::Product); // Добавить функцию к делегату
Этот оператор создает новый делегат UBHandler, который указывает на функцию
Product (), и комбинирует его с существующим делегатом, на который ссылается
ubh. В результате получается другой делегат, включающий указатели на обе функ-
ции — Sum () и Product О — в списке вызовов.
Последний цикл вызывает делегат ubh для каждого элемента массива things с ар-
гументом— значением 2. В результате для каждого объекта ThisClass будут вызваны
обе функции Sum () и Product () с аргументом 2, так что цикл сгенерирует следую-
щие десять строк вывода.
Хотя вы используете несвязанные делегаты очень простым способом, они при-
дают потрясающую гибкость вашим программам. Вы, например, можете передать
несвязанный делегат в качестве аргумента функции, чтобы вызов одной и той же
функции позволял вызывать разные комбинации функций экземпляров в разные
моменты, так чтобы несвязанный делегат выступал в качестве селектора функций.
Последовательность вызова функций делегатом определяется последовательностью
их добавления в список вызовов, так что делегат предоставляет средство управления
последовательностью вызовов функций.
Создание событий
Как уже было сказано ранее, сигнализация о событиях включает использование де-
легата, а делегат содержит указатели на функции, которые должны быть вызваны при
возникновении события. Большинство событий, с которыми вы имеете дело в своих
программах, ассоциированы с элементами управления вроде кнопок или меню, и эти
события инициируются в процессе взаимодействия пользователя с кодом программы.
Событие — это член ссылочного класса, который вы определяете с использовани-
ем ключевого слова event и имени класса-делегата.
public delegate void DoorHandler (String74 str);
// Класс с членом-событием
public ref class Door
public:
// Событие, которое вызовет функции, ассоциированные
//с объектом делегата DoorHandler
event DoorHandler74 Knock;
548 Глава 9
/ / Функция для возбуждения событии
void TriggerEvents()
Knock (’’Fred”);
Knock (’’Jane”);
Класс Door включает член — событие по имени Knock, которое соответствует де-
легату типа DoorHandler. Knock — член экземпляра класса, но вы можете специфи-
цировать событие и как статический член класса, применив ключевое слово static.
Можно также объявить событие как virtual. Когда инициируется событие Knock,
оно может вызвать функции со списком параметров и типом возврата, специфициро-
ванными делегатом DoorHandler.
Класс Door также имеет общедоступную функцию TriggerEvent (), которая ини-
циирует два события Knock, каждое со своим аргументом. Аргументы передаются
функциям, которые зарегистрированы (“подписаны”) для получения уведомлений о
событии Knock. Как видите, инициация события — это по сути то же самое, что вызов
делегата.
Класс, который обработает событие Knock, можно определить следующим образом:
public ref class AnswerDoor
public:
void Imln(String* name)
Console::WriteLine (Ь’’3аходи, {0}, открыто.”,name);
void ImOut(String* name)
Console::WriteLine(Ь”Уходи, {0}r меня нет.”, name);
}
Класс AnswerDoor включает две функции-члена, которые потенциально могут об-
работать событие Knock, поскольку обе имеют списки параметров и типы возврата,
идентифицированные в объявлении делегата DoorHandler.
Прежде чем зарегистрировать функции, которые должны принимать уведомления
о событии Knock, вы должны создать объект Door. Это можно сделать так:
Door* door = gcnew Door;
Теперь вы можете зарегистрировать функцию для получения уведомления о собы-
тии Knock в объекте door следующим образом:
AnswerDoor* answer = gcnew AnswerDoor;
door->Knock += gcnew DoorHandler(answer, &AnswerDoor::Imln);
Первый оператор создает объект типа AnswerDoor — это необходимо, потому что
функции Imln () и ImOut () являются нестатическими членами класса. Затем вы до-
бавляете экземпляр делегата типа DoorHandler к члену Knock объекта door. Это ана-
логично процессу добавления указателей на функции к делегату, и вы можете в дальней-
шем точно так же добавлять дополнительные функции-обработчики, которые должны
быть вызваны при возникновении события Knock. Рассмотрим это в примере.
Наследование классов и виртуальные функции 549
Практическое занятие | ОбрЭбОТКЭ СОбЫТИЙ
В следующем примере используются классы из предыдущего раздела для определе-
ния, возбуждения и обработки событий.
// Ех9—19.срр : главный файл проекта.
// Определение, возбуждение и обработка событий.
#include "stdafx.h”
using namespace System;
public delegate void DoorHandler (String'4 str);
// Класс с членом-событием
public ref class Door
{
public:
// Событие, которое вызовет функции, ассоциированные
//с объектом делегата DoorHandler
event DoorHandler74 Knock;
// Функция для возбуждения событий
void TriggerEvents ()
{
Knock(Ь”Фред”);
Knock(Ь"Джейн");
}
};
// Класс, определяющий функции-обработчики событий Knock
public ref class AnswerDoor
{
public:
void Imln (String74 name)
{
Console::WriteLine(Ь”3аходи, {0}, открыто.”, name);
}
void ImOut (String74 name)
{
Console::WriteLine(Ь"Уходи, {0}, меня нет.”,name);
}
};
int main(array<System::String Л> Лагдз)
{
Door74 door = gcnew Door;
AnswerDoor74 answer = gcnew AnswerDoor;
// Добавить обработчик события Knock - члена door
door->Knock += gcnew DoorHandler(answer, &AnswerDoor::Imln);
door->TriggerEvents (); // Инициировать событие Knock
// Изменить способ обработки события Knock
door->Knock -= gcnew DoorHandler(answer, &AnswerDoor::Imln);
door->Knock += gcnew DoorHandler(answer, &AnswerDoor::ImOut);
door->TriggerEvents(); // Инициировать событие Knock
return 0;
}
Выполнение этого примера даст следующий результат:
Заходи, Фред, открыто.
Заходи, Джейн, открыто.
Уходи, Фред, меня нет.
Уходи, Джейн, меня нет.
Press any key to continue . . .
550 Глава 9
Описание полученных результатов
Сначала вы определяете два объекта в функции ma i п ():
DoorA door = gcnew Door;
AnswerDoor74 answer = gcnew AnswerDoor;
Объект door имеет член-событие Knock, а объект answer — функцию-член, кото-
рая может быть зарегистрирована для вызова при наступлении событий Knock.
Следующий оператор регистрирует функцию-член Imln () объекта answer для по-
лучения уведомлений событий Knock объекта door:
door->Knock += gcnew DoorHandler(answer, &AnswerDoor::Imln);
Если это имеет смысл, вы можете зарегистрировать и другие функции для вызова
при наступлении события Knock.
Следующий оператор вызывает функцию-член Trigger Events () объекта door:
door->TriggerEvents(); // Инициировать событие Knock
Эта функция инициирует два события Knock — одно с аргументом "Фред", вто-
рое— с аргументом "Джейн". В результате этого для каждого события по одному разу
вызывается функция Imln (), что дает первые две строки вывода.
Конечно, может возникнуть необходимость реагировать на событие по-разному в
разное время, в зависимости от обстоятельств, и это демонстрируют следующие три
оператора main ():
door-Жпоск -= gcnew DoorHandler(answer, SAnswerDoor::Imln);
door->Knock += gcnew DoorHandler(answer, &AnswerDoor::ImOut);
door->TriggerEvents(); // Инициировать событие Knock
Первый оператор исключает указатель на функцию Imln () из события, а второй
оператор регистрирует функцию ImOut () для объекта answer с целью получения
уведомлений о наступлении событии. Когда инициируются события Knock в третьем
операторе, вызывается функция Imln (), поэтому результаты несколько отличаются.
Деструкторы и финализаторы в ссылочных классах
Вы можете определить деструктор для ссылочного класса точно так же, как
определяете деструктор в классе “родного” C++. Деструктор для ссылочного класса
вызывается, когда дескриптор выходит из области видимости, или объект является
частью другого объекта, который уничтожается. Вы можете также применить опе-
рацию delete к дескриптору объекта ссылочного класса, в результате чего будет вы-
зван деструктор. Главная причина для реализации деструкторов в классах “родного”
C++ заключается в необходимости иметь дело с данны ми-членам и, динамически раз-
мещенными в куче, но очевидно, что это не касается ссылочных классов, поэтому не-
обходимость в деструкторах здесь меньше. Они могут понадобиться, когда объекты
класса используют другие ресурсы, не находящиеся под управлением сборщика му-
сора, такие как файлы, которые должны быть закрыты обычным образом при уни-
чтожении объекта. Также вы можете очистить такие ресурсы в членах классов иного
рода, называемых финализаторами (finalizer).
Финализатор — это специальный вид функции-члена ссылочного класса, которая
вызывается явно или в результате применения к объекту операции delete. В произ-
водных классах финализаторы вызываются в том же порядке, в каком должны были
быть вызваны деструкторы, то есть финализатор самого базового класса вызывается
первым, за ним идут финализаторы всех последующих классов иерархии, и послед-
ним вызывается финализатор последнего производного класса.
Наследование классов и виртуальные функции 551
Финализатор класса может быть определен примерно так:
public ref class MyClass
{
// Определение финализатора
!MyClass()
{
// Код очистки уничтожаемого объекта...
.}
// Остальная часть определения класса...
};
Вы определяете в классе функцию-финализатор почти так же, как деструктор, но
вместо символа ~ перед именем класса (как в деструкторе), используется символ !.
Как и в деструкторе, вы не должны специфицировать тип возврата финализатора, а
спецификатор доступа игнорируется. Проиллюстрируем на небольшом примере ра-
боту деструкторов и финализаторов.
Практическое занятие | фиНаЛИЗЭТОрЫ И ДвСТруКТОрЫ
Пример демонстрирует, когда деструкторы и финализаторы вызываются в про-
грамме.
// Ех9_20.срр : главный файл проекта.
// Финализаторы и деструкторы
#include ’’stdafx.h"
using namespace System;
ref class MyClass
{
public:
// Конструктор
MyClass(int n) : value(n){}
// Деструктор
-MyClass()
{
Console::WriteLine("Вызван деструктор объекта MyClass({0})value);
}
// Финализатор
!MyClass()
{
Console: : WriteLine ("Вызван финализатор объекта MyClass({0}) value);
}
private:
int value;
};
int main(array<System::String A> Aargs)
{
MyClass74 objl = gcnew MyClass(l);
MyClass74 obj2 = gcnew MyClass (2);
MyClass74 obj3 = gcnew MyClass (3);
delete objl;
obj2->-MyClass();
Console::WriteLine(Е"Конец программы");
return 0;
}
552 Глава 9
Ниже показан вывод этого примера.
Вызван деструктор объекта MyClass(l) .
Вызван деструктор объекта MyClass(2) .
Конец программы
Вызван финализатор объекта MyClass(3).
Press any key to continue . . .
Описание полученных результатов
Класс MyClass имеет конструктор, деструктор и финализатор. Деструктор и фи-
нализатор просто выводят сообщение в командную строку, чтобы было видно, когда
каждый из них вызван. Кроме того, вы можете видеть, для которого именно объекта
вызван финализатор или деструктор, потому что они выводят значение поля value.
В функции main () создается три объекта типа MyClass, инкапсулирующие значе-
ния 1, 2 и 3, чтобы их можно было различать. Затем применяется операция delete
к objl и явно вызывается деструктор для obj 2. Вызовы деструкторов генерирует две
первых строки вывода.
Следующая строка вывода генерируется оператором, предшествующим return в
main (), так что последняя строка вывода, генерируемая финализатором для obj 3,
появляется уже после окончания ma i п (). Вывод показывает, что деструкторы вызы-
ваются, когда удаляется объект, либо деструктор вызывается явно. При этом в обоих
случаях подавляется выполнение финализаторов объектов. Объект, на который ссы-
лается obj3, уничтожается сборщиком мусора по завершении программы, поэтому
финализатор вызывается для очистки неуправляемых ресурсов.
Таким образом, если у класса есть и деструктор, и финализатор, только один из
них будет вызван при уничтожении объекта. Деструктор вызывается, если вы про-
граммно уничтожаете объект, а финализатор — когда он исчезает сам или выходит
из области видимости. Отсюда можно сделать вывод, что если вы полагаетесь на фи-
нализатор для очистки неуправляемых ресурсов при уничтожении объекта, то вы не
должны удалять его явно.
Если закомментировать операторы в ma i п(), которые уничтожают objl и obj2,
то вы увидите, что финализаторы этих объектов будут вызваны по завершении про-
граммы. С другой стороны, если вы закомментируете финализатор в MyClass, то
увидите, что деструктор для obj 3 не вызывается сборщиком мусора, так что никакой
очистки не происходит. Отсюда можно сделать вывод, что если вы хотите быть уве-
рены, что неуправляемые ресурсы, используемые объектом, корректно освобождают-
ся, независимо от того, как уничтожается объект, то вам следует реализовать в классе
и деструктор и финализатор.
Обобщенные классы
C++/CLI предоставляет вам возможность определения обобщенных (generic)
классов — когда специфический класс создается из типа обобщенного класса во вре-
мя выполнения. Вы можете определить обобщенные классы значений, обобщенные
ссылочные классы, обобщенные интерфейсные классы и обобщенные делегаты.
Обобщенный класс определяется с использованием одного или более параметра
типа, подобно тому, как определяются обобщенные функции, о которых шла речь в
главе 6.
Например, вот как можно определить обобщенную версию класса Stack, который
вы видели в Ех9 14:
Наследование классов и виртуальные функции
553
// Stack.h для Ех9_21
// Обобщенный стек магазинного типа
generic<typename Т> ref class Stack
private:
// Определение элементов, помещаемых в стек
ref struct Item
Т Obj; // Дескриптор объекта данного элемента
Item74 Next; // Дескриптор следующего элемента в стеке или nullptr
// Конструктор
Item(T obj, Item74 next): Obj (obj), Next (next) {}
Item74 Top; // Дескриптор элемента, находящегося на верхушке
public:
I / Затолкнуть объект в стек
void Push(T obj)
Top - gcnew Item(obj, Top); //Создать элемент и поместить его на верхушку
// Вытолкнуть объект из стека
Т Pop ()
if (Тор == nullptr) // Если стек пуст,
return Т(); // вернуть эквивалент null
Т obj = Top->Obj; // Получить объект из элемента
Тор = Top->Next; // Поместить на верхушку следующий
return obj;
}
Обобщенная версия класса теперь имеет параметр типа — Т. Обратите внимание,
что вы можете использовать ключевое слово class вместо type name, когда специфи-
цируете параметр — в данном контексте между ними разницы нет. Аргумент-тип заме-
няет Т при использовании обобщенного типа класса; Т заменяется аргументом типа в
определении класса, так что основное преимущество перед первоначальной версией
в том, что обобщенный тип намного безопаснее, причем без потери гибкости. Член
Push () оригинального класса принимает любой дескриптор, поэтому вы можете с
успехом поместить в один и тот же стек смесь объектов типа MyClass74, String74 или
дескриптор любого другого типа, в то время как экземпляр обобщенного типа при-
нимает только объекты того типа, который специфицирован как аргумент-тип при
объявлении обобщенного стека.
Взгляните на реализацию функции Pop (). Оригинальная версия возвращает
nullptr, если стек пуст, но вы не можете вернуть nullptr для параметра типа, по-
тому что аргумент-тип может быть типом значения. Решение заключается в возврате
Т(), то есть вызова конструктора без аргументов для типа Т. Это дает результат, экви-
валентный 0 для типа значения и nullptr для дескриптора.
Обратите внимание, что вы можете специфицировать конструктор в параметре типа
обобщенного класса, используя ключевое слово where, точно так же, как это делалось с обоб
щечными функциями в главе 6.
Вы можете создать стек из обобщенного типа Stacko, который хранит дескрип-
торы объектов Box, следующим образом:
Stack<Box74>74 stack = gcnew Stack<Box74>;
554 Глава 9
Аргумент-тип ВохА указывается между угловыми скобками, и оператор создает объ-
ект Stack<BoxA> в куче CLR. Этот объект позволяет сохранять в стеке дескрипторы
типа ВохА, а также всех других производных от него типов. Вы можете убедиться в
этом, рассмотрев измененную версию Ех9_14.
Практическое занятие ИСПОЛЬЗОВЭНИб ТИПЭ ОбобЩвННОГО КЛЭССЯ
Создайте новую консольную программу CLR по имени Ех9_21 и скопируйте заго-
ловочные файлы Container .h, Box.h и GlassBox.h из Ех9_14 в каталог этого проек-
та. Добавьте эти заголовки к проекту, щелкнув правой кнопкой на папке Header Files
(Заголовочные файлы) во вкладке Solution Explorer (Проводник решений) и выбрав
пункт Add1^ Existing Item (Добавить^Существующий элемент) из контекстного меню.
Вы можете добавить к проекту новый заголовочный файл Stack.h и ввести опреде-
ление обобщенного класса Stack, которое приведено в предыдущем разделе. Не за-
будьте о директиве #pragma once в начале файла.
// Ех9_21.срр : главный файл проекта.
// Использование обобщенного класса для определения стека
#include "stdafx.h”
#include "Box.h" // Для Box и Container
#include "GlassBox.h" // Для GlassBox (а также Box и Container)
#include "Stack.h" // Для обобщенного класса стека
using namespace System;
int main(array<System::String A> Aargs)
{
array<BoxA>A boxes = { gcnew Box(2.0, 3.0, 4.0),
gcnew GlassBox(2.0, 3.0, 4.0),
gcnew Box(4.0, 5.0, 6.0),
gcnew GlassBox(4.0, 5.0, 6.0)
};
Console::WriteLine(1"Массив ящиков, имеющих следующие объемы:");
for each(BoxA box in boxes)
box->ShowVolume(); // Вывод объема ящика
Console::WriteLine(Ь"\пТеперь затолкаем ящики в стек...");
Stack<BoxA>A stack = gcnew Stack<BoxA>; // Создать стек
for each(BoxA box in boxes)
stack->Push(box);
Console::WriteLine(
L"Выталкивание ящиков из стека представляет их в обратном порядке:");
ВохА item;
while((item = stack->Pop ()) != nullptr)
safe_cast<ContainerA>(item)->ShowVolume();
// Попробовать тип Stack для хранения целых чисел
Stack<int>A numbers = gcnew Stack<int>; // Создать стек
Console::WriteLine(L"\nTenepb поместим в стек целые числа:");
for (int i - 2 ; i<=12 ; i += 2)
{
Console: .-Write (L"{0,5}",i);
numbers->Push(i);
}
int number;
Console::WriteLine^"\п\пВыталкивание целых из стека производит:");
while((number = numbers->Pop()) != 0)
Console::Write(L"{0,5}", number);
Console::WriteLine();
return 0;
}
Наследование классов и виртуальные функции 555
Этот пример генерирует следующий вывод:
Массив ящиков, имеющих следующие объемы:
Полезный объем СВох равен 24
Полезный объем СВох равен 20.4
Полезный объем СВох равен 120
Полезный объем СВох равен 102
Теперь затолкаем ящики в стек...
Выталкивание ящиков из стека представляет их в обратном порядке:
Полезный объем СВох равен 102
Полезный объем СВох равен 120
Полезный объем СВох равен 20.4
Полезный объем СВох равен 24
Теперь поместим в стек целые числа...
2 4 6 8 10 12
Выталкивание целых из стека производит:
12 10 8 6 4 2
Press any key to continue . . .
Описание полученных результатов
Стек для хранения дескрипторов объектов Box определяется в следующем опера-
торе:
Stack<BoxA>A stack « gcnew Stack<BoxA>; // Создать стек
Параметром типа здесь выступает ВохА,
так что стек хранит дескрипторы объек-
тов Box или GlassBox. Код для помещения объектов в стек и выталкивания их обрат-
но в точности такой же, как в Ех 9 14. Вывод также идентичен. Отличие здесь в том
что вы не можете затолкнуть в стек объект, если его тип не унаследован от Box — не-
посредственно или опосредованно. То есть обобщенный тип гарантирует, что все
объекты в стеке будут объектами Box.
Для сохранения целых чисел теперь потребуется новый объект St ас ко:
Stack<int>A numbers = gcnew Stack<int>; // Создать стек
В оригинальной версии использовался один и тот же необобщенный объект
Stack и для хранения ссылок на объекты Box, и для хранения целых, демонстрируя
тем самым, что ни о какой безопасности типов там не могло быть речи. Здесь же вы
специфицируете аргумент-тип для обобщенного класса как тип значения int, так что
только объекты этого типа принимает функция Push ().
Циклы, которые выталкивают объекты из стека, демонстрируют, что возврат Т ()
из функции Pop () и в самом деле возвращает 0 для типа int, и nullptr для типа де-
скриптора ВохЛ.
Обобщенные интерфейсные классы
Можно определять обобщенные интерфейсы точно так же, как определяются
обобщенные ссылочные классы, а обобщенный ссылочный класс может быть опреде-
лен в терминах обобщенного интерфейса. Чтобы продемонстрировать, как это ра-
ботает, вы можете определить обобщенный интерфейс, которые может быть реали-
зован обобщенным классом St ас ко. Рассмотрим пример определения обобщенного
интерфейса.
556 Глава 9
// Интерфейс для операций со стеком
genericctypename Т> public interface class IStack
void Push(T obj); // Затолкнуть элемент в стек
Т Pop(); // Вытолкнуть элемент из стека
Этот интерфейс включает две функции, идентифицирующие операцию заталкива-
ния и выталкивания элемента из стека.
Определение обобщенного класса Stacko, реализующего обобщенный интер-
фейс IStacko, выглядит так:
genericctypename Т> ref class Stack : IStack<T>
private:
/ / Определение элементов для помещения в стек
ref struct Item
{
Т Obj; // Дескриптор объекта в данном элементе
ItemA Next; // Дескриптор следующего элемента в стеке или nullptr
// Конструктор
Item(T obj, Item* next): Obj(obj), Next(next){}
ItemA Top; // Дескриптор элемента, находящегося на верхушке
public:
// Затолкнуть объект в стек
virtual void Push(T obj)
Top = gcnew Item(obj, Top); //Создать элемент и поместить его на верхушку
/ / Вытолкнуть объект из стека
virtual Т Pop ()
if (Тор == nullptr) // Если стек пуст,
return Т(); // вернуть эквивалент null
Т obj = Top->Obj; // Получить объект их элемента
Тор - Top->Next; // поместить на верхушку следующий
return obj;
Изменения по сравнению с предыдущим определением обобщенного класса
Stacko выделены полужирным. В первой строке определения обобщенного класса
параметр типа Т используется в качестве аргумента-типа к интерфейсу IStack, так
что аргумент-тип, использованный для экземпляра класса Stacko, также применяет-
ся к интерфейсу. Функции Push () и Pop () в классе теперь должны быть специфици-
рованы как virtual, потому что эти функции являются виртуальными в интерфейсе.
Вы можете добавить заголовочный файл, содержащий интерфейс IStack, к предыду-
щему примеру, исправить определение обобщенного класса Stacko и перекомпили-
ровать программу, чтобы увидеть, как она работает с обобщенным интерфейсом.
Обобщенные классы коллекций
Класс коллекции — это класс, который организует и хранит объекты определен-
ным образом; типичными примерами классов коллекций являются связный список и
стек. Пространство имен System::Collections: :Generic содержит широкий диа-
Наследование классов и виртуальные функции 557
пазон обобщенных классов коллекций, которые реализуют строго типизированные
коллекции. Доступные обобщенные классы коллекций перечислены в табл. 9.3.
Таблица 9.3. Обобщенные классы коллекций
Тип Описание
List<T> Сохраняет элементы типа т в простом списке, который при необходимости
может расти в размере автоматически.
LinkedList<T> Сохраняет элементы типа т в двунаправленном связном списке.
stack<T> Сохраняет элементы типа т в стеке, использующем механизм “первый
вошел — последний вышел”.
Queue<T> Сохраняет элементы типа Т в очереди, использующей механизм “первый
вошел — первый вышел”.
Dictionary<K, v> Сохраняет пары “ключ-значение”, где ключи имеют тип к, а значения — тип V.
Я не стану сейчас слишком погружаться в детали всего этого, но кратко опишу три
из этих коллекций, которые, вероятно, вы сразу же захотите использовать в своих
программах. Для простоты я использую примеры, которые хранят элементы типов
значений, но конечно, классы коллекций также хорошо работают и со ссылочными
типами.
List<T> — обобщенный список
List<T> определяет обобщенный список, который при необходимости автомати-
чески увеличивается в размере. Вы можете добавлять элементы к списку, используя
функцию Add (), и обращаться к элементам, хранимым в List<T>, по индексу — так
же, как в массиве. Вот как определить список для хранения значений типа int:
List<int> numbers = gcnew List<int>;
Список имеет емкость по умолчанию, но вы можете специфицировать любую ем-
кость, которая вам необходима. Вот как определить список емкостью в 500 элемен-
тов:
List<int> numbers = gcnew List<int>(500);
Вы можете добавлять объекты в список, используя функции Add ():
for (int i = 0 ; i<1000 ; i++)
numbers->Add( 2*i+l);
Здесь добавляется 1 000 целых чисел к списку numbe г s. Список растет автоматиче-
ски, если его начальная емкость меньше 1000. Когда вы хотите вставить элемент в су-
ществующий список, вы можете использовать функцию Insert () для вставки элемен-
та, указанного вторым аргументом в позицию индекса, заданную первым аргументом.
Элементы в списке индексируются, начиная с нуля — как и в массивах.
Просуммировать содержимое списка можно следующим образом:
int sum =0;
for (int i = 0 ; i<numbers->Count ; i++)
sum += numbers [i];
Count — это свойство, которое возвращает текущее количество элементов в спи-
ске. Элементы списка могут быть доступны через индексируемое свойство по умолча-
нию, и вы можете устанавливать и читать значения таким образом. Обратите внима-
558 Глава 9
ние, что вы не можете увеличить емкость списка, используя индексируемое свойство
по умолчанию. Если вы используете индекс вне диапазона элементов списка, будет
возбуждено исключение.
Элементы списка можно просуммировать и так:
for each(int n in numbers)
sum +=n;
В вашем распоряжении имеется широкий диапазон других функций, которые вы
можете применить к списку, включая функции для удаления элементов, сортировки и
поиска содержимого в списке.
LinkedList<T> — обобщенный двухсвязный список
LinkedList<T> определяет связный список с прямыми и обратными указателями,
так что вы можете выполнять итерацию по списку в любом направлении. Определить
связный список для хранения значений с плавающей точкой можно следующим об-
разом:
LinkedList<double>A values = gcnew LinkedList<double>;
Добавить значения в такой список можно так:
for(int i = 0 ; i<1000 ; i++)
values->AddLast(2.5*i);
Функция AddL i s t () добавляет элемент в конец списка. Вы можете добавлять эле-
менты в начало списка с помощью функции AddF i г s t (). Альтернативно для тех же
целей можно использовать функции AddHead () и AddTail ().
Функция Find () возвращает дескриптор типа LinkedListNode<T>A, указывающий
узел в списке, который содержит значение, переданное в виде аргумента Find (). Вы
можете использовать этот дескриптор для вставки нового значения перед или после
найденного узла. Например:
LinkedListNode<double>A node - values->Find(20.0); //Найти узел, содержащий 20.0
if(node != nullptr)
values->AddBefore(node, 19.9); // Вставить 19.9 перед этим узлом
Первый оператор ищет узел, содержащий значение 20.0. Если он не существует,
функция Find () возвращает nullptr. Последний оператор выполняется, если node
не равно null, добавляя новое значение 19.9 перед найденным узлом. Вы можете
воспользоваться функцией AddAf ter () для добавления нового значения после дан-
ного узла. Поиск в связном списке относительно медленный, поскольку требуется вы-
полнять последовательную итерацию по всем элементам.
Просуммировать элементы списка можно следующим образом:
double sumd = 0;
for each(double v in values)
sumd += v;
Цикл for each выполняет итерацию по всем элементам списка и аккумулирует
сумму в sum.
Свойство Count возвращает количество элементов в связном списке, а свойства
Head и Tail возвращают значения, соответственно, первого и последнего элемента.
Свойства First и Last — альтернатива Head и Tail.
Наследование классов и виртуальные функции 559
Dictionary<TKey, TValue>— обобщенный словарь,
хранящий пары “ключ-значение”
Обобщенный класс коллекции Dictionaryo принимает два аргумента-типа, пер-
вый — тип ключа, второй — тип значения, ассоциированного с ключом. Словарь осо-
бенно удобен, когда нужно хранить пары объектов, из которых один является ключом
для доступа к другому объекту. Имя и номер телефона — пример пары “ключ-значе-
ние”, которую нужно сохранить в коллекции, поскольку обычно вам нужно извлекать
телефонный номер, используя имя в качестве ключа. Предположим, вы определили
классы Name и PhoneNumber для инкапсуляции имен и номеров телефонов соответ-
ственно. Вы можете определить словарь для хранения пар “имя-номер” следующим
образом:
Dictionary<NameA, PhoneNumberЛ>Л phonebook ® gcnew Dictionary<NameA, PhoneNumberл>;
Здесь два аргумента-типа — NameA и PhoneNumberА, так что ключ — дескриптор
имени, а значение — дескриптор номера телефона.
Добавить элемент в словарь phonebook можно так:
Name74 name = gcnew Name("Jim", "Jones");
PhoneNumberA number = gcnew PhoneNumber(914, 316, 2233);
phonebook->Add(name, number); // Добавить в словарь пару имя/номер
Для извлечения элемента из словаря можете воспользоваться индексируемым
свойством по умолчанию:
try
{
PhoneNumberA theNumber = phonebook[name];
}
catch(KeyNotFoundFoundExceptionA knfe)
{
Console::WriteLine(knfe);
}
Здесь вы применяете ключ в качестве индекса для индексируемого свойства по
умолчанию, который в данном случае является дескриптором объекта Name. Если
ключ присутствует, возвращается значение, иначе возбуждается исключение KeyNot-
FoundFoundException; таким образом, если есть вероятность, что будет указан ключ,
которого нет в словаре, код должен быть помещен в блок try.
Объект Diet^onaryo имеет свойство Keys, которое возвращает коллекцию, со-
держащую ключи словаря — так же, как свойство Values возвращает коллекцию зна-
чений. Свойство Count возвращает количество пар “ключ-значение”, хранящихся в
словаре.
Попробуем применить кое-что из этого в работающем примере. * В
Практическое занятие | ИСПОЛЬЗОВЭНИе ОбобЩвННЫХ КЛЭССОВ
коллекций
В этом примере испытываются три класса коллекций, описанных выше.
// Ех9_22.срр : главный файл проекта.
// Использование обобщенных классов коллекций
tfinclude "stdafx.h"
using namespace System;
560 Глава 9
using namespace System::Collections::Generic; // Для обобщенных коллекций
// Класс, инкапсулирующий имя
ref class Name
public:
Name (String"4 namel, String"4 name2) : First(namel),Second(name2){}
virtual String"4 ToStringO override { return First +L" "+Second;}
private:
String^ First;
String^ Second;
};
// Класс, инкапсулирующий номер телефона
ref class PhoneNumber
public:
PhoneNumber(int area, int local, int number):
Area(area),Local(local), Number(number){}
virtual String"4 ToStringO override
{ return Area +L" "+Local+L” "+Number; }
private:
int Area;
int Local;
int Number;
int main(array<System::String A> Aargs)
// Использование List<T>
Console::WriteLine^"Создание List<T> целых чисел:");
List<int>"4 numbers = gcnew List<int>;
for(int i = 0 ; i<1000 ; i++)
numbers->Add(2*i+l);
// Просуммировать содержимое списка
int sum = 0;
for (int i = 0 ; i<numbers->Count ; i++)
sum += numbers [i];
Console::WriteLine(L"HTor = {0}", sum);
// Использование LinkedList<T>
Console::WriteLine(Ь"\пСоздание LinkedList<T> значений double:");
LinkedList<double>A values = gcnew LinkedList<double>;
for(int i = 0 ; i<1000 ; i++)
values->AddTail(2.5*i);
double sumd = 0.0;
for each(double v in values)
sumd += v;
Console::WriteLine(L"HTor = {0}", sumd);
LinkedListNode<double>A node = values->Find(20.0);//Найти узел, содержащий 20.0
values~>AddBefore(node, 19.9);
values->AddAfter(values->Find(30.0), 30.1);
// Просуммировать содержимое связного списка еще раз
sumd = 0.0;
for each(double v in values)
sumd += v;
Console::WriteLine(П"Итог после добавления элементов = {0}", sumd);
Наследование классов и виртуальные функции 561
// Использование Dictionary<K,V>
Console::WriteLine(Ь”\пСоздание Dictionary<K,V> пар имя/номер:");
Dictionary<NameA, PhoneNumberA>A phonebook = gcnew Dictionary<NameA, PhoneNumberA>;
// Добавить в словарь пары ’’имя-номер”
Name74 name = gcnew Name (’’Джим”, ’’Джонс”);
PhoneNumberA number = gcnew PhoneNumber(914, 316, 2233);
phonebook->Add(name, number);
phonebook->Add (gcnew Name (’’Фред”, ’’Фонг”), gcnew PhoneNumbe r (123,234,3456));
phonebook->Add(gcnew Name (”Джанет”, ’’Смит”), gcnew PhoneNumber (515,224,6864));
// Вывести список всех номеров
Console::WriteLine(Ь”Список номеров:”);
for each(PhoneNumberА number in phonebook->Values)
Console::WriteLine(number);
// Вывести список имен с номерами
Console::WriteLine(Ь”Вывод списка пар имя/номер по ключам:”);
for each(NameA name in phonebook->Keys)
Console::WriteLine(L”{0} : {1}”, name, phonebook[name]);
return 0;
Вывод этого примера должен быть таким:
Создание List<T> целых:
Итог = 1000000
Создание LinkedList<T> значений double:
Итог = 1248750
Итог после добавления значений = 1248800
Создание Dictionary<K,V> пар имя/номер:
Список всех номеров:
914 316 2233
123 234 3456
515 224 6864
Вывод списка пар имя/номер по ключам:
Джим Джонс : 914 316 2233
Фред Фонг : 123 234 3456
Джанет Смит : 515 224 6864
Press any key to continue . . .
Описание полученных результатов
Обратите внимание на директиву using name space для пространства имен
System: :Collections : :Generic; это существенно, если вы хотите использовать
обобщенные классы коллекций, не специфицируя их полностью квалифицированные
имена.
Первый блок кода в main () использует коллекцию Listo, применяя код из пред-
ыдущего раздела. Он создает объект класса, который может хранить списки целых
значений, и затем помещает в него 1000 значений. Цикл, суммирующий содержимое
списка, использует индексируемое свойство по умолчанию для получения значений.
Это можно также написать в виде цикла for each. Не забывайте: индексируемое
свойство по умолчанию обращается только к элементам, уже содержащимся в спи-
ске. Можно изменить значение существующего элемента, применяя индексируемое
свойство по умолчанию, но таким образом нельзя добавить новый элемент. Чтобы
добавить новый элемент в конец списка, используется функция Add (); можно также
применить функцию Insert () для вставки элемента в заданную позицию индекса.
562 Глава 9
Следующий блок кода в main () демонстрирует использование коллекции
LinkedListo для сортировки значений типа double. Значения типа double добав-
ляются в конец связного списка с помощью функции AddTail () в цикле for. Точно
так же вы могли бы использовать функцию AddLst (), чтобы сделать то же самое.
Значения извлекаются и суммируются в цикле for each. Обратите внимание, что нет
индексируемого свойства по умолчанию для обращения к элементам связного списка.
В коде также показано использование функций Find (), AddBef ore () и AddAfter ()
для вставки элементов в определенную позицию связного списка.
Последний блок кода main () показывает применение коллекции Dictionaryo
для хранения телефонных номеров с именами в качестве ключей. Классы Name и
PhoneNumber реализованы с переопределением унаследованной функции ToString (),
чтобы позволить функции Console::WriteLine () выводить осмысленное представ-
ление объектов этих типов. Пары “имя-номер” добавляются в словарь phonebook.
Затем код перечисляет номера в словаре, используя цикл for each для итерации по
значениям, содержащимся в коллекции объектов, которые возвращаются свойством
Values объекта phonebook. Последний цикл проходит по именам в коллекции, кото-
рые возвращает свойство Keys, и использует индексируемое свойство по умолчанию
для доступа к значениям номеров. Здесь не нужен блок try, потому что можно быть
уверенным, что все ключи в коллекции Keys присутствуют в словаре. Если бы это
было не так, то означало бы серьезную проблему с реализацией обобщенного класса
Dictionaryo!
Резюме
В этой главе были раскрыты принципиальные идеи, касающиеся наследования
классов “родного” C++ и классов C++/CLI. Ниже перечислены фундаментальные
аспекты, которые следует иметь в виду.
□ Производный класс наследует все члены базового класса за исключением кон-
структоров, деструктора и перегруженной операции присваивания.
□ Члены базового класса, объявленные как private в базовом классе, не доступ-
ны ни одному производному классу. Чтобы получить эффект ключевого слова
private, но при этом обеспечить доступ в производном классе, следует исполь-
зовать ключевое слово protected.
□ Базовый класс может быть специфицирован для производного класса с ключе-
выми словами public, private или protected. В зависимости от указанного
ключевого слова для базового класса, уровень доступа наследованных членов
может быть модифицирован.
□ Если вы пишете конструктор производного класса, то должны обеспечить пра-
вильную инициализацию членов базового класса наряду с членами производно-
го класса.
□ Функция в базовом классе может быть объявлена как virtual. Это позволит
другим определениям функции, которые появляются в производных классах,
быть выбранными во время выполнения, в зависимости от типа объекта, для
которого выполняется вызов функции.
□ Деструкторы в базовых классах “родного” C++, имеющих виртуальные функ-
ции, следует также объявлять как virtual. Это гарантирует корректный выбор
деструктора для динамически созданных объектов производных классов.
Наследование классов и виртуальные функции 563
□ Класс “родного” C++ может быть назначен дружественным (friend) для друго-
го класса. В этом случае все члены функций дружественного класса могут об-
ращаться ко всем членам другого класса. Если класс А является дружественным
классу В, это не значит, что В будет дружественным для А, если только он не
объявлен таковым.
□ Виртуальная функция в базовом классе “родного” C++ может быть специфици-
рована как чистая (pure) добавлением = 0 в конец ее объявления. Такой класс
является абстрактным, и для него не может быть создано никаких объектов.
В любом классе-наследнике все чистые виртуальные функции должны быть
определены; если это не так, то наследник также является абстрактным.
□ Ссылочные классы C++/CLI могут быть унаследованы от других ссылочных
классов. Классы значений не могут быть производными.
□ Интерфейсный класс объявляет общедоступные функции, предоставляющие
специфические возможности, которые могут быть реализованы ссылочным
классом. Интерфейсный класс может содержать общедоступные функции, со-
бытия и свойства. Интерфейс также может определять статические данные-
члены, функции, события и свойства, которые наследуются классом, реализую-
щим интерфейс.
□ Интерфейсный класс может быть производным по отношению к другому ин-
терфейсному классу, и производный интерфейс содержит все члены обоих ин-
терфейсов.
□ Делегат — это объект, инкапсулирующий один или более указателей на функ-
ции, имеющие один и тот же тип возврата и список параметров. Вызов делега-
та вызывает все функции, на которые он указывает.
□ Член класса — событие может сигнализировать о наступлении события, вызы-
вая одну или более функций-обработчиков, зарегистрированных для этого со-
бытия.
□ Обобщенный класс — это параметризованный тип, экземпляр которого создает-
ся во время выполнения. Аргументы, указанные для параметров типа при соз-
дании экземпляра обобщенного типа, могут быть типами значений или типами
ссылочных классов.
□ Пространство имен System::Collections: :Generic содержит обобщенные
классы коллекций, определяющих безопасные в отношении типов коллекции
объектов любого типа C++/CLI.
□ Можно создать библиотеку классов C++/CLI в отдельной сборке, и такая би-
блиотека будет размещаться в файле .dll.
Теперь вы изучили все важнейшие языковые средства ANSI/.ISO C++ и C++/CLI.
Важно, чтобы вы чувствовали себя комфортно с механизмами определения и насле-
дования классов в обоих версиях языка. Программирование для Windows с помощью
Visual C++ 2005 включает интенсивное использование всех этих концепций.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Что неправильно в следующем коде?
564 Глава 9
class CBadClass
private:
int len;
char* p;
public:
CBadClass(const char* str): p(str), len(strlen(p)) {}
CBadClass(){}
2. Предположим, имеется класс CBird, приведенный ниже, который вы хотите исполь-
зовать в качестве базового для создания иерархии классов, описывающих птиц:
class CBird
protected:
int wingSpan;
int eggSize;
int airSpeed;
int altitude;
public:
virtual void fly() { altitude = 100; }
Разумно ли создать класс CHawk (сокол), наследуя его от CBird (птица)? А как
насчет COstrich (страус)? Обоснуйте ответы. Предложите иерархию птиц, ко-
торая учтет оба эти вида птиц.
3. Имеется следующий класс:
class CBase
protected:
int m__anlnt;
public:
CBase(int n): m_anlnt(n) { cout « "Базовый конструктор\п"; }
virtual void Print() const =0;
Какого рода класс CBase и почему? Унаследуйте от CBase класс, который уста-
навливает значение унаследованного члена m_alnt при конструировании и пе-
ста вашего класса.
4. Бинарное дерево — это структура, состоящая из узлов, в которой каждый узел
содержит указатель на “левый” и “правый” узлы плюс элемент данных, как по-
казано на рис. 9.7.
Дерево начинается с корневого узла, и он служит стартовой точкой для досту-
па ко всем узлам дерева. Любой (или оба) указатель узла может быть null. На
рис. 9.7 показано упорядоченное бинарное дерево, которое представляет со-
бой дерево, организованное таким образом, что значение каждого узла всегда
больше или равно значению его левого подузла и меньше или равно значению
правого подузла.
Определите класс “родного” C++ для определения упорядоченного бинарного
дерева для хранения целых чисел. Нужно будет также определить класс Node,
но он может быть вложенным классом в классе BinaryTree. Напишите про-
грамму, тестирующую работу класса BinaryTree, сохраняя в нем произвольную
последовательность целых чисел, а затем выводя их в порядке возрастания.
Подсказка: не бойтесь применять рекурсию.
Наследование классов и виртуальные функции 565
Корневой узел
Значение = 120
указатель
левого узла
указатель
правого узла
Узел
Узел
Значение = 43
Значение = 437
указатель
левого узла
указатель
правого узла
указатель
левого узла
указатель
правого узла
Узел
Узел
Узел
Значение = 17
Значение = 57
Значение = 766
null
указатель
правого узла
null
Узел
указатель
правого узла
null
Узел
null
Значение = 24
Значение = 88
null
null
null
null
Упорядоченное бинарное дерево
Рис. 9.7. Бинарное дерево
5. Реализуйте упражнение 4 в виде программы CLR. Если не получится выполнить
упражнение 4, просмотрите решение в загруженном коде и воспользуйтесь им в
качестве руководства при выполнении данного упражнения.
6. Определите обобщенный класс BinaryTree для любого типа, реализующего
интерфейсный класс IComparable, и продемонстрируйте его работу, используя
экземпляры обобщенного класса для хранения и извлечения сначала случайных
целых чисел, а затем — элементов следующего массива:
array<StringA>A words = {L"Success”, L"is", L"the", Liability”, L”to” ,
L”go”, L"from", L”one", L”failure”, L”to",
L"another", L"with”, L"no", L"loss", L”of",
L"enthusiasm"};
7. Выведите значения, извлеченные из бинарного дерева, в командной строке.
9
Технологии отладки
Если вы выполняли упражнения из предыдущих глав, то более чем вероятно, что
вам пришлось сражаться с ошибками в своем коде. В настоящей главе вы ознакоми-
тесь с тем, как базовые возможности отладки, встроенные в Visual C++ 2005, могут
помочь в этом. Мы с вами исследуем некоторые дополнительные инструменты, кото-
рые можно использовать для нахождения и устранения ошибок в программах, а так-
же рассмотрим некоторые способы, позволяющие снабдить ваши программы специ-
фическим кодом для проверки ошибок.
В настоящей главе вы узнаете о следующих вопросах.
□ Как запустить вашу программу под управлением отладчика Visual C++ 2005.
□ Как выполнить вашу программу шаг за шагом.
□ Как наблюдать изменения значений переменных в ваших программах.
□ Как наблюдать значение выражений в вашей программе.
□ Что такое стек вызовов.
□ Что такое утверждения (assertions) и как их использовать для проверки кода.
□ Как добавить специфичный отладочный код в программу.
□ Как обнаружить утечки памяти в программах на “родном” C++.
□ Как использовать средства слежения за выполнением и сгенерировать отладоч-
ный вывод в программах C++/CLI.
Что такое отладка?
Дефекты, или “жучки” (bugs) — это ошибки в вашей программе, а отладка (debug-
ging) — процесс нахождения и устранения этих ошибок. Без сомнений, вы уже знаете,
что отладка — неотъемлемая часть процесса программирования. Факты, касающиеся
ошибок в ваших программах достаточно удручающи.
568 Глава 9
Каждая программа, которую вы пишете, если она чуть сложнее совсем триви-
альной, содержит ошибки, которые вы должны обнаружить, найти и устра-
нить, если программа претендует на то, чтобы быть надежной и эффективной.
Обратите внимание на три фазы этого процесса — ошибка программы не обя-
зательно видна сразу; даже если она видна, вы можете не знать, где именно в
вашем коде она находится; и даже когда вы приблизительно знаете, где она мо-
жет находиться, может оказаться совсем нелегко определить, что именно явля-
ется причиной проблемы, и затем устранить ее.
□ Многие программы, которые вы напишете, будут содержать ошибки, даже по-
сле того, как вы решите, что уже тщательно протестировали их.
□ Ошибки могут оставаться скрытыми в программе, которая очевидно работает
правильно — иногда в течение нескольких лет. Однако проявиться они могут в
самый неподходящий момент.
□ Программы, выходящие за определенные пределы размеров и сложности поч-
ти всегда содержат ошибки, независимо от того, сколько времени и усилий
было потрачено на их тестирование. (Точный размер и степень сложности, ко-
торые гарантируют наличие ошибок невозможно определить точно, но Visual
C++ 2005 и ваша операционная система, несомненно, попадают в эту катего-
рию!)
Неразумно слишком концентрироваться на последнем пункте, если вы склонны
к неврозам, особенно когда часто летаете на самолетах или находитесь в непосред-
ственной близости к процессам, связанным с компьютерами, от которых зависит
ваше здоровье.
Многие потенциальные ошибки устраняются во время процесса компиляции или
компоновки, но некоторое количество их остается даже после окончательной сбор-
ки исполняемого модуля вашей программы. К несчастью, несмотря на тот факт, что
ошибки в программе неизбежны подобно смерти или налогам, отладка — это не точ-
ная наука; тем не менее, вы можете применить формальный структурированный под-
ход для устранения ошибок. Различают четыре основные стратегии, которые вы мо-
жете применить для обеспечения безболезненной, насколько возможно, отладки.
□ Не изобретайте колесо. Разберитесь и используйте библиотечные средства, по-
ставляемые вместе с Visual C++ 2005 (или другими коммерческими программ-
ными компонентами, которые вам доступны), чтобы ваши программы исполь-
зовали как можно больше заранее протестированного кода.
□ Разрабатывайте и тестируйте ваш код инкрементным образом. Тестируя инди-
видуально каждый важный класс и функцию, понемножку собирая отдельные
компоненты кода и тестируя их, вы можете существенно облегчить процесс
разработки и значительно снизить количество скрытых ошибок.
Применяйте защитное кодирование. Это значит, что код нужно писать таким
образом, чтобы он сам противостоял потенциальным ошибкам. Например,
объявляйте функции-члены классов “родного*’ C++, которые не модифицируют
объекты, как const. Используйте, где возможно, константные параметры. Не
применяйте “магических чисел” в своем коде — определяйте константные объ-
екты с требуемыми значениями.
□ Включайте отладочный код, который с самого начала проверяет и верифици-
рует данные и условия в вашей программе. Именно с этим вы ознакомитесь в
деталях в настоящей главе.
Технологии отладки
569
Ввиду важности получения в конечном итоге программ, которые свободны от оши-
бок, насколько это возможно, Visual C++ 2005 предлагает мощный арсенал средств
для нахождения ошибок. Прежде чем погрузиться в подробности их устройства, одна-
ко, посмотрим внимательно, как возникают ошибки.
Ошибки в программах
Конечно, главный источник ошибок в ваших программах — это вы сами. Эти
ошибки лежат в диапазоне от простых опечаток — просто нажатия не той клавиши,
до полностью неверной логики. Иногда мне самому трудно поверить, как я могу так
часто допускать столь глупые ошибки, но поскольку никто другой не касался моего
кода, и взяться им больше неоткуда — это должно быть правдой! Люди — рабы при-
вычек, так что вы наверняка обнаруживаете, что время от времени повторяете одни
и те же ошибки. Что печально, многие ошибки, которые бросаются в глаза другим,
остаются невидимыми для вас, и это — способ, которым ваш компьютер учит вас сми-
рению. Существуют два типа широко распространенных ошибок, которые вы можете
допустить в коде, и которые служат причиной ошибок программ.
Синтаксические ошибки. Это ошибки, происходящие от неправильной фор-
мы операторов; например, вы пропустили точку с запятой в конце оператора
или поместили двоеточие там, где должна быть запятая. Вам не нужно особо
беспокоиться о синтаксических ошибках. Компилятор распознает все синтак-
сические ошибки и, как правило, ясно указывает вам на них, так что исправить
их довольно легко.
Семантические ошибки. Есть ошибки, при которых код синтаксически кор-
ректен, но он не выполняет того, что вы подразумевали. Компилятор не может
знать о ваших намерениях, о том, чего вы хотите добиться в вашей программе,
поэтому не в состоянии обнаруживать семантические ошибки; однако, часто вы
получаете указание о том, что что-то не так, поскольку программа завершается
аварийно. Средства отладки в Visual C++ 2005 предназначены для того, чтобы
помочь обнаруживать семантические ошибки. Семантические ошибки могут
оказаться очень тонкими и трудно обнаружимыми, например, когда программа
иногда выдает неверный результат или изредка терпит крах. Возможно, наибо-
лее трудный случай — проявление таких ошибок в многопоточных программах,
когда конкурирующие потоки выполнения управляются неправильно.
Конечно, существуют ошибки в системном окружении, которое вы используете
(включая Visual C++ 2005), но это должно быть последним местом, в котором вы долж-
ны искать причину того, что ваша программа не работает. Даже когда вы решите, что
причина должна быть в компиляторе или операционной системе, в девяти случаях
из десяти вы ошибетесь. Безусловно, в Visual C++ 2005 присутствуют ошибки, однако
если вы хотите вовремя получать информацию о них вместе с доступными исправ-
лениями, то можете поискать информацию, предлагаемую на Web-сайте корпорации
Microsoft, посвященном Visual C++ 2005 (http: //msdn.microsoft. com/visualc/),
или, что еще лучше, подписаться на рассылку Microsoft Developer Network и получать
ежеквартальные обновления последних ошибок и исправлений.
Может оказаться полезным составить список обнаруженных ошибок в собствен-
ном коде для последующего изучения. Проверяя новый код на предмет наличия оши-
бок, которые были допущены в прошлом, часто удается сократить время отладки но-
вых проектов.
570 Глава 9
По природе программирования ошибки практически бесконечны в своем раз-
нообразии, но все же есть некоторые их виды, которые особенно распространены.
Возможно, вы знаете о большинстве из них, но все равно давайте кратко по ним
пройдемся.
Распространенные ошибки
Удобный способ каталогизации ошибок заключается в привязке их к симптомам,
которые они вызывают, потому что именно так они проявляются в первой инстан-
ции. Список из пяти распространенных симптомов, представленный в табл. 10.1, без
сомнений, нельзя считать исчерпывающим, поэтому вы определенно пополните его
по мере накопления программистского опыта.
Таблица 10.1. Пять распространенных симптомов ошибок
Симптом Возможные причины
Повреждение данных Отсутствие инициализации переменной
Выход за пределы допустимых значений целочисленного типа
Неверный указатель
Ошибка в выражении — индексе массива
Ошибка условия цикла
Ошибка в размере динамически распределенного массива
Отсутствие реализации конструктора копирования класса,
операции присваивания или деструктора
Необработанные исключения Неверный указатель или ссылка
Отсутствие обработчика catch
Зависание или крах программы Отсутствие инициализации переменной
Бесконечный цикл
Неверный указатель
Двукратное освобождение одного и того же участка памяти
Отсутствие реализации или ошибка в деструкторе класса
Некорректный потоковый ввод данных Чтение с использованием операции извлечения и функции
getline()
Некорректные результаты Опечатки: -= вместо ==, i вместо j и тому подобное
Отсутствие инициализации переменной
Выход за пределы допустимых значений целочисленного типа
Неверный указатель
Пропуск break в операторе switch
Обратите внимание на то, насколько много видов ошибок может быть вызвано не-
верными указателями. Это, наверно, наиболее частая причина возникновения таких
ошибок, которые трудно обнаружить, поэтому всегда дважды проверяйте все ваши
операции с указателями. Если вы будете уверены во всех случаях применения указа-
телей, то сможете избежать многих ловушек. Ниже перечислены частые причины по-
явления “плохих” указателей.
□ Отсутствие инициализации указателя после его объявления.
□ Отсутствие установки указателя на свободную память в null после освобожде-
ния выделенной памяти.
□ Возврат из функции адреса локальной переменной.
□ Отсутствие реализации конструктора копирования и операции присваивания
для объектов классов, которые размещаются в памяти свободного хранилища.
Технологии отладки
571
Даже если вы учтете все это, в вашем коде все равно останутся какие-то ошибки,
поэтому давайте рассмотрим инструментарий, предлагаемый Visual C++ 2005 для вы-
полнения отладки.
Базовые операции отладки
До сих пор вы создавали отладочные версии примеров программ, но не использо-
вали отладчик. Отладчик
это программа, которая управляет выполнением вашей
программы таким образом, что вы можете шагать по исходному коду строка за стро-
кой, или запускать ее выполнение до определенной точки, выбранной в исходном
коде. В каждой точке вашего кода, где отладчик останавливается, вы можете просмо-
треть или даже изменить содержимое переменных. Можно также изменить исходный
код, перекомпилировать и перезапустить программу сначала. Вы можете даже изме-
нять исходный код в процессе “шагания” по программе. Когда вы переходите к следу-
ющему оператору после модификации кода, отладчик автоматически перекомпилиру-
ет программу перед выполнением следующего оператора.
Чтобы понять базовые возможности отладки в Visual C++ 2005, используйте отлад-
чик с программой, в правильной работе которой вы относительно уверены. Тогда вы
просто сможете “подергать рычаги”, чтобы увидеть, как все работает. Возьмем про-
стой пример из главы 4, использующий указатели:
// Ех4_05.срр
// Работа с указателями
#include <iostream>
using namespace std;
int main ()
long* pnumber ® NULL; // Объявление и инициализация указателя
long numberl = 55, number2 = 99;
pnumber = &numberl;
*pnumber += 11;
cout « endl
// Сохранить адрес в указателе
// Увеличить numberl на 11
« ’’numberl = ” « numberl
” &numberl =
hex « pnumber;
pnumber = &number2;
numberl = *pnumber*10;
// Переставить указатель на адрес number2
// умножить number2 на 10
cout « endl
« ’’numberl = ” « dec « numberl
« ” pnumber = ” « hex « pnumber
« ” *pnumber = ” « dec « *pnumber;
cout « endl;
return 0;
Если у вас сохранился этот пример, просто откройте его проект; иначе вам при-
дется ввести его снова.
Когда вы пишете программу, а она ведет себя неправильно, отладчик позволяет
вам исследовать ее работу, выполняя по одному оператору, чтобы найти, где, что и
почему идет не так, а также увидеть состояние всех программных данных в любой
момент выполнения. Вы можете выполнять этот пример оператор за оператором и
наблюдать за содержимым переменных, которые вас интересуют. В данном случае мы
$72, Глава 9
хотим посмотреть на pnumber, содержимое памяти, на которую указывает pnumber
(то есть *pnumber), numberl и number2.
Прежде всего, нужно убедиться, что конфигурация сборки примера установ-
лена в Win32 Debug, а не Win32 Release (так по умолчанию, если вы не измените).
Конфигурация сборки выбирает набор установок проекта для операции сборки про-
граммы, которые можно видеть в пункте меню Project^Settings (Проект*=>Настройки).
Текущая активная конфигурация сборки показывается в паре выпадающих списков
панели инструментов Standard (Стандартные). Чтобы отобразить или удалить опре-
деленную панель инструментов, вы просто щелкаете правой кнопкой на панели и вы-
бираете или отменяете выбор той или иной панели в списке. Убедитесь, что флажок
стоит рядом с Debug (Отладка), чтобы отображалась панель инструментов отладки.
Она появляется автоматически, когда запущен отладчик, но вам стоит посмотреть,
что она содержит, прежде чем запускать отладчик. Конфигурацию сборки можно из-
менить, открыв выпадающий список и выбрав необходимые альтернативы. Вы также
можете использовать пункт меню Build^Configuration Manager... (Сборка^Диспетчер
конфигурации). Панель инструментов Standard показана на рис. 10.1.
Рис. 10.1. Панель инструментов Standard
Вы можете посмотреть, для чего служит каждая из кнопок панели инструментов,
просто проводя курсором мыши над кнопками панели. При этом будет всплывать
краткая подсказка, идентифицирующая функцию соответствующей кнопки.
Конфигурация проекта Debug включает дополнительную информацию в вашу ис-
полняемую программу во время компиляции, что позволяет потом использовать воз-
можности отладки. Эта дополнительная информация сохраняется в файле .pdb, ко-
торый создается в папке Debug вашего проекта. Конфигурация Release исключает эту
информацию, поскольку она требует накладных расходов, которые излишни в пол-
ностью протестированной рабочей программе. В версиях Visual C++ 2005 Professional
или Enterprise компилятор также оптимизирует код при компиляции рабочей вер-
сии программы. Оптимизация не включается при компиляции отладочной версии,
поскольку процесс оптимизации может изменять последовательность выполнения
кода, чтобы сделать его более эффективным, или даже вовсе исключать излишний
код. Поскольку это разрушает отображение “один к одному” между исходным кодом
и соответствующими блоками исполняемого машинного кода, оптимизация может за-
труднить или запутать пошаговое выполнение программы.
Панель инструментов Debug (Отладка) показана на рис. 10.2.
Debug
Рис. 10.2. Панель инструментов Debug
Если вы просмотрите всплывающие подсказки для кнопок этой панели, то получи-
те предварительное представление о том, что они делают — и вскоре вы ими начнете
пользоваться. С примером из главы 4 вам не придется применять все доступные воз-
можности отладки, но некоторые наиболее важные вы попробуете. После того, как
вы познакомитесь с пошаговым выполнением программы с применением отладчика,
Технологии отладки 573
Запустить отладчик можно щелчком на крайней слева кнопке панели инструментов
Debug, выбором пункта меню Debug^Start Debugging (Отладка^Начать отладку) либо
нажатием клавиши <F5>. Предположим, например, что вы используете панель инстру-
ментов. Отладчик имеет два главных режима выполнения — пошаговое выполнение
кода (что, по сути, означает выполнение по одному оператору программы за раз) и вы-
полнение до определенной точки, указанной в исходном коде. Точка в исходном коде,
где должен остановиться отладчик, определяется либо местоположением курсора,
либо — что удобнее — специально назначенной точкой останова, называемой точкой
прерывания (breakpoint). Давайте посмотрим, как определять точки прерывания.
Установка точек прерывания
Точка прерывания — это точка в вашей программе, в которой отладчик автомати-
чески прерывает выполнение. Вы можете специфицировать множество точек преры-
вания, так что при запуске вашей программы она будет останавливаться в интересую-
щих вас местах, которые вы можете выбрать по своему усмотрению. В каждой точке
прерывания можно просмотреть все переменные программы и изменить их значение,
если окажется, что они содержат не то, что нужно. Вы можете выполнить программу
Ех4_05 шаг за шагом, по одному оператору за раз, но для больших программ это бу-
дет непрактично. Обычно вам нужно просмотреть определенную область программы,
в которой по вашему предположению может содержаться ошибка. Следовательно,
обычно вы будете устанавливать точки прерывания там, где предполагаете наличие
ошибки, и запускать программу так, чтобы она остановилась на первой точке преры-
вания. Затем, начиная с этой точки, вы можете выполнять ее по одному шагу, причем
“один шаг” означает один оператор кода.
Чтобы установить точку прерывания в начале строки кода, вы просто щелкаете
кнопкой мыши на серой вертикальной колонке, находящейся слева от номера стро-
ки, напротив того оператора, на котором хотите приостановить выполнение. При
этом появляется красный круглый символ, называемый глифом (glyph), символизиру-
ющий наличие точки прерывания в данной строке. Удалить точку прерывания можно
двойным щелчком на глифе. На рис. 10.3 показана панель редактора с парой точек
прерывания, установленных в коде Ех4_05.
Во время отладки вы обычно будете устанавливать несколько точек прерывания,
выбирая место для каждой их них там, где изменяются переменные, которые, как вы
предполагаете, вызывают проблемы. Выполнение останавливается перед оператором,
помеченным точкой прерывания. Выполнение программы может быть прервано
только перед полным оператором, а не на полпути его выполнения. Если вы помести-
те курсор в строку, которая не содержит никакого кода (например, в строку над вто-
рой точкой прерывания на рис. 10.3), то точка прерывания будет установлена на этой
строке, и программа остановится в начале следующего исполняемого оператора.
Как было сказано, вы можете удалить точку прерывания двойным щелчком на
глифе красного цвета. Отключить точку прерывания можно также, щелкнув пра-
вой кнопкой мыши на содержащей ее строке и выбрав соответствующий пункт из
всплывающего меню. Можно удалить все точки прерывания в текущем активном
проекте, выбрав в меню пункт Debugs Delete All Breakpoints (Отладка^Удалить все
точки прерывания) или нажав комбинацию клавиш <Ctrl+Shift+F9>. Обратите вни-
мание, что при этом удаляются все точки прерывания из всех файлов проекта, даже
если они в данный момент и не открыты в панели редактора. Можно также отклю-
чить все точки прерывания, выбрав в меню пункт Debugs Disable All Breakpoints
(Отладка*^ отключить все точки прерывания).
574 Глава 9
Рис. 103. Панель редактора с установленными точками прерывания
Расширенные точки прерывания
Более усовершенствованный способ спецификации точек прерывания предлагает-
ся в окне, которое вы можете отобразить, нажав <Alt+F9> или выбрав Breakpoints
(Точки прерывания) из списка, отображаемого при выборе кнопки Windows (Окна) в
панели инструментов Debug — справа в конце. Это окно показано на рис. 10.4.
Breakpoints
New’ >
Columns’
Name
0’*' Ex4m05.cppr line 11
Ex4_05.cpp, line 20
Condition
(no condition)
(no condition)
Hit Count
break always
break always
Output] Breakpoints
Puc. 10.4. Окно Breakpoints (Точки прерывания)
Кнопка Columns (Колонки) в панели инструментов позволяет добавлять дополни-
тельные колонки для отображения в окне. Например, вы можете отображать имя ис-
ходного файла или имя функции, в которой установлена точка прерывания, или же
вы можете отобразить, что произошло, когда оператор был достигнут.
Дополнительные опции для точки прерывания устанавливаются щелчком правой
кнопкой мыши на строке с точкой прерывания в окне Breakpoints и выбором соот-
ветствующих пунктов из всплывающего меню. Наряду с установкой точки преры-
вания в определенном месте кода, вы также можете установить точку прерывания,
когда определенное булевское выражение вернет true. Это мощное средство, но оно
означает весьма ощутимые накладные расходы в программе, поскольку проверочное
выражение должно постоянно вычисляться заново. Как следствие, выполнение за-
Технологии отладки 575
медляется
;аже на самых быстрых машинах. Можно также указать, что выполнение
должно прерываться только после определенного количества проходов потока управ-
ления через данную точку. Это наиболее удобно для кода внутри циклов, когда вы не
хотите прерывать выполнение на каждой итерации. После установки любого условия
для точки прерывания внешний вид глифа изменится — на нем в центре появится
Установка точек трассировки
Точка трассировки (tracepoint) — специальная разновидность точки прерыва-
ния, которая имеет ассоциированное с ней действие. Вы создаете точку трассировки
щелчком правой кнопкой мыши по строке, в которой она должна быть установлена, и
выбором пункта Breakpoints When Hit (Точка прерывания^Когда попадают) из всплы-
вающего меню. После этого вы видите диалоговое окно, показанное на рис. 10.5.
When Breakpoint Is Hit
Specify what to do when the breakpoint is hit.
0 Print a message:
Function: ^FUNCTION, Thread: $TID $TNAM^
You can include the value of a variable or other expression in the message by
placing it in curly braces, such as "The value of x is {x}.11 To insert a curly brace,,
use ’'YC- To insert a backslash, use ’’VC'-
The following special keywordswill be replaced with their current values:
$ADDRESS - Current Instruction, $CALLER- Previous Function Name,
^CALLSTACK - Call Stack, ^FUNCTION - Current Function Name,
$PID - Process Id, $PNAME - Process Name
$ТЮ - Thread Id, $TNAME - Thread Name
Run a macro:
id Continue execution
OK
Cancel
Puc. 10.5. Диалоговое окно When Breakpoint Is Hit (Когда
происходит попадание в точку прерывания)
;ействием точки трассировки может быть печать сообщения и/или
Как видите
запуск макроса, к тому же вы можете выбрать — нужно ли останавливать выполнение
в точке трассировки или нет. Присутствие точки трассировки в строке исходного
кода, где выполнение не останавливается, помечается красным глифом в форме ром-
ба. Текст диалогового окна объясняет, как специфицировать печатаемое сообщение.
Например, вы можете печатать имя текущей функции и значение pnumber, указав в
текстовом поле следующее:
$FUNCTIONj Значение pnumber равно {pnumber}
Вывод, генерируемый по достижении точки трассировки, появляется в панели
Output (Вывод) в окне приложения Visual Studio.
576 Глава 9
Если выставлен флажок Run a macro: (Выполнить макрос:), то вы можете выбрать
макрос из длинного списка доступных стандартных макросов.
Запуск отладки
Существуют пять способов запуска вашего приложения в режиме отладки из пун-
ктов меню Debug (Отладка), показанного на рис. 10.6.
| Windows ►
г Start Debugging F5
Start With Application Verifier Shift+Alt+F5
l> Start Without Debugging Ctrl+F5
Attach to Process-
Exceptions... Ctrl+Alt+E
Step Into Fll
Step Over F10
Toggle Breakpoint F9
New Breakpoint ►
Delete All Breakpoints Ctrl+Shift+F9
J Disable All Breakpoints
Puc. 10.6. Пункты меню Debug
1. Опция Start Debugging (Начать отладку), также доступная через кнопку в па-
нели инструментов Debug, просто выполняет программу до первой точки пре-
рывания (если она есть), где выполнение останавливается. После того как вы
просмотрите все, что нужно, в этой точке прерывания, выбор того же пункта
меню или щелчок на кнопке панели инструментов вызывает продолжение вы-
полнения до следующей точки прерывания. Подобным образом вы можете
перемещаться по программе от одной точки прерывания к другой, и в каждой
просматривать значение критичных переменных, изменяя их значение при не-
обходимости. Если точек прерывания нет, то запуск отладчика таким способом
просто выполняет всю программу без остановок. Конечно, только то, что вы
запустили отладчик подобным способом, вовсе не означает, что вы должны про-
должать его использовать; при каждом останове выполнения можно выбрать
любой другой способ перемещения по коду.
2. Опция Start With Application Verifier (Начать с верификатором приложений)
предназначена для верификации кода родного C++ во время выполнения.
Верификатор приложений (Application Verifier) — это расширенный инструмент
для идентификации ошибок, связанных с неверными дескрипторами, использо-
ванием критических разделов или повреждением кучи. В настоящей книге эта
тема не рассматривается.
3. Опция Attach to Process (Присоединиться к процессу) позволяет отлаживать
программу, которая уже запущена. Эта опция отображает список процессов, за-
пущенных на вашей машине, и вы можете выбрать среди них процесс, который
хотите отлаживать. Это средство для опытных пользователей и лучше избегать
экспериментов с ним, если только вы не уверены в том, что собираетесь делать.
Можно очень легко заблокировать машину или вызвать другие проблемы, если
вмешаться в критические процессы операционной системы.
Технолог:
: отладки
577
4. Пункт Step Into (Войти), также доступный в виде кнопки в панели инструмен-
тов Debug, выполняет программу по одному оператору за раз, заходя в каждый
вложенный блок кода, включая каждую вызываемую функцию. Это может ока-
заться весьма утомительным, если использовать такой режим отладки на протя-
жении всего процесса, потому что, например, при этом будет выполняться весь
код библиотечных функций, что вряд ли вас заинтересует, если только вы не за-
няты их разработкой. Небольшое количество библиотечных функций написаны
на языке ассемблера, включая некоторые из тех, что поддерживают потоковый
ввод-вывод. Функции на языке ассемблера выполняются по одной машинной
инструкции за раз, что, как и можно ожидать, займет значительное время.
5. Пункт Step Over (Перешагнуть), также доступный в виде кнопки в панели ин-
струментов Debug, просто выполняет операторы вашей программы по одному
за раз, выполняя весь код, используемый функциями, которые могут быть вы-
званы внутри оператора, как сплошной поток операций, без остановок.
Имеется и шестая опция для запуска в режиме отладки, которая не появляется в
меню Debug. Вы можете щелкнуть правой кнопкой мыши на любой строке кода и вы-
брать из контекстного меню пункт Run to Cursor (Выполнить до курсора). Произойдет
именно то, что гласит этот пункт — программа будет запущена до строки, в которой
находится курсор, и затем ее выполнение будет прервано, позволив вам просмотреть
или изменить переменные программы. Какой бы способ вы ни выбрали для запуска
процесса отладки, вы можете продолжить выполнение, применив одну из пяти опций
в любой из промежуточных точек прерывания.
А теперь попробуем все это на примере. Запустите программу, используя опцию
Step Into, выбрав соответствующий пункт меню или щелкнув на кнопке в панели ин-
струментов, либо нажав клавишу <F11 >. После небольшой паузы (предполагается, что
проект уже собран), Visual C++ 2005 переключится в режим отладки.
Когда отладчик стартует, появятся два окна с вкладками ниже окна редактора. Вы
можете выбрать, что должно отображаться в любой момент времени в каждом из
этих окон, выбрав требуемую вкладку. Можно настроить, какие именно окна должны
появляться при запуске отладчика. Полный список всех окон отображается в выпада-
ющем меню Debug«=>Windows (ОтладкамОкна). Окно Autos (Автоматические) слева
показывает текущие значения автоматических переменных в контексте функции, вы-
полняемой в данный момент. Окно Call Stack (Стек вызовов) справа идентифицирует
вызовы функций, находящиеся в процессе выполнения, но нас сейчас более интере-
сует вкладка Output (Вывод) в том же окне. В панели редактора вы увидите, что от-
крывающая фигурная скобка вашей функции main () выделена стрелочкой, что ука-
зывает на то, что это — текущая точка выполнения программы. Все описанное можно
видеть на рис. 10.7.
Здесь вы также можете видеть точку прерывания в строке 11 и точку трассировки
в строке 17. В этот момент выполнения программы вы не можете выбирать какие-
либо переменные для просмотра, потому что ни одна из них еще не существует. До
тех пор, пока не будет выполнено объявление переменной, вы не сможете ни просмо-
треть ее значение, ни изменить его.
Во избежание пошагового выполнения всего кода в потоке функций, занимаю-
щихся вводом-выводом, вы используете средство Step Over, чтобы продолжить вы-
полнение до следующей точки прерывания. Это просто выполнит операторы вашей
функции main () по одному за раз, и выполнит весь код, используемый потоковыми
операциями (или любыми другими функциями, которые могут быть вызваны внутри
оператора) без остановок.
578 Глава 9
Е к 4_05 хрр
X
(Global Scape)
* moi nf)
f Exercising 10inters
♦ include <i □ st-re run>
using namespace std;
9:
ioi
long* рпудайег = NV'-L-
inn; tumber1
55, numbers - 99;
dec.axa-.on t in-tia.isatlot
ъ
pnurnbe г = iminfoe r 1;
4nurtib« *” 11-
cqu" < < endl
« number; = •
« " tnumberi
/j Store address in pointer
// incrtcnenc nurtherl by 11
pnuitiber;
pnumber
numberl
Ln.iTnher 2 ;
•pnoraber*10?
// Change poin er to address of Wo т-7
// 10 citftes numbers
cout
numberl
*pnuriher =
CDUt
endl;
Puc, 10,7. Панель редактора во время отладки
Просмотр значений переменных
Определение переменной, значение которой вы хотите просматривать, называет-
ся установкой наблюдения (setting a watch) над переменной. Для того чтобы уста-
новить наблюдение, необходимо уже иметь переменные, объявленные в программе.
Операторы объявления переменной можно выполнить, три раза выбрав Step Over.
Воспользуйтесь пунктом меню Step Over, кнопкой панели инструментов или нажмите
< F10> три раза, чтобы стрелка переместилась в начало строки 11:
pnumber = &numberl; / / Сохранить адрес в указателе
Теперь, если вы посмотрите в окно Autos, оно должно будет выглядеть, как пока-
зано на рис. 10.8 (хотя значение &numberl может отличаться в вашей системе, по-
скольку представляет местоположение в памяти). Обратите внимание, что значения
&numberl и pnumber не равны друг другу, потому что строка, в которой pnumber при-
сваивается адрес numberl (строка, на которую указывает стрелочка), еще не была вы-
полнена. Вы инициализировали pnumber нулевым указателем в первой строке функ-
ции, вот почему адрес содержит ноль. Если вы не инициализируете этот указатель
он может содержать произвольное “мусорное” значение, включая и ноль, конечно,
поскольку он содержит значение, оставшееся там от последней программы, которая
использовала эти конкретные четыре байта памяти.
Окно Autos содержит пять вкладок, включая вкладку Autos, отображаемую в дан-
ный момент, и все они описываются ниже.
□ Вкладка Autos (Автоматические) показывает автоматические переменные, ис-
пользуемые в текущем операторе и его непосредственном предшественнике
(другими словами, в операторе, указанном стрелочкой в окне редактора и пред-
шествующем ему).
□ Вкладка Locals (Локальные) показывает значения переменных, локальных по
отношению к текущей функции. В общем случае новые переменные входят в
Технологии отладки 579
область видимости по мере продвижения по программе и покидают ее при вы*
ходе из блока, в котором они объявлены. В данном случае это окно всегда пока-
зывает значения number 1, number2 и pnumber, поскольку в программе имеется
только одна функция ma i п (), состоящая их единственного блока кода.
Вкладка Threads (Потоки) позволяет просматривать и управлять потоками в
многопоточных приложениях.
Вкладка Modules (Модули) перечисляет детали модулей кода, выполняемых в
данный момент. Если ваше приложение терпит крах, вы можете определить,
в каком модуле это произошло, сравнив адрес, по которому произошел крах, с
диапазонами адресов в колонке Address (Адрес) этой вкладки.
На вкладке Watch 1 (Наблюдение 1) вы можете добавлять переменные, за кото-
рыми хотите наблюдать. Щелкните на строке в окне и введите имя переменной.
Вы также можете наблюдать за значением выражения C++, которое вводится
точно так же, как и имя переменной. Можно добавлять до трех дополнитель-
ных окон Watch (Наблюдение) через пункт меню Debugs Windows^ Watch (Отл
адка1^ Окна*=>Набл юдение).
Autos
Name
+ / anumber 1
, number I
number2
Eh pnumber
Value
DxOD 12AF54
99
0x00000000
Туре
eng4
long
long
long *
^Autos|4^Locats|tj]'Threads|i’*lModules|.gJWatdi I
Рис. 10.8. Окно Autos отображает
переменные отлаживаемой программы
Обратите внимание, что рядом с именем pnumber в окне Autos стоит знак плюса.
Этот знак появляется возле каждой переменной, для которой может быть отображе-
на дополнительная информация — для таких, как массив, указатель или объект класса.
В данном случае вы можете раскрыть представление переменной pnumber, щелкнув
на значке “плюс”. Если вы нажмете <FlO> два раза и щелкнете на
отладчик отобразит значение, хранимое в памяти по адресу, содержащемуся в указате-
ле, как показано на рис. 10.9.
возле pnumber, то
Autos
Name
/ *pnumber
number I
- / pnumber
Value Type
long
long
0x0012ff54 long *
66 long
66
Autos . ) Locals [^Threads 11 -7I Modules |^l Watch l
Рис. 10.9. Просмотр дополнительной
информации для переменной
580 Глава 9
Окно Autos автоматически предоставляет всю необходимую вам информацию,
отображая как значение адреса памяти, так и значение данных, находящихся по это-
му адресу. Целые значения могут отображаться либо в десятичном, либо в шестнад-
цатеричном виде. Для переключения между ними щелкните правой кнопкой мыши
в любой точке на вкладке Autos и выберите нужный пункт в контекстном меню. Вы
можете видеть переменные, локальные для текущей функции, перейдя на вкладку
Locals. Есть и другие способы просмотра переменных с помощью средств отладки
Visual C++ 2005.
Просмотр переменных в окне редактора
Если вам нужно увидеть значение одной переменной, и эта переменная в данный
момент видима в панели редактора, то простейший способ сделать это — установить
курсор над именем этой переменной на секунду. Появится всплывающая подсказка с
текущим значением этой переменной. Вы также можете увидеть более сложные выра-
жения, выделив их и установив курсор над выделенной областью. Опять-таки всплы-
вет краткая подсказка, отображающая значение. Попробуйте выделить выражение
*pnumber*10, находящееся чуть ниже. Наведение курсора на выделенную область
покажет текущее значение выражения. Обратите внимание, что это не работает,
если выражение не является завершенным, например, после исключения из выде-
ленного выражения символа *, разыменовывающего pnumber, или выделения просто
*pnumber*. При этом значение не отображается.
Изменение значения переменной
Окно Watch также позволяет изменять значения переменных, за которыми ведется
наблюдение. Вы должны использовать это в ситуациях, когда ясно, что отображаемое
значение переменной неверно, возможно, из-за наличия ошибок в вашей программе
либо из-за незавершенности кода. Если вы установите корректное значение, то ваша
программа не рухнет немедленно, и вы, возможно, сможете обнаружить еще немного
дополнительных ошибок. Если ваш код включает цикл с большим количеством итера-
ций, скажем, 30 000, то вы можете установить счетчик цикла равным 29 995, чтобы
пройти несколько последних итераций и убедиться, что цикл завершается корректно.
Понятно, что невозможно нажать клавишу <F10> 30 000 раз! Другое полезное приме-
нение этой возможности — установка переменной во время выполнения в значение,
вызывающее ошибку. Это позволит проверить работу кода, обрабатывающего ошиб-
ки, что иногда вообще не удается никаким другим способом.
Чтобы изменить значение переменной в окне Watch, выполните двойной щелчок
на отображаемом значении переменной и введите новое значение. Если переменная,
которую вы хотите изменить, является элементом массива, то вы должны развернуть
массив, щелкнув на символе + рядом с его именем, и затем изменить значение нуж-
ного элемента. Чтобы изменить значение переменной, отображаемой в шестнадца-
теричной нотации, вы можете либо ввести шестнадцатеричное число, либо ввести
десятичное, снабдив его префиксом 0п (ноль, за которым следует п), то есть, вы мо-
жете ввести значение как А9 или как 0п169. Если вы просто введете 169, это будет
интерпретироваться как шестнадцатеричное значение. Понятно, что вы должны
быть осторожны с новыми значениями, которые “вбрасываете” в свою программу.
Если только вы точно не знаете, какой эффект можно ожидать от таких изменений,
то легко можете обеспечить изменчивое поведение программы, что вряд ли прибли-
зит вас к получению нормально функционирующей программы.
Технологии отладки
581
Возможно, вы сочтете полезным запустить под управлением отладчика еще не-
сколько примеров из предыдущих глав. Это позволит вам лучше ощутить то, как от-
ладчик работает в различных условиях. Наблюдение за переменными и выражениями
здорово помогает разобраться в проблемах с вашим кодом, однако существуют и дру-
гие возможности для поиска и устранения ошибок. Посмотрим, как можно добавить
к программе код, который предоставит больше информации о том, когда и почему
что-то идет не так.
Добавление отладочного кода
Разрабатывая крупные программы, вы определенно нуждаетесь в добавлении спе-
циального кода, предназначенного для выявления ошибок, где только возможно, и
предоставлении трассирующего вывода, который поможет найти местоположение
ошибок. Наверняка вы не захотите заниматься пошаговым выполнением кода до тех
пор, пока не имеете представления о том, что собой представляет ошибка, и в какой
части кода ее следует искать. Код, который выполняет упомянутые вещи, необходим
только на этапе тестирования программы. Необходимость в нем отпадает, когда вы
уверены, что программа полностью работоспособна, и нет смысла нести дополни-
тельные расходы, связанные с его выполнением, а также терпеть неудобства, наблю-
дая отладочный вывод в завершенном продукте. По этой причине код, который вы
добавляете только для отладки, работает в отладочной версии программы, но не в
рабочей версии (если, конечно, она правильно реализована).
Вывод, генерируемый отладочным кодом, должен предоставлять возможность
вскрыть причины возникновения проблем, и если вы как следует продумаете его ре-
ализацию, способ встраивания его в вашу программу, то с его помощью можно будет
легко получить представление о том, в какой части программы следует искать ошиб-
ку. Затем вы можете применить отладчик, чтобы точно найти причину и местополо-
жение ошибки, после чего устранить ее.
Первый способ, которым вы можете проверить поведение программы, представ-
лен библиотечной функцией C++.
Использование утверждений
Заголовочный файл стандартной библиотеки <cassert> объявляет функцию
assert (), которую вы можете использовать для проверки логических условий внутри
вашей программы, когда не определен специальный символ препроцессора — NDEBUG.
Эта функция объявлена следующим образом:
void assert(int expression);
Аргумент функции специфицирует условие, которое должно быть проверено, но
эффект от функции assert () подавляется, если определен специальный символ пре-
процессора NDEBUG. Символ NDEBUG автоматически определяется в рабочей версии
программы, но не в отладочной версии. Поэтому утверждение (assertion) проверяет
свой аргумент в отладочной версии программы, но ничего не делает в рабочей вер-
сии. Если вы хотите отключить этот механизм в отладочной версии программы, то
можете определить NDEBUG явно, используя директиву #def ine. Чтобы это возымело
эффект, такую директиву необходимо поместить ранее директивы #include для за-
головочного файла <cassert> в исходном файле:
tfdefine NDEBUG // Отключить механизм утверждении в коде
#include <cassert> // Объявление assert()
582 Глава 9
Если выражение, переданное в качестве аргумента функции assert (), возвраща-
ет не ноль (то есть true), функция ничего не делает. Но если выражение равно нулю
(другими словами, false), и NDEBUG не определен, выводится диагностическое со-
общение, показывающее выражение, которое вернуло false, имя исходного файла и
номер строки в исходном файле, где это произошло. После отображения диагности-
ческого сообщения функция assert () вызывает abort () для завершения програм-
мы. Вот пример применения этого механизма утверждения в функции:
char* append(char* pStr, const char* pAddStr)
{
// Проверить, чтобы указатели были ненулевыми
assert(pStr != 0);
assert(pAddStr ! = 0);
// Код добавления pAddStr к pStr...
Вызов этой функции append () с нулевым указателем в качестве аргумента в про-
стои программе на моей машине вызывает выдачу следующего диагностического со-
общения:
Assertion failed: pStr != 0, file c:\beginning visual c++.net\examples\testassert\
testassert \ testassert.cpp, line 11
Неверное утверждение: pStr ! = 0, файл c:\beginning visual c++.net\examples\
testassert\testassert \ testassert.срр, строка 11
Утверждение также отображает окно сообщения, предлагающее три варианта вы-
бора, как показано на рис. 10.10.
Рис. 10.10. Окно сообщения, отображаемое утверждением
Щелчок на кнопке Abort (Прервать) немедленно завершает программу. Кнопка
Retry (Повторить) запускает отладчик Visual C++ 2005, так что вы можете по шагам
выполнить программу, чтобы подробнее выяснить причину отказа утверждения.
В принципе, кнопка Ignore (Игнорировать) позволяет продолжить выполнение про-
граммы, несмотря на ошибку, но обычно это неразумный выбор, поскольку результат,
скорее всего, будет непредсказуемым.
В качестве аргумента assert () можно использовать логическое выражение лю-
бого рода. Можно сравнивать значения, проверять указатели, верифицировать типы
объектов или выполнять любую другую полезную проверку, чтобы убедиться в кор-
ректности операции вашего кода.
Технологии отладки 583
Получение сообщения о невыполнении некоторого логического условия помога-
ет мало, так что обычно вам потребуется дополнительная помощь для обнаружения
и исправления ошибок. Посмотрим, как можно добавить диагностический код более
общей природы.
обавление собственного отладочного кода
Используя директивы препроцессора, вы можете добавить любой код, который
хотите, к своей программе — так, чтобы он компилировался и выполнялся только
в отладочной ее версии. Отладочный код полностью исключается из рабочей вер-
сии, потому он вообще не влияет на эффективность протестированной программы.
Отсутствие символа NDEBUG может служить управляющим механизмом для включения
отладочного кода; то есть символ, используемый для управления функцией assert ()
стандартной библиотеки, о которой говорилось выше, может послужить и вашему от-
ладочному коду. В качестве альтернативы, для лучшего и более позитивного управ-
ляющего механизма можно применять другой символ препроцессора — _DEBUG, кото-
рый всегда определяется автоматически Visual C++ в отладочной версии программы,
но который в рабочей версии не определен. Вы просто заключаете свой отладочный
код, который должен компилироваться и выполняться только во время отладки, меж-
ду парой директив препроцессора — #ifdef и #endif, проверяя наличие символа
_DEBUG, как показано ниже:
#ifdef _DEBUG
// Код для целей отладки. . .
#endif // _DEBUG
Код между #ifdef и #endif компилируется только при условии наличия опреде-
ления символа —DEBUG. Это значит, что когда ваша программа будет полностью проте-
стирована, вы можете собрать рабочую версию, полностью свободную от накладных
расходов, связанных с отладочным кодом. Этот код может делать все, что помогает в
процессе отладки — от простого вывода сообщения для трассировки последователь-
ности выполнения (каждая функция может фиксировать свой вызов), до выполнения
дополнительных вычислений с целью проверки и верификации данных или вызова
функций, обеспечивающих отладочный вывод.
Конечно, вы можете иметь столько блоков отладочного кода в своих программах,
сколько хотите. Можно также использовать свои собственные символы препроцессо-
ра для более выборочного включения отладочного кода. Одной из причин для этого
может быть то, что ваш отладочный код выдает слишком объемный вывод, так что вы
хотите генерировать его только тогда, когда это действительно необходимо. Другой
причиной может рассматриваться необходимость обеспечения “детализированного”
отладочного вывода, чтобы можно было выбирать и указывать — какой вывод следует
генерировать при каждом конкретном запуске. Но даже в этих случаях все равно бу-
дет хорошей идеей использовать символ _DEBUG для обеспечения общего контроля,
поскольку это позволяет автоматически обеспечить полную свободу рабочей версии
от накладных расходов отладочного кода.
Рассмотрим простой случай. Предположим, что вы используете два собственных
символа для управления кодом отладки: MY DEBUG, управляющий “нормальным” отла-
дочным кодом, и VOLUME DEBUG, который применяется для управления кодом, про-
изводящим гораздо больше вывода, необходимость в котором возникает лишь из-
редка. Вы можете организовать эти символы так, что они будут зависеть от символа
DEBUG:
584 Глава 9
#ifdef _DEBUG
#define MYDEBUG
#define VOLUMEDEBUG
#endif
Чтобы предотвратить объемный отладочный вывод, вам нужно просто закоммен-
тировать определение VOLUME DEBUG, и никакой символ не будет определен, если не
определен _DEBUG. Если ваша программа состоит из нескольких отладочных файлов,
то возможно, будет удобно собрать управляющие символы отладки где-то в одном за-
головочном файле и включить его с помощью #include в каждый файл, содержащий
отладочный код.
Давайте рассмотрим простой пример, чтобы увидеть, как добавление отладочного
кода в программу работает на практике.
Практическое занятие | ДобЗВЛеНИб ОТЛЭДОЧНОГО КОДЭ
Чтобы раскрыть этот и ряд других общих подходов к отладке, возьмем пример
программы, которая, будучи довольно простой, содержит ошибки, которые нужно
найти и устранить. Поэтому вы должны рассматривать весь код в остальной части
настоящей главы как подозрительный, в частности потому, что он не обязательно во-
площает хорошую практику программирования.
Для экспериментирования с операциями отладки начнем с определения класса,
представляющего имя персоны, и протестируем его в работе. В этом коде должно
быть много неправильного, поэтому не поддавайтесь искушению сразу исправить
явно ошибочный код; задача состоит в том, чтобы научиться применять средства
отладки для нахождения ошибок. Однако на практике многие ошибки проявляются
сразу же при запуске программы. Вам не обязательно использовать отладчик или до-
полнительный код для их нахождения.
Создайте пустое консольное приложение Win32 под названием ExlOOl. Далее до-
бавьте заголовочный файл Name. h, в котором будет находиться определение класса
Name. Класс представляет имя из двух членов-данных — указателей на строки, храня-
щие имя и фамилию человека. Если вы хотите иметь возможность объявлять масси-
вы объектов Name, то должны предоставить конструктор по умолчанию в дополнение
к любым другим конструкторам. Вам понадобится возможность сравнения объектов
Name, поэтому придется включить в класс перегруженные операции для их осущест-
вления. Определение класса Name в файле Name. h будет таким:
// Name.h — определение класса Name
ftpragma once
// Класс, определяющий имя персоны
class Name
{
public:
Name(); // Конструктор по умолчанию
Name(const char* pFirst, const char* pSecond); // Конструктор
char* getName(char* pName) const; // Получить полное имя
size_t getNameLength() const; // Получить длину полного имени
// Операции сравнения имен
bool operator<(const Name& name) const;
bool operator==(const Name& name) const;
bool operator>(const Name& name) const;
private:
char* pFirstname; char* pSurname;
Технолог;
: отладки
585
Теперь вы можете добавить файл Name. срр для хранения определений функций-
членов класса Name. Определение конструкторов показано ниже.
// Name.срр — реализация класса Name
#include "Name.h" // Определение класса Name
#include "DebugStuff.h" // Управление отладочным кодом
#include <cstring> // Для строковых функций в стиле С
#include <cassert> // Для утверждений
#include <iostream>
using namespace std;
/ / Конструктор по умолчанию
Name::Name()
#ifdef CONSTRUCTOR-TRACE
/ / Трассировка вызовов конструктора
cerr « "ХпВызван конструктор Name по умолчанию.";
#endif
pFirstname = pSurname = "\0";
}
// Конструктор
Name::Name(const char* pFirst, const char* pSecond):
pFirstname(pFirst), pSurname(pSecond)
// Проверить, что аргументы не равны null
assert(pFirst != 0);
assert(pSecond != 0);
#ifdef CONSTRUCTOR-TRACE
// Трассировка вызовов конструктора
cerr « "ХпВызван конструктор Name.";
#endif
Конечно, мы не хотим иметь объекты Name, включающие в себя члены — нулевые
указатели, поэтому конструктор по умолчанию присваивает им пустые строки. Мы ис-
пользуем здесь свой собственный символ управления отладкой CONSTRUCTOR-TRACE,
чтобы управлять выводом трассировки вызовов конструктора. Добавим определение
этого символа в заголовочный файл DebugStuff .h позднее. Сюда можно поместить
что угодно в качестве отладочного кода, например, отображение значений аргумен-
тов, но обычно лучше стараться сохранять этот код насколько возможно, простым,
иначе сам отладочный код может внести новые ошибки. Здесь мы просто идентифи-
цируем сам факт вызова конструктора.
В конструкторе присутствует два утверждения для проверки того, не были ли
переданы нулевые указатели в качестве аргументов. Вы можете комбинировать их в
одно, однако используя отдельные вызовы для каждого аргумента, можно идентифи-
цировать, какой именно указатель равен null (если только не оба сразу, конечно).
Вы можете пожелать проверить, не были ли переданы пустые строки, посчитав
символы, предшествующие завершающему \0, например. Однако для проверки это-
го утверждения лучше не использовать. Такие вещи могут происходить в результате
пользовательского ввода, так что для этого должны быть предусмотрены обычные
программные проверки, обрабатывающие ошибки, которые случаются в процессе
нормального течения событий. Важно улавливать разницу между ошибками в коде и
ошибочными условиями, которые могут случаться во время обычного выполнения
программы. Конструктор никогда не должен получать нулевые указатели, но имя
586 Глава 9
нулевой длины легко может получиться при нормальной работе (от клавиатурного
ввода, например). В этом случае было бы лучше читать в коде имена, проверяя это
условие перед вызовом конструктора класса Name. Вы же хотите, чтобы ошибки, воз-
никающие при нормальном использовании программы, обрабатывались в рабочей
версии программы?
Функция get Name () требует от вызывающего кода передачи адреса массива, со-
держащего имена:
// Возвращает полное имя в виде строки, включающей имя, пробел, фамилию
// Аргумент должен быть адресом символьного массива, достаточного,
/ / чтобы вместить полное имя
char* Name::getName(char* pName) const
assert(pName != 0);
// Проверка аргумента на равенство null
#ifdef FUNCTION_TRACE
// Трассировка вызова функции
cout « "\nName::getName() вызвана.”;
#endif
strcpy(pName, pFirstname);
pName[strlen(pName)] = '
// Копировать имя
// Добавить пробел
// Добавить фамилию и вернуть результат
return strcpy(pName+strlen(pName)+1, pSurname);
Здесь имеем утверждение для проверки, что переданный аргумент не null. Обра-
тите внимание, что нет никакой возможности убедиться, что указатель установлен на
массив, в котором достаточно места, чтобы уместить полное имя. В этом следует по-
лагаться на вызывающую функцию. Есть также отладочный код для трассировки вы-
зова функции. Наличие записей о последовательности вызовов вплоть до точки, где
произошла катастрофа, иногда незаменимо для выяснения причин возникновения
проблемы.
Функция-член getNameLength () — это вспомогательная функция, которая позво-
ляет пользователю объекта Name определить, сколько места должно быть выделено
для размещения полного имени:
// Возвращает общую длины имени
size_t Name:: getNameLength () const
#ifdef FUNCTION_TRACE
// Трассировка вызова функции
cout « ”\nName::getNameLength() вызвана.";
#endif
return strlen(pFirstname)tstrlen(pSurname);
Функция, которая намерена вызвать getName (), может использовать возвращен-
ное getNameLength () значение, чтобы определить, сколько места нужно для того,
чтобы уместить полное имя. Также здесь присутствует код трассировки.
В интересах инкрементной разработки класса вы можете пока пропустить опреде-
ление перегруженных операций сравнения. Необходимы только определения функ-
ций-членов, которые в действительности используются в программе, а эта начальная
версия тестовой программы будет предельно простой.
Символы препроцессора, предназначенные для управления выполнением отладоч-
ного кода, можно определить в заголовочном файле DebugStuff .h:
Технологии отладки 587
// DebugStuff.h — управление отладкой
#pragma once
#ifdef _DEBUG
#define CONSTRUCTOR—TRACE // Трассировка вызовов конструктора
#define FUNCTION—TRACE // Трассировка вызовов функции
#endif
Ваши управляющие символы определены, только если определен _DEBUG, поэтому
ничего из отладочного кода не будет включено в рабочую версию программы.
Теперь можно испытать класс Name в следующей функции main ():
// Ех10_01.срр : включение отладочного кода в программу
#include <iostream>
using namespace std;
#include "Name.h''
int main(int argc, char* argv[])
Name myName("Ivor"t "Horton")
// Попробовать одиночный объект
// Получить и сохранить имя в локальном символьном массиве
char theName [10];
cout « "ХпПолное имя: " « myName.getName (theName);
// Сохранить имя в массиве из свободного хранилища
char* pName = new char[myName.getNameLength()+1];
cout « "ХпПолное имя: " « myName. get Name (pName);
cout « endl;
return 0;
}
Теперь, когда весь код введен, дважды проверен и полностью корректен, вы може-
те запустить его, дабы воочию в этом убедиться.
Описание полученных результатов
Но он даже не компилируется! Основная проблема кроется в конструкторе N ате.
Параметры указаны как const, что и должно быть, но данные-члены — нет. Вы мог-
ли бы объявить их как const, но в любом случае придется копировать строки имен,
а не просто копировать указатели. Исправим определение конструктора следующим
образом:
// Конструктор
Name::Name (const char* pFirst, const char* pSecond)
// Проверка аргумента на равенство null
assert(pFirst != 0);
assert(pSecond 1= 0) ;
#ifdef CONSTRUCTOR-TRACE
// Трассировка вызова конструктора
cerr « "ХпВызван конструктор Name.";
#endif
pFirstname = new char [strlen (pFirst) +1];
strcpy (pFirstname, pFirst);
pFirstname = new char [strlen (pSecond) +1];
s trcpy (pSurname, pSecond);
Теперь вы копируете строки, так что все должно быть хорошо, не правда ли?
588 Глава 9
При перекомпиляции программы будут выданы некоторые предупреждения о
том, что функция strcpy () устарела и лучше использовать strcpy__s (), но strcpy ()
успешно работает, поэтому проигнорируем предупреждение в данном упражнении.
Однако при перезапуске программы она выдаст сбой почти немедленно. Вы може-
те видеть это в консольном окне, где получите сообщение от конструктора, так что
приблизительно видно, как далеко зашло выполнение. Перезапустите программу под
управлением отладчика, и вы увидите, что случилось.
Отладка программы
Когда начинается отладка, вы получаете окно сообщений, указывающее на необ-
работанное исключение. В отладчике есть всесторонний набор средство для пошаго-
вого выполнения вашего кода и трассировки последовательности вызовов. Щелкните
на кнопке Break (Прервать) в диалоговом окне, сообщающем о необработанном ис-
ключении, чтобы остановить выполнение. Программа будет находиться в точке, где
произошло исключение, и текущий выполняемый код окажется в окне редактора.
Исключение вызвано обращением к области памяти, находящейся вне юрисдикции
программы, поэтому сразу подозрение падает на неверный указатель.
Стек вызовов
Стек вызовов хранит информацию обо всех функциях, которые были вызваны и
которые все еще выполняются, поскольку еще не вернули управления. Как вы видели
ранее, окно Call Stack (Стек вызовов) показывает последовательность вызовов функ-
ций, приведших к текущей точке программы (рис. 10.11).
Call Stack ▼ V- X
Name Language
msvcr80d.dll! str catfunsigned char * dst=0x00417714, unsigned char * src=0x0012ff68) Line Asm
+ Ex 10_pl.exe!Name: :Name(const char * pFirst=0x0041T70c, const char * pSecond =0x00417 C++
Exl0_01.exe!mainCmt argc=lz char * * argv =0x003565f0) Line 14 C++
Exl0_01.exe!__tmainCRTStartupO Line 586 + 0x19 bytes C
Exl0_01.exe!mainCRTStartup0 Line 403 C
t » - * ’• V ♦ - ♦ - ‘ II]
кегпй I2.ail!7c8399r3(i fc
^icall Stack 14Breakpoints । iZ] Output
Puc. 10.11. Окно Call Stack
и продолжается вниз
Последовательность вызовов функций отображается, начиная с самого последне-
го вызова в верхней части, библиотечной функции strcat ()
до вызовов Kernel32 в самом низу окна, показанного на рис. 10.11. Каждая функ-
ция была вызвана прямо или непрямо той, которая находится ниже ее, и ни одна
из отображенных функций еще не выполнила возврат. Строки Kerne 132 показывают
системные процедуры, которые стартовали до запуска нашей функции main (). Вас
интересует роль вашего кода в этом, и вы можете видеть, начиная со второй строки,
что конструктор класса Name находится в процессе выполнения (еще не вернул управ-
ления), когда было возбуждено исключение. Если выполнить двойной щелчок на этой
строке, в окне редактора отобразится код этой функции и с помеченной строкой ис-
ходного кода, которая выполнялась, когда возникла проблема. В данном случае это:
strcpy(pSurname, pSecond);
Технологии отладки 589
Этот вызов стал причиной возбуждения необработанного исключения — но поче-
му? Исходная проблема не обязательно кроется здесь; здесь она только проявилась.
Это типичная ошибка, связанная с указателями. Взгляните на окно, показывающее
значение переменных в контексте конструктора Name, который в данный момент ото-
бражен в окне редактора (рис. 10.12).
Рис. 10.12. Окно Autos, отображающее неправильный указатель
Поскольку текущий контекст находится в функции-члене класса Name, окно Autos
отображает указатель this, который содержит адрес текущего объекта. Указатель
pSurname содержит роковой адрес Охсссссссс, который соответствует десятичному
3435973836! Поскольку у меня на компьютере точно меньше 3 миллиардов байт памя-
ти, это выглядит немного странно, и отладчик предполагает, что pSurname содержит
неправильный указатель и помечает его таким. Если посмотреть на pFirstname, с
ним тоже что-то не так. В этой точке кода (где копируется фамилия) первое имя уже
должно быть скопировано, но его содержимое является “мусором”.
Виновата предыдущая строка. Поспешное копирование кода привело к тому, что
память для pFirstname выделена второй раз вместо того, чтобы выделить ее для
pSurname. Копирование происходит по случайному адресу, и это вызывает возбужде-
ние исключения. Не хотите ли исправить, что вы сделали? Строка должна выглядеть
следующим образом:
pSurname = new char[strlen(pSecond)+1];
Это типичный случай, когда код, породивший адрес в указателе, не является тем
кодом, в котором проявляется ошибка. Вообще он может находиться очень далеко
от этого места. Простой просмотр указателя или указателей, участвующих в операто-
ре, ставшем причиной ошибки, часто может привести вас непосредственно к корням
проблемы, хотя иногда приходится долго искать. Но вы всегда можете добавить от-
ладочный код, если чувствуете, что увязли в этом.
Давайте исправим оператор в панели редактора на правильный и перекомпили-
руем проект с включенным изменением. Вы можете перезапустить программу внутри
отладчика после перекомпиляции, щелкнув на кнопке в панели инструментов Debug,
и тут же получите сюрприз — новое необработанное исключение. Без сомнения, это
новая проблема с указателем, и можно видеть из вывода консольного окна, что по-
следней вызванной функцией была getNameLength ():
Вызван конструктор Name.
Name::getName() вызвана.
Полное имя: Horton
Name::getNameLength() вызвана.
Вывод имени определенно неправильный, однако вы не знаете точно, где имен-
но находится проблема. Перезапуск и повторное пошаговое выполнение программы
должно помочь разобраться.
590 Глава 9
Переход к ошибке
Функция getNameLength () в данный момент отображается в окне редактора, и от-
ладчик указывает строку, в которой возникла проблема:
return strlen(pFirstname)tstrlen(pSurname)+1;
В окне Call Stack вы можете видеть, что точка выполнения программы находится
в функции-члене getNameLength (), которая только вызывает библиотечную функ-
цию strlen (), чтобы получить общую длину имени. Маловероятно, что сбой произо-
шел в функции strlen (), следовательно, что-то не так с частью объекта. Окно AutOS,
отображая переменные в контексте этой функции, показывает, что текущий объект
был поврежден, что видно на рис. 10.13.
Autos
Name
+i pFirstname
® .л pSurname
- 3 this
ш pFirstname
® pSurname
I Value Type
0x00356678 Ivor" . ▼ char *
0x74726f48 <Ead Ptr> - char *
0w0012ff53 {pFirstname=0x00356675 const • const
0x00356678 Ivor' . - char *
0x74726f48 <Bad Ptr> . char*
I -------------------—----------------
Autos I LocalsIt^ThreadSiModules i^v.atch 1
Рмс. 10.13. Окно AutOS показывает, что теку-
щий объект был поврежден
рядом с this, вы мо-
На текущий объект указывает this и, щелкнув на значке
жете просмотреть его данные-члены. Сразу видно, что проблема в члене pSurname.
Адрес, который он содержит
так. Более того, отладчик пометил его как недопустимый указатель.
Предполагая, что ошибки подобного рода не возникают в точке, где они проявля-
ются, вы можете вернуться назад, перезапустить программу и, выполняя ее по шагам,
следить, где объект Name будет “испорчен
(Перешагнуть) или нажать <F10>, чтобы перезапустить приложение, и повторно на-
жимая <F10>, пройти по шагам все операторы программы. После выполнения опера-
тора, определяющего объект myName, окно AutOS для функции main () покажет, что
он был сконструирован успешно, что и можно видеть на рис. 10.14.
;олжен ссылаться на строку “Horton”, но это явно не
Для этого вы можете выбрать Step Over
Name
Value Type
г myName
+ pFirstname
tl pSurname
/ theName
{pFirstname =0x00356678 Tvor^pSun Name
0x00356678 Ivor' t char *
0x003566c0 Tiorton" . - char*
OxO012ff44Hnnniilimiiraxf£ r char [10]
Sh Autos Л1 Locals |ig}-Threads |W Modules Watch 1
Puc. 10.14. Окно Autos показывает, что объ-
ект Name сконструирован успешно
В результате выполнения следующего оператора, который выводит имя, объект
myName повреждается. Это ясно видно в окне Autos для main () на рис. 10.15.
Технолог:
►<
: отладки 591
Autos
Value
Name
► > Name: igetName rel 0x0012ff5c Tdcrton
► / std::operator<<<s {...}
• 4 std::operator<«s
/ myName
F pFirstname
E pSurname
theName
std:: basic_cstream <char std:
std:: basic_ostream <char,s td:
{pFirstname =0x00356678 lvorrpSun Name
0x00356673 Ivor" - char *
0x74726f48 <Bad Ptr> - char *
Oxcccccccc <BadPtr> . ▼ char*
oxoo i2ff44 "Ivor iiiiiiiiiiiiiiixf ч - char[io]
Autos
Locals | {^Threads ,3 Modules.^] Watch 1
Puc. 10.15. Окно Autos отображает факт раз-
рушения объекта Name
Если исходить из предположения, что операция потокового вывода работает
корректно, значит ваша функция-член getName () делает что-то такое, чего делать
не должна. Перезапустите отладчик еще раз, но на этот раз используйте Step Into,
когда выполнение достигнет оператора вывода. Когда точка выполнения окажется в
первом операторе функции getName (), вы можете продолжить шагать по ее опера-
торам, используя Step Over. Следите за контекстным окном по мере продвижения по
функции. Вы увидите, что все замечательно до тех пор, пока не будет выполнен сле-
дующий оператор:
strcpy(pName+strlen(pName)+1, pSurname); // Добавить второе имя после пробела
Этот оператор вызывает повреждение pSurname для текущего объекта, на кото-
рый указывает this. Вы можете видеть это в окне Autos на рис. 10.16.
Autos
Name
+i / pName
±i / pSurname
Г=1 this
El pFirstname
i! j pSurname
I Value I Type
oxooi2ff44 Ivor iiiiiiniiiiiiixf a - diar *
0x74726f48 <Bad Ptr> - diar *
0x0012ff58 {pFirstname =0x00356678 const Name * const
0x00356678 "Ivor” diar *
0x74726f48 <Bad Ptr > - char *
----------------------------------------
Autob Ж11 orals rT^Threads Modules Watch 1
Puc. 10.16. Повреждение pSurname для текущего объекта
Как может копирование из объекта в другой массив испортить объект, особенно
учитывая, что pSurname передано в качестве аргумента для параметра const? Вы
должны посмотреть на адрес, находящийся в pName, чтобы понять это. Сравните его
с адресом, хранящимся в this. Между ними разница всего в 20 байт. Они не могут на-
ходиться так близко! Вычисление адреса позиции в pName неверно — просто потому,
что вы забыли, что копирование пробела перезаписало ограничивающий \0 в масси-
ве pName, и strlen (pName) не может вычислить корректную длину pName. Вся про-
блема вызвана этим оператором:
pName [strlen(pName) ] = ' // Добавить пробел
Он перезаписал \0, и это привело к тому, что следующий вызов strlen () выдал
неверный результат.
Этот код все равно излишне запутан. Применять библиотечную функцию
strcat () для соединения строки намного лучше, чем вызывать strcpy (), поскольку
это позволит исключить все излишние модификации указателя. То есть вы должны
переписать оператор вот так:
strcat(pName, " "
// Добавить пробел
592 Глава 9
Конечно, должен быть изменен и следующий оператор:
return strcat(pName, pSurname); // Добавить второе имя и вернуть итог
После этих изменений вы можете перекомпилировать и попробовать снова. На
этот раз программа работает удовлетворительно, что можно видеть из вывода:
Вызван конструктор Name.
Name::getName() вызвана.
Полное имя: Ivor Horton
Name::getNameLength() вызвана.
Name::getName()вызвана.
Полное имя: Ivor Horton
Press any key to continue . . .
Однако получение правильного вывода не всегда означает, что все хорошо, и в
данном случае это определенно не так. Вы получаете окно сообщения, отображаемое
библиотекой отладки (рис. 10.17), которое сообщает о повреждении стека.
Рис. 10.17. Окно сообщения о повреждении стека
Следующий код демонстрирует, в чем состоит проблема:
int main(int argc, char* argv[])
Name myName (" Ivor", "Horton ’’);
// Попробовать одиночный объект
/ / Получить и сохранить имя в локальном символьном массиве
char theName[10];
cout « "ХпПолное имя:
myName.getName(theName);
// Сохранить имя в массиве из свободного хранилища
char* pName
new char [myName.getNameLength () ] ;
cout « м\пПолное имя: " « myName.getName(pName);
cout « endl;
return 0;
Обе выделенные полужирным строки ошибочны. Первая представляет массив
из 10 символов для хранения имени. Фактически требуется 12 символов: 10 для двух
имен, один для пробела и один для \ 0 в конце. Вторая выделенная полужирным стро-
ка должна добавить 1 к значению, возвращенному функцией getNameLength (), что-
бы учесть \ 0 в конце. Таким образом, код main () должен быть таким:
Технолог:
: отладки
593
Name myName("Ivor", "Horton"); // Попробовать одиночный объект
// Получить и сохранить имя в локальном символьном массиве
char theName [ 12 ];
cout « "ХпПолное имя: " « myName.getName (theName) ;
/ / Сохранить имя в массиве из свободного хранилища
char* pName = new char[myName.getNameLength0+1];
cout « "ХпПолное имя: " « myName. get Name (pName);
cout « endl;
return 0;
Однако в определении члена класса getNameLength () присутствует и более се-
рьезная проблема. Там не добавляется 1 для пробела между именем и фамилией, так
что возвращенное значение всегда на один символ короче. Определение должно вы-
глядеть следующим образом:
size t Name::getNameLength() const
#ifdef FUNCTION_TRACE
// Трассировка вызова функции
cout « "\nName::getNameLength() вызвана.";
#endif
return strlen (pFirstname) +strlen (pSurname) +1;
Но и это еще не все. Возможно, вы изначально заметили, что наш класс содержит
серьезные ошибки, но давайте проведем тестирование, чтобы посмотреть, все ли из
них устранены.
Тестирование расширенного класса
Судя по выводу, все работает, так что теперь можно добавить к классу Name опреде-
ления перегруженных операций сравнения. Я полагаю, что это будет новый консоль-
ный проект Win32 под названием Ех10_02. Чтобы реализовать операции сравнения
для объектов Name, вы можете использовать функции сравнения, объявленные в за-
головочном файле <cstring>. Начните с операции “меньше чем”:
// Операция "меньше чем"
bool Name::operator<(const Name& name) const
int result = strcmp(pSurname, name.pSurname);
if(result < 0)
return true;
if(result == 0 && strcmp(pFirstname, name.pFirstname) < 0)
return true;
else
return false;
}
Теперь можно легко определить операцию > в терминах операции
// Операция "больше чем"
bool Name::operator>(const Name& name) const
return name
this;
594 Глава 9
Для определения равенства имен вы используете функцию strcmp () — вновь из
стандартной библиотеки:
// Операция равенства
bool Name: :operator== (const Name& name) const
if(strcmp(pSurname, name.pSurname) == 0 &&
strcmp(pFirstname, name.pFirstname) == 0)
return true;
else
return false;
Теперь расширим тестовую программу. Вы можете создать массив объектом Name,
инициализировать их некоторым произвольным образом, а затем сравнить элементы
массива, используя операции сравнения объектов Name. Ниже приведен новый вари-
ант функции main () наряду с функцией init () для инициализации массива Name:
// Ех10_02.срр : расширение тестовых операций
#include <iostream>
using namespace std;
#include "Name.h"
// Функция для инициализации массива произвольными именами
void init(Name* names, int count)
char* firstnames[] = { "Charles", "Mary", "Arthur", "Emily", "John"};
int firstsize = sizeof (firstnames)/sizeof (firstnames [0]);
char* secondnames [ ] = { "Dickens", "Shelley", "Miller", "Bronte", "Steinbeck"};
int secondsize = sizeof (secondnames)/sizeof (secondnames [0]);
char* first = firstnames [0];
char* second = secondnames [ 0 ];
0 ; i<count ; i++)
first s firstnames [i%firstsize];
else
second = secondnames [i%secondsize];
names [i] = Name (first, second);
int main(int argc, char* argv[])
Name myName("Ivor", "Horton"); // Попробовать одиночный объект
// Получить и сохранить имя в локальном символьном массиве
char theName[12];
cout « "ХпПолное имя: ” « myName.getName (theName);
// Сохранить имя в массиве из свободного хранилища
char* pName = new char[myName.getNameLength()+1];
cout « "ХпПолное имя: " « myName.getName(pName);
const int arraysize = 10;
Name names [arraysize];
11 Инициализировать имена
init(names, arraysize);
11 Попробовать сравнение
char* phrase = 0;
char* iName = 0;
char* jName = 0;
// Попробовать массив
// Хранит фразу сравнения
// Хранит полное имя
// Хранит полное имя
Технолог
[ отладки 595
I (
new char [names [ i ]. getNameLength 0+1];
// первого имени
for(int j
else if(names[i]
else if (names [i] = names [j]) // Излишне, но вызывает operator" ()
phrase » " равно
jName = new char [names [ j ]. getNameLength ()+1 ];// Массив для хранения
11 второго имени
cout « endl « names [i] .getName (iName) « " есть" « phrase
« names [ j ].getName (jName);
return 0;
Функция init () создает последовательную комбинацию из имен и фамилий, взя-
тых из массива имен, для инициализации массива объектов Name. Имена повторяют-
ся после генерации 25-го, но здесь вам нужны только 10.
Поиск следующей ошибки
Если вы запустите программу под управлением отладчика, используя кнопку Start
Debugging (Начать отладку) в панели инструментов Debug, она снова завершится
сбоем. Будет отображено окно сообщений, показанное на рис. 10.18.
Microsoft Visual Studio
\ Unhandled exception at 0x00411949 in Exl0_02.exe: OxCOOOOOFD: Stack
* \ overflow.
Break
Continue
Ignore
Puc. 10.18. Окно сообщения о сбое программы
то есть она вызывает
Окно сообщения указывает на переполнение доступной памяти стека, и если вы
выберете кнопку Break, то окно Call Stack покажет, что именно произошло не так.
Вы увидите последовательные вызовы функции operator ()
сама себя. Если взглянуть на ее код, то причина станет ясной: банальная опечатка.
Единственная строка в теле функции должна выглядеть так:
return name < * *this;
596 Глава 9
Исправьте, перекомпилируйте и попробуйте снова. На этот раз она работает пра-
вильно, но, к сожалению, класс все еще содержит дефект. В нем происходит утечка
памяти, которая не проявляется никакими симптомами, но в другом контексте они
могут оказаться губительными. Утечки памяти трудно обнаружить непосредственно,
но вы можете воспользоваться помощью Visual C++ 2005.
Отладка динамической памяти
Динамическое выделение памяти
потенциальный источник ошибок, и возмож-
но, наиболее распространенная из них в этом контексте — это утечки памяти. Просто
чтобы напомнить вам, скажем, что утечки происходят, когда вы используете опера-
цию new для выделения памяти, но нигде не используете операцию delete для ее
освобождения, когда она больше не нужна.
Помимо того, что вы просто забываете освободить выделенную память, следует
также помнить о том, что невиртуальные деструкторы в иерархии классов также мо-
гут быть причиной этой проблемы, потому что при уничтожении объекта вызывает-
ся неправильный деструктор, что вы видели ранее. Конечно, по завершении вашей
программы вся память освобождается; однако пока она работает, память остается за-
нятой вашей программой. Утечки памяти большую часть времени не проявляют ни-
каких симптомов — бывает, что вообще никогда, однако они наносят ущерб произво-
дительности вашей машины, потому что память остается недоступной для полезных
целей. Иногда это может привести к катастрофическому сбою программы, когда вся
свободная память оказывается занятой.
Для проверки использования памяти свободного хранилища вашей программой
в Visual C++ 2005 предусмотрен широкий диапазон диагностических процедур; они
используют специальную отладочную версию свободного хранилища. Все они объ-
явлены в заголовочном файле crtdbg.h. Все вызовы этих процедур автоматически
удаляются из рабочей версии программы, так что вам не нужно беспокоиться о до-
бавлении управляющих директив препроцессора.
Функции проверки свободного хранилища
Здесь мы приведем обзор средств, позволяющих проверить операции со свобод-
ным хранилищем, и обнаружить утечки памяти. Функции, объявленные в crtdbg. h,
проверяют свободное хранилище, используя записи о его состоянии, хранящиеся в
структуре типа _CrtMemState. Эта структура относительно проста и определена сле-
дующим образом:
typedef struct _CrtMemState
struct —CrtMemBlockHeader* pBlockHeader; // Указатель на последний
// выделенный блок
unsigned long ICounts[_MAX_BLOCKS];// Количество блоков каждого типа
unsigned long iSizes[_MAX_BLOCKS]; // Общее количество байт, выделенных
// для каждого типа блоков
unsigned long iHighWaterCount; // Максимальное количество байт,
// выделенных одновременно до
// настоящего момента
unsigned long ITotalCount; // Общее количество байт, выделенных
//на настоящий момент
} CrtMemState;
Технолог;
: отладки
597
Вам не придется непосредственно иметь дело с подробностями этого состояния
свободного хранилища, поскольку вы используете функции, предоставляющие ин-
формацию в более читабельной форме. Существует довольно много функций, уча-
ствующих в трассировке операций со свободным хранилищем, но мы рассмотрим
только пять наиболее интересных из них. Эти функции предлагают следующие воз-
можности.
□ Запись состояния свободного хранилища в любой точке.
Определение разницы между двумя состояниями свободного хранилища.
□ Вывод информации о состоянии.
□ Вывод информации об объектах в свободном хранилище.
□ Обнаружение утечек памяти.
Ниже приведены объявления этих функций вместе с кратким описанием того, что
они делают:
void _CrtMemCheckpoint(_CrtMemState* state);
Эта функция сохраняет текущее состояние свободного хранилища в структу-
ре _CrtMemState. Аргумент, передаваемый функции — это указатель на структуру
_CrtMemState, в которую записывается информация состояния.
int _CrtMemDifference(_CrtMemState* stateDiff,
const _CrtMemState* oldState,
const —CrtMemState* newState);
Эта функция сравнивает состояние, специфицированное третьим аргументом,
с предыдущим состоянием, указанным во втором аргументе. Разница сохраняется в
структуре CrtMemState, которая передается в первом аргументе. Если состояния
отличаются, функция возвращает ненулевое значение (true), а иначе возвращается
ноль (false).
void _CrtMemDumpStatistics(const _CrtMemState* state);
Это сбрасывает информацию о состоянии свободного хранилища, специфициро-
ванную аргументом, в выходной поток. Структура состояния, на которую указывает
аргумент, может быть состоянием, записанным функцией _CrtMemCheckpoint (), или
разницу между двумя состояниями, произведенную _CrtMemDif ference ().
void —CrtMemDumpAllObjectsSince(const —CrtMemState* state);
Функция сбрасывает информацию об объектах, размещенных в свободном храни-
лище с момента состояния свободного хранилища, указанного в аргументе; это состо-
яние должно быть ранее записано вызовом _CrtMemCheckpoint (). Если этой функ-
ции передать null, она сбрасывает информацию обо всех объектах, размещенных с
момента запуска вашей программы.
int _CrtDumpMemoryLeaks ();
Это функция, которая понадобится вам для данного примера, поскольку проверя-
ет утечки памяти и сбрасывает информацию о любой обнаруженной утечке. Вы може-
те вызвать эту функцию в любое время, но очень удобный механизм может вызывать
эту функцию автоматически по завершении вашей программы. Если вы включите
этот механизм, то получите автоматический детектор любых утечек памяти, произо-
шедших за время выполнения программы, так что давайте посмотрим, как это можно
сделать.
598 Глава 9
Управление отладочными операциями
свободного хранилища
Вы можете управлять отладочными операциями свободного хранилища, уста-
навливая флаг crtDbgFlag типа int. Этот флаг включает в себя пять отдельных
управляющих бит, в том числе один для включения автоматической проверки утечек
памяти. Вы специфицируете эти управляющие биты, используя идентификаторы, пе-
речисленные в табл. 10.2.
Таблица 10.2. Управляющие биты флага _crtDbgFlag
Управляющий бит Описание
_CRTDBG_ALLOC_MEM_DF
_CRTDBG_DELAY_FREE_MEM_DF
_CRTDBG_CHECK_ALWAYS_DF
_CRT DBG_CHECK_CRT_DF
__CRTDBG_LEAK_CHECK_DF
Когда этот бит установлен, включается отладка, так что можно от-
слеживать состояние свободного хранилища.
Когда этот бит установлен, он предотвращает освобождение памяти
операцией delete, так что вы можете определить, что случится при
условии нехватки памяти.
Когда этот бит установлен, включается автоматический вызов функ-
ции _CrtCheckMemory () при каждой операции new и delete. Эта
функция проверяет целостность свободного хранилища, проверяя,
например, что блоки не были перезаписаны сохранением значений,
с выходом за пределы массива. Если обнаруживается любой де-
фект, выводится отчет. Замедляет выполнение, но позволяет быстро
перехватывать ошибки.
Когда этот бит установлен, память, использованная внутри библио-
теки исполняющей системы, отслеживается в операциях отладки.
Заставляет выполнять проверку утечек памяти при завершении
программы, автоматически вызывая CrtDumpMemoryLeaks ().
Вы получаете вывод только в том случае, если ваша программа не
освобождает всю память, которая была выделена.
По умолчанию бит _CRTDBG_ALLOC_MEM_DF установлен, а все остальные — нет.
Для включения и отключения комбинаций этих бит вы должны использовать бито-
вые операции. Чтобы установить флаг __crtDbgFlag, вы передаете флаг типа int
функции _CrtDbgFlag (), которая реализует необходимую комбинацию индикато-
ров. Она активизирует переданный вами флаг и возвращает предыдущее состояние
_CrtDbgFlag. Один способ установки нужных индикаторов заключается в предва-
рительном получении текущего состояния флага __crtDbgFlag. Это делается вызо-
вом функции _CrtSetDbgFlag () с аргументом _CRTDBG_REPORT__FLAG, как показано
ниже:
int flag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); // Получить текущий флаг
Затем вы можете установить или снять индикаторы, комбинируя идентифика-
торы индивидуальных индикаторов с этим флагом посредством битовых операций.
Для включения индикатора выполняется операция логического ИЛИ с флагом.
Например, чтобы включить автоматическую проверку утечек памяти в flag, можно
записать так:
flag |= _CRTDBG_LEAK_CHECK_DF;
Чтобы отключить индикатор, нужно воспользоваться операцией логического И
для обращенного значения идентификатора и флага. Например, для отключения
Технологии отладки
599
трассировки памяти, используемой внутри библиотеки, вы можете записать следую-
щий код:
flag &= ~_CRTDBG_CHECK_CRT_DF;
Чтобы ввести в действие новый флаг, вы просто вызываете __CrtSetDbgFlag () с
этим флагом в качестве аргумента:
_CrtSetDbgFlag(flag);
Альтернативно вы можете объединить с помощью операции ИЛИ вместе
все индикаторы, которые вам нужны, и передать результат в качестве аргумента
_CrtSetDbgFlag (). Если вы хотите только выполнять проверку утечек памяти при
завершении программы, то можете поступить так:
_CrtSetDbgFlag (__CRTDBG_LEAK_CHECK_DF | _CRTDBG_ALLOC_MEM_DF) ;
Если вы хотите установить определенную комбинацию индикаторов вместо вклю-
чения и отключения отдельных бит в разных точках вашей программы, это сделать
проще всего. Теперь вы почти готовы применить средства отладки динамической па-
мяти к нашему примеру. Осталось только посмотреть, как определить, куда направля-
ется диагностический вывод этих средств.
Отладочный вывод свободного хранилища
Место назначения вывода функций отладки свободного хранилища — это не стан-
дартный поток вывода; по умолчанию он направляется в окно отладочных сообще-
ний. Если вы хотите видеть вывод на stdout, то должны настроить это. Есть две
функции, участвующие в этом: _CrtSetReportMode (), которая устанавливает основ-
ное назначение вывода, и CrtSetReportFile (), которая указывает специфичной
назначение потока. Функция _CrtSetReportMode () объявлена следующим образом:
int _CrtSetReportMode(int reportType, int reportMode);
Существуют три вида вывода, производимого функциями отладки свободного хра-
нилища. Каждый вызов функции _CrtSetReportMode () устанавливает назначение,
специфицированное вторым аргументом, для типа вывода, указанного первым аргу-
ментом. Вы задаете тип отчета с применением одного из идентификаторов, перечис-
ленных в табл. 10.3.
Таблица 10.3. Идентификаторы типа вывода
Идентификатор Описание
_CRT_WARN
_CRT_ERROR
_CRT_ASSERT
Предупреждающие сообщения различного рода. Вывод при обнаружении утечки
памяти — это предупреждение.
Катастрофические ошибки, свидетельствующие о неразрешимых проблемах.
Вывод от утверждений (но не вывод функции assert (), о которой говорилось ранее).
В заголовочном файле crtdbg.h определены два макроса ASSERT и ASSERTE, ко-
торые работают почти так же, как функция assert () из стандартной библиотеки.
Разница между этими двумя макросами состоит в том, что ASSERTE сообщает выраже-
ние утверждения при сбое, a ASSERT этого не делает.
Режим отчета указывается комбинацией идентификаторов, перечисленных в
табл. 10.4.
600 Глава 9
Таблица 10.4. Идентификаторы режима отчета
Идентификатор Описание
—CRTDBG—MODE—DEBUG Режим по умолчанию, направляет вывод в строку отладки, которую вы ви- дите в окне отладки при запуске программы под управлением отладчика.
—CRTDBG—MODE—FILE Вывод направляется в выходной поток.
—CRTDBG—MODE—WNDW Вывод отображается в окне сообщений.
—CRTDBG—REPORT—MODE Если вы специфицируете это, то функция CrtSetReportMode () просто возвращает текущий режим отчета.
Чтобы специфицировать более одного места назначения, вы просто объединяете
логическим ИЛИ нужные идентификаторы, используя операцию |. Вы устанавливаете
назначение каждого типа вывода отдельным вызовом функции CrtSetReportMode ().
Чтобы направить вывод об утечке памяти в файловый поток, можете установить ре-
жим отчета с помощью следующего оператора:
CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE);
Это просто устанавливает назначение в общем случае как файловый поток. Однако
вам еще нужно вызвать функцию —CrtSetReportFile () для конкретного указания
назначения.
Функция _CrtSetReportFile () объявлена следующим образом:
_HFILE _CrtSetReportFile(int reportType, _HFILE reportFile);
Второй аргумент может быть либо указателем на файловый поток (типа HFILE),
на котором я не буду останавливаться, либо одним из идентификаторов, перечислен-
ных в табл. 10.5.
Таблица 10.5. Идентификаторы направления вывода
Идентификатор Описание
-CRTDBG-FILE_STDERR Вывод направляется в стандартный поток ошибок, stderr.
—CRTDBG—FILE—STDOUT Вывод направляется в стандартный выходной поток, stdout.
—CRTDBG—REPORT—FILE Если специфицирован этот аргумент, то функция CrtSetReportFile () просто возвращает текущую установку назначения.
Чтобы установить вывод обнаружения утечек памяти в стандартный выходной по-
ток, вы можете записать так:
—CrtSetReportFile(_CRT—WARN, _CRTDBG_FILE_STDOUT);
Теперь у вас достаточно знаний о процедурах отладки свободного хранилища, что-
бы попытаться обнаружить утечки памяти в нашем примере.
Практическое занятие ОбнаруЖвНИе уТвЧвК ПЭМЯТИ
Даже если вы установите настройки проекта так, чтобы направить стандартный
выходной поток в файл, будет неплохой идеей сократить объем вывода, так что огра-
ничим размер массива имен пятью элементами. Ниже показана новая версия функ-
ции main () для Ех10_02, которая позволит использовать средства отладки свободно-
го хранилища, в общем, и обнаружение утечек памяти, в частности.
Технологии отладки 601
int main(int argc, char* argv[])
// Включить отладку свободного хранилища и проверку утечек памяти
_CrtSetDbgFlag ( __CRTDBG__LEAK__CHECK__DF | _CRTDBG_ALLOC_MEM_DF ) ;
_CrtSetReportMode (_CRT_WARN , _CRTDBG_MODE_FILE);
_CrtSetReportFile (_CRT_WARN , _CRTDBG_FILE_STDOUT) ;
Name myName("Ivor", "Horton"); // Попробовать одиночный объект
// Получить и сохранить имя в локальном символьном массиве
char theName[12];
cout « "ХпПолное имя: " « myName.getName (theName);
// Сохранить имя в массиве из свободного хранилища
char* pName = new char[myName.getNameLength()+1];
cout « "ХпПолное имя: " « myName.getName (pName);
const int arraysize = 5;
Name names[arraysize]; // Попробовать массив
// Инициализировать имена
init(names, arraysize);
// Попробовать сравнение
char* phrase =0; // Хранит фразу сравнения
char* iName = 0; // Хранит полное имя
char* jName = 0; // Хранит полное имя
for (int i = 0; i < arraysize ; i++) // Сравнить каждый элемент
iName = new char[names[i].getNameLength()+1]; // Массив для хранения
// первого имени
for (int j = i+1 ; j<arraysize ; j++) // co всеми прочими
if(names[i] <names[j])
phrase = " меньше чем ";
else if(names[i] > names [j])
phrase = " больше чем ";
else if(names[i] == names[j]) // Излишне, но вызывает operator==()
phrase = " равно ";
jName = new char[names[j].getNameLength()+1]; // Массив для хранения
// второго имени
cout « endl « names [i] .getName (iName) « " есть" « phrase
« names [j ] .getName (jName);
cout « endl;
return 0;
Чтобы еще больше сократить вывод, можете отключить трассирующий вывод, за-
комментировав символы в заголовочном файле DebugStuff .h:
// Debugstuff.h — управление отладкой
#pragma once
#ifdef _DEBUG
//#define CONSTRUCTOR_TRACE // Трассировка вызовов конструктора
//#define FUNCTION_TRACE // Трассировка вызовов функции
#endif
После этого перекомпилируйте пример и запустите его снова.
602 Глава 9
Описание полученных результатов
Код работает так, как и можно было ожидать. Вы получаете отчет о том, что ваша
программа действительно имеет утечки памяти, и в конце работы программы полу-
чаете список объектов в свободном хранилище. Вывод, сгенерированный средством
отладки свободного хранилища, начинается так:
Detected memory leaks!
Обнаружены утечки памяти!
Dumping objects ->
Сброшенные объекты ->
{143} normal block at 0x00355F08, 15 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
{143} нормальный блок по адресу 0x00355F08f 15 байт длиной.
Данные: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
{142} normal block at 0x00355EC8, 15 bytes long.
Data: <Emily Steinbeck> 45 6D 69 6C 79 20 53 74 65 69 6E 62 65 63 6B
{141} normal block at 0x0Q355E90, 12 bytes long.
Data: <Emily Miller> 45 6D 69 6C 79 20 4D 69 6C 6C 65 72
и заканчивается так:
{120} normal block at 0x003559D8, 8 bytes long.
Data: <Dickens > 44 69 63 6B 65 6E 73 00
{119} normal block at 0x0Q3559A0, 8 bytes long.
Data: <Charles > 43 68 61 72 6C 65 73 00
{118} normal block at 0x00355968, 11 bytes long.
Data: <Ivor Horton> 49 76 6F 72 20 48 6F 72 74 6F 6E
{117} normal block at 0x00355930, 7 bytes long.
Data: <Horton > 48 6F 72 74 6F 6E 00
{116} normal block at 0x003558F8, 5 bytes long.
Data: <Ivor > 49 76 6F 72 00
Object dump complete.
Дамп объектов завершен.
Объекты, о которых сообщается, что они остались в свободном хранилище, пред-
ставлены в порядке, обратном их размещению: самый последний идет в начале, а пер-
вый — в конце. Из вывода ясно, что класс Name выделяет память для своих членов и
никогда ее не освобождает. Последние три объекта соответствуют массиву pName, вы-
деленному в main (), и членам-данным объекта myName. Блоки для полных имен выде-
лены в main (), и они также не удалены из памяти. Проблема нашего класса состоит в
том, что мы забыли фундаментальное правило, касающееся классов, выделяющих па-
мять динамически; они всегда должны иметь деструктор, конструктор копирования и
операцию присваивания. Поэтому класс должен быть объявлен следующим образом:
class Name
public:
Name(); II Конструктор по умолчанию
Name(const char* pFirst, const char* pSecond); // Конструктор
Name (const Names rName) ; // Конструктор копирования
-Name () ;
char* getName(char* pName) const;
size t getNameLength () const;
// Деструктор
// Получить полное имя
// Получить длину полного имени
Технологии отладки 603
// Операции сравнения имен
bool operator<(const Name& name) const;
bool operator==(const Name& name) const;
bool operator>(const Name& name) const;
Name& operator»(const Names rName) ;
private:
char* pFirstname;
char* pSurname;
};
Конструктор копирования можно определить так:
Name:: Name(const Name& rName)
pFirstname = new char [strlen (rName.pFirstname) +1]; // Выделить память
/ / для имени
strcpy(pFirstname, rName.pFirstname); // и скопировать его.
pSurname = new char[strlen(rName.pSurname)+1]; // To же для фамилии...
strcpy(pSurname, rName.pSurname);
Деструктор должен просто освобождать память двух членов-данных:
Name::~Name()
delete[] pFirstname;
delete[] pSurname;
В операции присваивания вы должны сначала выполнить обычную проверку на
идентичность левой и правой частей:
Name& Name::operator=(const Name& rName)
if (this == &rName) // Если Ihs эквивалентно rhs,
return *this; //то просто вернуть объект
delete[] pFirstname;
pFirstname = new char [strlen(rName.pFirstname)+1]; // Выделить память
// для имени
strcpy(pFirstname, rName.pFirstname); // и скопировать его.
delete[] pSurname;
pSurname = new char[strlen(rName.pSurname)+1]; // To же для фамилии...
strcpy(pSurname, rName.pSurname);
return *this;
Вы также должны обеспечить корректную работу конструктора по умолчанию.
Если конструктор по умолчанию не распределяет память в свободном хранилище, есть
вероятность, что деструктор будет ошибочно пытаться освободить память, которая не
была выделена в свободном хранилище. Значит, вы должны модифицировать его:
Name::Name()
#ifdef CONSTRUCTOR_TRACE
// Трассировка вызовов конструктора
cerr « ”\пВызван конструктор Name по умолчанию.”;
#endif
// Выделить массив размером 1 для пустой строки
pFirstname = new char[l];
604 Глава 9
pSurname = new char[l];
pFirstname[0] = pSurname[0] = *\0’; // Записать нулевой символ
Если вы добавите в main () оператор для освобождения памяти, которая там была
выделена динамически, то программа должна будет выполняться без каких-либо со-
общений об утечках памяти. В main () потребуется добавить следующий оператор в
конце внутреннего цикла, управляемого j:
delete [] jName;
Кроме того, необходимо также добавить следующий оператор в конец внешнего
цикла, управляемого i:
delete[] iName;
И, наконец, следует освободить память, выделенную pName после всех циклов в
main ():
delete[] pName;
Отладка программ C++/CLI
При программировании на C++/CLI жизнь намного легче. Никаких сложностей,
связанных с поврежденными указателями или утечками памяти, в программах CLR
не возникает, так что это существенно упрощает задачу отладки по сравнению с “род-
ным” C++. Вы устанавливаете точки прерывания и трассировки в программах CLR
точно так же, как делаете это в коде на “родном” C++. Существует специфическая для
кода C++/CLI опция, предотвращающая вход отладчика в библиотечный код. Если вы
выберете пункт меню Tools'^ Options (Сервис^Параметры) при открытом отладчике
и выберете набор опций General (Общие) в группе DebCigging (Отладка), то диалого-
вое окно будет выглядеть, как показано на рис. 10.19.
Options
Genera I
i± Environment
• Performance Tools
+ Projects and Solutions
i- Source Control
i? Text Editor
• Database Tools
6 Debugging
General
Edit and Continue
Just- In-Ti m e
Native
Symbols
- Device Tools
4- HTML Designer
4- Microsoft Office Keyboard Settings
4-Test Tools
4- Windows Forms Designer
□ Ast before deleting all breakpoints
П ®reak all processes when one process breaks
3 Break when exception s cross AppDomai n or mar.aged/natirve Ьоьп da
3 Enable Mid ess-level debugging
□ Show disassembly if source is not available
[3 Enable brea kpo int fi Iters
□ Enable the exception assistant
ur*hon exceptions
I
Enable Just My Code [ Managed only)
A show a II members for non-user oh jects In va rlables windows (Vi:
0 Warn if no user code о n launch
□ Enable property evaluation and other implidtfunction calls
0 call ToString’) cn objects m variables windows (cs only)
Enable sou rce server support
rr т tfr.rr rx-tsar<‘r ne '.V’J
Ш Highlight entire source line for breakpoints and current statement
- Require sou rce' iles to exactly match the on g inol version
I I Redin’nt all ftutnulr Window text to the Immediate Window
Cancel
Puc. 10.19. Опции General (Общие) группы Debugging (Отладка)
Технологии отладки 605
Отметка выделенной на рис. 10.19 опции означает, что отладчик шагает только
по операторам вашего исходного кода, а библиотечный код выполняет обычным об-
разом.
Использование классов Debug и Trace
Классы Debug и Trace из пространства имен System: : Diagnostics предназна-
чены для трассировки выполнения программы в целях отладки. Возможности, пред-
лагаемые классами Debug и Trace, идентичны; разница состоит в том, что функции
Trace компилируются в сборки рабочих версий, в то время как функции Debug — нет.
Поэтому вы можете использовать функции класса Debug, когда просто отлаживаете
свой код, а функции класса Trace — когда хотите получить информацию Trace в ра-
бочей версии вашего кода для мониторинга производительности или для целей диа-
гностики и сопровождения. Вы также можете управлять тем, будет ли компилятор
включать код трассировки в вашу программу.
Поскольку функции и другие члены классов Debug и Trace идентичны, будут опи-
саны только возможности класса Debug.
Гэнерация вывода
Генерировать вывод можно с использованием функций Debug: :WriteLine () и
Debug: :Write (), которые пишут сообщения в место назначения вывода; разница
между этими двумя функциями состоит в том, что WriteLine () добавляет символ но-
вой строки после вывода, в то время как Write () этого не делает. Обе они доступны
в четырех перегруженных версиях; я использую функцию Write () в качестве приме-
ра, но версии WriteLine () имеют такие же списки параметров (табл. 10.6).
Таблица 10.6. Варианты функции Debug::Write ()
Функция
Debug::Write(StringA message)
Debug::Write (String74 message,
String74 category)
Описание
Пишет message в место назначение вывода.
Пишет categoryname, за которым следует message, в место
назначения вывода. Имя категории используется для организа-
ции вывода.
Debug::Write(Object74 value)
Debug::Write (Object74 value,
String74 category)
Пишет строку, возвращенную value->ToString (), в место
назначения.
Пишет categoryname, за которым следует строка, возвращен-
ная value->ToString (), в место назначения.
Writelf () и WriteLinelf () — условные версии функций Write () и WriteLine ()
в классе Debug; они перечислены в табл. 10.7.
Как видите, функции Writelf () и WriteLinelf () имеют дополнительный пара-
метр типа bool в начале списка параметров соответствующих функций Write () и
WriteLine (), и он определяет, должен ли осуществляться вывод.
Вы можете также выполнять вывод, используя функцию Debug
существует в двух перегруженных версиях (табл. 10.8).
Print (), которая
606 Глава 9
Таблица 10.7. Варианты функции Debug: :WriteIf ()
Функция Описание
Debug::Writelf(bool condition,
Debug::Writelf(bool condition,
ObjectA value,
StringA category)
StringA message)
Debug::Writelf(bool condition,
StringA message,
StringA category)
Debug::Writelf(bool condition,
ObjectA value)
Пишет message в место назначения вывода, если condition
равно true; иначе никакого вывода не генерирует.
Пишет categoryname, за которым следует message, в место
назначения вывода, если condition равно true; иначе ника-
кого вывода не генерирует.
Пишет строку, возвращенную value->ToString (), в место
назначения, если condition равно true; иначе никакого вы-
вода не генерирует.
Пишет categoryname, за которым следует строка, возвра-
щенная value->ToString (), в место назначения, если con-
dition равно true; иначе никакого вывода не генерирует.
Таблица 10.8. Варианты функции Debug::Print ()
Функция Описание
Print(StringA message)
Print(StringA format,
...array<ObjectA>A args)
Пишет message в место назначение вывода, за которым сле-
дует символ новой строки.
Работает аналогично форматированному выводу функции
Console::WriteLine (). Форматная строка определяет, как
будут располагаться в выводе следующие за ней аргументы.
Установка места назначения вывода
По умолчанию выходные сообщения отправляются в окно вывода IDE-среды, но
вы можете изменить это, используя слушатель (listener). Слушатель — это объект, ко-
торый направляет вывод отладки и трассировки в одно или множество мест назначе-
ния. Вот как можно создать слушатель и направить отладочный вывод в стандартный
выходной поток:
TextWriterTraceListenerA listener = gcnew TextWriterTraceListener( Console::Out);
Debug::Listeners->Add(listener);
Первый оператор создает объект TextWriterTraceListener, который направля-
ет вывод в стандартный выходной поток, который возвращает статическое свойство
Out класса Console. (Свойства In и Error класса Console возвращают, соответствен-
но, стандартный входной поток и стандартный поток ошибок.) Свойство Listeners
класса Debug возвращает коллекцию слушателей отладочного вывода, так что этот
оператор добавляет объект-слушатель в коллекцию. Вы можете добавить и другие
слушатели, которые дополнительно направят вывод еще куда-нибудь (возможно, в
файл).
Отступы вывода
Вы можете управлять отступами при выводе сообщений отладки и трассировки.
Это, в частности, удобно в ситуациях, когда функции вызываются на разной глубине.
Добавляя отступ в начале функции, и удаляя его в ее конце, можно легче идентифи-
цировать отладочный или трассирующий вывод, и вы сразу можете видеть глубину
вызова функции по величине отступа перед сообщением.
Технологии отладки 607
Чтобы увеличить текущий уровень отступа на единицу (по умолчанию одна едини-
ца отступа — это четыре пробела), вызывается статическая функция Indent () класса
Debug:
Debug::Indent();
/ / Увеличить уровень отступа на 1
Чтобы уменьшить текущий уровень отступа, вызывается функция Un indent ():
Debug::Unindent(); // Уменьшить уровень отступа на 1
Текущий уровень отступа сохраняется в свойстве IndentLevel класса Debug, так
что с его помощью вы можете как получить, так и установить текущий уровень от-
ступа, например:
Debug::IndentLevel « 2*Debug::IndentLevel;
Этот оператор удваивает текущий уровень отступа для последующего отладочного
вывода.
Количество пробелов, приходящихся на единицу отступа, записываются в стати-
ческом свойстве Indent Size класса Debug. Вы можете получить текущее значение
единицы отступа и изменить его. Например:
Console::WriteLine(L"Текущая единица отступа = {0}”, Debug::Indentsize);
Debug::Indentsize - 2; // Установить единицу отступа в 2 пробела
Первый оператор просто выводит значение единицы отступа, а второй устанавли-
вает его в новое значение. Последующие вызовы Indent () увеличат значение теку-
щего отступа на величину новой единицы, которая равна двум пробелам.
Управление выводом
Переключатели трассировки (trace switches) предоставляют возможность вклю-
чать и выключать любой отладочный или трассирующий вывод. Существуют два вида
переключателей трассировки, которые вы можете использовать.
□ Объекты ссылочного класса BooleanSwitch предоставляют возможность вклю-
чать и отключать сегмент вывода, в зависимости от текущего состояния пере-
ключателя.
□ Объекты ссылочного класса TraceSwitch предлагают более сложный меха-
низм управления, поскольку объект TraceSwitch имеет четыре свойства, соот-
ветствующие четырем уровням управления операторами вывода.
Вы можете создать объект BooleanSwitch для управления выводом в виде стати-
ческого члена класса:
public ref class MyClass
{
private:
static BooleanSwitchA errors =
gcnew BooleanSwitch(Ь”Переключатель ошибок”, 1"Управляет выводом ошибок");
public:
void Dolt()
{
// Код...
if(errors->Enabled)
Debug::WriteLine(1”0шибка в Dolt()");
// Еще код...
// Остальная часть класса...
608 Глава 9
Здесь показан объект error — статический член класса MyClass. Первый аргумент
конструктора BooleanSwitch — это отображаемое имя переключателя, используемое
для инициализации свойства DisplayName, а второй аргумент устанавливает значе-
ние свойства Description переключателя. Имеется и другой конструктор, принима-
ющий третий аргумент типа String74, который устанавливает свойство Value пере-
ключателя.
Свойство Enabled класса BooleanSwitch имеет тип bool и по умолчанию равно
false. Чтобы установить его в true, вы устанавливаете значение свойства как обычно:
errors->Enabled = true;
Функция Dolt () класса MyClass выводит отладочное сообщение об ошибке, толь-
ко когда переключатель error включен.
Ссылочный класс Trace Switch имеет два конструктора, принимающих такие же
параметры, как и конструкторы класса BooleanSwitch. Вы можете создать объект
TraceSwitch следующим образом:
TraceSwitch74 traceCtrl «
gcnew TraceSwitch (Ь’’Обновление", 1/’Трассирует операции обновления”);
Первый аргумент конструктора устанавливает свойство DisplayName, а второй —
значение свойства Description.
Свойство Level объекта TraceSwitch имеет тип класса перечисления TraceLevel,
и вы можете устанавливать это свойство для управления выводом трассировки в лю-
бое из значений, перечисленных в табл. 10.9.
Таблица 10.9. Перечисление TraceLevel
Значение Описание
TraceLevel::0ff
TraceLevel::Info
TraceLevel::Warning
TraceLevel::Error
Никакого трассирующего вывода.
Выводить информацию, предупреждения и сообщения об ошибках.
Выводить предупреждения и сообщения об ошибках.
Выводить сообщения об ошибках.
TraceLevel::Verbose
Выводить все сообщения.
Значение, которое вы устанавливаете, определяет генерируемый вывод. Чтобы
получить все сообщения, вы устанавливаете свойство следующим образом:
traceCtrl->Level = TraceLevel::Verbose;
Должно ли конкретное сообщение быть выдано, определяется вашим кодом трас-
сировки и отладки через проверку состояния одного из четырех свойств типа bool
объекта TraceSwitch (табл. 10.10).
Таблица 10.10. Свойства объекта Traceswi tch для определения необходимости
выдачи сообщений
Свойство Описание
TraceVerbose Возвращает значение true, когда все сообщения должны быть выведены.
Traceinfo Возвращает значение true, когда должны быть выданы информационные сообщения.
Tracewarning Возвращает значение true, когда должны быть выданы предупреждающие сообщения.
TraceError Возвращает значение true, когда должны быть выданы сообщения об ошибках.
Технологии отладки 609
По значениям этих свойств вы можете видеть, что установка свойства Level так-
же изменяет состояния этих свойств. Например, если вы устанавливаете свойство
Level равным TraceLevel:: Warning, то TraceWarning и TraceError устанавлива-
ются в true, a TraceVerbose и Traceinfo — в false.
Чтобы решить, нужно ли выводить определенное сообщение, вы проверяете соот-
ветствующее свойство:
if(traceCtrl->TraceWarning)
Debug::WriteLine(Ь”Это ваше последнее предупреждение!”);
Сообщение выводится, только когда свойство TraceWarning для traceCtrl уста-
новлено в true.
Утверждения
Классы Debug и Trace имеют статическую функцию Assert (), которая предостав-
ляет возможности, аналогичные функции assert () в “родном” C++. Первый аргумент
функции Debug::Assert () — булевское значение или выражение, которое заставля-
ет программу реагировать, когда значение аргумента false. При этом отображается
стек вызовов в диалоговом окне, как показано на рис. 10.20.
Assertion Failed: Abort=Quit, Retry=Debug, lgnore=Continue
at TraceTest.FunC() d:\beginning visual c++
2005\exa m p I e s\exl 0_03\exl 0_0 3\exl 0_0 3. cp p (5 0)
at TraceTest.FunB() d:\beginning visual c++
2005\exa m p I e s\exl 0_0 3 4exl 0_0 3\exl0_0 3. cp p (3 9)
at TraceTest.Fu0 d:\beginning visual c++
2005\exa m p I e s\exl 0_0 3\exl 0_0 3\exl 0_0 3. cp p (28)
at <Module>.mam(String[] args) d:\beginning visual c++
20 05\exa m p I e s\exl 0_0 3\exl 0_0 3\exl 0_0 3. cp p(77)
at <Module>.mainCRTStartupStrArray(String[] arguments)
f: \rtm\vcto о ls\crt_b I d\se If_x8 6\crt\src\m crtexe. cp p (318)
Abort
Ignore
Puc. 10.20. Диалоговое окно, отображаемое при нарушении утверждения
На рис. 10.20 показано утверждение, которое генерируется следующим примером.
Когда программа реагирует на утверждение, в диалоговом окне отображается стек
вызовов с номерами строк кода и именами функций, выполнявшихся в этой точке.
Здесь мы видим выполнение пяти функций, включая main ().
После возникновения утверждения вам предлагаются три варианта действий.
Щелчок на кнопке Abort (Прервать) завершает программу немедленно, кнопка Ignore
(Игнорировать) позволяет продолжить выполнение программы, а щелчок на кнопке
Retry (Повторить) дает возможность перейти к выполнению программы в режиме от-
ладки.
Доступны три перегруженных версии функции Assert О , перечисленные в
табл. 10.11.
610 Глава 9
Таблица 10.11. Перегруженные версии функции Assert ()
Функция Описание
Debug::Assert(bool condition) Когда condition равно false, отображается диалог, показы- вающий стек вызовов в этой точке.
Debug::Assert(bool condition, StringA message) То же, что и предыдущий вариант, но над информацией о стеке отображается message.
Debug::Assert(bool condition, String74 message, StringA details) То же, что и предыдущая версия, но в диалоге дополнительно отображается details.
Этот механизм легче всего понять, увидев его в действии, так что соберем вместе
пример, который продемонстрирует код отладки и трассировки в действии.
Практическое занятие | ИСПОЛЬЗОВЭНИв КЛЭССОВ DebU? И ТГЭСО
Этот пример — просто упражнение по использованию некоторых отладочных и
трассирующих функций, описанных выше. Создайте консольный проект CLR и моди-
фицируйте его следующим образом:
// Ех10—03.срр : главный файл проекта.
// Отладочный и трассирующий вывод для CLR
#include "stdafx.h"
using namespace System;
using namespace System::Diagnostics;
public ref class TraceTest
{
public:
TraceTest(int n):value(n){}
property TraceLevel Level
{
void set(TraceLevel level) {sw->Level = level; }
TraceLevel get(){return sw->Level; }
}
void FunA()
{
++value;
Trace::Indent();
Trace::WriteLine(L”CTapT FunA");
if(sw->Trace!nfo)
Debug::WriteLine(L"FunA работает...");
FunB();
Trace::WriteLine(Ь"3авершение FunA");
Trace::Unindent();
}
void FunB()
{
Trace::Indent();
Trace::WriteLine(Е"Старт FunB");
if(sw->TraceWarning)
Debug::WriteLine ^"Предупреждение в FunB...");
FunCO ;
Trace::WriteLine(Е"3авершение FunB");
Trace::Unindent();
}
Технологии отладки
611
void FunC()
Trace::Indent();
Trace::WriteLine(Ь"Старт FunC");
if(sw->TraceError)
Debug::WriteLine(L"Ошибка в FunC...;
Debug::Assert(value < 4);
Trace::WriteLine(Е"3авершение FunC");
Trace::Unindent();
private:
int value;
static TraceSwitchA sw =
gcnew TraceSwitch(L"Переключатель трассировки",
Е"Управляет выводом трассировки");
int main(array<System::String A> Aargs)
// Прямой вывод в командную строку
TextWriterTraceListenerA listener = gcnew TextWriterTraceListener ( Console::Out);
Debug::Listeners->Add(listener);
Debug::Indentsize - 2; // Установить размер отступа
array<TraceLevel>A levels = { TraceLevel::Off, TraceLevel:‘.Error,
TraceLevel::Warning ,TraceLevel::Verbose);
TraceTestA obj = gcnew TraceTest(0);
Console::WriteLine (L"3anycK теста трассировки и отладки... ");
for each(TraceLevel level in levels)
obj->Level = level; // Установить уровень сообщений
Console::WriteLine(Е"\пУровень трассировки {0}", obj->Level);
obj->FunA();
return 0;
Этот пример выдает диалог утверждения во время выполнения. Вы можете затем
выбрать продолжение либо прерывание выполнения, или же повторить попытку в
режиме редактирования, выбрав соответствующую кнопку в диалоговом окне.
В зависимости от того, как вы отреагируете на диалог утверждения, вывод будет
выглядеть так, как показано ниже.
Запуск теста трассировки и отладки...
Уровень трассировки Off
Старт FunA
Старт FunB
Старт FunC
Завершение FunC
Завершение FunB
Завершение FunA
Уровень трассировки Error
Старт FunA
Старт FunB
Старт FunC
Ошибка в FunC...
Завершение FunC
Завершение FunB
Завершение FunA
612 Глава 9
Уровень трассировки Warning
Старт FunA
Старт FunB
Предупреждение в FunB...
Старт FunC
Ошибка в FunC...
Завершение FunC
Завершение FunB
Завершение FunA
Уровень трассировки Verbose
Старт FunA
FunA работает...
Старт FunB
Предупреждение в FunB...
Старт FunC
Ошибка в FunC...
Fail: {Сбой:)
Завершение FunC
Завершение FunB
Завершение FunA
Press any key to continue . . .
Описание полученных результатов
В классе TraceTest определены три функции экземпляра: FunA (), FunB () и
FunC (). Каждая из них содержит вызов функции Trace
Indent() для увеличения
отступа отладочного вывода, а функция Trace:: WriteLine () вызывается для отсле-
живания запуска и завершения функции. Функция Trace : : Unindent () вызывается
непосредственно перед выходом из каждой функции, чтобы восстановить уровень от-
ступа, который был актуальным на момент ее вызова.
В классе TraceTest определен приватный член TraceSwitch по имени sw, кото-
рый управляет уровнем отображаемого отладочного вывода. Каждая из трех функций-
членов также вызывает Debug: :WriteLine () для вывода отладочного сообщения в
зависимости от уровня, установленного в члене класса sw.
Функция FunA () увеличивает значение члена класса value при каждом своем вы-
зове, а функция FuncA () выдает утверждение, если value превышает 3.
В main () создается объект TextWriterTraceListener, который направляет отла-
дочный и трассирующий вывод в командную строку:
TextWriterTraceListenerА listener = gcnew TextWriterTraceListener ( Console::Out);
Затем объект listener добавляется в коллекцию слушателей класса Debug:
Debug::Listeners->Add(listener);
Это направляет отладочный и трассирующий вывод в стандартный выходной по-
ток Console::Out.
Далее создается массив объектов TraceLevel, представляющих различные управ-
ляющие уровни отладочного и трассирующего вывода:
array<TraceLevel>A levels = { TraceLevel::Off, TraceLevel::Error,
TraceLevel::Warning , TraceLevel::Verbose};
После создания объекта TraceLevel для него устанавливаются уровни трассиров-
ки в цикле for each:
Технологии отладки 613
for each(TraceLevel level in levels)
obj->Level = level; // Установить уровень сообщений
Console::WriteLine(Ь"\пУровень трассировки {0}", obj->Level);
obj->FunA();
Уровень задается через свойство Level объекта obj. Это устанавливает свойство
Level в член sw типа TraceSwitch, который используется в функциях экземпляра
для управления выводом.
Из вывода программы легко заметить, что установленные отступы влияют на вы-
вод статической функции WriteLine () обоих классов — Trace и Debug. Вы также
можете видеть, как уровень, установленный в члене класса TraceSwitch, влияет на
вывод. Когда член value объекта obj класса TraceTest достигает 4, выдается диа-
логовое окно утверждения функции FunC (). Вы можете перезапустить пример и про-
верить эффект от щелчков на трех кнопках в диалоге утверждения.
Резюме
Отладка — весьма обширная тема, и Visual C++ 2005 предоставляет множество
средств отладки помимо тех, о которых было сказано в настоящей главе. Если вы хоро-
шо усвоили то, о чем шла речь в этой главе, то не поленитесь расширить полученные
знания о возможностях отладки, заглянув в документацию по Visual C++ 2005. Поиск по
слову “debugging” (“отладка”) должен выдать богатый список дополнительной инфор-
мации. Ниже перечислены важнейшие моменты, рассмотренные в этой главе.
□ Вы можете использовать библиотечную функцию assert (), которая объявле-
на в заголовке <cassert>, чтобы проверять логические условия в программах
на “родном” C++, которые всегда должны быть true.
□ Символ препроцессора _NDEBUG автоматически определяется в отладочной
версии программы “родного” C++. В рабочей версии он не определен.
□ Вы можете добавлять собственный отладочный код, заключая его в пару дирек-
тив #ifdef/#endif, проверяющих наличие _NDEBUG. Затем ваш отладочный
код включается только в отладочную версию программы.
□ Заголовочный файл crtdbg.h предоставляет объявления функций для обеспе-
чения отладки операций со свободным хранилищем.
□ Устанавливая _crt Dbg Flag соответствующим образом, вы можете включить ав-
томатическую проверку на предмет утечек памяти.
□ Для прямого вывода сообщений из отладочных функций свободного хранилища
вы можете вызывать функции _CrtSetReportMode () и _CrtSetReportFile ().
□ Операции отладки, использующие точки прерывания и точки трассировки, в
программах C++/CLI точно такие же, как в программах на “родном” C++.
□ Классы Debug и Assert, определенные в пространстве имен System: :
Diagnostics, предлагают функции для трассировки выполнения и генерации
отладочного вывода программ CLR.
□ Статическая функция Assert О классов Debug и Trace предоставляет меха-
низм утверждений в программах CLR.
Теперь, вооруженные знаниями об отладке вдобавок к вашим знаниям C++, вы го-
товы приступить к огромной задаче: программирование для Windows!
11
Концепции
программирования
для Windows
В этой главе вы познакомитесь с базовыми идеями, положенными в основу лю-
бой программы для Windows на C++. Сначала вы разработаете очень простой пример,
используя программный интерфейс (API — application programming interface) опера-
ционной системы Windows непосредственно. Это позволит понять, как Windows-при-
ложения работают “за сценой”, что пригодится вам, когда вы станете разрабатывать
приложения с использованием наиболее сложных средств, предлагаемых Visual C++
2005. Затем вы узнаете о том, что получите, создавая Windows-программы с примене-
нием библиотеки Microsoft Foundation Classes, лучше известной как MFC. И, наконец,
вы создадите базовую программу, используя Windows Forms, которая будет выполнять-
ся под CLR. Таким образом, до конца главы вы получите представление о каждом из
трех подходов к разработке приложений Windows.
В этой главы вы изучите следующие вопросы.
□ Базовая структура окна.
□ Что такое Windows API и как его использовать.
□ Что собой представляют сообщения Windows, и как иметь с ними дело.
□ Нотация, обычно используемая в программах для Windows.
□ Как создать элементарную программу с применением Windows API, и как она
работает.
□ Библиотека Microsoft Foundation Classes.
□ Базовые элементы программы на основе MFC.
□ Средство Windows Forms.
□ Базовые элементы программы Windows Forms.
616 Глава 11
Основы программирования для Windows
При работе в Visual C++ 2005 вам доступны три основных способа создания инте-
рактивных приложений Windows.
Q Использование Windows API. Это фундаментальный интерфейс, который пред-
усматривает операционная система для коммуникаций между собой и приложе-
нием, выполняющимся под ее управлением.
Использование Microsoft Foundation Classes, лучше известной как MFC. Это
набор классов C++, инкапсулирующих Windows API.
□ Использование Windows Forms. Это основанный на формах механизм разра-
ботки для создания приложений, выполняющихся под управлением CLR.
Эти три подхода перечислены в порядке от требующего наибольших усилий про-
граммирования до наименьших. С помощью Windows API вы пишете абсолютно весь
код — все элементы, составляющие графический пользовательский интерфейс (GUI)
вашего приложения, должны быть созданы программно. При использовании MFC
вы получаете некоторую помощь в построении GUI — в том, что вы можете собирать
элементы управления в диалоговом окне графически и программировать только вза-
имодействие с пользователем; однако, вам все еще приходится писать много кода.
Применяя Windows Forms, вы можете построить полный GUI, в том числе главное
окно приложения, собирая графически элементы управления, с которыми взаимо-
действует пользователь. Вы просто помещаете их в требуемые места окна формы, а
код их создания генерируется автоматически. Использование Windows Forms — самый
быстрый и легкий способ генерации приложения, поскольку объем кода, который вы
должны написать, значительно меньше по сравнению с двумя другими подходами.
Код приложения Windows Forms также пользуется всеми преимуществами выполне-
ния под управлением CLR.
Использование MFC предусматривает большие усилия по программированию,
чем Windows Forms, но при этом предлагает большие возможности по контролю про-
цесса построения GUI для программ, выполняемых на вашем ПК непосредственно.
Поскольку применение Windows API — наиболее трудоемкий способ разработки при-
ложений, я не стану погружаться в его подробности. Однако вы получите достаточ-
ное представление о Windows API, чтобы понять принципы механизма, позволяющие
всем Windows-приложениям скрытым образом взаимодействовать с операционной
системой. В этой главе раскрываются фундаментальные принципы организации всех
трех способов разработки программ для Windows, а далее в книге более подробно ис-
следуется применение MFC и Windows Forms. Конечно, также можно разрабатывать
приложения на C++, которые не требуют операционной системы Windows, и игровые
программы часто используют такой подход, когда требуется достичь максимальной
производительности графики. Хотя эта тема интересна сама по себе, она требует от-
дельной книги для изложения, поэтому я не стану в нее углубляться.
Прежде чем мы перейдем к рассмотрению примера в этой главе, следует догово-
риться о терминологии, используемой для описания окна приложения. Вы уже созда-
вали Windows-программу в главе 1, не написав самостоятельно ни одной строки кода,
и я воспользуюсь окном, сгенерированным тем примером, чтобы проиллюстрировать
различные элементы, из которых это окно состоит.
Концепции программировав
ея для Windows 617
Элементы окна
Конечно же, вы знакомы с большинством, если не со всеми основными элемен-
тами пользовательского интерфейса Windows-программы. Однако я все равно пере-
числю их — просто, чтобы быть уверенным, что у нас имеется общее понимание
значения используемых терминов. Лучший способ понять, чем могут быть элементы
окна — рассмотреть один из них. Аннотированная версия окна, отображенного при-
мером, который вы видели в главе 1, показана на рис. 11.1.
Фиксатор размера
Родительское окно MDI
Граница установки размера
Дочернее окно MDI Клиентская область дочернего окна
- л Клиентская область
Пиктограмма дочернего окна родительского окна
Панель
инструментов
Панель меню
Пиктограмма
панели заголовка
»rEx1_04jj:x1_041
-File Edit View Window Нй!р
Текст панели заголовка
дочернего окна Кнопка закрытия
Текст панели Кнопка разворачивания
за головка £нопка сворачивания
Панель состояния
Position (0,0)
increasing у
Ready
Рис. 11.1. Стандартные окна WLndows-программы
increasing х
Этот пример в действительности генерирует два окна. Большее окно, с панеля-
ми меню и инструментов, является главным, или родительским окном, а меньшее
окно — дочерним окном по отношению к родительскому. Хотя дочернее окно может
быть закрыто без закрытия родительского двойным щелчком на пиктограмме панели
заголовка, находящейся в его левом верхнем углу, закрытие родительского окна также
автоматически закрывает дочернее окно. Это связано с тем, что дочернее окно при-
надлежит и зависит от родительского. В общем случае одно родительское окно может
иметь множество дочерних окон, как вы вскоре увидите.
Наиболее фундаментальными частями типичного окна являются рамка, панель
заголовка, отображающая имя, которое вы присвоили окну, пиктограмма панели за-
головка, находящаяся в левой части панели заголовка, и клиентская область, пред-
618 Глава 11
ставляющая собой область в центре окна, не занятую панелью заголовка или рамкой.
Все это вы получаете бесплатно в Windows-программе. Как вы увидите, все, что вам
нужно сделать — это предоставить некоторый текст для панели заголовка.
Рамка определяет границы окна и может быть фиксированной либо изменяемого
размера. Если рамка с изменяемым размером, вы можете двигать ее, изменяя размеры
окна. Окно также может предоставлять фиксатор размера, который можно использо-
вать для изменения размера окна при установке его соотношения сторон — то есть от-
ношения ширины к высоте. Когда вы определяете окно, то можете модифицировать
поведение рамки и ее внешний вид по своему желанию. Большинство окон в правом
верхнем углу также имеют кнопки разворачивания, сворачивания и закрытия. Они
позволяют распахнуть окно до полного размера экрана, свернуть его в пиктограмму
или полностью закрыть.
Когда вы щелкаете на пиктограмме панели заголовка левой кнопкой мыши, ото-
бражается стандартное меню для изменения или закрытия окна, которое называется
системным или управляющим меню. Системное меню также появляется, когда вы-
полняется щелчок правой кнопкой мыши на панели заголовка окна. Хотя это и не
обязательно, всегда неплохо включать пиктограмму панели заголовка в любое глав-
ное окно, создаваемое вашей программой. Включение пиктограммы панели заголовка
предоставляет очень удобную возможность закрытия программы, когда что-то работа-
ет не так во время отладки.
Клиентская область — это часть окна, в которую программа обычно выводит текст
или графику. Вы обращаетесь с клиентской областью для этих целей точно так же,
как с двором, который был показан на рис. 7.1 в главе 7. Верхний левый угол клиент-
ской области имеет координаты (0, 0), причем х растет слева направо, а у — сверху
вниз.
Панель меню в окне не обязательна, но предоставляет, вероятно, наиболее удоб-
ный способ управления приложением. Каждое меню в панели отображает выпадаю-
щий список элементов меню, когда вы на нем щелкаете. Содержимое меню и физиче-
ское расположение многих объектов, отображаемых в окне, таких как пиктограммы
в панели инструментов, находящейся ниже, курсор и многие другие, определяются
ресурсным файлом. Мы еще обратимся к ресурсным файлам, когда приступим к на-
писанию более сложных Windows-программ.
Панель инструментов предлагает набор пиктограмм, которые обычно действуют
как альтернатива наиболее часто используемым опциям меню. Поскольку они обеспе-
чивают прямой путь обращения к функциям, которые ими вызывается, их примене-
ние часто облегчает и ускоряет использование программы.
Прежде чем двигаться дальше, я должен предупредить о терминологии, которой
вы должны придерживаться. Пользователи обычно склонны воспринимать окно как
вещь, которая появляется на экране с рамкой, ограничивающей его, и конечно же,
так оно и есть, но это только один из возможных видов окон. Тем не менее, окно
Windows — это общий термин, описывающий весь диапазон видимых сущностей.
Фактически почти все, что отображается, является окном, например, диалоговое
окно — это окно, и каждая его кнопка — тоже окно. Обычно я буду использовать тер-
минологию, называющую объекты тем, чем они являются — кнопки, диалоги и тому
подобное, однако вы должны иметь в виду, что все они также являются окнами, по-
скольку с каждым из них вы можете делать то, что и с обычным окном. Например, вы
вполне можете рисовать на поверхности кнопки.
Концепции программирования для Windows 619
Windows-программы и операционная система
Когда вы пишете код для Windows, ваша программа подчиняется операционной
системе, и Windows управляет ею. Ваша программа не должна напрямую взаимодей-
ствовать с оборудованием, и все коммуникации с внешним миром должны проходить
через Windows. Когда вы используете Windows-программу, то первоначально взаимо-
действуете с Windows, а она уже взаимодействует с вашей прикладной программой.
Ваша Windows-программа — это “хвост”, а сама Windows — “собака”, и ваша программа
“виляет хвостом” только тогда, когда Windows приказывает ей делать это.
Существует масса причин подобного положения вещей. Первая и важнейшая при-
чина заключается в том, что ваша программа почти всегда разделяет компьютер с
другими программами, которые могут выполняться в одно и то же время. Windows
постоянно должна сохранять контроль над разделяемыми ресурсами машины. Если
одному приложению будет разрешено захватить контроль над всей средой Windows,
это неизбежно потребует его значительного усложнения, из-за необходимости учиты-
вать работу других программ, и информация, предназначенная для других программ,
может быть потеряна. Вторая причина для сохранения контроля за Windows состоит
в том, что Windows заключает в себе стандартный пользовательский интерфейс, и
должна отвечать за соблюдение стандарта. Вы можете отображать информацию на
экране, только используя инструменты, предусмотренные в Windows, и только тогда,
когда это разрешено Windows.
Программирование, управляемое событиями
В главе 1 уже было показано, что Windows-программы являются управляемыми
событиями, поэтому такая программа, по сути, постоянно ожидает, когда что-нибудь
произойдет. Значительная часть кода, необходимого Windows-приложению, предна-
значена для обработки событий, вызванных внешними действиями пользователя,
но действия, которые не ассоциированные напрямую с вашим приложением, мо-
гут все же потребовать выполнения некоторого фрагмента кода вашей программы.
Например, если пользователь перетаскивает окно другого приложения, которое ра-
ботает параллельно с вашей программой, и это действие открывает часть клиентской
области окна, выделенной вашему приложению, ваше приложение должно будет пе-
рерисовать часть вашего окна.
Сообщения Windows
События в Windows-приложении представляют собой происшествия, подобные
щелчку кнопкой мыши или нажатию клавиши либо истечению определенного пе-
риода времени. Операционная система Windows записывает каждое событие в со-
общение и помещает его в очередь сообщении программы, которой это сообщение
предназначено. Таким образом, сообщение Windows — это просто запись данных,
имеющих отношение к событию, а очередь сообщений приложения — это просто по-
следовательность таких сообщений, ожидающая обработки приложением. Отправляя
сообщения, Windows может известить вашу программу о том, что нечто должно быть
сделано, или о том, что некоторая информация стала доступной, или же о том, что
произошло событие вроде щелчка кнопкой мыши. Если ваша программа правильно
организована, она соответствующим образом реагирует на сообщение. Может суще-
ствовать множество видов сообщений, и они могут поступать очень часто — по много
раз в секунду — например, когда передвигается мышка.
620 Глава 11
Windows-программа должна содержать функцию, специально предназначенную
для обработки таких сообщений. Эта функция часто называется WndProc () или
WindowProc (), хотя она не обязана иметь какое-то определенное имя, потому что
Windows обращается к функции через указатель, предоставленный вами. Поэтому
отправка сообщения вашей программе сводится к вызову Windows предоставленной
вами функции, обычно называемой WindowProc (), и к передаче необходимых дан-
ных вашей программе через аргументы этой функции. Обработка сообщения, пере-
данного с данными в эту функцию, полностью зависит от вас.
К счастью, вам не нужно писать код для обработки каждого сообщения. Вы мо-
жете фильтровать только те из них, которые интересуют вашу программу, передавая
остальные обратно Windows. Сообщение передается обратно Windows с помощью
стандартной Windows-функции по имени DefWindowProc (), которая обеспечивает их
обработку по умолчанию.
Windows API
Все взаимодействие между любым приложением Windows и самой системой
Windows использует программный интерфейс Windows, известный также под назва-
нием Windows API. Он состоит из многих сотен стандартных функций, с помощью
которых приложение взаимодействует с операционной системой Windows и наобо-
рот. Windows API был разработан в те времена, когда главным используемым языком
был С, задолго до появления C++, и по этой причине в нем часто используются струк-
туры вместо классов для передачи некоторого рода данных между Windows и вашим
приложением.
Windows API покрывает все аспекты взаимодействия между Windows и вашим при-
ложением. Поскольку количество функций, содержащихся в этом API, очень велико,
непосредственное использование их может оказаться достаточно сложным — иногда
непросто даже понять, что делает каждая из них. И здесь на помощь разработчику
приложений приходит Visual C++ 2005. Он упаковывает Windows API таким образом,
что его функции структурируются в объектно-ориентированной манере, и предлагает
облегченный способ применения интерфейса в C++ со значительной долей функци-
ональности по умолчанию. Это принимает форму библиотеки Microsoft Foundation
Classes, или MFC. К тому же, для приложений, ориентированных на CLR, существует
средство, называемое Windows Forms, где весь код, необходимый для создания GUI,
генерируется автоматически. Все, что вам остается сделать — предоставить код, необ-
ходимый для обработки событий способом, которого требует ваше приложение. Чуть
позже в этой главе мы с вами обратимся к созданию приложения Windows Forms, а
более подробно эта тема рассматривается в главе 22.
В Visual C++ также доступны мастера создания приложений (Application wizards),
служащие для создания базовых приложений различного рода, включая приложения
MFC и приложения, основанные на Windows Forms. Мастер создания приложений
может сгенерировать полностью работающую программу, включающую весь необхо-
димый код для базового приложения Windows, оставляя за вами только настройку его
под конкретные нужды. Пример из главы 1 иллюстрирует, насколько много функци-
ональности может предложить Visual C++ без необходимости каких-либо усилий по
кодированию с вашей стороны. Я расскажу об этом гораздо подробнее, когда мы за-
ймемся написанием некоторых наиболее практичных примеров с применением ма-
стера создания приложений MFC.
Концепции программирования для Windows 621
Типы данных Windows
В Windows определено значительное количество типов данных, используемых для
спецификации типов параметров и типов возврата функций в Windows API. Эти спе-
цифичные для Windows типы также распространяются на функции, определенные в
MFC. Каждый из этих типов Windows отображается на некоторый тип C++, но по-
скольку отображение между типами Windows и типами C++ может изменяться, вы
всегда должны применять типы Windows, где это возможно. Например, в прошлом
тип Windows WORD был определен в одной версии Windows как unsigned short, а в
другой версии — как unsigned int. На 16-разрядных машинах эти типы эквивалент-
ны, но на 32-разрядных машинах они определенно отличаются, так что всякий, кто
использует тип C++ вместо типа Windows, рискует столкнуться с проблемами.
Вы можете найти полный список типов данных Windows в документации, но в
табл. 11.1 перечислено несколько наиболее распространенных типов, с которыми
вам придется столкнуться.
Таблица 11.1. Наиболее распространенные типы данных Windows
Тип_______________Описание_______________________________________________________________
bool или Булевская переменная, которая может принимать значения true или false.
boolean Обратите внимание, что это не то же самое, что тип C++ по имени bool, который
может иметь значения true и false.
byte 8-битовый байт.
char 8-битовый символ.
dword 32-битовое беззнаковое целое, соответствующее типу unsigned long в C++.
handle Дескриптор объекта — 32-битовое целое значение, описывающее местоположение
объекта в памяти.
hbrush Дескриптор кисти. Кисть применяется для заполнения областей цветом.
hcursor Дескриптор курсора.
hdc Дескриптор контекста устройства — объекта, позволяющего рисовать в окне.
hinstance Дескриптор экземпляра.
lparam Параметр сообщения.
lpcstr Указатель на константную ограниченную нулем строку 8-битовых символов.
lphandle Указатель на дескриптор.
lresult Значение со знаком, полученное в результате обработки сообщения.
word 16-битовое беззнаковое целое, соответствующее типу unsigned short в C++.
Все прочие типы Windows будут представляться по мере необходимости в про-
цессе рассмотрения примеров. Все типы, используемые Windows, а также прототи-
пы функций Windows API, содержатся в заголовочном файле windows. h, так что вы
должны включать этот файл в код своих программ Windows.
Нотация программ Windows
Во многих программах Windows имена переменных имеют префикс, указываю-
щий вид значений, содержащихся в этих переменных, и то, как они используются.
Существует сравнительно немного префиксов, и часто они используются в комбина-
ции. Например, префикс Ipfn означает длинный указатель на функцию. В табл. 11.2
представлены некоторые примеры префиксов, с которыми вы можете столкнуться.
622 Глава 11
Таблица 11.2. Примеры префиксов, указывающих вид значений в переменных
Префикс Значение
Ь Логическая переменная типа bool — эквивалент int.
Ьу Тип unsigned char или byte.
с Тип char.
dw Тип DWORD, то есть unsigned long.
fn Функция.
h Дескриптор, используется для ссылки на что-либо.
1 Тип int.
1 Тип long.
1р Длинный (long) указатель.
п Тип int.
р Указатель.
s Строка.
sz Строка, ограниченная нулем.
w Тип WORD, то есть unsigned short.
Такое применение префиксов называется венгерской записью. Она была пред-
ложена для того, чтобы минимизировать вероятность неправильного применения
переменных, отличного от того, как они были определены, и как предполагалось их
использовать. Такие неверные интерпретации были нередки в языке С, предшествен-
нике C++. С появлением C++, с его строгой проверкой типов, исчезла необходимость
в специальных усилиях в виде применения нотации для того, чтобы избежать таких
проблем. Компилятор всегда помечает ошибкой несоответствие типов в ваших про-
граммах, и многие типы ошибок, которыми изобиловали ранние программы на С, в
C++ не случаются.
С другой стороны, венгерская запись все еще может помочь в понимании про-
грамм, в частности, когда вы имеете дело с большим количеством переменных
различного типа, служащими аргументами для функций Windows API. Поскольку
Windows-программы все еще пишутся на С, и, конечно же, поскольку параметры
функций Windows API все еще используют венгерскую запись, этот метод все еще до-
статочно распространен. Подробнее о венгерской записи можно прочитать по адресу
http://web.umr.edu/~cpp/common/hungarian.html.
Вы можете самостоятельно принимать решение относительно обязательности
использования венгерской записи. Ее можно вообще не использовать, но в любом
случае, если вы имеете представление о том, как она работает, то наверняка согласи-
тесь, что она облегчает понимание сущности аргументов к функциям Windows API.
Однако есть одно небольшое предупреждение. Со времени первоначальной разработ-
ки Windows некоторые аргументы функций API слегка изменились, но имена исполь-
зуемых переменных остались прежними. Как следствие, префикс может не вполне
соответствовать типу переменной.
Концепции программирования для Windows
623
Структура Windows-программы
Для минимальной Windows-программы, использующей Windows API, следует напи-
сать две функции. Это функция WinMain (), с которой начинается выполнение про-
граммы и происходит ее основная инициализация, и функция WindowProc (), вызы-
ваемая самой Windows для обработки сообщений приложения. Часть WindowProc ()
программы Windows — обычно большая ее часть, поскольку это место, в котором на-
ходится специфичный для приложения код, отвечающий на сообщения, иницииро-
ванные вводом пользователя того или иного рода.
Хотя эти две функции образуют полную программу, они не связаны между собой не-
посредственно. WinMain () не вызывает WindowProc (), это делает Windows. Фактически
Windows также вызывает и WinMain (). Это проиллюстрировано на рис. 11.2.
Функция WinMain () взаимодействует с Windows, вызывая некоторые из функций
Windows API. То же самое касается и WindowProc (). Интегрирующим фактором ва-
шей программы Windows выступает сама Windows, которая связана и с WinMain () и
с WindowProc (). Посмотрим, из чего могут состоять эти две функции и попробуем
собрать их в работающий пример простой программы Windows.
624 Глава 11
Функция WinMain () — это эквивалент функции main () консольной программы.
Именно здесь начинается выполнение и происходит базовая инициализация осталь-
ной части программы. Чтобы позволить Windows передать ей данные, WinMain ()
принимает четыре параметра и возвращает значение типа int. Ее прототип выгля-
дит следующим образом:
int WINAPI WinMain(HINSTANCE hlnstance,
HINSTANCE hPrevInstance,
LPSTR IpCmdLine,
int nCmdShow
);
Следом за спецификатором типа возврата int следует спецификация WINAPI, ко-
торая вам не знакома. Это определенный Windows спецификатор, который заставляет
обрабатывать имя функции и ее аргументы специальным образом, соответствующим
обработке функций на языках Pascal и Fortran. Это отличается от обычного способа
обработки функций в C++. Подробности не важны — это просто требование Windows,
так что вам следует помещать спецификатор WINAPI перед именами функций, вызы-
ваемых Windows.
Четыре аргумента, передаваемых Windows вашей функции WinMain (), содержат
важные данные. Первый аргумент, hlnstance, имеет тип HINSTANCE, представляю-
щий дескриптор экземпляра — выполняющейся программы. Дескриптор (handle) —
это целочисленное значение, идентифицирующее объект определенного рода, в
данном случае — экземпляр приложения. Действительное значение дескриптора не
важно. В любое время под управлением Windows может выполняться несколько про-
грамм. Это означает возможность того, что несколько копий одного и того же при-
ложения могут быть активными одновременно, и их необходимо как-то различать.
Поэтому дескриптор hlnstance идентифицирует конкретную копию. Если вы за-
пускаете более одной копии программы, каждая из них получает свое собственное
значение hlnstance. Как вы увидите вскоре, дескрипторы также применяются для
идентификации множества других вещей. Конечно, все дескрипторы, находящиеся в
определенном контексте — дескрипторы экземпляров приложения, например, долж-
ны отличаться друг от друга.
Следующий аргумент, передаваемый функции WinMain (), hPrevInstance, уна-
следован от 16-разрядных версий операционной системы Windows. В среде Windows
3.x этот параметр передавал дескриптор предыдущего экземпляра программы, если
таковой имелся. Если hPrevInstance равно NULL, вы знали, что данная программа
запущена впервые, и в конкретный момент в системе присутствует только один ее
экземпляр. Эта информация была необходима во многих случаях, поскольку про-
граммы, работающие под Windows 3.x, разделяли общее адресное пространство па-
мяти, и множество одновременно выполняющихся копий программы представляли
определенные сложности. По этой причине программисты часто ограничивали коли-
чество одновременно работающих экземпляров до одного и использовали аргумент
hPrevInstance, переданный WinMain () для проверки этого обстоятельства операто-
ром if.
Под 32-разрядными версиями системы Windows параметр hPrevInstance полнос-
тью утратил свое значение, поскольку каждое приложение выполняется в своем соб-
ственном адресном пространстве, и одно приложение не имеет представления о су-
ществовании другого, выполняемого параллельно. Это параметр теперь всегда равен
NULL, даже если запущен другой экземпляр приложения.
Концепции программирования для Windows 625
Следующий аргумент, IpCmdLine — это указатель на строку, содержащую команд-
ную строку, запустившую программу. Например, если вы запускаете ее, используя
пункт Run (Выполнить) из меню кнопки Start (Пуск) в Windows, эта строка содержит
все, что было указано в поле Open (Открыть). Наличие этого указателя позволяет по-
лучить любые значения параметров, которые могут появиться в командной строке.
Тип LPCSTR — это еще один Windows-тип, специфицирующий 32-битный (длинный)
указатель на строку.
Последний аргумент, nCmdShow, определяет внешний вид окна при его создании.
Оно может отображаться нормально либо в свернутом состоянии; например, ярлык
программы может специфицировать, что она должна запускаться в свернутом виде.
Этот аргумент может принимать одно из фиксированного набора значений, опреде-
ленных символическими константами вроде SW__SHOWNORMAL и SW_SHOWMINNOACTIVE.
Имеется множество других констант, подобных этой, которые определяют способ
отображения окна, и все они начинаются с SW_. Вот еще примеры: SW HIDE или
SW SHOWMAXIMIZED. Обычно вам незачем проверять значение nCmdShow. Вы просто
передаете его функции Windows API, отвечающей за отображение окна вашего при-
ложения.
Если вы хотите знать значение всех прочих констант, специфицирующих способ
отображения окна, то можете найти полный список, если поищете по слову WinMain
в библиотеке MSDN. Библиотека MSDN открыта для общего доступа по адресу
http://msdn.microsoft.com.
Функция WinMain () вашей программы Windows должна выполнять четыре дей-
ствия, которые перечислены ниже.
□ Сообщать Windows, какого вида окно требуется вашей программе.
□ Создавать окно программы.
Инициализировать окно программы.
Извлекать сообщения Windows, предназначенные программе.
Рассмотрим все эти действия по очереди и затем создадим полную функцию
WinMain().
Спецификация окна программы
Первый шаг в создании окна предусматривает просто определение типа окна, ко-
торое вы хотите создать. В Windows определен специальный тип структуры по имени
WNDCLASSEX, которая содержит данные, описывающие окно. Данные, хранящиеся в
экземпляре структуры, описывают класс окна, определяющий его тип. Не путайте это
с классами C++: MFC определяет класс, представляющий окно, но это совсем другое.
Вы должны создать переменную типа WNDCLASSEX и присвоить значения каждому из
ее членов (подобно заполнению формы). После того, как вы заполните значениями
все переменные, эту структуру можно передать Windows (через функцию, которую вы
увидите ниже), чтобы зарегистрировать класс окна. Когда это сделано, то всякий раз,
когда вы хотите создать окно этого класса, вы можете указать Windows, чтобы она ис-
кала класс, который уже был зарегистрирован.
Определение структуры WNDCLASSEX выглядит следующим образом:
struct WNDCLASS
UINT cbSize; // Размер этого объекта в байтах
UINT style; // Стиль окна
WNDPROC IpfnWndProc; // Указатель на функцию обработки сообщений
626 Глава 11
int cbClsExtra;
int cbWndExtra;
HINSTANCE hlnstance;
HICON hlcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR IpszMenuName;
LPCTSTR IpszClassName;
HICON hlconSm;
11 Дополнительный байт после класса окна
// Дополнительные байты после экземпляра окна
/ / Дескриптор экземпляра приложения
/ / Пиктограмма приложения
// Курсор окна
// Кисть, определяющая цвет фона
// Указатель на имя ресурса меню
// Указатель на имя класса
// Малая пиктограмма, связанная с окном
Вы конструируете объект типа WNDCLASSEX точно таким же способом, который вы
видели, когда речь шла о структурах, например:
WNDCLASSEX WindowClass; // Создать объект класса окна
Все, что необходимо сделать — это заполнить значения членов WindowClass.
Установка члена cbSize структуры упрощается применением операции sizeof:
WindowClass.cbSize - sizeof(WNDCLASSEX);
Член структуры style определяет различные аспекты поведения окна, в частно-
сти, условие, при котором окно должно быть перерисовано. Вы можете выбрать зна-
чение этого члена из множества опций, каждая из которых определена символиче-
ской константой, начинающейся с CS_.
Все возможные значения, константы стиля можно найти по слову WNDCLASSEX в библиоте-
ке MSDN, которая доступна по адресу http: //msdn. microsoft. com.
Когда требуются две или более опций, константы могут комбинироваться для по-
лучения составного значения с использованием операции битового ИЛИ (|), напри-
мер:
WindowClass.style = CS_HREDRAW I CS_VREDRAW;
Опция CS_HREDRAW указывает Windows, что окно должно быть перерисовано, если
изменяется его горизонтальная ширина. В предыдущем операторе выбрана перери-
совка окна в обоих случаях изменения размера. В результате Windows будет посылать
сообщение вашей программе, указывая, что вы должны перерисовать окно всякий
раз, когда ширина или высота окна изменяется пользователем. Каждая из возможных
опций стиля окна определена установкой в 1 уникального бита в 32-битовом слове.
Вот почему для их комбинирования используется операция битового ИЛИ. Эти биты,
указывающие определенный стиль, обычно называют флагами. Флаги применяются
очень часто, и не только в Windows, но также и в C++, потому что предлагают эффек-
тивный способ предоставления и обработки средств, которые могут включаться и от-
ключаться, либо параметров, принимающих значение true или false.
Член IpfnWndProc сохраняет указатель на функцию вашей программы, которая
обрабатывает сообщения для созданного вами окна. Префикс имени говорит о том,
что это длинный (long) указатель на функцию. Если вы хотите, чтобы эта функцией
для обработки сообщений приложения служила WindowProc (), то должны инициали-
зировать следующий член:
WindowClass.IpfnWndProc = WindowProc;
Следующие два члена — cbClsExtra и cbWndExtra — позволяют запросить у
Windows дополнительное пространство для ваших собственных целей. Примером
может служить ситуация, когда вы хотите ассоциировать дополнительные данные с
каждым экземпляром окна, чтобы ассистировать в обработке сообщений для каждо-
Концепции программирования для Windows
627
го экземпляра окна. Обычно вам не потребуется выделять это дополнительное про-
странство, и в этом случае вы просто устанавливаете значение членов cbClsExtra и
cbWndExtra равными нулю.
Член hlnstance содержит дескриптор текущего экземпляра приложения, так что
вы должны установить его равным значению hlnstance, переданному Windows функ-
ции WinMain().
Члены hl con, hCursor и hbrBackground представляют собой дескрипторы, ко-
торые, в свою очередь, определяют пиктограмму, представляющую приложение в
свернутом виде, курсор мыши, используемый окном, и цвет фона клиентской области
окна. (Как упоминалось ранее, дескриптор — это 32-разрядное целое число, использу-
емое в качестве идентификатора, представляющего что-либо.) Они устанавливаются
с использованием функций Windows API. Например:
WindowClass.hlcon « Loadlcon(0, IDI_APPLICATION);
WindowClass.hCursor = LoadCursor(0, IDC_ARROW);
WindowClass.hbrBackground =
static_cast<HBRUSH>(GetStockObject(GRAY_BRUSH));
Вызовами этих функций все три члена устанавливаются в стандартные значения
Windows. Пиктограмма по умолчанию предоставлена Windows, а курсор имеет вид
стандартной стрелочки, используемой большинством приложений Windows. Кисть —
это объект Windows, используемый для заполнения областей, в данном случае кли-
ентской области окна. Функция GetStockObject () возвращает обобщенный тип
всех ресурсных объектов, так что вы должны привести его к типу HBRUSH. В пред-
ыдущем примере он возвращает дескриптор стандартной серой кисти, поэтому цвет
фона окна устанавливается в серый. Эта функция также может использоваться для
получения других стандартных объектов для окна, например, шрифтов. Вы можете
также установить значение членов hlcon и hCursor равными null — в этом случае
Windows представит пиктограмму и курсор по умолчанию. Если установить в null
член hbrBackground, это будет означать, что ваша программа должна будет сама ри-
совать фон окна, и сообщения об этом будут передаваться вашему приложению тогда,
когда в этом возникнет необходимость.
Член IpszMenuName устанавливается в имя ресурса, определяющего меню окна,
или в ноль, если у окна не должно быть меню. Вы познакомитесь с созданием и ис-
пользованием ресурсов меню, когда будете использовать мастер создания прило-
жений.
Член IpszClassName структуры хранит имя, применяемое для идентификации
данного конкретного класса окна. Обычно вы будете использовать для этого имя при-
ложения. Это имя потребуется отслеживать, потому что оно вновь понадобится вам
при создании окна. Таким образом, этот член обычно будет устанавливаться следую-
щими операторами:
static char szAppNamef] = "OFWin"; // Определить имя класса окна
WindowClass.IpszClassName = szAppName; // Установить имя класса
Последний член — hlconSm, который идентифицирует маленькую пиктограмму,
ассоциированную с классом окна. Если вы специфицируете его как null, то Windows
будет искать маленькую пиктограмму, связанную с членом hlcon, и использует ее.
Фактически структура WNDCLASSEX заменяет другую структуру, WNDCLASS, которая
была использована для тех же целей. Старая структура не включала член cbSize, хра-
нящий размер структуры в байтах, и член hlconSm.
628 Глава 11
Создание окна программы
После того, как всем членам вашей структуры WNDCLASSEX присвоены нужные зна-
чения, следующий шаг состоит в том, чтобы сообщить об этом Windows. Вы делаете
это с использованием функции Windows API RegisterClassEx (). Если ваша структу-
ра называется WindowClass, то оператор, с помощью которого это делается, должен
быть таким:
RegisterClassEx(SWindowClass);
Просто, не правда ли? Адрес структуры передается в функцию, a Windows извле-
кает и сохраняет все значения, установленные в членах структуры. Этот процесс на-
зывается регистрацией класса окна. Просто чтобы напомнить: термин класс исполь-
зуется здесь в смысле классификации, и никак не связан с классом C++, так что не
путайте их. Каждый экземпляр приложения должен обеспечить регистрацию своего
класса окна. Если вы используете устаревшую структуру WNDCLASS, упомянутую выше,
то должны будете применить здесь другую функцию — Registerclass ().
После того, как Windows узнает характеристики требуемого вам окна и функцию,
предназначенную для обработки его сообщений, вы можете двинуться дальше и соз-
дать его. Для этого используется функция С г eat eWindow (). Определенный вами класс
окна описывает широкий диапазон характеристик окна, а дополнительные аргументы
функции С г eat eWindow () добавляют к ним еще некоторые. Поскольку приложение в
общем случае может состоять из нескольких окон, функция Cr eat eWindow () возвра-
щает дескриптор созданного окна, который вы можете сохранить, чтобы позднее об-
ращаться к этому конкретному окну. Существует много вызовов API, которые требуют
специфицировать дескриптор окна в качестве параметра, если вы хотите использо-
вать их. Рассмотрим пример типичного использования функции CreateWindow ():
HWND hWnd; // Дескриптор окна
hWnd = CreateWindow(
szAppName,
"A Basic Window the Hard Way”,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW-USEDEFAULT,
0,
0,
hlnstance,
0
// Имя класса окна
11 Заголовок окна
// Стиль окна - перекрываемое
// Позиция на экране по умолчанию
// левого верхнего угла как х,у...
/ / Размер окна по умолчанию - ширина...
// .. .и высота
// Нет родительского окна
// Нет меню
/ / Дескриптор экземпляра программы
// Никаких данных для создания окна
Переменная hWnd типа HWND — это 32-разрадный целочисленный дескриптор
окна. Вы используете эту переменную для записи значения, возвращенного функ-
цией CreateWindow (), которое идентифицирует окно. Первый аргумент, кото-
рый вы передаете в функцию — это имя класса. Оно используется Windows для
идентификации структуры WNDCLASSEX, которую вы передали до этого при вызове
RegisterClassEx (), так что информация из этой структуры может быть использова-
на в процессе создания окна.
Второй аргумент CreateWindow () определяет текст, который должен появить-
ся в панели заголовка. Третий аргумент специфицирует стиль, который имеет окно
после создания. Указанная здесь опция, WS_OVERLAPPEDWINDOW, в действительно-
сти комбинирует несколько опций. Она определяет окно как обладающее стиля-
Концепции программирования для Windows
629
ми WSJDVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX и
WS-MAXIMIZEBOX. Это дает в результате перекрываемое окно, предназначенное для
того, чтобы служить главным окном приложения, с панелью заголовка на толстой
рамке, имеющей пиктограмму, системное меню, а также кнопки разворачивания и
сворачивания. Окно, которое вы специфицируете, имеет толстую линию рамки, с по-
мощью которой можно менять размеры окна.
Следующие четыре аргумента определяют положение и размер окна на экране.
Первые два — это экранные координаты левого верхнего угла окна, а вторые два — ши-
рина и высота окна. Значение CW USEDEFAULT говорит о том, что вы хотите, чтобы
Windows назначила значение окна по умолчанию. Это сообщает Windows о том, что-
бы она располагала последовательно открываемые окна каскадом — последовательно
друг за другом. CW_USEDEFAULT применяется только к окнам, специфицированным
как WSJDVERLAPPED.
Следующее значение аргумента — ноль, говорящее о том, что создаваемое окно не
является дочерним (то есть окном, зависящим от родительского окна). Если нужно соз-
дать дочернее окно, то в этом аргументе необходимо указать дескриптор родительского
окна. Следующий аргумент также установлен в ноль, и это говорит о том, что никакого
меню не требуется. Затем вы специфицируете дескриптор текущего экземпляра про-
граммы, который был передан данной программе от Windows. Последний аргумент при
создании окна равен нулю, поскольку в данном примере вам нужно простое окно. Если
вы захотите создать клиентское окно многодокументного интерфейса (MDI — multiple-
document interface), то последний аргумент должен указывать на структуру, связанную с
этим. Позднее из настоящей книги вы узнаете подробнее об MDI.
Обратите внимание, что Windows API также включает функцию CreateWindowEx (), кото-
рую вы используете для создания окон с расширенной информацией о стиле.
После вызова функции CreateWindow () окно существует, но пока еще не отобра-
жено на экране. Чтобы отобразить его, вы должны вызвать другую функцию Windows
API:
ShowWindow(hWnd, nCmdShow); // Отобразить окно
Здесь требуются только два аргумента. Первый идентифицирует окно и равен
дескриптору, возвращенному функцией CreateWindow (). Второй имеет значение
nCmdShow, переданное WinMain (), и идентифицирует способ отображения окна на
экране.
Инициализация окна программы
После вызова функции ShowWindow () окно появляется на экране, но все еще не
имеет содержимого, так что вы должны заставить программу нарисовать клиентскую
область окна. Вы можете просто собрать вместе весь необходимый для этого код не-
посредственно в функции WinMain (), но этого будет недостаточно: в этом случае со-
держимое клиентского окна останется статичным, и вы не можете вывести в окно то,
что необходимо, после чего забыть об этом. Любое действие пользователя, которое
модифицирует окно каким-либо образом, подобное перетаскиванию границы или
окна целиком, обычно требует перерисовки окна и его клиентской области.
Когда клиентская область должна быть перерисована по любой причине, Windows
посылает определенное сообщение вашей программе, на которое ваша функция
WindowProc () должна отреагировать, реконструируя клиентскую область окна.
630 Глава 11
Таким образом, лучший способ обеспечить перерисовку клиентской области “в пер-
вой инстанции” — это поместить код перерисовки клиентской области в функцию
WindowProc () и позволить Windows отправлять необходимые запросы на перерисов-
ку клиентской области окна вашей программе. Всякий раз, когда вы решаете в своей
программе, что окно должно быть перерисовано (например, когда изменили что-то в
нем), то должны сообщить Windows о необходимости обратной отправки сообщения
о том, что окно должно быть перерисовано.
Вы можете попросить Windows отправить вашей программе сообщение о перери-
совке клиентской области, вызывая другую функцию Windows API — UpdateWindow ().
Необходимый для этого оператор выглядит следующим образом :
UpdateWindow (hWnd); // Вызвать перерисовку клиентской области окна
Эта функция требует только одного аргумента: дескриптора окна hWnd, который
идентифицирует ваше конкретное окно программы. Это может показаться излишне
сложным, поскольку, как уже говорилось ранее, для обработки сообщений вам нужна
функция WindowProc (), но позвольте мне объяснить, почему нужно делать именно
так.
Очередизованные и неочередизованные сообщения
Выше, представляя идею механизма сообщений Windows, я все несколько упро-
стил. Фактически есть два вида сообщений Windows.
Существуют очередизованные сообщения (queued messages), которые Windows
помещает в очередь, а функция WinMain () должна извлекать их из этой очереди для
обработки. Код WinMain (), который делает это, называется циклом сообщений.
Очередизованные сообщения включают те, что вызваны пользовательским вводом с
клавиатуры, перемещением мыши и щелчками на кнопках мыши. Сообщения от тай-
мера и сообщения Windows, запрашивающие перерисовку окна, также являются оче-
редизо ванны ми.
Но есть и неочередизованные сообщения, в результате которых функция
WindowProc () вызывается непосредственно Windows. Множество неочередизован-
ных сообщений возникают вследствие обработки очередизованных сообщений. Что
вы делаете в цикле сообщений в WinMain () — это извлекаете сообщение, которое
Windows поместила в очередь вашего приложения и затем просите Windows вызвать
вашу функцию WindowProc () для того, чтобы обработать его. Почему бы Windows
не вызывать WindowProc () всякий раз, когда это необходимо? Да, это возможно, но
она не работает подобным образом. Причина состоит в том, что Windows управляет
множеством приложений, выполняющихся параллельно.
Цикл сообщений
Как уже говорилось, извлечение сообщений из очереди выполняется с примене-
нием стандартного механизма программирования Windows, называемого подкачкой
сообщений (message pump) или циклом сообщений (message loop). Необходимый
для этого код должен выглядеть следующим образом:
MSG msg; // Структура сообщения Windows
while (GetMessage (&msg, 0, 0, 0) == TRUE)
// Получить сообщение
TranslateMessage(&msg);
DispatchMessage(&msg);
// Трансляция сообщения
// Диспетчеризация сообщения
Концепции программирования для Windows
631
Обработка сообщения состоит из трех шагов.
□ GetMessage () — получает сообщение из очереди.
□ TranslateMessage () — выполняет все необходимые преобразования извлечен-
ного сообщения.
□ DispatchMessage () — заставляет Windows вызвать функцию WindowProc () ва-
шего приложения для обработки сообщения.
Операция GetMessage () важна, поскольку она вносит существенный вклад в спо-
соб одновременной работы Windows с множеством приложений. Рассмотрим ее бо-
лее подробно.
Функция GetMessage () извлекает сообщение, помещенное в очередь окна прило-
жения, и сохраняет информацию о сообщении в переменной msg, на которую указы-
вает первый аргумент. Переменная msg, представляющая собой структуру типа MSG,
содержит множество членов, которые вы здесь не используете. Для полноты картины
рассмотрим определение этой структуры.
struct MSG
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM 1Param;
DWORD time;
POINT pt;
// Дескриптор окна
// Идентификатор сообщения
// Параметр сообщения (32-битный)
// Параметр сообщения (32-битный)
// Время, когда сообщение было помещено в очередь
// Положение курсора мыши
Член wParam — пример слегка обманчивого префикса венгерской записи, о кото-
ром упоминалось ранее. Вы можете предположить, что он имеет тип WORD (то есть
int), что было верно для ранних версий Windows, но теперь он имеет тип WPARAM,
который является 32-разрядным целочисленным значением.
Точное значение членов wParam и iParam зависит от типа сообщения. Иденти-
фикатор (ID) сообщения — член message — это целочисленное значение, которое
может быть одним из набора значений, предопределенных в заголовочном файле
windows .h в виде символических констант. Все они начинаются с WM_, и типичными
примерами могут служить WM_PAINT — для перерисовки экрана и WM_QUIT — для за-
вершения программы. Функция GetMessage () всегда возвращает TRUE, если только
не получено сообщение WM_QUIT для завершения программы, когда она возвращает
FALSE при отсутствии ошибок — в этом случае возвращается -1. Таким образом, цикл
while продолжается до тех пор, пока не будет сгенерировано сообщение о выходе
для закрытия приложения, либо пока не возникнет ошибочное условие. В обоих слу-
чаях вы должны завершить программу, передав значение wParam обратно в Windows
оператором return.
Второй аргумент при вызове GetMessage () — это дескриптор окна, для которо-
го вы хотите получить сообщение. Этот параметр может быть использован для из-
влечения сообщений для одного кона, отдельно от другого. Если аргумент равен О,
как здесь, то GetMessage () извлекает все сообщения для приложения. Это — простой
способ извлечения всех сообщений для приложения, независимо от того, сколько
окон оно имеет. Кроме того, это также наиболее безопасный способ, поскольку мож-
но быть уверенным, что вы получите все сообщения для своего приложения. Когда,
например, пользователь Windows-программы закрывает окно приложения, оно за-
крывается перед тем, как генерируется сообщение WM_QUIT. Следовательно, если вы
только извлекаете сообщения, указывая дескриптор окна функции GetMessage (), то
632 Глава 11
не можете извлечь сообщение WM_QUIT, и ваша программа не сможет корректно за-
вершиться.
Последние два аргумента GetMessage () — целые числа, содержащие минималь-
ное и максимальное значения идентификаторов сообщений, которые вы хотите из-
влечь из очереди. Это позволяет извлекать сообщения выборочно. Обычно диапа-
зоны указываются символическими константами. Использование WM_MOUSE FIRST и
WM MOUSELAST в качестве этих двух аргументов, например, позволяет выбрать только
сообщения мыши. Если оба аргумента равны нулю, как в данном примере, то извлека-
ются все сообщения.
Многозадачность
Если в очереди нет сообщений, функция GetMessage () не возвращает управление
в вашу программу. Windows позволяет выполняться другим программам, и вы получи-
те значение, возвращенное GetMessage () только тогда, когда сообщение появится в
очереди. Этот механизм был основным в обеспечении возможности выполнения мно-
жества приложений в старых версиях Windows и назывался кооперативной многоза-
дачностью, поскольку зависел от того, как конкурирующие приложения передавали
управление процессору время от времени. После того, как ваша программа вызвала
GetMessage (), если только для вашей программы нет сообщений, выполняется дру-
гое приложение, и ваша программа получает другую возможность сделать что-либо
только после того, как другое приложение освободит процессор — возможно, вызо-
вом GetMessage (), когда для него не будет сообщений, хотя это не единственная воз-
можность.
В современных версиях Windows операционная система может прервать при-
ложение после истечения периода времени и передать управление другому прило-
жению. Этот механизм называется вытесняющей многозадачностью, поскольку
приложение может быть прервано в любом событии. Однако при вытесняющей мно-
гозадачности вы все равно должны программировать цикл сообщений в W i nMa i п (),
используя GetMessage (), как и ранее, и оставляя возможность время от времени пе-
редавать управление процессором Windows при долго выполняющихся вычислениях
(это обычно делается с помощью API-функции PeekMessage ()). Если вы не сделаете
этого, ваше приложение может оказаться не в состоянии реагировать на сообщения
для перерисовки окна приложения, когда оно возникнет. Это может быть связано с
причинами, мало зависящими от вашего приложения — например, когда закрывается
перекрывающееся окно другого приложения.
Концептуальная структура функции GetMessage () показана на рис. 11.3.
Внутри цикла while первый вызов функции TranslateMessage () запрашива-
ет Windows выполнение некоторой работы по преобразованию сообщений, имею-
щих отношение к клавиатуре. Затем вызов функции DispatchMessage () заставляет
Windows осуществить диспетчеризацию сообщения, или, другими словами, вызы-
вать функцию WindowProc () в вашей программе для обработки сообщения. Возврат
DispatchMessage () не происходит до тех пор, пока WindowProc () не закончит об-
работку сообщения. Сообщение WM_QUIT говорит о том, что программа должна за-
вершиться, поэтому при этом в приложение возвращается FALSE, что останавливает
цикл сообщений.
Полная функция WinMain ()
Теперь вы видели все, из чего должна состоять функция WinMain (). Поэтому мож-
но собрать все это в готовую функцию.
Концепции программирования для Windows 633
Рис» 113. Концептуальная структура функции GetMessage ()
// Листинг OFWIN_1
int WINAPI WinMain(HINSTANCE hlnstance, HINSTANCE hPrevInstance,
LPSTR IpCmdLine, int nCmdShow)
WNDCLASSEX WindowClass; // Структура для хранения атрибутов окна
static LPCTSTR szAppName = L"OFWin"; // Определить класс окна
HWND hWnd; // Дескриптор окна
MSG msg; // Структура сообщения окна
WindowClass.cbSize = sizeof (WNDCLASSEX); // Установить размер структуры
// Перерисовать окно при изменении размера
WindowClass.style = CS HREDRAW | CS VREDRAW;
634 Глава 11
// Определить функцию - обработчик сообщений
WindowClass.IpfnWndProc = WindowProc;
WindowClass.cbClsExtra ~ 0; // Никаких дополнительных байт после структуры
WindowClass.cbWndExtra =0; // класса окна в экземпляре окна
WindowClass.hlnstance = hlnstance; // Дескриптор экземпляра приложения
// Установить пиктограмму приложения по умолчанию
WindowClass.hlcon = Loadlcon(0, IDI_APPLICATION);
// Установить стандартный курсор мыши в виде стрелочки
WindowClass.hCursor = LoadCursor(0, IDC_ARROW);
11 Установить серую кисть для рисования фона
WindowClass.hbrBackground =
static_cast<HBRUSH>(GetStockObject(GRAY_BRUSH));
WindowClass.IpszMenuName =0; // Нет меню
WindowClass.IpszClassName = szAppName; // Установить имя класса
WindowClass.hlconSm = 0; // Маленькая пиктограмма по умолчанию
/ / Теперь зарегистрировать класс окна
RegisterClassEx(&WindowClass);
I / Теперь можно создать окно
hWnd = CreateWindow(
szAppName,
"A Basic Window the Hard Way
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW__USE DEFAULT,
CW_USEDEFAULT,
0,
0,
hlnstance,
0
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
// имя класса окна
// Заголовок окна
/ / Стиль окна - перекрываемое
// Позиция на экране по умолчанию
// левого верхнего угла как х,у. .
//Размер окна по умолчанию - ширина.
// . . .и высота
// Нет родительского окна
// Нет меню
// Дескриптор экземпляра программы
// Никаких данных для создания окна
// Отобразить окно
// Заставить перерисовать клиентскую область окна
// Цикл сообщений
while(GetMessage(&msg, 0, 0, 0) == TRUE) // Получить сообщение
TranslateMessage(&msg); // Трансляция сообщения
DispatchMessage(&msg); // Диспетчеризация сообщения
return static_cast<int>(msg.wParam); // Конец, возврат в Windows
Описание полученных результатов
После объявления переменных, которые вам понадобятся в функции, все члены
структуры WindowClass инициализированы и окно зарегистрировано.
Следующий шаг — вызов функции CreateWindows О для создания данных, необхо-
димых для физического появления окна, на основе переданных аргументов и данных,
установленных в структуру WindowClass, которая была ранее передана Windows с ис-
пользованием функции RegisterClassEx (). Вызов ShowWindow () заставляет отобра-
зить окно в соответствии с режимом, специфицированным в nCmdShow, а функция
UpdateWindow () сигнализирует, что должно быть генерировано сообщение для пере-
рисовки клиентской области окна.
Концепции программирования для Windows
635
И в конце запускается цикл сообщений, который продолжает извлекать сообщения
до тех пор, пока не получит сообщение WM QUIT, после чего функция GetMessage ()
вернет FALSE и цикл завершится. Значение члена wParam структуры msg передается
назад Windows в операторе return.
Функции обработки сообщений
Функция WinMain () не содержит ничего специфичного для приложения помимо
общего внешнего вида окна этого приложения. Весь код, который заставляет прило-
жение вести себя так, как вы хотите, включается в часть программы, занятую обра-
боткой сообщений. Это функция WindowProc (), которую вы идентифицируете для
Windows в структуре WindowClass. Windows вызывает эту функцию всякий раз, когда
осуществляется диспетчеризация сообщения для главного окна вашего приложения.
Этот пример прост, поэтому весь код обработки сообщений помещается в одну
функцию — WindowProc (). В более общем случае функция WindowProc () отвечает за
анализ поступившего сообщения, определяет, какому окну оно адресовано, и затем
вызывает одну из набора функций, каждая из которых предназначена для обработ-
ки определенного сообщения в контексте определенного конкретного окна. Однако
в том, что касается общей последовательности операций и способа, которым функ-
ция WindowProc () анализирует поступающее сообщения, то здесь все одинаково для
большинства контекстов приложений.
Функция WindowProc ()
Прототип нашей функции WindowProc () выглядит следующим образом:
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM iParam);
Тип возврата LRESULT — это тип, определенный Windows, и он обычно эквивален-
тен long. Поскольку функция вызвана Windows через указатель (вы установили этот
указатель в WinMain (), когда заполняли структуру WNDCLASSEX), ее следует квалифи-
цировать как CALLBACK. Это еще один спецификатор, определенный в Windows и слу-
жащий для определения способа обработки аргументов функции. Четыре аргумента,
переданные функции, представляют информацию относительно конкретного сообще-
ния, послужившего причиной вызова функции. Значение каждого из этих аргументов
описано в табл. 11.3.
Таблица 11.3. Аргументы функции WindowProc ()
Аргумент Значение
HWDN hWnd
UINT message
WPARAM wParam
LPARAM IParam
Дескриптор окна, в котором произошло событие, послужившее причиной события.
Идентификатор сообщения — 32-битное значение, указывающее тип сообщения.
32-битное значение, содержащее дополнительную информацию, зависящую от
типа сообщения.
32-битное значение, содержащее дополнительную информацию, зависящую от
типа сообщения.
Окно, к которому относится входящее сообщение, идентифицируется первым ар-
гументом — hWnd, передаваемым функции. В данном случае есть только одно окно, по-
этому вы можете проигнорировать этот аргумент.
636 Глава 11
Сообщения идентифицируются значением message, передаваемым WindowProc ().
Вы можете сравнить это значение с символьными константами, каждая из которых
относится к определенному сообщению. Все они начинаются с WMи типичным при-
мером является WM_PRINT, что соответствует запросу на перерисовку части клиент-
ской области окна, и WM_LBUTTONDOWN, указывающее, что нажата левая кнопка мыши.
Вы можете найти полный набор этих сообщений, запустив поиск в библиотеке MSDN
по фрагменту WM_.
Декодирование сообщения Windows
Процесс декодирования сообщения, которое присылает Windows, обычно выпол-
няется в операторе switch в функции WindowProc () на основе значения message.
Выбор типов сообщений, которые вы собираетесь обрабатывать, сводится к размеще-
нию оператора case для каждого из них внутри switch. Типичная структура такого
оператора switch с включением произвольных case выглядит так:
switch(message)
case WM_PAINT:
// Код рисования клиентской области
break;
case WM-LBUTTONDOWN:
// Код обработки нажатия левой кнопки мыши
break;
case WM_LBUTTONUP:
I / Код обработки отпускания левой кнопки мыши
break;
case WM_DESTROY:
// Код обработки уничтожения окна
break;
default:
/ / Код обработки всех прочих сообщений
Каждая Windows-программа имеет где-то в себе нечто подобное, хотя оно и может
быть скрыто от взглядов, как это будет в Windows-программах, которые вы напише-
те позднее, используя MFC. Каждая конструкция case соответствует определенному
значению идентификатора сообщения и представляет соответствующую обработку
сообщения. Любое сообщение, которое программа не собирается обрабатывать инди-
видуально, обрабатывается в операторе default, который должен отправить сообще-
ние обратно Windows вызовом DefWindowProc (). Это функция Windows API, обеспе-
чивающая обработку сообщения по умолчанию.
В сложных программах, имеющих дело с широким диапазоном возможных со-
общений Windows, конструкция switch может оказаться очень большой и достаточ-
но громоздкой. Когда вы используете мастер создания приложений для генерации
Windows-приложения, вам не придется беспокоиться об этом, поскольку он все сдела-
ет за вас, и вам никогда не придется видеть функцию WindowProc (). Все, что потре-
буется сделать — предоставить код обработки определенного сообщения, в котором
вы заинтересованы.
Рисование клиентской области окна
Система Windows посылает сообщение WM_PAINT в программу, чтобы просиг-
нализировать, что клиентская область приложения должна быть перерисована.
Поэтому в нашем примере вы должны перерисовать текст окна в ответ на сообщение
WM PAINT.
Концепции программирования для Windows
637
Вы не можете рисовать в окне так, как вам взбрело в голову. Прежде чем вы сможе-
те рисовать в окне приложения, необходимо сообщить Windows о своем намерении и
получить от Windows разрешение на это. Это делается вызовом функции Windows API
BeginPaint (), которая должна быть вызвана только в ответ на сообщение WM_PAINT.
Она используется следующим образом:
HDC hDC;
PAINTSTRUCT PaintSt;
hDC = BeginPaint(hWnd,
Тип HDC определяет то,
// Дескриптор дисплейного контекста
// Структура, определяющая область для перерисовки
&PaintSt); // Подготовиться к рисованию в окне
что называется дисплейным контекстом, или в более
широком смысле — контекстом устройства. Контекст устройства представляет связь
между независимыми от устройств функциями Windows API вывода информации на
экран или принтер и драйверами устройств, поддерживающими вывод на конкрет-
ные устройства, подключенные к вашему ПК. Контекст устройства также можно трак-
товать как маркер авторизации, который Windows выдает вам по запросу и разрешает
выводить некоторую информацию. Без контекста устройства вы просто не можете
генерировать какой-либо вывод.
Функция BeginPrint () предоставляет в качестве возвращаемого значения дис-
плейный контекст и требует двух аргументов. Окно, в котором вы собираетесь рисо-
вать. идентифицируется его дескриптором — hWnd, который вы передаете в первом
аргументе. Второй аргумент — адрес переменной PaintSt типа PAINTSTRUCT, куда
Windows помещает информацию об области, которую необходимо перерисовать в от-
вет на сообщение WM_PAINT. Я опущу детали, поскольку мы пока не будем использо-
вать его. Мы просто перерисуем всю клиентскую область. Координаты клиентской
области можно получить в структуру RECT следующим образом:
RECT aRect; // Рабочий прямоугольник
GetClientRect(hWnd, &aRect);
Функция GetClientRect () получает координаты левого верхнего и правого ниж-
него углов клиентской области для окна, специфицированного первым аргументом.
Эти координаты сохраняются в структуре aRect типа RECT, которая передается во
втором аргументе в виде указателя. Затем вы можете использовать это определе-
ние клиентской области вашего окна, выводя в окно текст функцией DrawText().
Поскольку ваше окно имеет серый фон, вы должны сделать фон выводимого текста
прозрачным; в противном случае текст появится на белом фоне. Это можно сделать
следующими вызовами функций Windows API:
SetBkMode(hDC, TRANSPARENT); // Установить режим фона текста
Первый аргумент идентифицирует контекст устройства, а второй — режим отобра-
жения фона. По умолчанию принят режим OPAQUE.
После этого можно выводить текст с помощью следующего оператора:
DrawText(hDC
&aRect,
DTJSINGLELINE|
DT_CENTER|
DT VCENTER
// Дескриптор контекста устройства
soft! What light through yonder window breaks?”,
Индикатор строки, ограниченной null
Прямоугольник, в котором выполняется рисование текста
// Формат текста - одна строка
// - центрирование в строке
// - центрирование по высоте aRect
Первый аргумент функции DrawText () — ваше разрешение на рисование в окне;
это дисплейный контекст hDC. Следующий аргумент — текстовая строка, которую нуж-
но вывести. С тем же успехом вы могли бы определить ее в переменной и передать
638 Глава 11
указатель на текст в качестве второго аргумента при вызове функции. Следующий
аргумент, имеющий значение -1, указывает на то, что ваша строка ограничена
символом null. Иначе здесь нужно было бы указать количество символов строки.
Четвертый аргумент — указатель на структуру RECT, определяющую прямоугольник,
в котором вы хотите выводить текст. В данном случае это полная клиентская область
окна, определенная в aRect. Последний аргумент определяет формат текста в прямо-
угольнике. Здесь скомбинированы спецификации, объединенные битовым ИЛИ (|).
Строка выводится в одну линию с центрированием текста по горизонтали и по верти-
кали внутри прямоугольника. Это красиво располагает ее по центру окна. Существует
также множество других опций, которые включают возможность размещения текста
в верхней или нижней часть прямоугольника с выравниванием влево или вправо.
После того, как вы вывели все, что хотели отобразить, следует сообщить Windows,
что рисование в клиентской области завершено. Для каждого вызова функции
BeginPaint () должен существовать соответствующий вызов EndPaint (). Поэтому
для завершения обработки сообщения WM_PAINT необходим такой оператор:
EndPaint(hWnd, &PaintSt); II Завершить операцию перерисовки окна
Аргумент hWnd идентифицирует окно вашей программы, а второй аргумент —
адрес структуры PAINTSTRUCT, которая была заполнена функцией BeginPaint ().
Завершение программы
Вы можете предположить, что закрытие окна завершает приложение, но для того,
чтобы получить такое поведение, необходимо добавить еще немного кода. Причина
того, что приложение по умолчанию не закрывается с закрытием его окна, состоит в
том, что вам может понадобиться выполнить некоторую очистку. Кроме того, может
случиться, что приложение имеет более одного окна. Когда пользователь закрывает
окно двойным щелчком на пиктограмме в панели заголовка или щелчком на кнопке
Close (Закрыть), генерируется сообщение WM_DESTROY. Поэтому чтобы закрыть при-
ложение, вы должны обработать сообщение WM_DESTROY в функции WindowProc ().
Вы делаете это, генерируя сообщение WM_QUIT в следующем операторе:
PostQuitMessage(0);
Здесь аргумент — это код возврата. Данная функция Windows API делает именно
то, о чем говорит ее имя — посылает сообщение WM_QUIT в очередь сообщений ва:
го приложения. В результате функция GetMessage () в WinMain () возвращает FALSE
и завершает цикл сообщений, тем самым завершая программу.
Полная функция WindowProc ()
Мы рассмотрели все элементы, необходимые для построения полной функции
WindowProc О нашего примера. Ниже приведен полный код этой функции.
// Листинг OFWIN_2
LRESULT WINAPI WindowProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM IParam)
HDC hDC; // Дескриптор дисплейного контекста
PAINTSTRUCT Paintst; // Структура, определяющая область рисования
RECT aRect; // Рабочий прямоугольник
switch(message) // Выборочная обработка сообщений
case WM__PAINT: // Сообщение для перерисовки окна
hDC = BeginPaint(hWnd, &PaintSt); // Подготовиться к перерисовки окна
Концепции программирования для Windows
639
// Получить верхний левый и нижний правый углы клиентской области
GetClientRect(hWnd, &aRect);
SetBkMode(hDC, TRANSPARENT); //Установить режим отображения фона текста
/ / Теперь отобразить текст в клиентской области окна
DrawText(hDC,
L’’But, soft! What
-1,
&aRect,
DT__S INGLE LINE |
DTJ3ENTER|
DT_VCENTER
EndPaint(hWnd, &PaintSt);
return 0;
/ / Дескриптор контекста устройства
light through yonder window breaks?",
// Индикатор строки, ограниченной null
// Прямоугольник, в котором выполняется
// рисование текста
// Формат текста — одна строка
// - центрирование в строке
// - центрирование по высоте aRect
// Завершить операцию перерисовки окна
case WM_DESTROY:
PostQuitMessage(0);
return 0;
/ / Окно уничтожается
default: // Любые другие сообщения
// нас не интересуют, поэтому
// вызывается обработка сообщения по умолчанию
return DefWindowProc(hWnd, message, wParam, IParam);
Описание полученных результатов
Все тело функции состоит из одного оператора switch. Выбирается определенная
ветвь case на основе идентификатора сообщения, переданного функции в параме-
тре message. Поскольку этот пример прост, вы должны обрабатывать только два раз-
ных сообщения WM_PAINT и WM_DESTROY. Все прочие сообщения передаются обрат-
но Windows вызовом функции DefWindowProc () в части default оператора switch.
Аргументы DefWindowProc () — те же, что переданы функции, так что ьы просто
передаете их обратно без изменений. Обратите внимание на наличие оператора
return в конце обработки сообщения каждого типа. Для обработанных сообщений
возвращается нулевое значение.
Простая Windows-программа
Поскольку у нас написаны WinMain () и WindowProc () для обработки сообщений,
мы имеем все необходимое для того, чтобы создать полный исходный файл Windows-
программы, применяя только Windows API. Полный исходный файл просто состоит
из директивы #include для заголовочного файла windows . h, прототипа функции
WindowProc, а также самих функций WinMain и WindowProc, которые вы уже видели:
// Ех11_01.срр -- "родная" Windows-программа для отображения текста в окне
#include <windows.h>
LRESULT WINAPI WindowProc(HWND hWnd, UINT message,
WPARAM wParam, LPARAM IParam) ;
// Сюда вставить код WinMain() (Листинг OFWIN_1)
// Сюда вставить код WindowProc () (Листинг OFWIN_2)
Конечно, вам понадобится создать проект для этой программы, но вместо выбора
консольного приложения Win32, как вы делали до сих пор, вам придется создать про-
640 Глава 11
ект, используя шаблон проекта Win32. Вы должны создать его как пустой проект и
затем добавить файл Exll Ol. срр с кодом.
Практическое занятие
Простая программа Windows API
Если вы соберете и запустите этот код, то увидите окно, показанное на рис. 11.4.
Рис. 11,4, Результат выполнения программы Ех11__01
Обратите внимание, что это окно обладает множеством свойств, предоставлен-
ных операционной системой и не требующих никаких усилий по программированию
с вашей стороны. Границы окна можно перетаскивать, меняя их размеры, также мож-
но перетаскивать и все окно по экрану. Кнопки разворачивания и сворачивания тоже
работают. И конечно, все эти действия оказывают влияние на программу. При всякой
модификации положения или размера окна в очередь поступает сообщение WM_PAINT
и ваша программа перерисовывает свою клиентскую область, но вся работа по рисо-
ванию и модификации самого окна выполняет Windows.
Системное меню и кнопка Close (Закрыть) — это также стандартные принадлеж-
ности вашего окна, поскольку были указаны соответствующие опции в структуре
WindowClass. Опять-таки Windows берет управление на себя. Единственный дополни-
тельный эффект в вашей программе происходит от посылки сообщения WM_DESTROY
при закрытии окна, что было описано ранее.
Организация Windows-программ
В предыдущем примере вы видели элементарную Windows-программу, использу-
ющую Windows API и отображающую короткую строку. Вряд ли может кому-нибудь
пригодиться программа, не реализующая никакой полезной функциональности, од-
нако она служит для иллюстрации двух важнейших компонентов Windows-програм-
мы: функции WinMain (), представляющей инициализацию и настройку, и функции
WindowProc (), обрабатывающей сообщения Windows. Отношение между ними про-
иллюстрировано на рис. 11.5.
Концепц
программирования для Windows
641
L — — — — — — — — — — — — — — — — — — — —
Рис. 11.5. Отношения между функциями WinMain () и WindowProc ()
Это может быть не совсем очевидно на основе кода, который вы видите, но та-
кая структура является сердцем всех Windows-программ. Понимание организации
Windows-приложений часто может помочь, когда вы пытаетесь разобраться, почему
в приложении что-то работает не так, как должно. Функция WinMain () вызывается
Windows в начале выполнения программы, а функция WindowProc (), которая ино-
гда называется WndProc (), вызывается операционной системой при каждой передаче
сообщения окну вашего приложения. Обычно в приложении присутствует отдельная
функция WindowProc () для каждого окна приложения.
Функция WinMain () обрабатывает все сообщения для данного окна, которые не
были поставлены в очередь, включая инициированные в цикле сообщений WinMain ().
Таким образом, WindowProc () имеет дело с обоими видами сообщений. Это потому,
что код в цикле сообщений определяет, какого рода сообщение он извлек из очереди,
и затем осуществляет диспетчеризацию его для обработки в WindowProc (). Функция
WindowProc () — это место, где находится код специфичной для приложения реакции
на каждое сообщение Windows, который должен обрабатывать все коммуникации с
пользователем, обрабатывая сообщения Windows, сгенерированные действиями поль-
642 Глава 11
зователей, такими как перемещения мышки и щелчки ее кнопками или ввод инфор-
мации с клавиатуры.
Сообщения, помещенные в очередь, в большей степени вызваны пользователь-
ским вводом посредством мыши или клавиатуры. Неочередизованные сообщения,
для которых Windows непосредственно вызывает вашу функцию WindowProc () — это
сообщения, которые либо созданы вашей программой, либо являются результатом
получения сообщения из очереди с последующей ее диспетчеризацией, либо сообще-
ния, связанные с управлением окном, такие как работа с меню и линейками прокрут-
ки или же изменение размера окна.
Библиотека Microsoft Foundation Classes
Библиотека Microsoft Foundation Classes (MFC) — это набор предопределенных
классов, на которых построено программирование для Windows в Visual C++. Эти
классы представляют объектно-ориентированный подход к программированию
Windows, инкапсулирующий Windows API. MFC не строго следует объектно-ориенти-
рованным принципам по части инкапсуляции и сокрытия данных — главным образом
потому, что большая часть кода MFC была написана до того, как эти принципы были
установлены.
Процесс написания Windows-программы включает создание и использование
объектов MFC либо объектов классов, унаследованных от MFC. В основном вы на-
следуете собственные классы от MFC с существенной помощью со стороны специ-
ализированных инструментов Visual C++ 2005, что упрощает задачу. Объекты этих
основанных на MFC типов классов включают функции-члены для взаимодействия с
Windows, обработки сообщений Windows, а также отправки сообщений друг другу.
Эти производные классы, конечно же, наследуют все члены своих базовых классов.
Унаследованные функции выполняют практически всю черновую работу, обеспечива-
ющую работу Windows-приложения. Все, что вы должны сделать — добавить данные
и функции-члены для настройки поведения классов и обеспечения специфичной для
приложения функциональности, которая требуется от вашей программы. Занимаясь
этим, вы будете применять большинство приемов, почерпнутых из предыдущих
глав — в частности, касающихся наследования классов и виртуальных функций.
Нотация MFC
Все классы MFC имеют имена, начинающиеся с С, вроде CDocument или CView.
Если вы используете те же соглашения, определяя собственные классы или наследуя
их от классов из библиотеки MFC, то ваши программы будут легче восприниматься.
Данные-члены классов MFC снабжены префиксом ш_. Я также буду следовать этому
соглашению в примерах, использующих MFC.
Вы обнаружите, что MFC применяет венгерскую запись для многих имен перемен-
ных — в частности тех, что происходят от Windows API. Как вы помните, это преду-
сматривает префикс р для указателей, п — для int, 1 — для long, h — для дескрипторов
и так далее. Например, имя m_lpCmdLine ссылается на член данных класса (посколь-
ку включает префикс т_) типа “указатель на long”. Такая практика явного указания
типа переменной в ее имени была важна в среде С из-за недостаточного контроля
типов; благодаря тому, что можно было определить тип по имени, снижалась вероят-
ность неправильной интерпретации значений. Отрицательной стороной таких имен
переменных была некоторая громоздкость, из-за которой код выглядел сложнее, чем
Концепции программирования для Windows 643
он был на самом деле. Поскольку C++ обладает строгим контролем типов, исключа-
ющим случаи неверного использования переменных, которые были возможны в С,
подобная нотация уже не столь важна, поэтому я не применяю ее для переменных в
примерах этой книги. Однако я оставлю префикс р для указателей и некоторые дру-
гие простые обозначения типов, поскольку это помогает повысить читабельность
кода.
Как структурирована программа MFC
Из главы 1 вам известно, что вы можете произвести Windows-программу, приме-
нив мастер создания приложений, без написания единой строки кода. Конечно, при
этом используется библиотека MFC, но Windows-программу, использующую MFC, так-
же можно написать без применения мастера создания приложений. Если вы напише-
те с нуля минимальную программу на основе MFC, то получите лучшее представление
о ее фундаментальных элементах.
Простейшая программа, которую можно написать с применением MFC, несколь-
ко менее сложна, чем пример, который мы написали ранее на основе одного только
Windows API. Пример, который мы разработаем здесь, будет иметь окно, но никакого
текста в нем. Этого достаточно для демонстрации основ, так что попробуем.
Практическое занятие
Минимальное приложение MFC
Создайте новый проект, выбрав пункт меню File^New*^Project (Файл^Создать*^
Проект), как вы это делали много раз. Вы здесь не будете использовать мастер соз-
дания приложений, создающий базовый код, поэтому выберите в качестве шаблона
проекта Win32 Project (Проект Win32) и установите опцию Empty project (Пустой
проект) в следующем диалоге. После того, как проект будет создан, выберите пункт
ProjectsЕх11_02 properties (Проект^Свойства Ех11_02) из главного меню и на вклад-
ке General (Общие) окна Configuration Properties (Свойства конфигурации) щелкните
на свойстве Use of MFC (Использовать MFC), чтобы установить значение Use MFC в
Shared DLL (Разделяемая DLL).
После создания этого проекта вы можете создать в нем исходный файл Ех11_02. срр.
Так, чтобы видеть весь код программы в одном месте, поместите в этот файл опре-
деления класса вместе с их реализацией. Чтобы достичь этого, просто добавьте код
вручную в окне редактора — его будет не так много.
Для начала добавьте оператор, включающий заголовочный файл afxwin.h, кото-
рый содержит определения многих классов MFC. Это позволит вам наследовать ваши
собственные классы от MFC.
#include <afxwin.h> // Для библиотеки классов
Чтобы получить завершенную программу, вам достаточно унаследовать два клас-
са от MFC: класс приложения и класс окна. Вам даже не придется писать функцию
WinMain (), как вы делали в предыдущем примере этой главы, поскольку она автома-
тически предоставляется библиотекой MFC “за сценой”. Посмотрим, как можно опре-
делить эти два класса.
Класс приложения
Класс CWinApp является фундаментальным для любой Windows-программы, на-
писанной с применением MFC. Объект этого класса включает все необходимое для
644 Глава 11
запуска, инициализации и закрытия приложения. Вам нужно создать приложение,
наследуя собственный класс приложения от CWinApp. Вы определите специализиро-
ванную версию класса, подходящую для нужд вашего приложения. Ниже показан не-
обходимый для этого код.
class COurApp: public CWinApp
public:
virtual BOOL Initlnstance();
Как можно ожидать от столь простого примера, в этом случае нет никакой осо-
бой специализации. Вы включили только один член в определение класса: функцию
Initlnstance (). Эта функция определена в базовом классе как виртуальная, то есть
она не является новой в вашем производном классе; вы просто переопределяете
функцию базового класса приложения. Все прочие данные и функции-члены, кото-
рые вам понадобятся, наследуются от CWinApp без изменений.
Класс приложения оснащен достаточным числом членов-данных, определенных
в базе, и многие из них соответствуют переменным, используемым в качестве аргу-
ментов в функциях Windows API. Например, член m_pszAppName хранит указатель
на строку, определяющую имя приложения. Член m_nCmdShow специфицирует окно
приложения, которое должно отображаться при его запуске. Сейчас вам не нужно по-
гружаться в дебри всех наследованных данных-членов. Вы увидите их применение в
процессе разработки специфичного для приложения кода.
Наследуя собственный класс приложения от CWinApp, вы должны переопределить
виртуальную функцию Initlnstance (). Ваша версия вызывается версией WinMain (),
предоставленной MFC, и вы включаете в эту функцию код создания и отображения
вашего окна приложения. Однако перед тем как вы напишете Initlnstance (), я дол-
жен представить вам класс библиотеки MFC, который определяет окно.
Класс окна
Вашему приложению MFC необходимо окно в качестве интерфейса пользова-
теля, которое носит название обрамляющего окна (frame window). Вы наследуете
класс окна приложения от MFC-класса CFrameWnd, предназначенного специально для
этой цели. Поскольку класс CFrameWnd представляет все необходимое для создания и
управления окном вашего приложения, все, что необходимо добавить к производно-
му классу окна — это конструктор. Он позволит вам специфицировать панель заголов-
ка окна, отвечающую контексту приложения:
class COurWnd: public CFrameWnd
public:
// Конструктор
COurWnd()
Create (0, L"0ur Dumb MFC Application’’);
Функция Create (), которая вызывается в конструкторе, унаследована от базо-
вого класса. Она создает окно и присоединяет его к создаваемому объекту COurWnd.
Обратите внимание, что объект COurWnd — это не то же самое, что окно, отображае-
мое Windows — объект класса и физическое окно представляют собой разные вещи.
Концепции программирования для Windows 645
4
Значение первого аргумента функции Create (), равное 0, указывает на то, что вы
хотите использовать для окна атрибуты по умолчанию базового класса; как вы пом-
ните из предыдущего примера настоящей главы, работающего с Windows API непо-
средственно, вы должны определить атрибуты окна. Второй аргумент указывает имя
окна, используемое в панели заголовка окна. Для вас не будет сюрпризом наличие и
других параметров в функции Create (), но все они имеют значения по умолчанию,
которые вполне удовлетворительны, так что пока их можно проигнорировать.
Завершение программы
Имея определенный класс окна для приложения, вы можете написать функцию
Initlnstance () класса COurApp:
BOOL COurApp::Initlnstance(void)
// Сконструировать объект окна в свободном хранилище
m_pMainWnd = new COurWnd;
m_pMainWnd->ShowWindow(m_nCmdShow); // .. .и отобразить его
return TRUE;
Это переопределяет виртуальную функцию, определенную в базовом классе
CWinApp, и как уже говорилось, эта функция вызывается из WinMain (), автоматиче-
ски предоставленной вам библиотекой MFC. Функция Initlnstance () конструирует
объект главного окна приложения в свободном хранилище, используя для этого опе-
рацию new. Вы сохраняете возвращенный адрес в переменной m_pMainWnd, которая
является унаследованным членом вашего класса COurApp. Эффект от этого состоит в
том, что объектом окна владеет объект приложения. Вам даже не нужно беспокоить-
ся об освобождении памяти созданного объекта — функция WinMain () позаботится о
необходимой очистке.
Единственный элемент, который вам понадобится для завершения программы,
хоть и довольно ограниченной — это объект приложения. Экземпляр нашего класса
приложения COurApp должен существовать перед запуском WinMain (), поэтому вы
должны объявить его в глобальном контексте с помощью следующего оператора:
COurApp AnApplication; // Определение объекта приложения
Причина того, что этот объект должен существовать в глобальном контексте, со-
стоит в том, что это — приложение, а приложение должно существовать до того, как
его можно будет запустить. Функция WinMain (), предоставленная MFC, вызывает
функцию-член Initlnstance () объекта приложения для конструирования объекта
окна, и потому неявно предполагает, что объект предположения уже существует.
Гэтовый продукт
Теперь, когда вы увидели весь код, можете добавить его в исходный файл проек-
та Ех 11 _02 .срр. В Windows-программе классы обычно определяются в файлах .h, а
функции-члены, которые не определены внутри определения класса, помещаются в
файлы . срр. Ваше приложение коротко, так что вы можете поместить все необхо-
димое в единственный файл . срр. Преимущество этого заключается в том, что вы
можете видеть весь код сразу в одном месте. Таким образом, код программы структу-
рирован следующим образом:
646 Глава 11
// Exll__02.cpp
// Элементарная программа MFC
#include <afxwin.h> // Для библиотеки классов
// Определение класса приложения
class COurApp:public CWinApp
public:
virtual BOOL Initlnstance();
// Определение класса окна
class COurWnd:public CFrameWnd
public:
// Конструктор
COurWnd ()
Create (0, L"0ur Dumb MFC Application”);
// Функция для создания экземпляра главного окна приложения
BOOL COurApp::Initlnstance(void)
// Конструировать объект окна в свободном хранилище
m__pMainWnd = new COurWnd;
m_pMainWnd->ShowWindow(m_nCmdShow); // ...и отобразить его
return TRUE;
// Определение объекта приложения в глобальном контексте
COurApp AnApplication; // Определение объекта приложения
Вот и все, что нужно. Выглядит несколько странно, потому что здесь нет никакой
функции WinMain (), но, как упоминалось ранее, эту функция предоставляет библио-
тека MFC.
Теперь вы готовы к тому, чтобы построить и запустить приложение. Выберите
пункт меню Builds Build Ex11__2.exe (Сборка=>Собрать Exll_2.exe), щелкните на соот-
ветствующей кнопке в панели инструментов или просто нажмите <Ctrl+Shift+B> для
построения решения. Вы можете выполнить только компиляцию и компоновку, для
чего следует нажать <Ctrl+F5>. Внешний вид вашей минимальной программы MFC
показан на рис. 11.6.
Размер окна можно изменять, перетаскивая его границу, можно перемещать само
окно по экрану, сворачивать и разворачивать обычным образом. Единственная допол-
нительная функция, которую поддерживает эта программа — это “закрытие”, для чего
вы можете использовать системное меню, кнопку Close в верхнем правом углу окна
или просто нажатие комбинации клавиш <Alt+F4>. Не так много, но с учетом того,
насколько мало строк кода понадобилось для этого, достаточно впечатляюще.
Использование Windows Forms
Форма Windows — это сущность, представляющая окно некоторого рода. Под
окном я подразумеваю окно в его самом общем смысле — область экрана, которая
может быть кнопкой, диалогом, обычным окном или видимым GUI-компонентом лю-
бого рода. Форма Windows инкапсулируется подклассом класса System: : Windows : :
Forms: : Form, но вы поначалу не должны особенно беспокоиться об этом, поскольку
Концепции программирования для Windows
647
весь код
необходимый для создания формы, создается автоматически. Чтобы уви-
деть, насколько это просто, создайте базовое окно, используя Windows Forms со стан-
дартным меню.
Our Dumb MFC Application
Puc. 11.6. Окно приложения Exll 2
JL
Практическое занятие
Приложение Windows Forms
Выберите в качестве типа проекта CLR в диалоговом окне New Project (Новый
проект) и укажите Windows Forms Application (Приложение Windows Forms) в каче-
стве шаблона проекта. Диалоговое окно New Project показано на рис. 11.7.
Введите в качестве имени проекта Ех11__03. Когда вы щелкнете на кнопке ОК, ма-
стер создания приложений сгенерирует код приложения формы Windows и отобра-
зит окно конструктора, содержащее форму, как она будет отображена приложением.
Это показано на рис. 11.8.
Теперь вы можете графически вносить изменения в форму в панели конструктора,
и эти изменения автоматически отразятся в коде создания формы. Для начала може-
те перетащить нижний угол формы, чтобы увеличить размер ее окна. Можно также
изменить текст в панели заголовка — для этого щелкните правой кнопкой мыши на
клиентской области формы и выберите пункт Properties (Свойства) из контекстного
меню. При этом отобразится окно Properties (Свойства), которое позволит изменять
свойства формы. Из списка свойств справа от панели конструктора выберите Text
(Текст) и затем введите новый текст панели заголовка в соседней колонке, показы-
вающей значение свойства. (Я ввел “Simple Form Window”.) После нажатия клавиши
<Enter> новый текст появится в панели заголовка формы.
Просто для того, чтобы убедиться, насколько просто добавлять что-либо в окно
формы, отобразите панель инструментов, выбрав закладку в правой части окна, если
она присутствует, либо нажав <Ctrl+Alt+X>, либо выбрав Toolbox (Панель инструмен-
тов) в меню View (Вид).
648 Глава 11
Рис. 11.7. Диалоговое окно New Project
Рис. 11.8. Окно конструктора с пустой формой
Концепции программирования для Windows 649
ft
каждое со своим выпадающим списком
Найдите опцию MenuStrip (Полоса меню) в списке и перетащите ее в окно фор-
мы на панели вкладки конструктора. Щелкните правой кнопкой мыши на элементе
Menus tripl, который появится под окном формы, и выберите Insert Standard Items
(Добавить стандартные элементы) из контекстного меню. После этого вы получи-
те в окне формы панель меню, наполненную стандартными меню File (Файл), Edit
(Правка), Tools (Сервис), Help (Справка)
элементов. Результат этой операции показан на рис. 11.9.
Если вы соберете проект, нажав <Ctrl+Shift+B>, и затем запустите его на выпол-
нение, нажав <Ctrl+F5>, то увидите окно формы, отображенное вместе с его меню.
Естественно, пункты меню пока ничего не делают, поскольку вы не добавили никако-
го кода, обрабатывающего события их выбора, но пиктограммы в правой части пане-
ли заголовка работают, так что вы можете закрыть приложение. Далее в настоящей
книге вы узнаете о том, как разработать полноценное приложение Windows Forms,
включающее обработку событий.
Рис. 11.9. Стандартные элементы управления Windows Forms
650 Глава 11
Резюме
В настоящей главе были показаны три разных способа создания элементарного
Windows-приложения в среде Visual C++ 2005, и вы получили представление о су-
щественных различиях между этими тремя подходами. Из остальных глав книги вы
узнаете более подробно, как разрабатывать приложения, используя MFC и Windows
Forms.
Ниже перечислены наиболее важные моменты, о которых шла речь в настоящей
главе.
□ Windows API предоставляет стандартный программный интерфейс, через кото-
рый приложения взаимодействуют с операционной системой.
□ Все Windows-приложения включают функцию WinMain (), вызываемую опе-
рационной системой для запуска их выполнения. Функция WinMain () также
включает код получения сообщений от операционной системы.
□ Операционная система Windows вызывает определенную функцию приложе-
ния для обработки специфических сообщений. Приложение идентифицирует
функцию обработки сообщений для каждого окна приложения, вызывая функ-
цию Windows API.
□ Библиотека MFC состоит из набора классов, инкапсулирующих Windows API и
упрощающих программирование с применением Windows API.
□ Приложение Windows Forms выполняется под управлением CLR. Окна прило-
жения Windows Forms проектируются графически, при этом весь необходимый
код генерируется автоматически.
12
Программирование
для Windows
с использованием MFC
В настоящей главе вы вступите на дорогу серьезной разработки Windows-приложе-
ний с использованием библиотеки Microsoft Foundation Classes (MFC). Вы получите
представление о коде, который генерирует мастер создания приложений (Application
Wizard) для программ MFC, и о том, какие опции можно включать в ваш код.
В этой главе вы изучите следующие вопросы.
□ Базовые элементы программы, основанной на MFC.
□ Отличие между приложениями однодокументного интерфейса (Single Docu-
ment Interface — SDI) и многодокументного интерфейса (Multiple Document
Interface — MDI).
□ Как использовать мастер MFC Application Wizard для генерации программ SDI
и MDI.
□ Какие файлы генерирует MFC Application Wizard, и каково их содержимое.
□ Как структурированы программы, генерируемые MFC Application Wizard.
□ Ключевые классы программ, сгенерированных MFC Application Wizard, и как
они взаимосвязаны.
□ Общий подход к настройке программ, сгенерированных MFC Application
Wizard.
В последующих главах вы будете постепенно расширять программы, сгенериро-
ванные в этой главе, путем инкрементного добавления кода. В конечном итоге вы по-
лучите масштабную работающую Windows-программу, которая будет включать почти
652 Глава 12
все базовые приемы программирования пользовательского интерфейса, изученные
на этом пути.
Концепция “документ-представление” в MFC
Когда вы пишете приложения с применением MFC, это подразумевает следование
определенной структуре построения вашей программы с размещением и обработкой
данных приложения определенным образом. Это может показаться ограничивающим
фактором, но на самом деле большей частью это вовсе не так, и выигрыш в скорости
и простоте реализации, который вы получаете, перевешивает любые вероятные не-
достатки. Структура программы MFC включает две ориентированные на приложение
сущности: документ (document) и представление (view), поэтому давайте рассмо-
трим, в чем состоит их суть и как они используются.
Что такое документ?
Документ — это имя, присвоенное коллекции данных вашего приложения, с кото-
рыми взаимодействует пользователь. Хотя слово документ выглядит как нечто, подраз-
умевающее текстовую природу, на самом деле документ не ограничивается текстом.
Это могут быть данные игры, геометрическая модель, текстовый файл, коллекция
данных о распределении апельсиновых деревьев в Калифорнии — словом, все, что хо-
тите. Термин документ— это просто принятое обозначение данных приложения, рас-
сматриваемых как единое целое.
Для вас не должно быть сюрпризом, что документ в вашей программе определяет-
ся как объект класса документа. Ваш класс документа наследуется от класса библиоте-
ки MFC по имени С Document, и вы добавляете свои собственные данные-члены для
хранения элементов, необходимых вашей программе, а также функции-члены, под-
держивающие их обработку. Ваше приложение не ограничено единственным типом
документа; вы можете определить множество классов документов, если приложение
имеет дело с несколькими разными представлениями документов.
Обработка данных приложения подобным образом позволяет использовать стан-
дартные механизмы, предусмотренные в MFC для управления коллекцией данных
приложения как единым элементом, а также сохранять и извлекать данные, содер-
жащиеся в объектах документов на диске. Эти механизмы наследуются кодом класса
вашего документа от базового класса, определенного в библиотеке MFC, так что вы
автоматически получаете широкий набор функциональности, встроенной в ваше при-
ложение, не написав при этом никакого собственного кода.
Документные интерфейсы
У вас есть выбор, со сколькими документами одновременно будет иметь дело ваша
программа — только с одним либо с несколькими. Однодокументный интерфейс,
обозначаемый сокращенно как SDI (Single Document Interface), поддерживается биб-
лиотекой MFC для программ, которым нужно открывать по одному документу за раз.
Программа, использующая этот интерфейс, называется SDI-приложением.
Для программ, которые должны открывать по несколько документов одновремен-
но, вы можете применить многодокументный интерфейс, сокращенно называемый
MDI (Multiple Document Interface). В MDI, наряду с возможностью открывать множе-
ство документов одного типа, существует также возможность организовать одновре-
Программирование для Windows с использованием MFC 653
менную обработку документов разного типа, причем каждый документ отображается
в своем собственном окне. Конечно, вы должны предоставить код соответствующей
обработки для всех различных представлений документов, которые собираетесь под-
держивать. В приложении MDI каждый документ отображается в дочернем окне при-
ложения. У вас есть также дополнительный вариант построения интерфейса, кото-
рый называется архитектурой множества документов верхнего уровня (multiple
top-level document architecture), когда окно каждого документа является дочерним по
отношению к рабочему столу.
Что такое представление?
Представление всегда относится к определенному объекту документа. Как вы уже
видели, документ в вашей программе содержит набор данных приложения, а пред-
ставление — это объект, предлагающий механизм отображения некоторых или всех
данных, хранящихся в документе. Он определяет, как данные отображаются в окне, и
как пользователь может взаимодействовать с ними. Подобно тому, как вы определяе-
те документ, вы также будете определять собственный класс представления, наследуя
его от класса MFC по имени CView. Обратите внимание, что объект представления
и окно, в котором он отображается — не одно и то же. Окно, в котором появляется
представление, называется обрамляющим окном (frame window). В действительно-
сти представление отображается в своем собственном окне, которое в точности за-
полняет клиентскую часть обрамляющего окна. На рис. 12.1 показан документ с двумя
представлениями.
Документ
Sales by Month
January 300
February 400
March 400
April 500
May 400
June 400
July 500
August 300
September 500
October 600
November 700
December 800
Представление 1
Представление 2
Рис, 12,1, Документ с двумя представлениями
654 Глава 12
В примере на рис. 12.1 каждое представление отображает все данные, которые
содержит документ, в разной форме, хотя при необходимости представление также
может отображать только часть данных документа.
Объект документа может иметь столько объектов, ассоциированных с ним, сколь-
ко вам нужно. Каждый объект представления может обеспечивать свое отличающее-
ся представление данных документа или подмножества его данных. Например, если
вы имеете дело с текстом, то разные представления могут отображать независимые
блоки текста из одного и того же документа. Если программа имеет дело с графиче-
скими данными, вы можете отобразить все данные документа в отдельных окнах в
разном масштабе или в разных форматах. На рис. 12.1 показан документ, содержащий
числовые данные — месячные объемы продаж продукта, причем одно представление
отображает гистограмму, отражающую производительность продаж, а второе — те же
данные в форме графика.
Связь документа с его представлениями
Библиотека MFC включает механизм интеграции документа с его представления-
ми и каждого обрамляющего окна с текущим активным представлением. Объект до-
кумента автоматически поддерживает список указателей на ассоциированные с ним
представления, а объект представления имеет член данных, содержащий указатель
на документ, с которым он связан. Каждое обрамляющее окно сохраняет указатель
на текущий активный объект представления. Координация между документом, пред-
ставлением и обрамляющим окном устанавливается объектами другого класса MFC,
называемыми шаблонами документа (document templates).
Шаблоны документа
Шаблон документа управляет объектами документов в вашей программе, а также
тем, как окна и представления ассоциированы с каждым из них. Существует один ша-
блон документа для каждого из типов документов, которые определены в вашем при-
ложении. Если есть два или более документов одного и того же типа, для управления
ими понадобится только один шаблон документа. Чтобы точнее описать роль шаб-
лона документа, можно сказать, что шаблон документа создает объекты документов,
а представления документа создаются объектом обрамляющего окна. Объект прило-
жения, являющийся фундаментальным для каждого приложения MFC, создает сам
объект шаблона документа. На рис. 12.2 показано графическое представление этих
отношений.
На этой диаграмме с помощью пунктирных стрелок показано использование указа-
телей для связи объектов. Эти указатели позволяют функциям-членам объекта одного
класса обращаться к общедоступным (public) данным и функциям-членам в интер-
фейсе другого объекта.
Классы шаблонов документа
Библиотека MFC включает два класса для определения шаблонов документа. Для
SDI-приложений используется библиотечный класс CSingleDocTemplate. Он относи-
тельно прост, потому что приложение SDI имеет только один документ и обычно толь-
ко одно представление. MDI-приложения несколько более сложны. Они могут иметь
множество одновременно активных документов, поэтому для определения их шаблона
документа требуется другой класс — CMultiDocTemplate. С большей частью этих клас-
сов вы познакомитесь по мере продвижения в разработке кода приложения.
Программирование для Windows с использованием MFC 655
Объект приложения
Указатель на:
создает
Шаблон документа
Указатель на:
создает
создает
Объект документа
Обрамляющее окно
Указатель на:
Указатель на:
создает
4
Объект представления
*
Указатель на
Рис, 12.2. Отношения между шаблоном документа, документом,
обрамляющим окном и представлением
Ваше приложение и MFC
На рис. 12.3 показаны четыре базовых класса, которые появляются почти в каж-
дом Windows-приложении на основе MFC.
□ Класс приложения СМуАРр.
□ Класс обрамляющего окна CMyWnd.
Класс представления CMyView, который определяет, как должны отображаться
данные, содержащиеся в CMyDoc, в клиентской области окна, созданного объ-
ектом CMyWnd.
□ Класс документа CMyDoc, который определяет документ, содержащий данные
приложения.
Действительные наименования этих классов специфичны для каждого конкретно-
го приложения, но наследование от MFC в основном одно и то же, хотя могут быть
и альтернативные базовые классы, в частности, класс представления. Как вы увидите
чуть позже, в MFC предусмотрены разные вариации класса представления, включа-
ющие широкий набор предварительно разработанной функциональности, что по-
656 Глава 12
зволяет сэкономить немало усилий по кодированию. Обычно вам не придется рас-
ширять класс, определяющий шаблон документа для вашего приложения, так что
стандартного класса MFC CSingleDocTemplate для SDI-программы обычно достаточ-
но. Когда вы создаете программу MDI, вашим классом шаблона документа является
CMultiDocTemplate, который также унаследован от CDocTemplate.
Стрелки на диаграмме направлены от базового класса к производному. Показанные
здесь библиотечные классы MFC формируют довольно сложную структуру наследова-
ния, но фактически представляют лишь малую часть полной структуры MFC. Обычно
вам не обязательно знать все детали иерархии MFC, но важно иметь общее представ-
ление об унаследованных членах ваших классов. Вы не увидите никаких определений
базовых классов в своей программе, но унаследованные члены производных классов
вашей программы происходят от непосредственного базового класса, а также от всех
непрямых базовых классов из иерархии MFC. Чтобы понять, какие члены присутству-
ют в классах вашей программы, вы должны знать, от каких классов они унаследова-
ны. После того, как вы это узнаете, вы можете поискать их описание в справочной
системе.
Еще одно, о чем вам не нужно беспокоиться — это помнить о том, какие классы
должны присутствовать в вашей программе и какие базовые классы использовать в
их определении. Как вы увидите ниже, обо всем этом заботится Visual C++ 2005.
Создание приложений MFC
При разработке Windows-программ на базе MFC используются четыре основных
инструмента.
1. Мастер создания приложений (Application Wizard) для начального создания
базового кода прикладной программы. Вы пользуетесь этим мастером при каж-
дом создании проекта, в результате чего код генерируется автоматически.
2. Контекстное меню проекта в Class View (Представление классов) для добавле-
ния новых классов и ресурсов к вашему проекту. Контекстное меню отображает-
ся при щелчке правой кнопкой мыши в Class View, а новый класс создается вы-
бором пункта Add^Class (Добавить^Класс) в этом меню. Ресурсы — это нечто,
включающее неисполняемые данные, вроде графических изображений, пикто-
грамм, меню и диалоговых окон. Пункт Add1^Resource (Добавить^ Ресурс) того
же контекстного меню позволяет добавить новый ресурс.
3. Контекстное меню класса используется в Class View для расширения и настрой-
ки существующих классов программы. Для этого применяются пункты Add^Add
Function (Добавить^ Добавить функцию) и Add^Add Variable (Добавить^
Добавить переменную) этого меню.
4. Редактор ресурсов применяется для создания или модификации таких объек-
тов, как меню и панели инструментов.
На самом деле доступно несколько редакторов ресурсов; каждый используется в
определенной конкретной ситуации, в зависимости от вида ресурса, который вы ре-
дактируете. Редактирование ресурсов рассматривается в следующей главе, но пока по-
звольте забежать немного вперед и создать приложение MFC.
Процесс создания приложения MFC почти так же прост, как создание консоль-
ной программы; на этом пути у вас будет лишь немногим больше вариантов выбора.
Как вы уже видели, все начинается с создания нового проекта выбором пункта меню
Программирование для Windows с использованием MFC 657
File^New^ Project (Файл1^Создать^Проект) или же нажатием комбинации клавиш
<Ctrl+Shift+N>. После этого отображается диалоговое окно New Project (Новый про-
ект), в котором вы можете выбрать MFC в качестве типа проекта и MFC Application
(Приложение MFC) — в качестве используемого шаблона приложения. Вы также
должны ввести имя проекта, которое может быть любым. Я использовал TextEditor,
как показано на рис. 12.4. Вам не придется превращать этот конкретный пример в
какое-то серьезное приложение, так что можете выбрать любое имя на свой вкус.
Microsoft
Foundation
Classes
CObject
CCmdTarget
CWinThread
CDocument
CDocTemplate
CWinApp
CFrameWnd
CView
CSingleDocTemplate
CMyApp
CMyWnd
CMyView
CMyDoc
Классы вашего приложения
Рис. 123. Базовые классы, присутствующие почти в каждом Windows-приложении на
основе MFC
658 Глава 12
Как вы знаете, имя, присвоенное проекту — TextEditor в данном случае — слу-
жит именем папки, содержащей все файлы проекта, но также оно используется и для
создания имен классов, которые Application Wizard генерирует для вашего проекта.
Когда вы щелкаете на кнопке ОК в диалоговом окне New Project, то видите перед со-
бой диалоговое окно MFC Application Wizard (Мастер создания приложений MFC), в
котором можете указать опции своего приложения (рис. 12.5).
Как видите,
диалоговое окно объясняет все активные установки проекта, и в пра
вой его части доступен набор опций, которые можно выбрать. Вы можете выбрать
любую из них, дабы посмотреть, что получится — у вас всегда есть возможность вер-
нуться к базовому диалоговому окну Application Wizard, щелкнув на кнопке Previous
(Назад). Выбор любой из этих опций справа предоставит вам полный набор даль-
нейших уточняющих настроек, так что в сумме их очень много. Я не стану обсуждать
их все, а только опишу вкратце те из них, которые, скорее всего, представят наи-
больший интерес, а остальные оставлю для вашего самостоятельного исследования.
Изначально Application Wizard позволяет вам выбрать SDI-приложение, MDI-прило-
жение либо приложение на основе диалогового окна. Давайте для начала создадим
приложение SDI и рассмотрим все опции, которые потребуется выбрать в процессе.
Создание SDI-приложения
Выберите опцию Application Туре (Тип приложения) из списка в правой части
налогового окна. По умолчанию будет выбран переключатель Multiple documents
(Множество документов), что означает многодокументный интерфейс — MDI, и внеш-
ний вид MDI-приложения показан в левой верхней части диалогового окна, чтобы вы
знали, чего ожидать. Выберите переключатель Single document (Один документ), и
представление приложения слева вверху изменится, как показано на рис. 12.6.
Рис. 12А. Создание проекта TextEdi tor
Программирование для Windows с использованием MFC 659
MFC Application Wizard - TextEditor
Puc. 12.5. Диалоговое окно MFC Application Wizard (Мастер создания при-
ложений MFC)
Рис. 12.6. Выбор в качестве типа приложения Single document (Один документ)
660 Глава 12
Теперь рассмотрим прочие опции, которые вы видите здесь для данного типа при-
ложений; они перечислены в табл. 12.1.
Таблица 12.1. Опции SDI-приложений
Опция
Dialog based (Основанное на диалоге)
Multiple top-level documents (Мно-
жество документов верхнего уровня)
Document/View architecture support
(Поддержка архитектуры “документ-
представление")
Resource language (Язык ресурсов)
Use Unicode libraries (Использовать
библиотеки Unicode)
Описание
Окном приложения является диалоговое окно, а не обрамляю-
щее окно.
Документы отображаются в дочерних окнах рабочего стола, а
не в дочерних окнах приложения, как это происходит в MDI-
приложении.
Эта опция выбрана по умолчанию, так что вы получаете встро-
енный код для поддержки архитектуры “документ-представ-
ление”. Если вы отключите эту опцию, то эта поддержка не
предоставляется, и вы должны реализовать все необходимое
самостоятельно.
Этот выпадающий список отображает выбор языков, при-
меняемых в ресурсах вашего приложения, таких как меню и
текстовые строки.
Поддержка Unicode обеспечивается Unicode-версиями библио-
тек MFC; если вы хотите использовать их, то должны пометить
этот флажок.
Вы должны снять отметку с флажка Use Unicode libraries (Использовать библиоте-
ки Unicode), который по умолчанию отмечен. Если оставить его отмеченным, то при-
ложение будет ожидать ввода в кодировке Unicode, и в файлах будут сохраняться сим-
волы Unicode. Это сделает их нечитаемыми для программ, ожидающих текста ASCII.
Вы также можете выбирать между Windows Explorer (Проводник Windows) и MFC
Standard (Стандартное MFC) для стиля проекта. Первый из них реализует окно при-
ложения с клиентской областью, разделенной на две панели: в левой отображаются
данные в форме дерева, а в правой — стандартный текст.
Вы также можете выбрать вариант использования кода MFC для вашей програм-
мы. По умолчанию применяется библиотека MFC как разделяемая DLL (динамически
подключаемая библиотека) — это означает, что ваша программа компонуется с про-
цедурами библиотеки MFC во время выполнения. Это позволяет уменьшить размер
исполняемого файла, который вы генерируете, но требует наличия MFC DLL на ма-
шине, где он должен запускаться. Эти два модуля вместе (исполняемый модуль . ехе
вашей программы и MFC .dll) могут оказаться больше, чем когда вы статически
скомпонуете библиотеку MFC в исполняемый файл. Если выбрана статическая ком-
поновка, процедуры библиотеки MFC включаются в исполняемый модуль вашей про-
граммы при ее сборке. Статически скомпонованные приложения работают немного
быстрее, чем те, что компонуются с библиотекой MFC динамически, так что прихо-
дится выбирать компромисс между использованием памяти и скоростью выполнения.
Если вы оставите включенной опцию по умолчанию, предусматривающую использо-
ание MFC как разделяемой DLL-библиотеки, то несколько программ, выполняющих-
ся одновременно, могут разделять единственную копию библиотеки в памяти.
В диалоговом окне Document Template Strings (Строки шаблона документа) вы
можете ввести расширение файлов, которые создает ваша программа. Для этого при-
мера вполне подойдет расширение . txt. Вы можете также ввести в этом диалоговом
окне Filter Name (Имя фильтра), что будет именем фильтра, который будет появлять-
Программирование для Windows с использованием MFC 661
ся в диалоговых окнах Open (Окрытие) и Save As (Сохранение) для фильтрации фай-
лов, так что будут отображаться только файлы с указанным расширением.
Если выбрать элемент User Interface Features (Возможность интерфейса пользова-
теля) из списка в правой панели окна MFC Application Wizard, вы получите дальней-
ший набор опций, которые могут быть включены в ваше приложение (табл. 12.2).
Таблица 12.2. Дополнительные опции SDI-приложений
Опция
Thick Frame (Толстая рамка)
Minimize box (Кнопка сворачивания)
Maximize box (Кнопка разворачивания)
Minimized (Свернутое)
Maximized (Развернутое)
Initial status bar (Начальная панель
состояния)
Split window (Разделить окно)
Standard docking toolbar (Стандартная
стыкуемая панель инструментов)
Browser style toolbar (Панель инстру-
ментов в стиле браузера)
Описание
Позволяет изменять размер окна приложения, перетаскивая
его границу. Выбрано по умолчанию.
Эта опция также выбрана по умолчанию и предоставляет кноп-
ку сворачивания в правом верхнем углу окна приложения.
Эта опция также выбрана по умолчанию и предоставляет кноп-
ку разворачивания в правом верхнем углу окна приложения.
Если выбрана эта опция, приложение стартует с окном, свер-
нутым в пиктограмму.
Если выбрана эта опция, окно приложения при запуске будет
развернуто на весь экран.
Эта опция добавляет панель состояния в нижнюю часть
окна приложения, включающую индикаторы клавиш <CAPS
LOCK>, <NUM LOCK> и <SCROLL LOCK>, а также строку
сообщений, отображающую вспомогательные сообщения для
меню и кнопок панели инструментов. Эта опция также до-
бавляет команды меню для сокрытия и отображения панели
состояния.
Эта опция предоставляет разделительную черту для каждого
из основных представлений приложения.
Эта опция добавляет в окно приложения панель инструмен-
тов со стандартным набором кнопок, являющихся альтер-
нативой стандартным пунктам меню. Панель инструментов
предоставляется по умолчанию. Стыкуемая (dockable) панель
инструментов может быть прикреплена к любой грани окна
приложения, так что вы можете поместить ее там, где вам
удобно. Добавление кнопок к панели инструментов будет
описано в главе 13.
Это добавляет в окно приложения панель инструментов в
стиле браузера Internet Explorer.
Имеется еще пара средств в наборе опций Advanced Features (Дополнительные
возможности), о которых вам следует знать. Одно из них — Printing and print pre-
view (Печать и предварительный просмотр), которое выбрано по умолчанию, а дру-
гое — Context-sensitive help (Контекстно-чувствительная справка), которое вы по-
лучите, если отметите соответствующий флажок. Отметка флажка Printing and print
preview добавляет стандартные пункты Page Setup (Параметры страницы), Print
Preview (Предварительный просмотр) и Print (Печать) в меню File (Файл), а мастер
Application Wizard предоставит код для их поддержки. Включение опции Context-
sensitive help дает базовый набор возможностей поддержки контекстной подсказки.
Понятно, что вы должны будете добавить специфическое содержимое в справочные
файлы, если хотите использовать это средство.
662 Глава 12
Если в диалоговом окне MFC Application Wizard выбрать опцию Generated Classes
(Сгенерированные классы), вы увидите список классов, которые Application Wizard
генерирует в коде вашей программы, как показано на рис. 12.7.
MFC Application Wizard - TextEditor
•fl
Generated Classes
Overview
Generated dasses:
Application T /ре
Compound Document Support
Document Template Strings
Database Support
CTextEd i torView
CTextE d ito rAp p
CT extEditorDcc
CMainFrame
User Interface Features
Class name:
.h file:
CT extE d ito rVie w
Advanced Features
TextEditorView.h
Generated Classes
Base dass:
CV le w
iCEditView
CFormView
CHtm I Ed it View
CHtmlView
CListView
CP.i ch EditView
CScrollView
CTr e e Vie w
C\ iew
T extE d ito rVi e w.cpp
Cancel
Puc. 12.7. Просмотр сгенерированных классов
Можно выделить любой класс в списке, щелкнув на его имени, и в находящихся
ниже полях будет отображено имя класса, имя заголовочного файла, в котором на-
ходится его определение, имя базового класса, а также имя файла, содержащего реа-
лизацию функций-членов класса. Определение класса всегда содержится в файле . h,
а исходный код функций-членов — в файле . срр.
В случае класса CTextEditorDoc вы можете изменить все за исключением базо-
вого класса; однако если выбрать CtextEditorApp, то единственное, что можно
будет изменить — это имя класса. Попробуйте щелкнуть на именах других классов в
списке. Для СМа in Frame вы сможете изменить все за исключением базового класса,
а для класса CTextEditorView , показанного на рис. 12.7, вы сможете изменить так-
же и базовый класс. Щелкните на кнопке со стрелкой вниз для отображения списка
классов, из которого можно выбрать базовый класс; этот список показан на рис. 12.7.
Возможности, встраиваемые в ваш класс представления, зависят от выбранного базо-
вого класса (табл. 12.3).
Поскольку мы назвали приложение TextEditor, что говорит о его способности
редактировать текст, выберем в качестве базового класса CEditView, чтобы автома-
тически получить базовые возможности редактирования.
Щелкните на кнопке Finish (Готово), чтобы получить исходные файлы полностью
рабочей базовой программы, сгенерированные мастером MFC Application Wizard на
основе выбранных вами опций.
Программирование для Windows с использованием MFC 663
Таблица 12.3. Базовые классы и возможности
Базовый класс
CEditView
СFormView
CHtmlEditView
CHtmlView
CListView
Возможности класса представления
Предоставляет простую возможность редактирования многострочных текстов,
включая поиск с заменой, а также печать.
Обеспечивает представление формы; форма — это диалоговое окно, содержащее
элементы управления, управляющие отображением данных и пользовательским
вводом. По сути это та же функциональность, что предлагается в приложении
Windows Forms для CLR, о чем пойдет речь в главе 21.
Этот класс расширяет класс CHtmlView и добавляет возможность редактирова-
ния HTML-страниц.
Обеспечивает представление, в котором могут отображаться Web-страницы и ло-
кальные HTML-документы.
Позволяет использовать архитектуру “документ-представление” со списочными
элементами управления.
CRichEditView
CScrollView
CTreeView
CView
Предоставляет возможность отображения и редактирования документов, содер-
жащих форматируемый текст.
Обеспечивает представление, автоматически добавляющее линейки прокрутки,
когда отображаемые данные того требуют.
Предоставляет возможность использовать архитектуру “документ-представление”
с древовидными элементами управления.
Предоставляет базовые возможности просмотра документов.
Вывод мастера MFC Application Wizard
Все программные файлы, сгенерированные мастером создания приложений, со-
храняются в папке проекта TextEditor, вложенной в папку решения с тем же име-
нем. Кроме того, во вложенной в папку решения папке res будут находиться файлы
ресурсов. IDE-среда поддерживает несколько способов просмотра информации, име-
ющих отношение к вашему проекту, как показано в табл. 12.4.
Таблица 12.4. Способы просмотра информации, относящейся к проекту
Вкладка/панель Содержимое
Solution Explorer
(Проводник
решения)
Class View
(Представление
классов)
Resource View
(Представление
ресурсов)
Property Pages
(Страницы свойств)
Показывает файлы, включенные в ваш проект. Файлы размещены по категори-
ям в виртуальных папках под названиями Header Files (Заголовочные файлы),
Resource Files (Ресурсные файлы) и Source Files (Исходные файлы).
Отображает классы, присутствующие в вашем проекте, вместе с их членами. Также
выводятся глобальные сущности, определенные вами. Классы показаны в верхней
панели, а в нижней — члены класса, выбранного в данный момент .в верхней панели.
Щелкая правой кнопкой мыши на элементах, отображаемых в Class View, можно
получить меню, используемое для просмотра определения выбранной сущности.
Здесь отображаются ресурсы вроде пунктов меню и кнопок панели инструментов,
использованных в вашем проекте. Щелчок правой кнопкой мыши на ресурсе ото-
бражает меню, позволяющее редактировать текущий ресурс или добавлять новые.
Здесь отображаются версии, которые вы можете собрать из вашего проекта.
Отладочная версия включает дополнительные возможности для облегчения от-
ладки кода. Рабочая версия дает в результате более компактный исполняемый
файл и собирается тогда, когда код полностью протестирован и отлажен для про-
изводственного применения. Щелкнув правой кнопкой мыши на версии — Debug
(Отладочная) или Release (Рабочая) — вы можете вызвать контекстное меню, в
котором добавить панель свойств или отобразить свойства, установленные для
версии в данный момент. Панель свойств позволяет установить настройки для
компилятора и компоновщика.
664 Глава 12
Если вы щелкнете правой кнопкой мыши на TextEditor в панели Solution Explorer
и выберете в контекстном меню пункт Properties (Свойства), будет отображено окно
свойств проекта, показанное на рис. 12.8.
TextEditor Property Ра es
tonftgu ratio п:
ActrvefDeh ug)
--П
v platform:
Actrve(Wi n 32)
Configuration Manager^.
Ш Common Properties
1-1 Configuration Proportion
General
Debugging
Ri C/C+-
R) Linker
i*i Manifest Tool
@ Resources
id XML Document Generate
m Browse info rmation
w Cui Id Events
in Custom Build Step
« Code Analysis
(*i Web Deployment
(*) Application Vo rifier
в General
Output Directo v
Intermediate Directory
Extensions to Delete on Clean
Guild Log File
Inherited Project Pro pe rty Sheets
El Project Defaults
Configuration Type
Use of MFC
Use of ATL
Minimize CRT Use in ATL
Character Set
$(SolutionD ir)$ JConfiguration Name) *
$(Conf igurationNa me)
’ .obj; *. il k; * .tib; ’ .4i; * .till; ’ ,tm p; * □ rsp; ’ .pgc; *.pgd;!
$( IntD ir)\B uildLog. him
Application (.exe)
Use MFC in a Shared DLL
Not Using ATL
No
Use Unicode Character Set
Common Language Runtime support No Common Language Runtime support
Whoe Program Optimization No Whole Program Optimization
Output Directory
Specifies a relative path to the output file directory; can include environment variables
I______________________________________________________________________________
OX
Cancel
I
Pwc. 12.8. Окно свойств проекта
В левой панели отображаются группы свойств, которые можно выбрать для показа
на правой панели. В данный момент отображается группа свойств General (Общие),
и вы можете менять значение свойства в правой панели щелчком на нем и выбором
нового значения из выпадающего окна списка справа от имени свойства, либо в не-
которых случаях в результате ввода нового значения с клавиатуры.
рядом с каждой из папок Source Files,
Просмотр файлов проекта
Если вы выберете вкладку Solution Explorer и развернете список щелчком на
рядом с TextEditor, а затем щелкнете на
Header Files и Resource Files, то увидите полный список всех файлов проекта, как
показано на рис. 12.9.
На рис. 12.9 показана панель в виде плавающего окна, чтобы сделать список фай-
лов полностью видимым; вы можете сделать любую из панелей плавающей, щелкнув
на стрелке вниз на вершине панели и выбрав из списка возможных позиций. Как ви-
дите, всего в проекте присутствует 17 файлов. Вы можете просмотреть содержимое
любого файла, выполнив двойной щелчок на его имени. Содержимое выбранного
файла отображается в окне редактора. Попробуйте это с файлом ReadMe .txt. Вы
увидите, что он содержит краткое описание содержимого каждого из файлов, состав-
ляющих проект. Я не стану здесь повторять описание этих файлов, поскольку они
предельно ясно описаны в ReadMe. txt.
Программирование для Windows с использованием MFC
665
f • Ioгег - TextEditor
Solution
4 x
U Solution TextEditor' (1 project)
0- 10 TextEditor;
Й-- — Header Files
i h°| MainFrm.h
jh| Resource.h
ь] stdafxh
TextEd ito r.h
Li TextEditorDoc.h
jh*| TextEd ito rView. h
6- Resource Files
T extEditor.ico
TextEd ito r.rc
=> TextEd ito r.rc2
T extEditorDoc.ico
И Toolbar.bmp
Й- —, Source Files
MainFrm.cpp
СЛ stdafxcpp
CJ1 TextEd ito r.cpp
c~] TextEditorDoccpp
<>3 TextEditorView.cpp
@ ReadMe.txt
^Solution Explorer Class View|qflResource View
Puc. 12,9, Полный список всех файлов проекта
Просмотр классов
Доступ к проекту, предоставляемый во вкладке Class View, часто более удобен, чем
в Solution Explorer, потому что классы являются основой всей организации прило-
жения. Когда вы хотите взглянуть на код, то обычно, прежде всего, вас интересуют
определение класса или реализация функций-членов класса, и из Class View можно
получить доступ к тому и другому. Иногда, однако, нужен и Solution Explorer. Если вы
хотите проверить директивы #include в файле . срр, то с помощью Solution Explorer
вы можете открыть непосредственно интересующий вас файл.
В панели Class View вы можете развернуть элемент TextEditor, чтобы отобра-
зить классы, определенные в приложении. Щелчок на имени любого класса покажет
члены этого класса в нижней панели. В панели Class View, показанной на рис. 12.10,
выбран класс CTextEditorDoc.
На рис. 12.10 показана панель Class View в пристыкованном состоянии. Пикто-
граммы символизируют различные сущности, которые вы можете отобразить, а опи-
сание их значения вы можете найти в документации по Class View.
Легко заметить, что имеется четыре класса, о которых говорилось выше, и ко-
торые представляют основу приложения MFC: CTextEditorApp — для приложения,
CMainFrame — для обрамляющего окна приложения, CTextEditorDoc — для докумен-
та и CTextEditorView — для представления. Также есть класс CAboutDlg, определя-
ющий объекты поддержки диалогового окна, которое появляется при выборе пун-
кта меню Help^About (Справка^О программе) в приложении. Если выбрать Global
Functions and Variables (Глобальные функции и переменные), вы увидите, что оно
содержит два определения: объект приложения theApp и indicators, представля-
ющий собой массив индикаторов, записывающих состояния клавиш <CAPS LOCK>,
<NUM LOCK> и <SCROLL LOCK>, отображаемые в панели состояния.
Чтобы просмотреть код определения класса в панели редактора, вы просто вы-
полняете двойной щелчок на имени соответствующего класса в дереве Class View.
Аналогично, чтобы просмотреть код функции-члена,
дважды щелкните на имени
функции.
666 Глава 12
Class View
<Search>
н TextEditor
’• =v Global Functions and Variables
s Macros and Constants
+i CAboutDIg
i+iCMainFrame
® .. CTextEditorApp
i+ CTextEditorDoc
W ~CTextEditorDoc(void)
W AssertValid(void) const
* CreateObject(void]
* CTextEditorDocfvoid)
* DumpfCDumpContext &dc) const
* _GetBaseClass(void)
* GetMessageMap(void) const
• * GetRuntimeClassfvoid) const
• * GetThisClassfvoid)
* GetThisMessageMap(void)
=* OnNewDocumentfvoid)
* Serialize(CArchive &ar)
L=J classCTextEditorDoc
♦ ®
Puc. 12.10. Просмотр членов класса
CTextEdi tor Doc в панели Class View
Обратите внимание, что вы можете перетаскивать грани любой из панелей в окно
IDE, чтобы увидеть содержимое кода. Вы можете скрыть или показать набор панелей
Solution Explorer, щелкнув на кнопке Autohide (Автоматически скрывать) в правой ча-
сти панели заголовка этой панели.
Определения классов
Я не стану здесь погружаться в детали реализации классов — вы просто получите
представление о том, как они выглядят, а я подчеркну несколько важных аспектов.
Если выполнить Двойной щелчок на имени класса в Class View, отображается код
определения класса. Для начала взглянем на класс приложения — CTextEdi tor Арр.
Определение этого класса приведено ниже.
// TextEditor.h : главный заголовочный файл для приложения TextEditor
//
#pragma once
#ifndef AFXWIN_H_
#error "включите ’stdafx.h* перед включением этого файла для РСН”
#endif
#include "resource.h" // главные символы
// CTextEditorApp:
//В TextEditor.срр находится реализация этого класса
//
class CTextEditorАрр : public CWinApp
public:
CTextEditorApp();
11 Переопределения
public:
virtual BOOL Initlnstance();
Программирование для Windows с использованием MFC 667
// Реализация
afx_msg void OnAppAbout();
DECLARE—MESSAGЕ_МАР()
extern CTextEditorApp theApp;
Класс CTextEditorApp унаследован от CWinApp и включает конструктор, виртуальную
функцию Initlnstance (), функцию OnAppAbout () и макрос DECLARE_MESSAGE_MAP ().
Макрос — это не код C++. Это имя, определенное директивой препроцессора # de fine, заме-
няемое некоторым текстом, который обычно является кодом C++, но также может быть и
константами или символами определенного рода.
Макрос DECLARE_MESSAGE_MAP () касается определения того, какие сообщения
Windows какими функциями-членами класса обрабатываются. Макрос присутствует
в определении любого класса, который может обрабатывать сообщения Windows.
Конечно, наш класс приложения наследует множество функций и данных-членов
от базового класса, и вы познакомитесь с ними по мере расширения примеров про-
грамм. Если посмотреть в начало кода определения класса, можно заметить директи-
ву tfpragma once, предотвращающую многократное включение данного файла. Далее
идет группа директив препроцессора, гарантирующих включение файла stdaf х. h
перед данным файлом. Обрамляющее окно приложения нашей SDI-программы созда-
ется объектом класса CMainFrame, который определен следующим образом:
class CMainFrame : public CFrameWnd
protected: // создавать только из сериализации
CMainFrame();
DECLARE—DYNCREATE(CMainFrame)
// Атрибуты
public:
// Операции
public:
// Переопределения
public:
virtual BOOL PreCreateWindow(CREATESTRUCT& cs) ;
// Реализация
public:
virtual ~CMainFrame ();
#ifdef _DEBUG
virtual void AssertValidO const;
virtual void Dump(CDumpContext& de) const;
#endif
protected: // встроенные члены управляющий панелей
CStatusBar m_wndStatusBar;
CToolBar m_wndToolBar;
I / Сгенерированные функции отображения сообщений
protected:
afx_msg int OnCreate(LPCREATESTRUCT IpCreateStruct);
DECLARE—MESSAGE_MAP()
Этот класс унаследован от CFrameWnd, который предоставляет большую часть функ-
циональности, необходимой обрамляющему окну нашего приложения. Производный
класс включает защищенные (protected) данные-члены — m wndStatusBar и
668 Глава 12
m_wndToolBar, которые являются экземплярами классов MFC CStatusBar и
CToolBar соответственно. Эти объекты создают и управляют панелью состояния, по-
являющейся в нижней части окна приложения, и панелью инструментов, предлагаю-
щей кнопки для быстрого доступа к стандартным функциям меню.
Определение класса CTextEditorDoc , созданного мастером MFC Application
Wizard, выглядит так:
class CTextEditorDoc : public CDocument
protected: // создавать только из сериализации
CTextEditorDoc();
DECLARE—DYNCREATE(CTextEditorDoc)
// Атрибуты
public:
// Операции
public:
/ / Переопределения
public:
virtual BOOL OnNewDocument();
virtual void Serialize(CArchive& ar);
I/ Реализация
public:
virtual ^CTextEditorDoc();
#ifdef _DEBUG
virtual void AssertValidO const;
virtual void Dump(CDumpContext& de) const;
#endif
protected:
// Сгенерированные функции отображения сообщений
protected:
DECLARE—MESSAGE_MAP()
Как и в случае предыдущего класса, большая часть “мяса” поступает от базового
класса, и потому здесь не видна. Макрос DECLARE—DYNCREATE (), который следует за
конструктором (он также использовался в классе CMainFrame), позволяет создавать
объект данного класса динамически, синтезируя его из данных, прочитанных из фай-
ла. Когда вы сохраняете объект SDI-документа, обрамляющее окно, содержащее пред-
ставление, сохраняется наряду с вашими данными. Это позволяет потом все восста-
новить, прочтя обратно. Чтение и запись объекта документа в файл поддерживается
процессом, называемым сериализацией. В последующих примерах, которые мы будет
разрабатывать, вы увидите, как с помощью сериализации можно записывать свои соб-
ственные документы в файл и затем реконструировать их по данным этого файла.
Класс документа также содержит в своем определении макрос DE С LAREMES SAGE-
MAP (), чтобы при необходимости позволить обрабатывать сообщения Windows функ-
циями-членами класса.
Класс представления в нашем SDI-приложении определен так, как показано ниже.
class CTextEditorView : public CEditView
protected: // создавать только из сериализации
CTextEditorView();
DECLARE—DYNCREATE(CTextEdi tо rVi ew)
Программирование для Windows с использованием MFC 669
// Атрибуты
public:
CTextEditorDoc* GetDocument() const;
// Операции
public:
// Переопределения
public:
virtual BOOL PreCreateWindow(CREATESTRUCT& cs) ;
protected:
virtual BOOL OnPreparePrinting(CPrintlnfo* plnfo);
virtual void OnBeginPrinting(CDC* pDC, CPrintlnfo* plnfo);
virtual void OnEndPrinting(CDC* pDC, CPrintlnfo* plnfo);
// Реализация
public:
virtual -CTextEditorView();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& de) const;
tfendif
protected:
11 Сгенерированные функции отображения сообщений
protected:
DECLARE_MESSAGE_MAP()
#ifndef _DEBUG // отладочная версия в TextEditorView.cpp
inline CTextEditorDoc* CTextEditorView::GetDocument () const
{ return reinterpret_cast<CTextEditorDoc*>(m_pDocument); }
#endif
Как мы специфицировали в диалоговом окне MFC Application Wizard, класс пред-
ставления унаследован от класса CEditView, который уже включает базовые средства
обработки текста. Функция GetDocument () возвращает указатель на объект доку-
мента, соответствующий представлению, и вы будете использовать его для доступа к
данным в объекте документа, когда добавите свои собственные расширения к классу
представления.
Создание исполняемого модуля
Чтобы откомпилировать и скомпоновать программу, выбурите пункт меню
Builds Build Solution (Сборка^Собрать решение), нажмите <Ctrl+Shift+R> или щел-
кните на кнопке Build (Сборка) в панели инструментов.
В коде, сгенерированном мастером создания приложений, присутствуют две реа-
лизации функции GetDocument () — члена класса CTextEditorView. Одна из них на-
ходится в файле . срр класса CEditView и используется в отладочной версии програм-
мы. Обычно именно ее вы и будете использовать в процессе разработки программы,
поскольку она обеспечивает верификацию значения указателя на документ. (Документ
хранится в унаследованном члене данных m_pDocument класса представления.)
Версию, которая применяется в рабочей версии вашей программы, можно найти по-
сле определения класса в файле TextEditorView. h. Эта версия объявлена как inline
и не проверяет указатель на документ. Функция GetDocument () просто предоставляет
связь с объектом документа. Вы можете вызывать любую из этих функций в интерфей-
се класса документа, используя указатель на документ, возвращенный функцией.
670 Глава 12
По умолчанию отладочные возможности включены в вашу программу. Наряду со
специальной версией Get Document (), в этом случае в код MFC включается множе-
ство разнообразных проверок. Если вы хотите изменить это, можете воспользоваться
выпадающим списком в панели инструментов Build для выбора конфигурации версии,
которая не содержит отладочного кода.
При компиляции вашей программы с включенной отладкой компилятор не обнаруживает
неинициализированных переменных, так что может оказаться полезным иногда выпол-
нять сборку рабочей версии даже на этапе тестирования вашей программы.
Предварительно скомпилированные заголовочные файлы
Первый раз, когда вы компилируете и компонуете программу, это занимает не-
которое время. Второй и все последующие разы это должно происходить немного
быстрее благодаря средству Visual C++ 2005, называемому предварительно скомпи-
лированными заголовочными файлами (precompiled headers). Во время начальной
компиляции компилятор сохраняет вывод от компиляции заголовочных файлов в
специальном файле с расширением . pch. При последующих сборках этот файл ис-
пользуется повторно, если источники в заголовках не изменились, что позволяет сэ-
кономить время на компиляцию заголовков.
Вы можете определить, используются ли предварительно скомпилированные заго-
ловки, и управлять тем, как они обрабатываются, через вкладку Properties (Свойства).
Щелкните правой кнопкой мыши на TextEditor и выберите в контекстном меню
пункт Properties. Если вы развернете узел C/C++ в отображенном диалоговом окне,
то сможете выбрать Precompiled Headers (Предварительно скомпилированные заго-
ловочные файлы), чтобы установить это свойство.
Запуск программы
Чтобы выполнить программу, нажмите <Ctrl+F5>. Поскольку в качестве базового
для CTextEditorView выбран класс CEditView, программа представляет полностью
функционирующий простой текстовый редактор. Вы можете вводить текст в окне,
как показано на рис. 12.11.
Рис. 12.11. Работа программы TextEditor
Обратите внимание, что приложение имеет линейки прокрутки для просмотра
текста за пределами видимой области внутри окна, и конечно, вы можете изменять
размеры окна, перетаскивая его границы. Все пункты меню полностью функциони-
руют, так что вы можете сохранять и загружать файлы, вырезать и вставлять текст, а
Программирование для Windows с использованием MFC 671
также печатать текст в окне — и все это без написания даже одной строки кода! При
перемещении курсора над кнопками панели инструментов или над пунктами меню в
панели состояния появляются сообщения, описывающие функции этих кнопок или
пунктов меню, а если задержать курсор над кнопкой на секунду, всплывает подсказка,
отображающая ее назначение. (Вы узнаете подробнее об этих всплывающих подсказ-
ках в главе 13.)
Как работает программа
Как и в тривиальном примере MFC-приложения, который рассматривался ранее
в этой главе, объект приложения создается в глобальном контексте нашей SDI-про-
граммы. Вы можете видеть это, развернув элемент Global Functions and Variables
(Глобальные функции и переменные) в Class View и затем дважды щелкнув на theApp.
В окне редактора вы увидите следующий оператор:
CTextEditorApp theApp;
Это объявление объекта theApp как экземпляра нашего класса приложения
CTextEditorApp. Приведенный оператор находится в файле TextEditor.срр, кото-
рый также содержит объявления функций-членов класса приложения и определение
класса CAboutDlg.
После того, как создан объект theApp, вызывается функция WinMain (), предо-
ставленная MFC. Она, в свою очередь, вызывает две функции-члена объекта theApp.
Сначала вызывается Initlnstance (), предназначенная для выполнения всей необ-
ходимой инициализации приложения, а затем Run (), выполняющая начальную об-
работку сообщений Windows. Функция WinMain () не присутствует явно в исходном
коде проекта, поскольку она предоставляется библиотекой классов MFC и вызывает-
ся автоматически при запуске приложения.
Функция Initlnstance ()
Вы можете получить доступ к коду функции Initlnstance () двойным щелчком на
ее имени в Class View после выделения класса CTextEditorApp, или, если вы спеши-
те, можете просто взглянуть на код, следующий немедленно за определением объекта
theApp. Версия, созданная мастером MFC Application Wizard, выглядит так:
BOOL CTextEditorApp::Initlnstance ()
// InitCommonControlsEx() требуется под Windows XP, если манифест приложения
// указывает на использование ComCtl32.dll версии 6 и последующих для разрешения
// визуальных стилей. В противном случае создание окна завершится неудачей.
INITCOMMONCONTROLSEX InitCtrls;
InitCtrls.dwSize = sizeof(InitCtrls);
// Установите это для включения всех классов общих элементов управления,
// которые хотите использовать в своем приложении.
InitCtrls.dwICC = ICC_WIN95_CLASSES;
InitCommonControlsEx(&InitCtrls);
CWinApp: .‘Initlnstance () ;
// Инициализировать библиотеки OLE
if (JAfxOlelnit())
AfxMessageBox(IDP_OLE_INIT_FAILED);
return FALSE;
AfxEnableControlContainer();
672 Глава 12
11 Стандартная инициализация.
/ / Если вы не используете эти средства и желаете уменьшить размер финального
// исполняемого файла, можете удалить специфические процедуры инициализации,
//в которых не нуждаетесь.
// Измените ключ реестра, под которым будут сохраняться настройки.
// TODO: вы должны модифицировать эту строку, указав конкретное название
/ / компании или организации
SetRegistryKey(__Т("Local AppWizard-Generated Applications"));
LoadstdProfileSettings (4); // Загрузить опции из стандартного INI-файла
// (включая MRU)
// Зарегистрировать шаблоны документов приложения.
// Шаблоны документов служат в качестве
// соединений между документами, обрамляющими окнами and представлениями
CSingleDocTemplate* pDocTemplate;
pDocTemplate = new CSingleDocTemplate (
IDR_MAINFRAME,
RUNTIME—CLASS(CTextEditorDoc),
RUNTIME—CLASS (CMainFrame) , // главное обрамляющее окно SDI
RUNTIME-CLASS (CTextEditorView)) ;
if (! pDocTemplate)
return FALSE;
AddDocTemplate(pDocTemplate);
// Разобрать командную строку для стандартных команд оболочки, DDE,
// открытия файла
CCommandLinelnfo cmdlnfo;
ParseCommandLine (cmdlnfo);
// Выполнить диспетчеризацию команд, заданных в командной строке.
// Вернуть FALSE, если приложение было запущено с /RegServer, /Register,
// /Unregserver или /Unregister.
if (! ProcessShellCommand (cmdlnfo))
return FALSE;
// Выло инициализировано единственное окно, поэтому показать и обновить его
mjpMainWnd->ShowWindow(SW-SHOW) ;
mjpMainWnd->UpdateWindow () ;
// Вызывать DragAcceptFiles, только если имеется суффикс
//В SDI-приложенииэто происходит после ProcessShellCommand
return TRUE;
Те части кода, о которых я хочу рассказать, выделены полужирным. Строка, пере-
данная функции SetRegistryKey (), используется для определения ключа реестра,
под которым сохраняется информация программы. Вы можете изменить ее по своему
желанию. Если я изменю аргумент на “Horton”, то информация о программе будет со-
хранена под следующим ключом реестра:
HKEY_CURRENT_USER\Software\Horton\TextEditor\
Под этим ключом сохраняются все настройки приложения, включая список по-
следних использованных программой файлов.
Вызов функции LoadStdProfileSettings () загружает настройки приложения,
которые были сохранены в последний раз. Конечно, при первом запуске программы
ничего загружено не будет.
Объект шаблона документа создается динамически внутри Initlnstance () следу-
ющим оператором:
Программирование для Windows с использованием MFC 673
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CTextEditorDoc) ,
RUNTIME_CLASS(CMainFrame), // главное обрамляющее окно SDI
RUNTIME_CLASS(CTextEditorView));
Первый параметр конструктора CSingleDocTemplate — символ I DR_MA IN FRAME —
определяет меню и панель инструментов, используемую типом документа. Следующие
три параметра определяют документа, главное обрамляющее окно и объекты клас-
са представления, которые должны быть связаны вместе внутри шаблона документа.
Поскольку здесь мы имеем дело с SDI-приложением, в программе присутствует только
одно представление, управляемое через объект шаблона документа. RUNTIME_CLASS () —
макрос, позволяющий определять тип объекта класса во время выполнения.
Здесь присутствует много чего другого для настройки экземпляра приложения, о
чем вам беспокоиться не следует. Вы можете добавить в функцию Initlnstance ()
свою собственную инициализацию, необходимую приложению.
Функция Run ( )
Функция Run () в классе CTextEditorApp унаследована от базового класса прило-
жения CWinApp. Поскольку функция объявлена как virtual, вы можете заменить ее
версию из базового класса своей собственной, но обычно в этом нет необходимости,
так что вам не нужно об этом беспокоиться.
Функция Run () получает все сообщения от Windows, предназначенные приложе-
нию, и обеспечивает доставку каждого из них соответствующей функции программы,
которая, если она существует, должна его обработать. Таким образом, эта функция
продолжается до тех пор, пока выполняется приложение. Она прекращает свою ра-
боту, когда вы закрываете приложение.
Таким образом, вы можете разделить работу приложения на четыре шага.
1. Создание объекта приложения theApp.
2. Выполнение функции WinMain (), которую предоставляет MFC.
3. WinMain () вызывает Initlnstance (), которая, в свою очередь, создает шаблон
документа, главное обрамляющее окно, документ и представление.
4. WinMain () вызывает Run (), которая выполняет главный цикл сообщений про-
граммы, получая и обрабатывая сообщения Windows.
Создание MDI-приложения
Теперь давайте создадим MDI-приложение, используя мастер MFC Application
Wizard. Назовем проект Sketcher — с тем прицелом, чтобы в последующих главах
расширить его, построив в конечном итоге программу для рисования. Вам не при-
дется слишком ломать голову над этой процедурой, потому что от процесса создания
SDI-приложения, который вы прошли в предыдущем разделе, ее отличают всего три
момента. Нужно будет оставить опцию по умолчанию — MDI, а не заменять ее на SDI.
В наборе опций Document Template Settings (Настройки шаблона документов) диало-
гового окна MFC Application Wizard потребуется специфицировать расширение файла
. ske. Кроме того, вы должны будете оставить CView в качестве базового класса для
CSketcherView в наборе опций Generated Classes (Сгенерированные классы).
В диалоговом окне Generated Classes (Сгенерированные классы) видно, что для
вашего приложения сгенерирован дополнительный класс по сравнению с примером
TextEditor, как показано на рис. 12.12.
674 Глава 12
MFC Application Wizard - Sketcher
Generated Classes
Overview
Application T /ре
Compound Document Support
Document Template Strings
Database Support
User Interface Features
Advanced Features
Generated Classes
Generated dasses:
CS ketche rView
CS ketch erApp
CS ketch erDoc
CMainFrame
CChildFrame
Class name:
CS ketche rView
Base dass:
CView ! v
.h file:
S ketch erView.h
. cpg file:
S ketch e rVi e w.cpp
< Previous
I
Cancel
I — ff
Puc. 12.12. Диалоговое окно Generated Classes (Сгенерированные классы)
для проекта Sketcher
Этим дополнительным классом является CChildFrame, унаследованный от клас-
са MFC CMDIChildWnd. Этот класс представляет обрамляющее окно для представ-
ления документа, появляющегося внутри окна приложения, созданного объектом
CMainFrame. В SDI-приложении имеется только один документ с единственным
представлением, так что представление отображается в клиентской области главно-
го обрамляющего окна. В MDI-приложении вы можете иметь множество открытых
документов, и каждый документ может иметь множество представлений. Чтобы обе-
спечить это, каждое представление документа в программе имеет свое собственное
дочернее обрамляющее окно, созданное объектом класса CChildFrame. Как вы виде-
ли ранее, на самом деле представление отображается в отдельном окне, которое в
точности заполняет клиентскую область обрамляющего окна.
Запуск программы
Выполните сборку программы точно так же, как в предыдущем примере. Затем,
если вы запустите ее, то получите окно приложения, показанное на рис. 12.13.
В дополнение к главному окну приложения вы получаете отдельное окно доку-
мента с заголовком Sketchl. Sketchl — начальное имя документа по умолчанию, и
если вы сохраните его, он получит расширение . ske. Вы можете создавать дополни-
тельные представления для документа, выбирая пункт меню Windows New Window
(Окно1^Новое окно). Можно также создать новый документ, выбирая пункт меню
File^New (Файл ^Создать), так что в приложении будет одновременно присутство-
вать два активных документа. Ситуация с двумя активными документами, для каждого
из которых открыто по два представления, показана на рис. 12.14.
Пока вы не можете создавать никаких данных в приложении, поскольку мы не до-
бавили никакого кода для этого, однако весь код для создания документов и представ-
лений уже включен мастером MFC Application Wizard.
Программирование для Windows с использованием MFC 675
*
Рис. 12.13. Работа приложения Sketcher
Два представления документа Sketch 1
Это создано экземпляром CMainFrame
два пред-
ставления
документа
Sketch2
Меню и панель инструментов созданы объектом CmainFrame,
однако они разделяются
Создаются как экземпляры CChildFrame
Рис. 12.14. Работа приложения Sketcher с двумя активными документами
676 Глава 12
Резюме
В этой главе внимание было сосредоточено в основном на способах использования
мастера MFC Application Wizard. Вы увидели базовые компоненты программ MFC, ге-
нерируемые этим мастером как для SDI-, так и для MDI-приложений. Все наши приме-
ры MFC созданы с помощью мастера MFC Application Wizard, так что хорошей идеей
будет иметь в виду общую структуру и отношения между классами. Возможно, вы пока
не чувствуете себя уверенно в том, что касается деталей, но пока не беспокойтесь об
этом. Когда мы начнем разрабатывать приложения в последующих главах, все значи-
тельно прояснится.
Ниже перечислены ключевые моменты, раскрытые в этой главе.
□ Мастер MFC Application Wizard генерирует полный работающий каркас при-
ложения Windows, которое вы можете адаптировать под свои требования.
□ Мастер создания приложений может генерировать приложения с однодоку-
ментным интерфейсом (SDI), которые работают с единственным документом
в одном представлении, или программы с многодокументным интерфейсом
(MDI), которые могут обрабатывать множество документов с множеством пред-
ставлений одновременно.
□ Четыре важнейших класса SDI-приложения, которые наследованы от фунда-
ментальных классов:
• класс приложения;
• класс обрамляющего окна;
• класс документа;
• класс представления.
□ Программа может иметь только один объект приложения. Он определяется ав-
томатически мастером MFC Application Wizard в глобальном контексте.
□ Объект класса документа сохраняет специфичные для приложения данные, а
класс представления отображает содержимое объекта документа.
□ Объект шаблона документа используется для связывания документа, представ-
ления и окна. Для SDI-приложения это делает класс CSingleDocTemplate, а для
MDI-приложения — класс CDocTemplate. Оба эти класса — фундаментальные и,
как правило, нет необходимости наследовать от них какие-то специфичные для
приложения версии.
Упражнения
Предложить примеры программирования для этой главы не представляется воз-
можным, поскольку на самом деле в ней были представлены лишь базовые механиз-
мы создания приложений MFC. Также нет особого смысла придумывать упражнения
и ответы к ним, поскольку вы либо и так видите ответы на экране, либо можете най-
ти их в тексте.
Тем не менее, исходные коды упражнений и их решения можно загрузить с Web-
сайта издательства.
1. Что такое отношение между документом и представлением?
2. В чем состоит назначение шаблона документов в MFC-программе для Windows?
Программирование для Windows с использованием MFC
677
3. Почему следует проявлять внимательность и планировать структуру программы
заранее при использовании мастера создания приложений?
4. Напишите код простой программы текстового редактора. Выполните сборку от-
ладочной и рабочей версий и оцените типы и размеры файлов, полученных в
обоих случаях.
5. Сгенерируйте приложений текстового редактора несколько раз, пробуя различ-
ные оконные стили в окне Advanced Options (Дополнительные параметры) ма-
стера создания приложений.
13
Работа с меню и панелями
инструментов
В предыдущей главе было показано, как устроено простое каркасное приложение,
сгенерированное мастером MFC Application Wizard, и как взаимодействуют его части.
В настоящей главе мы начнем изменять каркасное MDI-приложение под названием
Sketcher, постепенно превращая его в полезную программу. Первый шаг на этом
пути — разобраться, как в Visual C++ 2005 определяются меню и как создаются функ-
ции для обслуживания специфичных для приложения пунктов меню, которые вы до-
бавляете к своей программе. Вы также увидите, как добавить к приложению кнопки
панели инструментов. В этой главы вы изучите следующие вопросы.
□ Как программы на основе MFC обрабатывают сообщения.
□ Ресурсы меню, их создание и модификация.
□ Свойства меню, их создание и модификация.
□ Как создать функцию для обслуживания сообщения, генерируемого при выбо-
ре пункта меню.
□ Как добавить обработчики для обновления свойств меню.
□ Как добавить кнопки панели инструментов и ассоциировать их с имеющимися
пунктами меню.
Взаимодействие с Windows
Как было показано в главе 11, операционная система Windows общается с вашей
программой, посылая ей сообщения. Большую часть рутинной работы по обработке
сообщений берет на себя MFC, так что вам вообще не нужно беспокоиться о создании
функции WndProc (). Библиотека MFC позволяет предоставить функции для обработ-
ки индивидуальных сообщений, в которых вы заинтересованы, и игнорировать все
680 Глава 13
остальные. Эти функции мы будем называть обработчиками сообщений, или просто
обработчиками. Поскольку ваше приложение построено на базе MFC, обработчик со-
общений — это всегда функция-член одного из классов вашего приложения.
Ассоциация между определенным сообщением и функцией вашей программы,
предназначенной для его обработки, устанавливается картой сообщений — каждый
класс в вашей программе, который может обрабатывать сообщения Windows, имеет
такую карту. Карта сообщений класса — это просто таблица функций-членов, обра-
батывающих сообщения Windows. Каждая позиция в карте сообщений ассоциирует
функцию с определенным сообщением; когда данное сообщение поступает, вызывает-
ся соответствующая функция. В карте сообщений класса присутствуют только те со-
общения, что имеют отношение к этому классу.
Карта сообщений класса создается автоматически мастером MFC Application
Wizard, когда вы создаете проект, или мастером Add Class Wizard, когда вы добавляете
класс, обрабатывающий сообщения, в свою программу. Добавления и удаления пози-
ций в карте сообщений в основном осуществляется с помощью мастера Class Wizard,
но бывают случаи, когда приходится модифицировать карту сообщений вручную.
Начало карты сообщений в вашем коде отмечается макросом BEGINJMESSAGE_МАР (),
а его конец — макросом END_MESSAGE_MAP ().
Давайте посмотрим, как работает карта сообщений на основе примера Sketcher.
Что такое карты сообщений?
Карта сообщений устанавливается мастером MFC Application Wizard для каж-
дого из основных классов вашей программы. В экземпляре MDI-программы, такой
как Sketcher, карта сообщений определена для каждого из классов СSketcherАрр,
CSketcherDoc, CSketcherView, CMainFrame и CChildFrame. Вы можете видеть кар-
ту сообщений для класса в файле . срр, содержащем его реализацию. Конечно, функ-
ции, включенные в карту сообщений, также должны быть объявлены в определении
класса, но они идентифицируются здесь специальным образом. Взгляните на приве-
денное ниже определение класса СSketcher Арр.
class CSketcherApp : public CWinApp
public:
CSketcherApp();
// Переопределения
public:
virtual BOOL Initlnstance() ;
// Реализация
afx_msg void OnAppAbout () ;
DECLARE__MESSAGE__MAP ( )
В классе CSketcherApp объявлен только один обработчик сообщений —
OnAppAbout (). Слово afx_msg в начале строки объявления функции OnAppAbout (
предназначено только для того, чтобы отличить обработчик сообщений от других
функций-членов класса. Оно преобразуется препроцессором в пробел, так что не ока-
зывает никакого влияния на компиляцию программы.
Макрос DECLARE MESSAGE MAP () указывает, что класс может содержать функции-
члены, являющиеся обработчиками сообщений, поэтому такие классы будут иметь в
себе макрос, включенный как часть определения класса мастером MFC Application
Wizard или Add Class Wizard, который вы будете использовать для добавления ново-
Работа с меню и панелями инструментов 681
го класса в проект, в зависимости от того, что именно отвечает за его создание. На
рис. 13.1 показаны классы MFC, унаследованные от CCmdTarget, который применял-
ся в наших примерах до сих пор.
Классы, используемые непосредственно, либо прямые предки классов нашего прило-
жения, выделены полужирным. Таким образом, CSketcherApp имеет CCmdTarget как
непрямой базовый класс, и потому всегда включает макрос DECLARE—MESSAGE—МАР ().
Все классы представлений (и прочие), унаследованные от CWnd, также имеют его.
Рис, 13.1. Классы MFC, унаследованные от CCmdTarget
Если вы добавляете в класс собственные члены непосредственно, то лучше оста-
вить DECLARE—MESSAGE—МАР () в последней строке определения класса. Если вы буде-
те добавлять члены после DECLARE—MESSAGE—МАР (), вам придется указывать специ-
фикаторы доступа для них: public, protected или private.
Определения обработчиков сообщений
Если определение класса включает макрос DECLARE—MESSAGE—MAP (), то реализа-
ция класса должна содержать BEGIN_MESSAGE—MAP () и END_MESSAGE—MAP (). Если вы
посмотрите на Sketcher. срр, то увидите следующий код, представляющий часть ре-
ализации CSketcherApp:
BEGIN_MESSAGE-MAP(CSketcherApp, CWinApp)
0N_COMMAND(ID_APP_ABOUT, &CSketcherApp::OnAppAbout)
/ / Стандартные файловые команды для документа
ON_COMMAND(ID_FILE_NEW, &CWinApp::OnFileNew)
ON_COMMAND(ID_FILE_OPEN, &CWinApp::OnFileOpen)
// Стандартная команда настройки печати
ON_COMMAND(ID—FILE—PRINT—SETUP, &CWinApp::OnFilePrintSetup)
END_MESSAGE_MAP()
Это и есть карта сообщений. Макросы BEGIN_MESSAGE-MAP () и END_MESSAGE—MAP ()
указывают границы карты сообщений, и каждый из обработчиков сообщений появ-
ляется в классе между этими двумя макросами. В предыдущем случае код обрабаты-
682 Глава 13
вает только одну категорию сообщений — тип сообщений WM_COMMAND, называемый
командными сообщениями, которые генерируется, когда пользователь выбирает
пункт меню или нажимает клавишу ускоренного доступа. (Если это покажется не-
складным, то лишь потому, что существует другой тип сообщений WM_COMMAND, назы-
ваемых управляющими сообщениями уведомления, как вы увидите далее в главе.)
Карта сообщений знает, какой пункт меню выбран или какая нажата клавиша по
идентификатору (ID), включенному в сообщение. В предыдущем коде присутствует
четыре макроса ON_COMMAND — по одному для каждого из командных сообщений, ко-
торые нужно обработать. Первый аргумент этого макроса — ID, ассоциированный
с одной конкретной командой, и макрос ON COMMAND привязывает имя функции к
команде, специфицированной этим ID. Поэтому, когда поступает сообщение, соот-
ветствующее идентификатору ID APP ABOUT, вызывается функция OnAppAbout ().
Аналогично, для сообщения, соответствующего идентификатору ID_FILE_NEW, вызы-
вается функция OnFileNew () .Этот обработчик определен в базовом классе CWinApp,
как и два следующих.
Макрос BEGIN_MESSAGE_MAP () получает два аргумента. Первый идентифицирует
текущее имя класса, для которого определяется карта сообщений, а второй обеспечи-
вает подключение к базовому классу для нахождения обработчика сообщений. Если
обработчик не найден в классе, определяющем карту сообщений, выполняется про-
смотр карты сообщений базового класса.
Обратите внимание, что идентификаторы команд, такие как ID_APP_ABOUT, яв-
ляются стандартными и определенными в MFC. Это соответствует сообщениям от
стандартных пунктов меню и кнопок панели инструментов. Префикс I D_ использу-
ется для идентификации команды, ассоциированной с пунктом меню или кнопкой
панели инструментов, как вы увидите далее, когда пойдет речь о ресурсах. Например,
ID_FILE_NEW— это идентификатор, соответствующий выбору пункта меню File1^
New (Фай л ^Создать), a ID_APP_ABOUT соответствует пункту меню Help1^ About
(Справка^О программе).
Есть и другие символы помимо WM_COMMAND, которые Windows использует для
идентификации стандартных сообщений. Каждое из них снабжено префиксом WM_,
что означает “Windows Message” (“сообщение Windows ”). Эти символы определены
в Winuser .h, который включен в Windows .h. Если вы хотите ознакомиться с ними,
то найдете Winuser.hB одной из вложенных в VC папок, содержащих установленную
систему Visual C++ 2005.
Существует достаточно симпатичный способ просмотра файла . h. Если имя такого файла
присутствует в окне редактора, вы можете щелкнуть на нем правой кнопкой мыши и выбрать в
контекстном меню пункт Open Document "Filename.h" (Открыть документ "Filename.h ").
Сообщения Windows часто содержат дополнительные данные, используемые для
уточнения идентификации определенного сообщения, специфицированного задан-
ным ID. Сообщение WM_COMMAND, например, посылается для большого диапазона
команд, включая те, что вызваны выбором пунктов меню или щелчками на кнопках
панели инструментов.
Следует отметить, что когда вы добавляете обработчики сообщений вручную, то
не должны отображать сообщение (или в случае командных сообщений — идентифи-
катор команды) на более чем один обработчик в классе. Если вы это сделаете, ничего
не плохого не произойдет, но второй обработчик никогда не будет вызван. Обычно
обработчики сообщений добавляются через окно свойств, и в таком случае отобра-
зить сообщение на более чем один обработчик нельзя. Если вы хотите увидеть окно
свойств класса, щелкните правой кнопкой мыши на имени класса в панели Class View
Работа с меню и панелями инструментов 683
(Представление классов) и выберите Properties (Свойства) из контекстного меню.
Затем вы добавляете обработчик сообщений, выбирая кнопку Messages (Сообщения)
в верхней части окна Properties (рис. 13.2). Определить, какая кнопка является кноп-
кой Messages, можно, наводя курсор мыши на каждую из кнопок и прочитав всплы-
вающую подсказку.
Щелчок на кнопке Messages вызывает список идентификаторов сообщений; од-
нако прежде чем двинуться дальше, я должен рассказать немного больше о типах со-
общений, которые вы можете обрабатывать.
Properties
CMainFrame VCCodeClass
В CMainFrame
WM_ACHVATE
WM_ACnVATEAPP
WM_AS KCB FO RM ATNAN
WM_CANCELMODE
WM_CAPTU RECHANGED
WM_CHANGECB CHAIN
WM_CHANGEUISTATE
WM_CHAR
WM_CHARTOITEM
WM_CHHDACnVATE
WM_CLOSE
WM_COMPACTING
WM_COMPARETTEM
WM_CONTEXTMENU
WM_COPYDATA
WM_CREATE OnCreate
WM_CTLCOLOR
WM_DEADCHAR
WM_DELETEITEM
WM_DESTROY
WM_DESTROYCLIPBOAF
WM_DEVMODECHANGE
WM_D RAWCLIPBOARD
CMainFrame
Puc. 13.2. Окно Properties (Свойства) для сообщений
Категории сообщений
Существуют три категории сообщений, с которыми может иметь дело ваша про-
грамма, и категория, к которой оно относится, определяет то, как это сообщение об-
рабатывается. Категории сообщений перечислены в табл. 13.1.
Стандартные сообщения Windows из первой категории отмечены идентификатора-
ми, снабженными префиксом WM_, и определены в Windows. Мы напишем обработчи-
ки для некоторых из этих сообщений в следующей главе. Сообщения из второй кате-
гории — особая группа сообщений WM_COMMAND, с которыми вы познакомитесь в главе
16, когда речь пойдет о диалоговых окнах. С сообщениями из последней категории,
поступающими от пунктов меню и панели инструментов, мы будем иметь дело в на-
стоящей главе. В дополнение к идентификаторам сообщений, определенным MFC для
стандартных меню и панелей инструментов, вы можете определять собственные иден-
тификаторы для пунктов меню и кнопок панели инструментов, специфичных для ва-
шей программы. Если вы не предусмотрите идентификаторов для них, то MFC автома-
тически сгенерирует собственные, на основе текста соответствующих пунктов меню.
684 Глава 13
Таблица 13.1. Категории сообщений
Категория сообщений Описание
Сообщения Windows
Управляющие сообщения
уведомления
Командные сообщения
Это стандартные сообщения Windows, начинающиеся с префикса wm_,
за исключением сообщений wm command, о которых речь пойдет ниже.
Примерами сообщений Windows может служить wm_paint , указывающее
на необходимость перерисовки клиентской области окна, и wm_lbuttonup,
сигнализирующее о том, что левая кнопка мыши была отпущена.
Это сообщения wm_command, отправляемые элементами управления (та-
кими как окно списка) окну, создавшему элемент, либо от дочернего окна
родительскому. Параметры, ассоциированные с сообщением wm command,
позволяют различать сообщения, поступившие от разных элементов управ-
ления в вашем приложении.
Это также сообщения wm command, инициированные элементами пользо-
вательского интерфейса, такими как пункты меню и кнопки панели инстру-
ментов. MFC определяет уникальные идентификаторы для стандартных
сообщений меню и панели инструментов.
Обработка сообщений в ваших программах
Вы не можете помещать обработчик сообщения там, где вам вздумается. Правиль-
ное местоположение обработчика зависит от вида сообщений, которые нужно об-
работать. Первые две категории сообщений, описанные выше, то есть стандартные
сообщения Windows и управляющие сообщения уведомления, всегда обрабатываются
объектами классов-наследников CWnd. Классы обрамляющих окон и классы представ-
лений, например, наследованы от CWnd, поэтому они могут иметь функции-члены для
обработки сообщений Windows и управляющих сообщений уведомления. Классы при-
ложений, классы документов и классы шаблонов документов не наследуются от CWnd,
поэтому они не могут обрабатывать эти сообщения.
Использование окна свойств класса для добавления обработчиков избавляет от не-
обходимости помнить о том, куда следует помещать обработчики, поскольку допуска-
ются только те из них, которые разрешены для класса. Например, если выбран класс
CSketcherDoc, то вы не увидите никаких сообщений WM_ в окне свойств класса.
Для стандартных сообщений Windows в классе CWnd предусмотрена обработка по
умолчанию. Таким образом, если ваш производный класс не включает обработчик
для стандартного сообщения Windows, оно обрабатывается обработчиком по умолча-
нию, определенным в базовом классе. Если вы предусматриваете обработчик в своем
классе, то иногда все равно нуждаетесь в вызове обработчика базового класса, чтобы
правильно обработать сообщение. При создании своего собственного обработчика
его скелетная реализация предоставляется, когда вы выбираете нужный обработчик
в окне свойств класса, и эта реализация включает вызов базового обработчика, когда
это необходимо.
Обработка командных сообщений построена более гибко. Вы можете поместить
такие обработчики в класс приложения, в класс документа и в класс шаблона доку-
мента, и, конечно, в классы представлений и окна вашей программы. Но что случит-
ся, если командное сообщение отправлено вашему приложению, и при этом есть мно-
жество мест, где оно может быть обработано?
Работа с меню и панелями инструментов 685
4
Как обрабатываются командные сообщения
Все командные сообщения поступают в главное обрамляющее окно приложения.
Главное обрамляющее окно затем пытается обработать сообщение, маршрутизируя
его в специфической последовательности в классы вашей программы. Если один
класс не может обработать сообщение, оно передается следующему.
Последовательность, в которой классы получают возможность обработать команд-
ное сообщение, для SDI-программы описана ниже.
1. Объект представления.
2. Объект документа.
3. Объект шаблона документа.
4. Объект главного обрамляющего окна.
5. Объект приложения.
Объект представления первым получает возможность обработать командное сооб-
щение, и если в нем не определено никакого обработчика, следующий объект класса
получает шанс его обработать. Если ни один из классов не имеет определенного об-
работчика, осуществляется обработка Windows по умолчанию, что, по сути, означает,
что сообщение отбрасывается.
Для MDI-программы все лишь ненамного сложнее. Хотя существует возможность
наличия множества документов, каждый с множеством представлений, в маршрутиза-
цию командного сообщения вовлечены только активное представление и ассоцииро-
ванный с ним документ. Последовательность маршрутизации командного сообщения
в MDI-программе описана ниже.
1. Активный объект представления.
2. Объект документа, ассоциированного с активным представлением.
3. Объект шаблона документа для активного документа.
4. Объект обрамляющего окна активного представления.
5. Объект главного обрамляющего окна.
6. Объект приложения.
Можно изменить последовательность маршрутизации сообщений, однако подоб-
ная необходимость возникает настолько редко, что в этой книге она не рассматрива-
ется.
Расширение программы Sketcher
Теперь добавим код к программе Sketcher, созданной в предыдущей главе, для
реализации функциональности, необходимой для создания рисунков. Нужно будет
предоставить код для рисования линии, окружностей, прямоугольников и кривых с
различным цветом и толщиной линий, а также для добавления аннотаций к рисунку.
Данные рисунка будут храниться в документе и нужно будет обеспечить возможность
иметь несколько представлений одного и того же документа в разных масштабах.
Для представления всего необходимого при реализации всех этих возможностей
потребуется несколько глав, но хорошей стартовой точкой будет добавление элемен-
тов меню для выбора типов элементов, которые вы собираетесь рисовать, а также
для выбора цвета рисования. Мы сделаем постоянными выбранный тип элемента и
цвет, чтобы то и другое оставалось в силе до тех пор, пока вы не измените его.
686 Глава 13
Для того чтобы добавить пункты меню к программе Sketcher, необходимо выпол-
нить следующие шаги.
□ Определить элементы меню, которые должны появиться в панели главного
меню, а также каждое из их подменю.
□ Решить, какие классы вашего приложения должны обрабатывать сообщения
для каждого пункта меню.
□ Добавить функции обработки сообщений меню в соответствующие классы.
□ Добавить в классы функции для обновления внешнего вида меню, чтобы пока-
зать текущие выбранные установки.
□ Для каждого нового элемента меню добавить соответствующую кнопку в панель
инструментов, снабженную всплывающей подсказкой.
Элементы меню
Мы рассмотрим два аспекта обращения с меню в MFC: создание и модификация
внешнего вида меню в приложении и обработка выбора определенного пункта меню,
то есть определение обработчика сообщений для него. Сначала посмотрим, как соз-
даются новые пункты меню
Создание и редактирование ресурсов меню
Меню определены вне кода программы — в ресурсном файле, и спецификации
меню называют ресурсом. Существует еще несколько других типов ресурсов, кото-
рые можно включить в приложение; типичными примерами являются диалоги, пане-
ли инструментов и кнопки этих панелей. С большинством из них вы познакомитесь в
процессе усовершенствования приложения Sketcher.
Наличие определения меню в ресурсах позволяет изменять физическое представ-
ление меню, не затрагивая кода, обрабатывающего события этого меню. Например,
вы можете перевести пункты меню с английского на русский, французский, норвеж-
ский или любой другой язык, без необходимости модификации или перекомпиляции
кода программы. Код, обрабатывающий сообщения, поступающие от пользователь-
ского выбора пунктов меню, никак не зависит от того, как выглядит пункт меню, а
только от того факта, что он был выбран. Конечно, если вы добавляете элементы в
меню, то должны добавить и некоторый код для каждого из них, дабы они действи-
тельно делали что-то полезное!
Наша программа Sketcher уже имеет меню, а это означает, что она уже имеет ре-
сурсный файл. Доступ к содержимому ресурсного файла программы Sketcher можно
получить, выбрав панель Resource View (Представление ресурсов) или, если у вас уже
отображается панель Solution Explorer, дважды щелкнув на Sketcher. гс. Это пере-
ключит вас на Resource View, где отображаются ресурсы. Если вы развернете ресурс
меню, щелкнув на символе +, то увидите, что там определено два меню, помеченные
идентификаторами IDR_MAINFRAME и IDR SketcherTYPE. Первое из них применяет-
ся, когда в приложении нет открытых документов, а второе — когда открыт хотя бы
один документ. Библиотека MFC использует префикс IDR_ доя идентификации ресур-
са, определяющего полное меню для окна.
Вы будете модифицировать только меню, имеющее идентификатор IDR__
SketcherTYPE. Вам незачем обращать внимание на IDR_MA IN FRAME, поскольку ваши
новые пункты меню имеют смысл только в том случае, когда документ открыт. Вы
Работа с меню и панелями инструментов 687
можете вызвать редактор ресурса для меню двойным щелчком на его ID в панели
Resource View. Если сделать это с IDR_SketcherTYPE, появится панель редактора, по-
казанная на рис. 13.3.
Рис. 13.3. Панель редактора
Добавление пункта к панели меню
Чтобы добавить новый элемент меню, вы просто щелкаете на рамке меню с тек-
стом Туре Неге (Вводите здесь), чтобы выбрать его, и затем вводите его имя. Если вы
введете символ амперсанда (&) перед буквой в элементе меню, то эта буква будет иден-
тифицирована как “горячая клавиша” для вызова этого меню с клавиатуры. Введите в
качестве первого пункта меню E&lement (Элемент). Этим вы выберете 1 как букву со-
кращенного вызова, так что сможете вызывать элемент меню, нажимая комбинацию
<Alt+l>. Вы не можете использовать Е, поскольку она уже задействована для пункта
Edit (Правка). Завершив ввод имени меню, вы можете выполнить двойной щелчок на
новом пункте меню, чтобы отобразить его свойства, как показано на рис. 13.4.
Свойства — это просто параметры, определяющие, как пункт меню будет выгля-
деть и вести себя. На рис. 13.4 показаны свойства пункта меню, сгруппированные по
категориям. Если вы хотите, чтобы они отображались в алфавитном порядке, просто
щелкните на второй кнопке слева. Обратите внимание, что свойство Popup установ-
лено в True по умолчанию; это объясняется тем, что новый элемент меню находит-
ся на вершине панели меню, поэтому обычно при выборе он представляется в виде
всплывающего меню. Щелчок на любом свойстве в левой колонке позволяет моди-
фицировать его значение в правой колонке. В данном случае вы хотите оставить все,
как есть, так что можете просто закрыть окно Properties. Никакого идентификатора
для пункта всплывающего меню не требуется, потому что его выбор просто отобра-
жает меню, находящееся под ним, и нет никакого события, которое нужно было бы
обрабатывать. Заметьте, что при этом вы получаете новый пустой пункт для первого
элемента всплывающего меню — точно такой же, как и в панели меню.
Было бы лучше, чтобы меню Element появилось между меню View (Вид) и Window
(Окно), поэтому поместите курсор на пункте Element и, не отпуская левую кнопку
мыши, перетащите его в позицию между элементами меню View и Window и отпусти-
те кнопку. После того, как будет изменена позиция нового элемента меню Element,
688 Глава 13
следующий шаг состоит в наполнении соответствующего ему всплывающего меню но-
выми пунктами.
Рис. 13.4. Свойства нового пункта меню
Добавление пунктов к меню Element
Выберите первый пункт (помеченный в данный момент как Туре Неге) во всплы-
вающем меню, щелкнув на нем; затем введите &Line (Линия) в качестве заголовка
и нажмите клавишу <Enter>. Выполнив двойной щелчок на этом первом элементе
всплывающего меню, можно просмотреть его свойства (рис. 13.5).
Свойства модифицируют внешний вид пункта меню, а также специфицируют
идентификатор сообщения, передаваемого вашей программе при выборе этого пун-
кта. Здесь мы видим уже определенный пункт ID ELEMENT LINE, но вы можете из-
менить его на что-либо другое по своему желанию. Иногда удобно специфицировать
идентификатор самостоятельно, например, когда сгенерированный получается слиш-
ком длинным, либо его смысл не ясен. Если вы решите определить свой собственный
ID, то должны следовать соглашению, принятому в MFC относительно префиксов
ID_, чтобы указать, что это — идентификатор команды для пункта меню.
Поскольку этот элемент — часть всплывающего меню, свойство Popup по умолча-
нию установлено в False. Вы можете сделать его еще одним всплывающим меню со
своим списком элементов, установив свойство Popup равным True. Как видно на рис.
13.5, допустимые значения свойства Popup можно отобразить, выбрав стрелку вниз.
Разве вам не нравится такой способ вкладывать всплывающие меню друг в друга?
Вы можете ввести текстовую строку в качестве значения свойства Prompt, и когда
пункт меню подсвечен, эта строка появляется в панели состояния вашего приложе-
ния. Если вы оставите его пустым, в панели состояния не появится ничего. Я сове-
тую вам ввести Line в качестве значения свойства Prompt. Обратите внимание, что
Работа с меню и панелями инструментов 689
вы получаете краткую подсказку о назначении выбранного свойства в нижней части
окна свойств. Вы хотите, чтобы элементом рисования, выбранным по умолчанию
при запуске приложения, была линия, поэтому можете установить значение свойства
Checked в True, чтобы получить метку возле данного пункта меню, что сообщит о
том, что выбран режим Line. Не забудьте добавить код для обновления меток пун-
ктов меню при выборе другого элемента для рисования. Свойство Break может из-
менить внешний вид всплывающего меню, сдвигая пункт в новую колонку. Здесь это
вам не понадобится, так что оставьте все, как есть. Закройте окно Properties, чтобы
сохранить установленные значения.
Рис, 13.5, Свойства элемента всплывающего меню Line
Модификация существующих пунктов меню
Если вы обнаружите, что допустили ошибку и захотите изменить существующий
элемент меню, или просто вам понадобится проверить корректность установок
свойств, то вернуться к элементу меню очень легко. Просто выполните двойной щел-
чок на элементе, который вас интересует, и будет отображено окно его свойств. Вы
можете изменить любые из них и закрыть окно, когда все будет сделано. Если необхо-
димый вам пункт находится во всплывающем меню, которое не отображено в данный
момент, просто щелкните на элементе в панели меню, чтобы отобразить требуемое
всплывающее меню.
Завершение меню
Теперь можно двигаться дальше и создать остальные элементы всплывающе-
го меню Element, которые вам нужны: & Rectangle (Прямоугольник), &Circle
(Окружность) и Cur&ve (Кривая). Конечно, все они должны иметь свойство Checked,
690 Глава 13
же процедуру, что описана
и
равное False. Вы не можете использовать С в качестве горячей клавиши последнего
элемента, поскольку эти клавиши должны быть уникальными, а С уже назначено пун-
кту меню Circle. Можно принять наименования идентификаторов по умолчанию:
ID_ELEMENT__RECTANGLE, ID_ELEMENT_CIRCLE и ID_ELEMENT_CURVE. Можно также
установить значения свойства Prompt для этих пунктов соответственно в Rectangle,
Circle и Curve.
Вам еще понадобится меню Color в панели меню — для выбора цвета рисования —
в виде всплывающего меню с пунктами Black, Red, Green и Blue. Вы можете создать
его, начиная с пустого пункта в панели меню, используя
выше. Установите метку (Checked) на пункт Black, чтобы цветом по умолчанию был
черный. Для пунктов нового меню вы можете использовать идентификаторы, создан-
ные по умолчанию (I D_COLOR_BLACK и так далее). Можете также добавить к каждому
пункту подсказки для показа в панели состояния. После того, как все будет готово,
перетащите пункт Color в место, находящееся справа от Element, чтобы меню вы-
глядело, как показано на рис. 13.6.
Обратите внимание, что следует позаботиться о том, чтобы одна и та же буква
не использовалась более одного раза в качестве горячей клавиши во всплывающем
меню или в главном меню. При создании новых пунктов меню никакой проверки
войников” не выполняется, но если по завершении редактирования вы щелкнете
правой кнопкой мыши, установив курсор на панель меню, то получите всплывающее
меню, содержащее пункт Check Mnemonics (Проверить мнемонические сокращения).
Выбор его запустит проверку всего меню на предмет наличия дублированных горя-
чих клавиш. Неплохо бы выполнять такую проверку после каждого редактирования
меню, поскольку очень легко непреднамеренно наплодить двойников.
На этом завершается расширение меню добавления элементов и цветов. Не за-
будьте сохранить файл, убедившись, что внесенные дополнения надежно сохранены.
Далее потребуется решить, какие классы должны будут обрабатывать сообщения от
новых пунктов меню, и добавить в них соответствующие функции-члены для обра-
ботки каждого сообщения. Для этого используется мастер добавления обработчиков
событий (Event Handler Wizard).
Sketcher. rc (IDR...herT¥PE - Menu)* ▼ X
File Edit View Element Color Window Help
Black
Туре Неге
G гее п
Blue
ас
Puc. 13.6. Внешний вид завершенного меню
Работа с меню и панелями инструментов
691
Добавление обработчиков сообщений меню
Чтобы создать обработчик пункта меню, щелкните правой кнопкой мыши на нуж-
ном пункте и выберите из появившегося контекстного меню пункт Add Event Handler
(Добавить обработчик событий). Если вы сделаете это с пунктом меню Black из
всплывающего меню Color, то увидите диалоговое окно, показанное на рис. 13.7.
Как видите, мастер Event Handler Wizard уже выбрал имя для функции-обработчи-
ка. Вы можете изменить его, но, на мой взгляд, OnColorBlack кажется вполне под-
ходящим именем.
Очевидно, что вы должны специфицировать тип сообщения, выбрав один из воз-
можных, показанных в диалоговом окне. Список Message type: (Тип сообщений:) в
окне на рис. 13.7 содержит два вида сообщений, которые могут инициироваться кон-
кретным ID меню. Каждый тип сообщений служит своей цели при работе с пунктом
меню (табл. 13.2).
Рис. 13.7. Мастер добавления обработчиков событий
Таблица 13.2. Типы сообщений в мастере Event Handler Wizard
Тип сообщения
Описание
COMMAND
UPDATE_COMMAND_UI
Сообщения этого типа используются, когда выбран определенный пункт меню.
Обработчик должен выполнять действие, отвечающее выбранному пункту, на-
пример, установить текущий цвет для объекта документа или установить теку-
щий тип элемента.
Эти сообщения генерируются, когда меню должно быть обновлено — установ-
лена или снята отметка, например, в зависимости от его состояния. Это сооб-
щение возникает перед отображением всплывающего меню, так что вы можете
настроить внешний вид пунктов меню перед тем, как пользователь увидит их.
692 Глава 13
Это работает довольно просто. Когда вы щелкаете на отдельном пункте в панели
меню, посылается сообщение UPDATE_COMMAND_UI для каждого элемента меню до
того, как оно будет отображено. Это дает возможность выполнить любое необходи-
мое обновление свойств пунктов меню перед тем, как пользователь их увидит. Когда
эти сообщения обработаны, и любые изменения свойств элементов завершены, меню
отображается. Когда затем вы щелкаете на одном из пунктов меню, посылается со-
общение COMMAND для этого пункта. Сначала поговорим о сообщении COMMAND, а к
UPDATE_COMMAND_UI в этой главе вернемся чуть позже.
Поскольку события в пунктах меню приводят к выдаче командных сообщений, вы
можете выбрать в качестве места их обработки любой из классов, определенных в
данный момент в приложении Sketcher. Но как решить, где именно следует обраба-
тывать сообщение пункта меню?
Выбор класса для обработки сообщений меню
Прежде чем вы сможете решить, какой класс должен обрабатывать сообщения до-
бавленных вами пунктов меню, необходимо решить, что вы хотите делать с этими
сообщениями.
Нужно, чтобы тип элемента и выбранный цвет были модальными, то есть установ-
ленный текущий элемент и цвет должны сохраняться до тех пор, пока они не будет
изменены. Это позволит вам создать столько синих окружностей, сколько вы захоти-
те, а когда вам понадобятся красные, вы просто смените цвет. Существуют две основ-
ные возможности обработки установки цвета и выбора типа элемента: установка их
представлением или документом. Если устанавливать представлением, то поскольку
у одного документа может быть несколько представлений, у каждого представления
будет свой текущий цвет и элемент. Это значит, что вы сможете рисовать красные
окружности в одном представлении, переключиться на другое и обнаружить, что там
вы рисуете синие прямоугольники. Это приводит к путанице и вряд ли соответствует
тому, как вы хотите, чтобы они работали.
Поэтому лучше иметь текущий цвет и элемент на уровне документа. Тогда вы, пе-
реключаясь от одного представления на другое, продолжите рисовать те же элемен-
ты тем же цветом. Могут существовать другие отличия между представлениями вроде
масштаба отображения документа, но все же операция рисования будет согласована
между представлениями.
Это говорит о том, что лучше сохранять текущий цвет и элемент в объекте доку-
мента. Тогда они будут доступны объектам представлений, ассоциированным с объ-
ектом документа. Конечно, если у вас одновременно активно более одного докумен-
та, то каждый документ должен иметь свои собственные установки цвета и элемента.
Поэтому имеет смысл обрабатывать сообщения от ваших новых пунктов меню в клас-
се С Sketcher Do с и сохранять информацию о текущем выборе в объекте этого класса.
Я думаю, что вы готовы приступить к созданию обработчика для пункта меню Black.
Создание функций сообщений меню
Выберите имя класса CSketcherDoc в диалоговом окне мастера Event Handler
Wizard, щелкнув на нем. Также потребуется щелкнуть на типе сообщений COMMAND.
Затем вы можете щелкнуть на кнопке Add and Edit (Добавить и редактировать). При
этом диалоговое окно закроется, а код созданного обработчика в классе CSketcherDoc
будет отображен в окне редактора. Функция выглядит следующим образом:
Работа с меню и панелями инструментов 693
void CSketcherDoc::OnColorBlack()
// TODO: добавьте сюда код обработчика команды
}
Выделенная полужирным строка — это место, куда вы должны поместить код, ко-
торый будет обрабатывать сообщение, явившееся результатом выбора пункта меню
пользователем. Мастер также обновит определение класса CSketcherDoc:
class CSketcherDoc : public CDocument
protected: // создавать только из сериализации
CSketcherDoc();
DECLARE_DYNCREATE(CSketcherDoc)
// Атрибуты
public:
/ / Операции
public:
// Переопределения
public:
virtual BOOL OnNewDocument();
virtual void Serialize(CArchive& ar);
/ / Реализация
public:
virtual -CSketcherDoc();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& de) const;
#endif
protected:
// Сгенерированные функции отображения сообщений
protected:
DECLARE MESSAGE MAP()
public:
af x__msg void OnColorBlack ();
};
Метод OnColorBlack () добавлен в качестве общедоступного члена класса с пре-
фиксом afx msg, указывающим на то, что это — обработчик сообщений.
Теперь можете добавить обработчики сообщений COMMAND для других ID меню
Color, и то же самое сделать для идентификаторов меню Element. Вы можете соз-
дать функции обработчиков сообщений для пунктов меню всего за четыре щелчка
мыши. Щелкните правой кнопкой мыши на пункте меню, выберите в контекстном
меню пункт Add Event Handler (Добавить обработчик событий), щелкните на имени
класса CSketcherDoc в диалоговом окне мастера Event Handler Wizard и щелкните на
кнопке Add and Edit.
Мастер Event Handler Wizard добавит обработчики в определение класса
CSketcherDoc, которое после этого будет выглядеть так:
class CSketcherDoc: public CDocument
protected:
// Сгенерированные функции отображения сообщении
protected:
DECLARE MESSAGE MAP()
694 Глава 13
public:
afx__msg void OnColorBlack();
afx_msg void OnColorRedO ;
afx_msg void OnColorGreen();
afx_jnsg void OnColorBlue ();
afx msg void OnElementLine();
void OnEleroentRectangle();
void OnElementCircle ();
void OnElementCurve ();
afx_msg
afx msg
Как видите, для каждого из обработчиков, специфицированных вами в диалого-
вом окне мастера Event Handler Wizard, добавляется объявление. Каждое из объявле-
ний функций снабжено префиксом af х_msg, указывающим на то, что это — обработ-
чик сообщений.
Мастер Event Handler Wizard также автоматически обновляет карту сообщений
в реализации вашего класса СSketcherDoc, добавляя в нее все новые обработчики.
Если вы заглянете в SketcherDoc. срр, то увидите там следующую карту сообщений:
BEGIN_MESSAGE_MAP(CSketcherDoc, CDocument)
ON_COMMAND(ID__COLOR_BLACK, OnColorBlack)
ON_COMMAND(ID_COLOR_RED, OnColorRed)
ON_COMMAND(ID_COLOR_GREEN, OnColorGreen)
ON_COMMAND(ID_COLOR_BLUE, OnColorBlue)
ON_COMMAND(ID_ELEMENT_LINE, OnElementLine)
ON__COMMAND (ID_ELEMENT_RECTANGLE, OnElementRectangle)
ON_COMMAND (ID_ELEMENT_CIRCLE, OnElementCircle)
ON_COMMAND (ID_ELEMENT_CURVE, OnElementCurve)
END_MESSAGE_MAP()
Мастер Event Handler Wizard добавил макрос ON_COMMAND () для каждого обработчи-
ка, который вы идентифицировали. Это ассоциирует имя обработчика с идентифика-
тором сообщения, поэтому, например, функция-член OnColorBlack () вызывается для
обработки сообщения COMMAND для пункта меню с идентификатором I D_COLOR_BLACK.
Каждый из обработчиков, сгенерированных Event Handler Wizard, является, по
сути, скелетом. Например, взгляните на код OnColorBlue (). Он также находится в
файле SketcherDoc.срр, и вы найдете его там, прокрутив текст вниз, или переклю-
чившись на Class View и дважды щелкнув на имени функции после разворачивания
дерева для класса CSketcherDoc (только сначала убедитесь, что файл сохранен).
void CSketcherDoc::OnColorBlue()
// TODO: добавьте сюда код обработчика команды
Как видите, обработчик не принимает аргументов и ничего не возвращает. На дан-
ный момент он вообще ничего не делает, и в этом нет ничего удивительного, посколь-
ку Event Handler Wizard не знает, что вы собираетесь делать с этими сообщениями!
Кодирование функций сообщений меню
Теперь подумаем, что вы должны делать с сообщениями COMMAND для новых пун-
ктов меню. Я уже говорил, что если вы хотите записывать текущий элемент и цвет
в документе, то для того и другого потребуется добавить данные-члены в класс
CSketcherDoc.
Работа с меню и панелями инструментов
695
Добавление членов для хранения режимов цвета и элемента
Вы можете добавить необходимые данные-члены в определение класса CSketcherDoc,
просто непосредственно редактируя существующее определение, но давайте лучше
воспользуемся мастером добавления переменной-члена (Add Member Variable Wizard).
Отобразите диалоговое окно мастера, щелкнув правой кнопкой мыши на имени
класса CSketcherDoc в Class View и затем выбрав Add^Add Variable (Добавить1^
Добавить переменную) из появившегося контекстного меню. После этого вы увидите
диалоговое окно мастера, показанное на рис. 13.8.
Рис. 13.8. Мастер добавления переменной-члена
олжен быть доступен непосред-
Я уже вводил информацию в диалоговое окно для переменной m_Element, хра-
нящей текущий тип элемента, который должен быть нарисован. Я выбрал для него
модификатор доступа protected, потому что он не
ственно извне класса. Я также выбрал для него тип unsigned int, поскольку для
идентификации каждого типа элемента используются положительные целые числа.
Когда вы щелкаете на кнопке Finish (Готово), переменная добавляется в определение
класса в файле CSketcherDoc .h.
Добавим вручную член класса CSketcherDoc для хранения цвета элемента — про-
сто чтобы показать, что это возможно. Его именем будет m__Color, типом — COLORREF,
определенный в Windows API для представления цвета как 32-разрядного целого.
Вы можете добавить объявление члена m_Color в класс CSketcherDoc следующим
образом:
class CSketcherDoc : public CDocument
// Сгенерированные функции отображения сообщении
protected:
DECLARE MESSAGE МАР()
696 Глава 13
public:
afx_msg void OnColorBlack();
afx__msg void OnColorRedO ;
afx__msg void OnColorGreen ();
afx_msg void OnColorBlue();
afx_msg void OnElementLine ();
afx_msg void OnElementRectangle();
afx_msg void OnElementCircle();
afx_msg void OnElementCurve ();
protected:
// Текущий тип элемента
unsigned int m_Element;
COLORREF m__Color; // Текущий цвет рисования
Член m Color также является protected, поскольку нет смысла открывать к нему
доступ извне. Вы всегда можете добавить функции для обращения или изменения зна-
чений защищенных или приватных членов класса, пользуясь тем преимуществом, что
можно сохранить полный контроль над тем, какие значения могут быть установлены.
Инициализация новых данных-членов класса
Теперь вы должны решить, как представлять тип элемента. Можно просто уста-
новить в m—Element уникальное числовое значение, но это будет означать введение
в программу “магического числа”, значение которого будет менее очевидно любому,
кто станет читать код. Лучше определить набор констант, которые можно будет ис-
пользовать для установки значений переменной-члена Element. Таким образом, вы
можете применять стандартные мнемонические имена для ссылки на данный тип эле-
мента. Типы элементов можно определить с помощью следующих операторов:
// Определения типов элементов.
// Каждое значение типа должно быть уникальным
const unsigned int LINE = 101U;
const unsigned int RECTANGLE = 102U;
const unsigned int CIRCLE = 103U;
const unsigned int CURVE = 104U;
Константы, инициализирующие типы элементов — произвольные беззнаковые це-
лые числа. При желании вы можете выбрать другие значения, до тех пор, пока они
будут отличаться. Если вы захотите в будущем добавить дополнительные типы — это
сделать будет очень легко.
Для значений цвета будет неплохой идеей применить константные переменные,
инициализированные значениями, используемые Windows для определения цвета.
Вы можете сделать это с помощью приведенных ниже строк кода.
// Значения цвета для рисования
const COLORREF BLACK = RGB(0,0,0);
const COLORREF RED = RGB(255,0, 0) ;
const COLORREF GREEN = RGB(0,255,0);
const COLORREF BLUE = RGB(0,0,255) ;
Каждая константа инициализируется RGB () — стандартным макросом, опреде-
ленным в Wingdi .h, который представляет собой заголовочный файл, включенный
в Windows. h. Три его аргумента определяют соответственно красную, зеленую и си-
нюю составляющие цвета. Каждый аргумент должен быть целым числом в диапазоне
от 0 до 255, при этом границы представляют полное отсутствие цветового компонен-
та и его максимальное значение. RGB (0,0,0) соответствует черному цвету, поскольку
Работа с меню и панелями инструментов 697
в нем нет ни красной, ни зеленой, ни синей составляющих. RGB (255,0,0) создает
значение цвета с максимальным значением красного компонента при полном отсут-
ствии зеленого и синего. Все прочие цвета вы можете создать путем комбинирования
красной, зеленой и синей компонент.
Эти константы нужно куда-то поместить, поэтому давайте создадим новый заго-
ловочный файл и назовем его OurConstants .h. Создать новый файл можно, щел-
кнув правой кнопкой мыши на папке Header Files (Заголовочные файлы) на вклад-
ке Solution Explorer и выбрав в контекстном меню Add^Add New Item (Добавить1^
Добавить новый элемент). Введите в диалоговом окне имя заголовочного файла
OurConstants, затем щелкните на кнопке Open (Открыть). После этого вы сможете
ввести определения констант в окне редактора, как показано ниже.
// Определения констант
#pragma once
// Определения типов элементов.
// Каждое значение типа должно быть уникальным
const unsigned int LINE = 101U;
const unsigned int RECTANGLE = 102U;
const unsigned int CIRCLE = 103U;
const unsigned int CURVE = 104U;
///////////////////////////////////
// Значения цвета для рисования
const COLORREF BLACK = RGB(0,0,0);
const COLORREF RED = RGB(255,0,0);
const COLORREF GREEN = RGB(0,255,0);
const COLORREF BLUE = RGB(0,0,255);
///////////////////////////////////
Как вы помните, директива препроцессора #pragma once нужна для того, чтобы
гарантировать, что определения не будут включены в исходный файл . срр более
одного раза. Операторы заголовочного файла включаются в исходный файл дирек-
тивой #include только в том случае, если они не были включены ранее. После того,
как заголовок включен в файл, он уже не будет включен снова.
После сохранения заголовочного файла вы можете добавить оператор #include в
начало файла Sketcher.h:
#include "OurConstants.h"
Любому файл . срр, содержащему директиву #include для Sketcher. h, будут до-
ступны константы.
Вы можете проверить, что новые константы стали частью проекта, развернув
ветку Global Functions and Variables (Глобальные функции и переменные) на вкладке
Class View. Если все в порядке, вы увидите там имена цветов и типов элементов на-
ряду с глобальной переменной theApp.
Модификация конструктора класса
Важно убедиться, что данные-члены, которые вы добавили в класс CSketcherDoc,
правильно инициализируются при создании документа. Вы можете добавить код ини-
циализации к конструктору класса, как показано ниже:
CSketcherDoc::CSketcherDoc()
m_El emen t (LINE), mjColor(BLACK)
// TODO: добавьте сюда код конструирования объекта
698 Глава 13
Мастер уже решил, что член m_Element необходимо инициализировать нулем, по-
этому исправьте начальное значение на LINE. Затем нужно будет добавить инициали-
затор для члена m_Color со значением BLACK, чтобы все соответствовало определен-
ным вами меткам в пунктах меню.
Теперь вы готовы к добавлению кода в функции-обработчики для пунктов меню
Element и Color. Это можно сделать на вкладке Class View. Щелкните на имени пер-
вой функции-обработчика
строку к этой функции, так что код станет выглядеть так:
OnColorBlack (). Вам понадобится добавить всего одну
void CSketcherDoc::OnColorBlack()
m Color = BLACK; // Установить черный цвет рисования
}
Единственное, что должен сделать обработчик — установить соответствую-
щий цвет. В интересах согласованности, новая строка кода заменяет комментарий
(/ / TODO:), который был представлен первоначально. Затем вы можете аналогичным
образом добавить новые строки во все обработчики пунктов меню.
Обработчики пунктов меню элементов очень похожи. Так, обработчик для пункта
Elements Line (Элемент^Линия) принимает следующий вид.
void CSketcherDoc::OnElementLine()
m Element
LINE; // Установить тип элемента - линию
Таким образом, у вас будут готовы восемь обработчиков сообщений. Теперь можно за-
ново собрать пример и посмотреть, как он работает.
Запуск расширенного примера
Если только не было допущено опечаток, компиляция и компоновка программы
;олжна пройти без ошибок. Когда вы запустите программу, то увидите окно, показан-
ное на рис. 13.9.
Рис. 13.9. Работа программы Sketcher
Работа с меню и панелями инструментов 699
Как видите, новые меню расположены в правильных местах панели меню, к ним
добавлены все необходимые пункты, и вы можете видеть сообщения Prompt в панели
состояния в нижней части главного окна, когда курсор мыши находится на пункте
меню. Вы также можете проверить, что комбинации клавиш <Alt+C> и <Alt+l> рабо-
тают корректно. Что не работает, как надо — так это отметки текущего выбранного
цвета и элемента, которые остаются на своих начальных позициях. Посмотрим, как
это можно исправить.
UPDATE
Добавление обработчиков сообщений для
обновления пользовательского интерфейса
Чтобы корректно устанавливать отметки новых пунктов меню, к каждому тако-
му пункту вам придется добавить обработчик сообщений другого вида
COMMAND_UI (означает команду обновления пользовательского интерфейса).
Обработчики сообщений этого типа специально предназначены для обновления
свойств пунктов меню непосредственно перед их отображением.
Вернемся к просмотру файла Sketcher. г с в окне редактора. Щелкните правой
кнопкой мыши на элементе Black в меню Color и выберите из контекстного меню
пункт Add Event Handler (Добавить обработчик событий). Затем можете выбрать
UPDATE_COMMAND_UI в качестве типа сообщения и класс CSketcherDoc (рис. 13.10).
Aw. 13.10, Добавления обработчика сообщений UPDATE COMMAND UI
Имя функции обновления будет сгенерировано автоматически: OnUpdateColorBlack ().
Поскольку это кажется подходящим именем функции, щелкайте на кнопке Add and
Edit, позволяя мастеру Event Handler Wizard сгенерировать ее. Наряду с генерацией
определения скелета функции в SketcherDoc. срр, ее объявление будет добавлено к
определению класса. Также для нее будет предусмотрена строка в карте сообщений:
ON_UPDATE_COMMAND_UI (ID_COLOR_BLACK, OnUpdateColorBlack)
700 Глава 13
Здесь используется макрос ON__UPDATE_COMMAND_UI (), идентифицирующий толь-
ко что сгенерированную функцию как обработчик для сообщений обновления интер-
фейса, ассоциированный с указанным ID. Теперь вы можете ввести код нового обра-
ботчика, но сначала добавим обработчики обновления для всех пунктов меню Color
и Element.
Кодирование обработчика команды обновления
Вы можете обратиться к коду обработчика OnUpdateColorBlack() в классе
CsketcherDoc, выбрав функцию на вкладке Class View. Ниже показан скелетный код
функции.
void CsketcherDoc::OnUpdateColorBlack(CCmdUI* pCmdUI)
// TODO: добавьте сюда код обработчика команд обновления
// пользовательского интерфейса
Аргумент, переданный обработчику — это указатель на объект типа класса CCmdUI.
Он представляет собой класс MFC, единственное назначение которого — использо-
ваться в обработчиках обновлений, но наряду с пунктами меню он применяется к
кнопкам панели инструментов. Указатель указывает на объект, идентифицирующий
элемент, сгенерировавший сообщение обновления, так что вы используете его для об-
новления его внешнего вида перед тем, как он будет отображен. Класс CCmdUI вклю-
чает пять функций-членов, работающих с элементами пользовательского интерфейса.
Операции, которые представляет каждый из них, перечислены в табл. 13.3.
Таблица 13.3. Операции класса CCmdUI
Метод Описание
ContinueRouting () Передает сообщение обработчику со следующим приоритетом.
Enable () Включает или выключает соответствующий интерфейсный элемент.
Setcheck () Устанавливает отметку флажка для соответствующего интерфейсного элемента.
SetRadio () Включает или отключает кнопку в группе переключателей.
SetText () Устанавливает текст соответствующего интерфейсного элемента.
Мы используем третью функцию — SetCheck (), потому что, похоже, она делает
то, что нам нужно. Эта функция объявлена в классе CCmdUI следующим образом:
virtual void SetCheck(int nCheck =1);
Функция установит отметку на пункте меню, если вы передадите 1 в качестве аргу-
мента, и снимет эту отметку, если в аргументе будет передан 0. Параметр имеет значе-
ние по умолчанию 1, так что если нужно просто установить отметку на пункт меню,
ее можно вызывать без аргумента.
В нашем случае необходимо установить метку на пункте меню, если он соответ-
ствует текущему выбранному цвету. Поэтому вы можете написать обработчик обнов-
ления OnUpdateColorBlack () следующим образом:
void CsketcherDoc::OnUpdateColorBlack (CCmdUI* pCmdUI)
// Пометить пункт меню, если выбран черный цвет
pCmdUI->SetCheck(m Color—BLACK);
Работа с меню и панелями инструментов 701
Добавленный оператор вызывает функцию SetCheck () для пункта меню Colors
Black (Цвет1^ Черный), а аргумент — выражение m_Color==BLACK дает в результате 1,
когда m Color равно BLACK, и 0 — в противоположном случае. Таким образом, эффект
заключается в том, что пункт меню помечается, только если текущий цвет, хранимый
в m Color, равен BLACK, а это как раз то, что вам нужно.
Обработчики обновлений для всех пунктов меню всегда вызываются перед ото-
бражением меню, поэтому вы можете кодировать все остальные обработчики анало-
гичным образом, гарантируя, что помеченным будет только пункт, соответствующий
выбранному цвету.
void CSketcherDoc::OnUpdateColorBlue(CCmdUI* pCmdUI)
// Пометить пункт меню, если выбран синий цвет
pCmdUI->SetCheck (m_Color=BLUE) ;
}
void CSketcherDoc::OnUpdateColоrGreen(CCmdUI* pCmdUI)
{
// Пометить пункт меню, если выбран зеленый цвет
pCmdUI->SetCheck (m_Color=GREEN) ;
}
void CSketcherDoc::OnUpdateColorRed(CCmdUI* pCmdUI)
{
// Пометить пункт меню, если выбран красный цвет
pCmdUI->SetCheck (m_Color=RED) ;
}
Типичный обработчик обновления для пункта меню Element кодируется так:
void CSketcherDoc::OnUpdateElementLine(CCmdUI* pCmdUI)
// Пометить пункт меню, если текущий элемент
pCmdUI->SetCheck (m Element=LINE) ;
Все остальные обработчики обновлений кодируются в той же манере:
void CSketcherDoc::OnUpdateElementCurve(CCmdUI* pCmdUI)
// Пометить пункт меню, если текущий элемент
pCmdUI->SetCheck(m Elemen t==CURVE) ;
void CSketcherDoc::OnUpdateElementCircle(CCmdUI *pCmdUI)
// Пометить пункт меню, если текущий элемент — окружность
pCmdUI->SetCheck(m Element=CIRCLE) ;
void CSketcherDoc::OnUpdateElementRectangle(CCmdUI* pCmdUI)
// Пометить пункт меню, если текущий элемент — прямоугольник
pCmdUI->SetCheck(m_Element—RECTANGLE);
Если вы ухватили идею, все довольно просто, не правда ли?
Испытание обработчиков обновления
Добавив код для всех обработчиков обновления, вы снова можете собрать и запу-
стить приложение Sketcher. Теперь, когда вы изменяете цвет или тип элемента, это
отражается и в меню, как показано на рис. 13.11.
702 Глава 13
Sketcher - Sketcherl П
File Edit View Element Color Window Help
Puc. 13.11. Работа расширенной программы Sketcher
На этом весь код, необходимый для пунктов меню, готов. Не забудьте все сохра-
нить, прежде чем переходить к следующему этапу. В наше время панели инструмен-
тов — обязательный компонент любой Windows-программы, так что следующим ша-
гом, который мы предпримем, будет добавление кнопок панели инструментов для
поддержки наших меню.
Добавление кнопок панели инструментов
Выберите вкладку Resource View (Представление ресурсов) и откройте ресурс —
панель инструментов. Вы увидите, что там содержится тот же ID, что и в главном
меню
I DR MAINFRAME. Если выполнить двойной щелчок на этом ID, откроется окно
редактора, показанное на рис. 13.12.
Рис. 13.12. Редактирование пиктограммы
для кнопки панели инструментов
Кнопка панели инструментов — это массив точек 16x15, содержащий графическое
представление функции, которую она вызывает. На рис. 13.12 видно, что редактор
ресурсов предлагает увеличенное представление кнопки панели инструментов, так
Работа с меню и панелями инструментов
703
что вы можете видеть и манипулировать отдельными пикселями. Если вы щелкнете
на новой кнопке в правом конце представленного ряда, то сможете нарисовать эту
кнопку. Прежде чем начать редактирование, перетащите новую кнопку примерно на
половину ее ширины вправо. Это отделит ее от соседней слева, и тем самым будет на-
чат новый блок кнопок.
Вы должны сохранять последовательность блоков кнопок в том же порядке, что
и панель меню, так что сначала создавайте кнопки для выбора типа элемента. Ниже
перечислены кнопки редактирования из редактора ресурсов, который появляется в
окне приложения Visual C++ 2005, которыми вы будете пользоваться.
□ Карандаш для рисования индивидуальных точек.
□ Резинка для стирания индивидуальных точек.
□ Заполнитель областей текущим цветом.
□ Увеличитель представления кнопки.
□ Рисование прямоугольника.
□ Рисование эллипса.
□ Рисование кривой.
Если оно не показано, вы можете отобразить окно для выбора цвета, щелкнув пра-
вой кнопкой мыши на кнопке панели инструментов и выбрав Show Colors Window
(Показать окно выбора цвета) из контекстного меню. Убедитесь, что выбран черный
цвет и используете инструмент “карандаш” для того, чтобы нарисовать диагональную
линию в увеличенном образе новой кнопки. Фактически, если вам нужно ее увели-
чить еще больше, можно воспользоваться кнопкой редактирования Magnification Tool
(Инструмент увеличения), чтобы увеличить ее в 8 раз относительно реального раз-
мера. Если в процессе рисования допущена ошибка, вы можете выбрать кнопку ре-
дактирования Erase Tool (Инструмент очистки), но при этом нужно убедиться, что
выбранный для нее цвет соответствует цвету фона редактируемой кнопки. Вы так-
же можете стирать индивидуальные точки, щелкая на них правой кнопкой мыши, но
опять-таки при этом следует убедиться, что установлен правильный цвет фона. Для
установки цвета фона просто щелкните на соответствующем цвете правой кнопкой
мыши. После того, как вы будете удовлетворены тем, что нарисовали, следующим ша-
гом будет редактирование свойств кнопки панели инструментов.
Редактирование свойств кнопки панели инструментов
Дважды щелкните на новой кнопке панели инструментов, чтобы вызвать окно
свойств, показанное на рис. 13.13.
Окно свойств показывает ID кнопки по умолчанию, но вы хотите ассоциировать
кнопку с пунктом меню Elements Line (Элемент1^ Линия), который был определен ра-
нее, поэтому щелкните на ID и затем на стрелке вниз, чтобы отобразить альтернатив-
ные значения. Затем можете выбрать в выпадающем списке ID_ELEMENT_LINE. Если
щелкнуть на Prompt, можно обнаружить, что в панели состояния будет отображена
та же строка, что и для пункта меню, поскольку она ассоциирована с ID. Для заверше-
ния определения кнопки закройте окно свойств.
Теперь можете двигаться дальше и спроектировать остальные три кнопки эле-
ментов. С помощью кнопки редактирования с прямоугольником можно нарисовать
прямоугольник, а посредством кнопки с кругом — эллипс. Кривую можно нарисовать
карандашом, составив ее из отдельных точек. Затем каждую кнопку потребуется ассо-
циировать с соответствующим ID пункта меню, который был определен ранее.
704 Глава 13
ZW. 13.13. Окно свойств кнопки панели инструментов
Теперь добавьте кнопки выбора цвета. Первую кнопку выбора цвета также нуж-
но будет сдвинуть вправо, чтобы начать новую группу кнопок. Кнопки цветов мож-
но сделать очень простыми, и просто покрасить каждую из них в цвет, который она
выбирает. Это можно сделать, выбрав соответствующий цвет переднего плана и за-
тем редактирующую кнопку для заливки сплошным цветом. Опять-таки вы должны
использовать для новых кнопок уже готовые идентификаторы ID_COLOR_BLACK,
ID_COLOR_RED и так далее. Окно редактирования панели инструментов должно вы-
глядеть подобно тому, как показано на рис. 13.14.
Sketcher.rc (IDR...NFRAME - Toolbar)
Рис. 13.14. Создание пиктограммы для кнопки установки цвета
Вот и все, что необходимо сделать на данный момент, так что сохраните ресурс-
ный файл и выполните сборку новой версии программы Sketcher.
Испытание кнопок панели инструментов
Итак, еще раз соберем приложение и запустим его. Вы должны увидеть окно при-
ложения, показанное на рис. 13.15.
Работа с меню и панелями инструментов
705
Рис. 13.15. Работа программы Sketcher после добавления
кнопок панели инструментов
И тут происходит нечто удивительное. Добавленные вами кнопки панели инстру-
ментов уже отображают установки по умолчанию, которые вы определили для пун-
ктов меню. Если вы наведете курсор на одну из новых кнопок, то в панели состоя-
ния появится соответствующая подсказка. Новые кнопки работают как полноценная
замена пунктов меню, и каждый новый выбор, выполненный через меню или через
панель инструментов, отображается в виде нажатой кнопки и пометки соответствую-
щего пункта меню.
Если вы закроете окно представления документа Sketcherl, то увидите, что ваши
кнопки панели инструментов автоматически станут серыми и недоступными. При
открытии нового окна документа они опять становятся активными и доступными.
Вы можете также попробовать перетащить панель инструментов курсором мыши.
Ее можно пристыковать к любому краю главного окна приложения либо оставить в
“свободном плавании”. Можно также включать и выключать ее, выбирая опцию меню
View«=>Toolbar (Вид1^ Панель инструментов). И все это вы получаете, не написав ни
одной строчки кода!
Добавление всплывающих подсказок
Есть еще одна деталь, которую вы можете добавить к вашим кнопкам панели ин-
струментов замечательно просто: всплывающие подсказки (tooltips). Всплывающая
подсказка — это маленькая рамка, которая появляется рядом с кнопкой панели ин-
струментов, когда вы задерживаете курсор на ее поверхности. Эта рамка содержит
текстовую строку, дающую дополнительное пояснение назначения данной кнопки.
Чтобы добавить всплывающие подсказки, выберите вкладку Resource View и после
раскрытия списка ресурсов щелкните на папке String Table (Таблица строк) и дважды
щелкните на ресурсе. Он содержит идентификаторы и строки подсказок, ассоцииро-
ванные с пунктами меню, которые вы добавили ранее вместе с этими подсказками
под каждым заголовком. Чтобы добавить всплывающую подсказку, вы просто добав-
ляете в конец текста заголовка символ новой строки (\п), за которым следует текст
всплывающей подсказки. Что касается текста подсказки для строки состояния, кото-
706 Глава 13
рую вы ввели ранее, то можете выполнить двойной щелчок, чтобы войти в режим
редактирования, и добавить \п в конец текста подсказки в колонке заголовка, тем са-
мым изменив существующий заголовок для идентификатора ID_ELEMENT_LINE с Line
на ЫпеХпУстанавливает режим рисования линий. Таким образом, текст заголовка
будет иметь две части, разделенные символом \п, где первая часть будет подсказкой
для строки состояния, а вторая — текстом всплывающей подсказки.
Добавьте \п, за которым следует текст всплывающей подсказки, к тексту заголовка
для каждого ID пунктов меню Element и Color. Не забывайте начинать текст каж-
дой всплывающей подсказки с \п. Это все, что потребуется сделать. После сохране-
ния ресурса String Table вы можете выполнить сборку приложения и запустить его.
Помещение курсора над одной из новых кнопок через секунду или две вызовет появ-
ление всплывающей подсказки.
Резюме
В этой главе вы узнали, как MFC соединяет сообщение с функцией-членом класса
для его обработки, и написали свои первые обработчики сообщений. Большая часть
работы при создании Windows-программ заключается в разработке обработчиков со-
общений, поэтому важно иметь четкое представление о том, как происходит этот
процесс. Когда мы приступим к рассмотрению других обработчиков сообщений, вы
увидите, что процесс их добавления почти такой же.
Вы расширили стандартное меню и панель инструментов, используя программу,
сгенерированную мастером MFC Application Wizard, которая является хорошей осно-
вой для кода приложения, который мы добавим в следующей главе. Хотя в программе
еще и нет никакой скрытой функциональности, операции меню и панели инструмен-
тов выглядят очень профессионально — благодаря сгенерированному мастером карка-
су и мастеру Event Handler Wizard.
Ниже перечислены важнейшие моменты, с которыми вы познакомились в этой
главе.
□ В MFC определены обработчики сообщений для класса в карте сообщений, ко-
торая появляется в файле . срр с определением класса.
□ Командные сообщения, поступающие от меню и панелей инструментов, могут
быть обработаны в любом классе, унаследованном от CCmdTarget. К ним от-
носятся класс приложения, классы обрамляющего окна — главного и дочерних,
класс документа и класс представления.
□ Сообщения, отличные от командных, могут быть обработаны только в классах,
производных от CWnd. К ним относятся обрамляющие окна и классы представ-
лений, но не классы приложений и документов.
□ Библиотека MFC соблюдает предопределенную последовательность поиска клас-
сов в вашей программе, чтобы найти обработчик для командного сообщения.
□ Вам всегда следует использовать мастер Event Handler Wizard для добавления в
программу обработчиков событий.
□ Физическое представление меню и панелей инструментов определяется в ре-
сурсных файлах, которые редактируются с помощью встроенного редактора
ресурсов.
Работа с меню и панелями инструментов 707
Пункты меню, которые могут порождать командные сообщения, идентифици-
руются символическими константами с префиксом ID. Эти идентификаторы
используются для установления ассоциации обработчика с сообщением от пун-
кта меню.
□ Чтобы ассоциировать кнопку панели инструментов с определенным пунктом
меню, вы присваиваете ей тот же самый ID, что и для пункта меню.
□ Чтобы добавить всплывающую подсказку для кнопки панели инструментов, соот-
ветствующую элементу меню, вы добавляете текст всплывающей подсказки к эле-
менту с ID данного пункта меню в колонку заголовка ресурса String Table. Текст
всплывающей подсказки отделяется от текста подсказки меню символом \п.
В следующей главе вы добавите код, необходимый для рисования элементов в
представлении, и воспользуетесь созданными в этой главе меню и кнопками панели
инструментов для выбора того, что нужно рисовать и какого цвета. Именно там про-
грамма Sketcher начнет соответствовать своему названию.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Добавьте пункт Ellipse (Эллипс) к всплывающему меню Element (Элемент).
2. Реализуйте обработчики сообщений команды и обновления этого пункта в клас-
се документа.
3. Добавьте кнопку панели инструментов, соответствующую пункту меню Ellipse, и
затем добавьте всплывающую подсказку для этой кнопки.
4. Модифицируйте обработчик команды обновления для пунктов меню цвета, что-
бы текущий выбранный пункт меню отображался заглавными буквами, а осталь-
ные — прописными.
14
Рисование в окне
В этой главе мы добавим немного “мяса” к приложению Sketcher. Мы сосредото-
чим внимание на получении графического вывода, отображаемого в окне приложе-
ния. До конца этой главы вы научитесь рисовать почти все элементы, для которых
добавили пункты меню в прошлой главе. Ниже перечислены основные вопросы, рас-
сматриваемые в настоящей главе.
□ Какие координатные системы представляет Windows для рисования в окне.
□ Контекст устройства и для чего он необходим.
□ Как и когда ваша программа рисует в окне.
□ Как определить обработчики для сообщений мыши.
□ Как определять ваши собственные классы фигур.
□ Как программировать рисование фигур в окне с помощью мыши.
□ Как заставить программу захватить мышь.
Основы рисования в окне
Прежде чем мы приступим к рисованию с применением MFC, будет полезно по-
лучить представление о том, что происходит за кулисами операционной системы
Windows, когда вы рисуете в окне. Подобно любой другой операции под Windows, за-
пись в окно на экране вашего дисплея обеспечивается функциями Windows API. Хотя,
в принципе, это не очень точно отражает ситуацию; способ работы Windows некото-
рым образом ее усложняет.
Для начала: вы не можете просто что-то записать в окно и забыть об этом. Суще-
ствует множество событий, которые требуют, чтобы ваше приложение перерисовы-
вало окно, такие как изменение размера окна, в котором вы рисуете, или когда часть
вашего окна, которая была ранее скрыта, открывается пользователем при перемеще-
нии другого окна.
710 Глава 14
К счастью, вам не нужно беспокоиться обо всех деталях подобных случаев, по-
скольку на самом деле всеми этими сообщениями управляет Windows; однако, это
значит, что вы можете только писать постоянные данные в окно, когда приложение
получает специфическое сообщение Windows, которое требует этого действия. Это
также означает, что вы должны быть готовы в любой момент заново реконструиро-
вать все, что ранее нарисовали в окне.
Когда все окно или его часть должна быть перерисована, Windows посылает ва-
шему приложению сообщение WM_PAINT. Оно перехватывается библиотекой MFC,
которая посылает сообщение функции-члену одного из ваших классов. Чуть позже в
настоящей главе я объясню, как вы можете реализовать обработку.
Клиентская область окна
Окно не имеет фиксированного положения на экране и даже не имеет фиксиро-
ванной видимой области, потому что может быть передвинуто с помощью мыши, и
его размер может быть изменен перетаскиванием его границ. Как же вы узнаете, где
именно на экране следует рисовать?
К счастью, вам этого знать и не нужно. Поскольку Windows предоставляет согла-
сованный способ рисования в окне, вам не приходится беспокоиться о точном ме-
сте на экране; если бы это было не так, рисование в окне чересчур бы усложнилось.
Windows обеспечивает это, поддерживая координатную систему для клиентской обла-
сти окна, локальную по отношению к окну. Она всегда использует левый верхний угол
клиентской области в качестве точки отсчета. Все точки внутри клиентской области
определены относительно этой начальной точки, как показано на рис. 14.1.
Горизонтальное и вертикальное расстояние точки от левого верхнего угла клиент-
ской области всегда одно и то же, независимо от местоположения окна на экране и
его текущего размера. Конечно, Windows необходимо отслеживать местоположение
окна, и когда вы рисуете что-то в точке клиентской области, она должна определять,
где именно эта точка находится на экране.
Это точка отсчета для клиентской области окна
Местоположение этой точки определяется расстояние х и у
Sketcherl
h Sketcher - Sketcher
zile Edit View Element Color Window Help
Ready
Рис. 14,1. Клиентская область окна
Рисование в окне 711
Интерфейс графических устройств Windows
Окончательное ограничение, накладываемое Windows, состоит в том, что вы в
действительности не можете выводить данные на экран непосредственно. Весь вы-
вод на экран вашего дисплея является графическим, независимо от того, выводятся
линии, окружности или же текст. Windows требует, чтобы вы определяли весь вывод,
используя интерфейс графических устройств (Graphical Device Interface — GDI).
GDI позволяет программировать графический вывод независимо от оборудования,
на котором он отображается, а это означает, что ваша программа может работать на
разных машинах с разными дисплейными устройствами. В дополнение к экранам дис-
плеев Windows интерфейс GDI также поддерживает принтеры и плоттеры, так что
вывод данных на принтер или плоттер включает, по сути, те же механизмы, что и
отображение информации на экране.
Что такое контекст устройства?
Когда вы хотите нарисовать что-то на устройстве графического вывода, подоб-
ном экрану дисплея, вы должны использовать контекст устройства (device context).
Контекст устройства — это структура данных, определенная Windows и содержащая
информацию, позволяющую Windows транслировать ваши запросы на вывод, кото-
рые поступают в форме независимых от устройства вызовов функций GDI, в дей-
ствия физического вывода на конкретное устройство. Указатель на контекст устрой-
ства можно получить с помощью вызова функции Windows API.
Контекст устройства предоставляет вам выбор координатных систем, называемых
режимами отображения (mapping modes), которые автоматически преобразуются в
клиентские координаты. Посредством функций GDI вы также можете изменить мно-
гие параметры, которые влияют на вывод в контекст устройства; такие параметры
называются атрибутами. Примерами атрибутов, которые вы можете менять, являет-
ся цвет рисования, цвет фона, толщина линии, используемой при рисовании, а так-
же шрифт, применяемый при выводе текста. Доступны также функции GDI, которые
предоставляют информацию о физическом устройстве, с которым вы имеете дело.
Например, может возникнуть потребность убедиться, что дисплей компьютера, вы-
полняющего вашу программу, поддерживает 256 цветов, или что принтер поддержи-
вает вывод битовых изображений.
Режимы отображения
Каждый режим отображения в контексте устройства идентифицируется иден-
тификатором (ID), подобно тому, как это делается с сообщениями Windows. Каждый
символ имеет префикс ММ_, указывающий на то, что речь идет о режиме отображе-
ния (mapping mode). Windows поддерживает режимы отображения, перечисленные
в табл. 14.1.
Вам не придется использовать все эти режимы, работая с примерами этой книги;
однако те, что вы будете применять, формируют хорошее пересечение с перечислен-
ными, так что у вас не должно быть проблем в применении других режимов, когда
это понадобится.
ММ—ТЕХТ — режим отображения по умолчанию для контекста устройства. Если вам
нужно использовать другой режим отображения, вы должны предпринять шаги по
его замене. Обратите внимание, что направление положительной оси у в режиме
ММ_ТЕХТ противоположно тому, к которому вы привыкли в школьном курсе геоме-
трии, что показано на рис. 14.2.
712 Глава 14
Таблица 14.1. Режимы отображения, поддерживаемые Windows
Режим отображения Описание
MMJTEXT
Логической единицей является один пиксель устройства с положительными х
слева направо и положительными у от вершины до дна клиентской области.
MM_LOENGLISH
Логической единицей является 0,01 дюйма с положительными х слёва направо
и положительными у, отсчитываемым от вершины клиентской области вверх.
MM_HIENGLISH
Логической единицей является 0,001 дюйма с направлениями х и у как
В MMJLOENGLISH.
ММ LOMETRIC
Логической единицей является 0,1 миллиметра с направлениями х и у как
В MM_LOENGLISH.
MM_HIMETRIC
Логической единицей является 0,01 миллиметра с направлениями х и у как
В MM_LOENGLISH.
MM_ISOTROPIC
Логическая единица произвольной длины, но одинаковая для обеих осей — х и у.
Направления х и у — как в mm loenglish.
MM_ANISOTROPIC
Режим подобен mm isotropic, но допускает разные длины логических единиц
по осям х и у.
MM-TWIPS
Логической единицей является twip, причем каждый twip составляет 0,05
точки, а точка — 772 дюйма. Таким образом, twip соответствует 71440 дюйма,
то есть 6,9x10"4 дюйма. (Точка — единица измерения для шрифтов.)
Направления х и у — как в mm loenglish.
Рис. 14.2. Режим отображения ММ TEXT
По умолчанию точка в верхнем левом углу клиентской области имеет координаты
(0, 0) во всех режимах отображения, хотя и возможно переместить начало координат
из верхнего левого угла клиентской области, если вы этого хотите. Например, неко-
торые приложения, представляющие данные в графическом виде, перемещают нача-
ло координат в центр клиентской области, чтобы упростить рисование графиков по
числовым данным. При начале координат в левом верхнем углу в режиме ММ__ТЕХТ
точка, отстоящая на 50 пикселей от левой границы и на 100 пикселей вниз от верши-
ны клиентской области, будет иметь координаты (50, 100). Конечно, поскольку еди-
ницами измерения являются пиксели, эта точка окажется ближе к верхнему левому
углу клиентской области, если ваш монитор имеет разрешение 1280x1024, чем при
разрешении 1024x768. Обратите внимание, что установка DPI (dot per inch — точек
на дюйм) вашего дисплея влияет на представление во всех режима^ отображения.
713
в этом случае они ограничены 16-ю
Установка по умолчанию предполагает 96 DPI, поэтому если DPI для вашего дисплея
установлено в другое значение, это повлияет на внешний вид. Координаты всегда
измеряются 32-битными целыми числами, если только вы не программируете для
старых операционных систем Windows95/98
битами. Максимальный физический размер всего рисунка варьируется с физической
длиной единицы координат, что определяется режимом отображения.
Направления координатных осей х и у в режиме MM_LOENGLISH и всех остальных
режимах отображения одинаковы, но отличаются от ММ_ТЕХТ. Координатные оси для
MM_LOENGLISH показаны на рис. 14.3. Хотя положительные значения у согласуются с
тем, что вы учили в школе (значение у увеличивается по мере движения по экрану
снизу вверх), MM—LOENGLISH — все же несколько необычный режим, поскольку точка
начала координат находится в левом верхнем углу клиентской области, так что для
точек внутри видимой клиентской области у всегда отрицательно.
В режиме отображения MM_LOENGLISH единицы измерения вдоль осей составляют
0,01 дюйма, так что точка с координатами (50, —100) находится на полдюйма от левой
границы и на один дюйм ниже вершины клиентской области. Объект имеет всегда
один и тот же размер на экране, независимо от разрешения монитора, на котором
он отображается. Если вы рисуете что-либо в режиме MM_LOENGLISH с отрицатель-
ной координатой х или положительной у, это оказывается за пределами клиентской
области и потому невидимо, поскольку точка отсчета (0, 0) по умолчанию располо-
жена в левом верхнем углу. Однако точку начала координат можно сдвинуть, вы-
звав функцию Windows API SetViewportOrg () (или функцию-член класса MFC CDC
SetViewportOrg (), о которой мы поговорим ниже).
Механизм рисования в Visual C++
Библиотека MFC инкапсулирует интерфейс Windows для вашего экрана и принте-
ра, избавляя вас от необходимости беспокоиться о большей части деталей програм-
мирования графического вывода. Как было показано в предыдущей главе, программа,
сгенерированная мастером Application Wizard, уже включает класс, унаследованный
от MFC-класса CView, специально спроектированного для отображения данных доку-
мента на экране.
714 Глава 14
Класс представления в вашем приложении
Мастер создания приложений MFC (MFC Application Wizard) сгенерировал класс
CSketcherView для отображения информации из документа в клиентской области
окна документа. Определение класса включает переопределения некоторых виртуаль-
ных функций, одна из которых заслуживает особого интереса, а именно — OnDraw ().
Она вызывается всякий раз, когда клиентская область документа должна быть пере-
рисована. Это функция, которая вызывается каркасом приложения, когда вашей про-
граммой принимается сообщение WM_PAINT.
Функция-член OnDraw о
Реализация функции-члена OnDraw () , созданной мастером MFC Application
Wizard, показана ниже.
void CSketcherView::OnDraw(CDC* /*pDC*/)
CSketcherDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if(!pDoc)
return;
// TODO: добавить сюда код рисования для собственных данных
Указатель на объект типа класса С DC передается функции-члену класса представле-
ния OnDraw (). Этот объект включает функции-члены, вызывающие функции Windows
API, который позволяют вам рисовать в контексте устройства. Обратите внимание,
что имя параметра закомментировано, так что вы должны либо убрать комментарий
с него, либо подставить собственное имя перед тем, как сможете использовать указа-
тель.
Поскольку вы поместите весь код, необходимый для рисования документа, в эту
функцию, мастер Application Wizard включил объявление указателя pDoc и инициали-
зировал его, применив функцию GetDocument (), которая возвращает адрес объекта
документа, связанного с текущим представлением:
CSketcherDoc* pDoc = GetDocument();
Функция GetDocument () в действительности извлекает указатель на документ из
m_pDocument, унаследованной переменной-члена объекта представления. Функция
выполняет важную задачу приведения указателя, хранящегося в этой переменной, к
типу, соответствующему классу документа приложения — CSketcherDoc. Это необхо-
димо, чтобы компилятор получил доступ к членам класса документа, определенного
вами, иначе он имел бы доступ только к членам базового класса. Поэтому pDoc ука-
зывает на объект документа в вашем приложении, ассоциированный с текущим пред-
ставлением , и будет использовать его для обращения к данным, хранящимся в объек-
те документа, когда понадобится нарисовать его.
Следующая строка:
ASSERT_VALID(pDoc);
просто проверяет, что pDoc содержит корректный адрес, а оператор if, следую-
щий за ней, проверяет, что pDoc не равен null.
Имя параметра pDC для функции OnDraw () означает “указатель на контекст устрой-
ства” (pointer to Device Context). Объект класса CDC, на который указывает аргумент
pDC, передаваемый функции OnDraw () — это ключ к рисованию окна. Он представля-
сование в окне
715
ет контекст устройства плюс инструменты, необходимые для рисования графики и
текста, так что вам определенно следует познакомиться с ним поближе.
Класс cdc
Вы должны выполнять все рисование в программе с использованием членов
класса С DC. Все объекты этого класса и его производных классов содержат контекст
устройства и функции-члены, необходимые для того, чтобы посылать графику и текст
на дисплей и принтер. В нем есть также функции-члены, предназначенные для полу-
чения информации об используемом физическом устройстве вывода.
Поскольку объекты класса С DC могут представлять почти все, что вам, вероятно,
понадобится для графического вывода, в классе определено множество функций-чле-
нов — фактически, более ста. Поэтому здесь мы рассмотрим только те, что понадобят-
ся нашей программе Sketcher, а остальные — позднее, по мере необходимости.
Обратите внимание, что MFC включает некоторые более специализированные
классы для графического вывода, которые унаследованы от CDC. Например, вы будете
использовать объекты класса CClientDC, поскольку он унаследован от CDC и содержит
все члены, которые мы будем обсуждать далее. Преимущество CClientDC перед CDC
состоит в том, что он всегда содержит контекст устройства, представляющий только
клиентскую область окна, а это как раз то, что необходимо в большинстве случаев.
Отображение графики
В контексте устройства вы рисуете сущности вроде линий, окружностей и текста
относительно текущей позиции. Текущая позиция — это точка в клиентской области,
которая была установлена ранее, после рисования предыдущей сущности, либо уста-
новлена явным вызовом функции, которая для этого предназначена. Например, вы
можете расширить функцию OnDraw (), чтобы установить текущую позицию:
void CSketcherView::OnDraw(CDC* pDC)
CsketcherDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if(!pDoc)
return;
pDC->MoveTo (50, 50) ; // Установить текущую позицию в точку 50, 50
Отмеченная полужирным строка вызывает функцию Move То () для объекта С DC, на
который указывает pDC. Эта функция-член просто устанавливает текущую позицию в
координаты х и у, специфицированные в аргументах. Как вы видели ранее, режимом
отображения по умолчанию является ММ ТЕХТ, так что координаты выражаются в пик-
селях, и текущая позиция будет установлена в точку, отстоящую на 50 пикселей вправо
от левого края окна и на 50 пикселей вниз от верхнего края клиентской области.
Класс С DC переопределяет функцию Move То () для обеспечения гибкости в указа-
нии позиции, которую вы хотите указать в качестве текущей. Существуют две версии
этой функции, объявленные в классе С DC следующим образом:
CPoint MoveTo(int х, int у); // Перейти в позицию х, у
CPoint MoveTo(POINT aPoint); // Перейти в позицию, определенную aPoint
Первая версия принимает координаты х и у как отдельные аргументы. Вторая при-
нимает один аргумент типа POINT, который представляет собой структуру, определен-
ную, как показано ниже:
716 Глава 14
typedef struct tagPOINT
{
LONG x;
LONG y;
) POINT;
Координаты являются членами структуры и имеют тип LONG (определенный в
Windows API как 32-битное целое). Вы можете использовать класс вместо структуры,
и в этом случае применять объект класса С Point в любом месте, где может быть ис-
пользован объект POINT. Класс CPoint имеет данные-члены х и у типа LONG, и при-
менение объектов CPoint обладает тем преимуществом, что в классе также определе-
ны функции-члены для операций как с CPoint, так и с POINT. Это может показаться
загадочным, поскольку кажется, что появление CPoint делает POINT устаревшим, но
вспомните, что Windows API был построен до появления MFC, и объекты POINT ис-
пользуются в Windows API, поэтому с ними рано или поздно вам доведется столкнуть-
ся, а объекты CPoint применяются в примерах, так что вы имеете возможность уви-
;еть их функции-члены в действии.
Возвращаемым значением функции MoveTo () является объект CPoint, который
специфицирует текущую позицию, какой она была перед перемещением. Вы можете
подумать, что это несколько странно, но представьте ситуацию, когда вам нужно пе-
рейти в новую позицию, нарисовать что-либо, а затем вернуться обратно. Вы можете
не знать текущей позиции перед перемещением, и после того, как перемещение про-
изойдет, она будет утеряна, так что вам предоставляется возможность вернуться в ис-
ходную позицию, когда это необходимо.
Рисование линий
В функции OnDraw () за вызовами MoveTo () следуют вызовы функции LineTo (),
которая рисует линию в клиентской области от текущей позиции в точку, специфици-
рованную ее аргументами, как проиллюстрировано на рис.
Класс С DC также определяет две версии функции LineTo () со следующими про-
тотипами:
BOOL LineTo (int х, int у); // Рисовать линию до позиции х, у
BOOL LineTo(POINT aPoint); // Рисовать линию до позиции, определенной aPoint
исование в окне
717
в противном случае.
Это предлагает вам такую же гибкость в указании аргумента, как и Move То (). Вы мо-
жете использовать объект С Point в качестве аргумента второй версии функции. Функ-
ция возвращает TRUE, если линия была нарисована, и FALSE
Когда выполняется функция LineTo (), текущая позиция изменяется в точку, ука-
занную в конце линии. Это позволяет рисовать серии соединенных линий простыми
последовательными вызовами LineTo () для каждой из них. Взгляните на следующую
версию функции OnDraw ():
void CSketcherView::OnDraw(CDC* pDC)
CSketcherDoc* pDoc = GetDocument();
ASSERT—VALID(pDoc);
if(IpDoc)
return;
pDC->MoveTo(50,50);
pDC->LineTo(50,200);
pDC->LineTo(150,200);
pDC->LineTo(150,50);
pDC->LineTo(50,50);
/ / Установить текущую позицию
// Нарисовать вертикальную линию вниз на 150 единиц
// Нарисовать горизонтальную линию вправо на 100 единиц
// Нарисовать вертикальную линию вверх на 150 единиц
// Нарисовать горизонтальную линию влево на 100 единиц
Если вы включите это в программу Sketcher и запустите ее на выполнение, будет
отображено окно документа, показанное на рис. 14.5.
Четыре вызова функции LineTo () обеспечили рисование прямоугольника против
часовой стрелки, начиная с верхнего левого угла. Первый вызов использует текущую
позицию, установленную функцией Move То (); последующие вызовы применяют теку-
щую позицию, установленную предыдущим вызовом функции LineTo (). Вы можете
использовать это для рисования любой фигуры, состоящей из последовательности
линий, каждая из которых соединена с предыдущей. Конечно, можно также в любой
момент использовать MoveTo () для изменения текущей позиции.
Л Sketcher - Sketcherl
File Edit View Element Color Window Help
t3 Sketcherl
Puc. 14.5. Работа программы Sketcher с прямоугольниками
718 Глава 14
Рисование окружностей
У вас есть выбор среди нескольких функций-членов класса С DC для рисования
окружностей, но все они предназначены для рисования эллипсов. Как вы знаете
из школьного курса геометрии, окружность — это частный случай эллипса, у кото-
рого большая и малая оси равны. Поэтому вы можете использовать функцию-член
Ellipse () для рисования окружности. Как и все другие замкнутые фигуры, поддер-
живаемые классом С DC, функция Ellipse () заполняет свою внутреннюю часть уста-
новленным вами цветом. Внутренний цвет определяется кистью (brush), выбранной
в контексте устройства. Текущая кисть контекста устройства определяет способ за-
полнения замкнутых фигур.
Библиотека MFC предлагает класс CBrush, который служит для определения ки-
сти. Можно установить цвет объекта CBrush, а также определить шаблон заполнения
замкнутых фигур. Если вы хотите нарисовать незакрашенную замкнутую фигуру, мож-
но использовать пустую кисть, которая оставит внутреннюю часть фигуры пустой.
Чуть позже в этой главе я еще вернусь к разговору о кистях.
Другой способ рисования незакрашенных окружностей предполагает применение
функции Аге (), которая не работает с кистями. Преимущество ее состоит в том, что
вы можете рисовать любые дуги и эллипсы, а не только замкнутые. Доступны две вер-
сии этой функции в классе С DC:
BOOL Arc (int xl, int yl, int x2, int y2, int x3, int y3, int x4, int y4) ;
BOOL Arc(LPCRECT IpRect, POINT StartPt, POINT EndPt);
В первой версии (xl, yl) и (x2, y2) определяют верхний левый и нижний пра-
вый углы прямоугольника, в который вписана полная кривая. Если вы укажете в этих
координатах углы квадрата, то нарисованная кривая будет сегментом окружности.
Точки (хЗ, уЗ) и (х4, у4) определяют начальную и конечную точки рисуемого сег-
мента. Сегмент рисуется против часовой стрелки. Если вы определите координаты
(хЗ, уЗ) идентичными (х4, у4), то получите полную замкнутую кривую.
Во второй версии Аге () описанный прямоугольник задается объектом RECT, и ука-
затель на этот объект передается в первом аргументе. Функция также принимает ука-
затель на объект класса CRect, имеющий четыре общедоступных переменных-члена:
left, top, right и bottom. Они соответствуют координатам х и у левой верхней и
правой нижней точек соответственно. Класс также предлагает множество функций-
членов, оперирующих объектами CRect, часть из которых мы используем чуть позже.
Объекты POINT с именами StartPt и EndPt во второй версии Аге () определяют
начальную и конечную точки дуги, которую нужно нарисовать.
Рассмотрим пример кода, использующего обе версии функции Аге ().
void CSketcherView::OnDraw(CDC* pDC)
CsketcherDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if(!pDoc)
return;
pDC->Arc(50,50,150,150,100,50,150,100); // Нарисовать 1-ю (большую) окружность
11 Определить ограничивающий прямоугольник для 2-й (меньшей) окружности
CRect* pRect = new CRect(250,50,300,100);
CPoint Start(275,100); // Начальная точка дуги
CPoint End(250,75); // Конечная точка дуги
pDC->Arc(pRect,Start, End); // Нарисовать вторую окружность
delete pRect;
719
Обратите внимание, что для определения ограничивающего прямоугольника
здесь используется класс CRect вместо структуры RECT и объекты класса CPoint вме-
сто структур POINT. Вы также позднее будете работать с объектами CRect, хотя, как
вы увидите, они обладают некоторыми ограничениями. Функция Аге () не требует
установки текущей позиции, поскольку положение и размер дуги полностью опре-
деляются передаваемыми ей аргументами. Текущая позиция при рисовании дуги не
важна — она остается на прежнем месте, где и была до начала рисования дуги. Хотя
координаты могут быть до 32 768, максимальная ширина и высота прямоугольника,
ограничивающего фигуру, составляет 32 767, потому что это максимальное положи-
тельное значение, которое может быть представлено 16-битным целым числом.
Теперь попробуйте запустить Sketcher с этим кодом в функции OnDraw (). Вы
должны получить результат, показанный на рис. 14.6.
Sketcher - Sketched
File Edit View Element Color Window. Help
Sketched
I
Puc. 14.6. Работа программы Sketches: с дугами
Попробуйте изменить размеры границ. Клиентская область будет автоматически
перерисована при сокрытии и открытии дуг на рисунке. Вспомните, что разрешение
экрана влияет на масштаб отображения. Чем меньше разрешение экрана, тем больше
и дальше от верхнего левого угла получатся дуги.
Рисование в цвете
Все, что мы рисовали до сих пор, появлялось на экране в черном цвете. Рисование
подразумевает использование объекта пера (реп), который имеет цвет и толщину, и
пока что мы применяли объект пера по умолчанию, предоставленный контекстом уст-
ройства. Конечно, вы не обязаны поступать так — можно создать свое собственное перо
требуемой толщины и цвета. В MFC определен класс С Реп, призванный помочь в этом.
Все замкнутые кривые, которые вы рисуете, заполнялись текущей кистью кон-
текста устройства. Как уже упоминалось, вы можете определить кисть как экзем-
пляр класса CBrush. Давайте рассмотрим некоторые возможности объектов С Реп и
CBrush.
720 Глава 14
Создание пера
Простейший способ создать объект пера — это объявить объект класса СРеп;
СРеп аРеп; // Объявление объекта пера
Этот объект нуждается в инициализации нужными вам свойствами. Это делается
с помощью функции-члена CreatePen (), объявленной в классе СРеп следующим об-
разом:
BOOL CreatePen (int aPenStyle, int aWidth, COLORREF aColor);
Функция возвращает TRUE, если перо удалось успешно инициализировать, и
FALSE — в противном случае. Первый аргумент определяет стиль линии, которую вы
хотите использовать для рисования. Ее необходимо специфицировать одним из сим-
волических значений, перечисленных в табл. 14.2.
Таблица 14.2. Стили пера
Стиль пера Описание
PS SOLID Перо рисует сплошную линию.
PS-DASH Перо рисует пунктирную линию. Этот стиль линии допускается только в случае,
когда ширина пера равна 1.
PS DOT Перо рисует точечную линию. Этот стиль линии допускается только в случае, когда
ширина пера равна 1.
PS_dashdot Перо рисует штрих-пунктирную линию. Этот стиль линии допускается только в слу-
чае, когда ширина пера равна 1.
ps_dashdotdot Перо рисует штрих-пунктирную линию с двойными точками. Этот стиль линии до-
пускается только в случае, когда ширина пера равна 1.
PS_NULL
Перо не рисует ничего.
PS_INSIDEFRAME
Перо рисует сплошную линию, но в отличие от ps solid, точки, специфицирую-
щие линию, появляются на грани пера, а не в его центре, так что рисуемый объект
никогда не выходит за пределы ограничивающего прямоугольника.
Второй аргумент функции CreatePen () определяет ширину линии. Если aWidth
равно 0, рисуется линия в 1 пиксель шириной, независимо от текущего режима ото-
бражения. Для значений 1 и более ширина пера зависит от принятой в режиме ото-
бражения единицы измерения. Например, значение 2 для aWidth в режиме ММ_ТЕХТ
составляет 2 пикселя, а в режиме MM_LOENGLISH ширина пера при этом составит 0,02
дюйма.
Последний аргумент специфицирует цвет, используемый для рисования пером,
так что вы можете инициализировать перо с помощью следующего оператора:
аРеп.CreatePen(PS—SOLID, 2, RGB(255,0,0)); // Создать сплошное красное перо
Если предположить, что установлен режим отображения ММ_ТЕХТ, это перо рису-
ет красную сплошную линию толщиной 2 пикселя.
Использование пера
Чтобы использовать перо, вы должны выбрать его в контексте устройства, в котором
выполняется рисование. Для этого служит функция-член класса CDC Selectobject ().
Чтобы выбрать перо, которое вы хотите использовать, упомянутая функция вызыва-
ется с указателем на объект пера в качестве аргумента. Функция возвращает указатель
Рисование в окне 721
на предыдущий использованный объект пера, так что вы можете сохранить его и вос-
становить старое перо по завершении рисования. Ниже показан типичный оператор
выбора пера.
СРеп* pOldPen = pDC->SelectObject(&аРеп); // Выбрать аРеп в качестве пера
Чтобы восстановить старое перо, вы просто вызываете эту функцию опять, пере-
давая ей указатель, возвращенный предыдущим вызовом:
pDC->SelectObject(pOldPen);
// Восстановить старое перо
Вы можете увидеть все это в действии, изменив предыдущую версию функции
OnDraw () в классе CSketcherView:
void CSketcherView::OnDraw(CDC* pDC)
CSketcherDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if(!pDoc)
return;
// Объявить объект пера и инициализировать его
/ / как красное сплошное перо шириной в 2 пикселя
СРеп аРеп;
аРеп.CreatePen(PS_SOLID, 2, RGB(255, 0, 0)) ;
СРеп* pOldPen = pDC->SelectObject(&аРеп); // Выбрать аРеп в качестве пера
pDC->Arc(50,50,150,150,100,50,150,100); //Нарисовать 1-ю (большую) окружность
// Определить ограничивающий прямоугольник для 2-й (меньшей) окружности
CRect* pRect = new CRect(250,50,300,100);
CPoint Start (275,100); // Начальная точка дуги
CPoint End(250,75); // Конечная точка дуги
pDC->Arc(pRect,Start, End); // Нарисовать вторую окружность
delete pRect;
pDC->SelectOb ject (pOldPen); // Восстановить старое перо
Если вы соберете и запустите приложение Sketcher с этой версией функции
OnDraw (), то получите те же дуги, что и раньше, но линии будут толще и красного
цвета. Можно с пользой поэкспериментировать с этим примером, пробуя разные
комбинации аргументов функции CreatePen () и наблюдая эффект от их примене-
ния. Обратите внимание, что возвращаемое значение функции CreatePen () игнори-
руется, так что вы рискуете столкнуться со сбоем этой функции, который не будет
обнаружен в программе. В данном случае это не имеет значения, поскольку програм-
ма все еще достаточно тривиальна, но когда вам придется разрабатывать серьезные
программы, важность проверки сбоев подобного рода возрастет.
Создание кисти
Объект класса СВ rush инкапсулирует кисть Windows. Вы можете определять кисть
как сплошную, штрихованную или закрашенную по шаблону. Кисть — это на самом
деле блок пикселей размером 8x8, повторяющийся в пределах области, которую
кисть заполняет.
Чтобы определить кисть сплошного цвета, вы можете специфицировать цвет при
создании объекта кисти, например:
CBrush aBrush(RGB (255,0,0)) ; // Определить кисть красного цвета
722 Глава 14
Этот оператор определяет кисть красного цвета. Значение, переданное конструк-
тору, должно иметь тип COLORREF; такой тип возвращается макросом RGB (), так что
это удобный способ спецификации цвета.
Существует и другой конструктор для определения штрихованной кисти. Он при-
нимает два аргумента — первый задает тип штриховки, а второй — цвет, как и ранее.
Аргумент типа штриховки может принимать одно из значений, перечисленных в
табл. 14.3.
Таблица 14.3. Стили штриховки
Стиль штриховки Описание
HS_HORIZONTAL
Горизонтальная штриховка.
HS_vertical Вертикальная штриховка.
HS BDIAGONAL Диагональная штриховка слева направо сверху вниз под 45 градусов.
HS FDIAGONAL Диагональная штриховка слева направо снизу вверх под 45 градусов.
HS CROSS Пересекающаяся штриховка из горизонтальных и вертикальных линий.
hs diagcross Пересекающаяся штриховка из диагональных линий.
Поэтому, чтобы получить перекрестную штриховку из наклоненных под 45 граду-
сов линий, вы можете определить объект CBrush следующим оператором:
CBrush aBrush(HS_DIAGCROSS, RGB(255,0,0));
Вы можете также инициализировать объект CBrush способом, аналогичным объ-
екту СРеп, используя функцию-член класса CreateSolidBrush () для сплошной кисти
и CreateHatchBrush () — для штрихованной кисти. Они требуют тех же аргументов,
что и эквивалентные конструкторы. Например, вы можете создать ту же штрихован-
ную кисть, что и раньше, с помощью показанных ниже операторов:
CBrush aBrush; // Определить объект кисти
aBrush.CreateHatchBrush(HS_DIAGCROSS, RGB(255,0,0));
Использование кисти
Чтобы использовать кисть, вы выбираете кисть в контексте устройства, вызывая
функцию-член класса CDC по имени Selectobject () способом, аналогичным выбору
пера. Эта функция-член перегружена для поддержки выбора объектов кистей в кон-
текст устройства. Чтобы выбрать кисть, определенную выше, вы можете просто за-
писать так:
pDC->SelectObject(aBrush); // Выбрать кисть в контекст устройства
Доступно множество стандартных кистей. Каждая из стандартных кистей иден-
тифицируется предопределенной символической константой, и всего их существует
семь. Вот они:
GRAY_BRUSH
BLACK-BRUSH
HOLLOW BRUSH
LTGRAY_BRUSH
WHITE_BRUSH
NULL BRUSH
DKGRAY_BRUSH
Имена этих кистей достаточно самоочевидны. Чтобы использовать одну из них,
вы вызываете функцию-член класса CDC по имени SelectStockObject (), передавая
ей в качестве аргумента символическое имя кисти, которую вы хотите использовать.
Рисование в окне 723
Чтобы выбрать нулевую кисть, которая оставляет внутренность замкнутой фигуры
незакрашенной, вы можете написать:
pDC->SelectStockObject(NULL_BRUSH);
Здесь pDC — указатель на объект С DC, как и ранее. Вы можете также использо-
вать с этой функцией одно из стандартных перьев. Символы стандартных перьев:
BLACK_PEN, NULL—PEN (которое не рисует ничего) и WHITE_PEN.
Функция SelectStockObject () возвращает указатель на объект, замененный в
контексте устройства. Это позволяет сохранить его для последующего восстановле-
ния, когда вы закончите рисование.
Поскольку функция работает с разнообразными объектами — вы видели здесь
кисти и перья, но она также работает и со шрифтами — типом ее возврата является
CGdiObject*. Класс CGdiObject — это базовый класс для всех классов интерфейсных
объектов графических устройств, а потому указатель на этот класс может использо-
ваться для хранения указателей на объект любого из этих типов. Однако вы должны
привести значение возвращаемого указателя к соответствующему типу, чтобы иметь
возможность потом выбрать старый объект для его восстановления. Это связано с
тем, что используемая функция Selectobject () перегружена для каждого из типов
выбираемых объектов. Не существует версии объекта Selectobject (), которая бы
принимала в качестве аргумента указатель на CGdiObject, но есть версии, принимаю-
щие аргументы типов CBrush*, С Реп*, а также указатели на другие объекты GDI.
Типичный шаблон кодирования для использования стандартной кисти с последу-
ющим восстановлением старой кисти по
CBrush* pOldBrush = (CBrush*)pDC->SelectStockObject(NULL_BRUSH);
// рисовать что-то...
pDC->SelectObject(pOldBrush); // Восстановить старую кисть
Позднее вы используете это в примере настоящей главы.
завершении работы:
Практическое рисование графики
Теперь вы знаете, как рисовать линии и дуги, так что можно приступить к реа-
лизации того, для чего задумана программа Sketcher. Другими словами, вам нужно
решить, как заставит работать ее пользовательский интерфейс.
Поскольку программа Sketcher — инструмент рисования, вы не хотите загружать
пользователя вычислением координат. Простейший механизм рисования предусма-
тривает использование мыши. Например, чтобы нарисовать линию, пользователь
может установить курсор в начальную ее точку и нажать левую кнопку мыши, а затем
определить ее конечную точку, переместив в нее курсор мыши при нажатой левой
кнопке. Было бы идеально, чтобы линия непрерывно отображалась в процессе пере-
мещения курсора с нажатой левой кнопкой мыши (то, что графические дизайнеры
называют “эластичным соединением”). Когда отпускается левая кнопка мыши, линия
должна быть зафиксирована. Процесс проиллюстрирован на рис. 14.7.
Аналогичным образом вы можете позволить рисовать окружности. Первое нажа-
тие левой кнопки мыши должно определить центр и, по мере перемещения курсора
с нажатой кнопкой мыши, программа должна отслеживать его положение. При этом
окружность должна непрерывно перерисовываться, причем текущая позиция курсора
определяет точку на линии окружности. Как и в случае рисования линии, окружность
должна быть зафиксирована при отпускании левой кнопки мыши. Этот процесс про-
иллюстрирован на рис. 14.8.
724 Глава 14
По мере перемещения курсора
линия непрерывно обновляется
Рис. 14. Т. Рисование линии
Левая кнопка
мыши нажата
Окружность фиксируется
при отпускании кнопки мыши
Перемещение
курсора
Левая кнопка мыши
отпущена
По мере перемещения курсора
окружность непрерывно обновляется
Рис. 14.8. Рисование окружности
Точно так же легко, как вы рисуете линию, можно рисовать и прямоугольник
показано на рис. 14.9.
как
Рисование в окне
725
Левая кнопка
По мере перемещения курсора
прямоугольник непрерывно обновляется
Рис. 14.9. Рисование прямоугольника
Первая точка определяет позицию, в которой нажата левая кнопка мыши. Это —
один угол прямоугольника. Позиция курсора в процессе его перемещения с нажатой
кнопкой мыши определяет противоположный по диагонали угол прямоугольника.
Прямоугольник определяется окончательно, когда отпускается левая кнопка мыши.
С кривой немного иначе. Кривую можно определять с указанием произвольного
количества точек. Предлагаемый для использования механизм проиллюстрирован на
рис. 14.10.
сегментов линий, соединяемых
в позициях курсора
Рис. 14.10. Рисование кривой
726 Глава 14
Как и с другими фигурами, первая точка определяется позицией курсора в момент
нажатия левой кнопки мыши. Последовательные позиции фиксируются по мере пе-
ремещения мыши и соединяются сегментами линий, формирующими кривую, так что
след курсора мыши определяет нарисованную кривую.
Теперь, когда вы знаете, как пользователь определяет элемент, очевидным следу-
ющим шагом для понимания того, как это реализовать, должно быть изучение про-
граммирования для мыши.
Программирование для мыши
Чтобы обеспечить программе возможность рисования фигур описанным выше
способом, необходимо знать некоторые сведения, касающиеся мыши.
□ Нажатие кнопки мыши сигнализирует о начале операции рисования.
□ Местоположение курсора при нажатии кнопки мыши определяет начальную
точку фигуры.
□ Перемещение мыши после обнаружения нажатия кнопки мыши — сигнал рисо-
вания фигуры, и позиция курсора представляет точку определения фигуры.
□ Позиция курсора во время отпускания кнопки мыши сигнализирует о том, что
нарисована финальная версия фигуры.
Как вы можете предположить, вся эта информация предоставляется Windows в фор-
ме сообщений, отправленных вашей программе. Реализация процесса рисования ли-
ний и окружностей почти полностью состоит из написания обработчиков сообщений.
Сообщения от мыши
Когда пользователи программы рисуют фигуру, они взаимодействуют с опреде-
ленным представлением документа. Поэтому класс документа является очевидным
местом для размещения обработчиков сообщений мыши. Выполните щелчок правой
кнопкой мыши на имени класса CSketcherView в Class View и затем отобразите окно
его свойств, выбрав пункт Properties из контекстного меню. Если затем вы щелкнете
на кнопке сообщений (подождите отображения всплывающих подсказок, если не зна-
ете, где она находится), то увидите список идентификаторов сообщений. Затем вы
увидите список идентификаторов стандартных сообщений Windows, отправляемых
классу, с префиксом WM_.
Пока вам нужно знать о трех сообщениях от мыши, перечисленных в табл. 14.4,
поэтому прокрутим список вниз, чтобы добраться до них (рис. 14.11).
Таблица 14.4. Три сообщения от мыши
Сообщение Описание
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_MOUSEMOVE
Сообщение посылается, когда нажимается левая кнопка мыши.
Сообщение посылается, когда отпускается левая кнопка мыши.
Сообщение посылается, когда мышь перемещается.
Эти сообщения достаточно независимы друг от друга и посылаются представлени-
ям документа в вашей программе, даже если вы не предусматриваете обработчиков
для них. Вполне вероятно получение в окне сообщения WM_LBUTTONUP без предвари-
Рисование в окне 727
тельного получения WM_LBUTTONDOWN. Это может случиться, если кнопка нажата, ког-
[а курсор находился на другом окне, а затем был перемещен на ваше окно представле-
ния прежде, чем кнопка была отпущена.
ГТ----Т--------------------
Properties
CSketcherView VCCode Class
WM_LBUTTONDBLCLK
WM_LBUTTONDOWN
WM_LBUTTONUP
WM_M В UTTO N D В LCLK
WMJY1BUTTONDOWN
WM_MBUTTONUP
WM_MDIACnVATE
WM_MEASUREITEM
WM_MENUCHAR
WM_MEhlUSELECT
WM_MO U S EACTWATE
WM_MO US EMOVE
WM_MOUSEWHEEL
WM_MOVE
WM_MOVING
WM_NCACTIVATE
WM_NCCALCSEE
WM_N CCREATE
WM_NCDESTROY
WM_NCHITTEST
WM_N CLB UTTO N D В LCL
WM_NC1_BUTTONDOWN
WM_NCLBUTTONUP
WM_hlCMBUTTONDBLCl
CSketcherView
Puc. 14.11. Список сообщений от мыши
Если вы посмотрите на список в окне свойств, то увидите там и другие сообщения
мыши, которые могут произойти. Вы можете выбрать для обработки любое из них
или же все, в зависимости от требований вашего приложения. Определите в общих
чертах, что необходимо делать с этими тремя сообщениями, которые вас интересуют,
на основе процесса рисования фигур, который вы видели ранее:
VM LBUTTONDOWN
С этого начинается процесс рисования элемента. Получив это сообщение, вы
олжны предпринять следующие действия.
1. Отметить начало процесса рисования.
2. Записать текущую позицию как первую точку определения элемента.
WM_MOUSEMOVE
Это сообщение означает промежуточную стадию, когда вы хотите создать и нари-
совать временную версию текущего элемента, но только при условии, что левая кноп-
ка мыши нажата. Ниже перечислены выполняемые действия.
1. Проверить, что левая кнопка мыши нажата.
2. Если да, удалить предыдущую версию текущего элемента, которая была нарисо-
вана.
3. Нарисовать новую версию элемента по двум известным точкам.
728 Глава 14
WM_LBUTTONUP
Это указывает на то, что процесс рисования элемента завершен, так что осталось
предпринять следующие действия.
1. Сохранить финальную версию элемента на основе записанной начальной точ-
ки, вместе с позицией курсора, в которой кнопка была отпущена, в качестве
второй точки.
2. Зафиксировать конец процесса рисования элемента.
Теперь сгенерируем обработчики для этих трех сообщений мыши.
Обработчики сообщений мыши
Вы можете создать обработчик для одного из сообщений мыши, щелкнув на ID
для выбора и затем выбрав стрелку вниз в соседней колонке; попробуйте, например,
выбрать <add> OnLButtonUp для сообщения ID_LBUTTONUP. Повторите процесс для
каждого сообщения из WM_LBUTTONDOWN и WM_MOUSEMOVE. Функции, сгенерирован-
ные в классе CSketcherView: OnLButtonDown(), OnLButtonUp() и OnMouseMove().
У вас нет возможности изменить имена этих функций, потому что они переопределя-
ют версии, уже определенные в базовом для CSketcherView классе. Посмотрим, как
можно реализовать эти обработчики.
Начнем с обработчика сообщения WM_LBUT TON DOWN. Ниже приведен сгенериро-
ванный скелетный код:
void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point)
// TODO: добавьте сюда код обработчика сообщении и/или вызов
// обработчика по умолчанию
CView::OnLButtonDown(nFlags, point);
Как видите, здесь присутствует вызов обработчика базового класса. Это гаранти-
рует, что обработчик базового класса будет вызван, если вы не добавите никакого
собственного кода. В данном случае у вас нет необходимости вызывать обработчик
базового класса, хотя при желании это сделать можно. Необходимость вызова обра-
ботчика базового класса зависит от обстоятельств.
В общем случае комментарий, указывающий, куда вы должны добавлять собствен-
ный код — хорошая подсказка. Когда предполагается, как в данном примере, что вы-
зов обработчика базового класса не обязателен, вы можете пропустить его, добавляя
собственный код обработки сообщения. Обратите внимание, что положение коммен-
тария по отношению к вызову обработчика базового класса также важно, поскольку
иногда вы должны вызвать обработчик базового класса перед своим кодом, а иногда —
после. Комментарий указывает, где должен появиться ваш код по отношению к вызо-
ву обработчика базового класса.
Обработчик вашего класса получает два аргумента: nFlags, имеющий тип UINT
и содержащий набор флагов состояния, которые указывают на нажатие различных
клавиш, и объект point типа CPoint, определяющий позицию курсора при нажатой
левой кнопке мыши. Тип UINT определен в Windows API, и соответствует 32-битному
целому числу.
Значение nFlags, передаваемое функции, может быть любой комбинацией симво-
лических значений, перечисленных в табл. 14.5.
Рисование в окне 729
Таблица 14.5. Символические значения, используемые в аргументе nFlags
Флаг
MK_CONTROL
MK_LBUTTON
MK_MBUTTON
MK_RBUTTON
MK_SHIFT
Описание
Соответствует нажатой клавише <Ctrl>.
Соответствует нажатию левой кнопки мыши.
Соответствует нажатию средней кнопки мыши.
Соответствует нажатию правой кнопки мыши.
Соответствует нажатой клавише <Shift>.
Возможность обнаружения нажатия клавиши в обработчике сообщений позволя-
ет поддерживать разные действия в зависимости от дополнительных обстоятельств.
Значение nFlags может содержать более одного из этих индикаторов, каждый из
которых соответствует определенному биту в слове, так что вы можете проверить
использование определенной клавиши, применив для этого операцию двоичного И.
Например, для проверки нажатия клавиши <Ctrl> можно записать так:
if(nFlags & MK_CONTROL)
// Что-то делать...
Выражение nFlags & MK_CONTROL будет иметь значение TRUE, если переменная
имеет установленный бит MK_CONTROL. Таким образом, вы можете выполнять разные
действия при нажатии левой кнопки мыши, в зависимости от того, нажата ли клави-
ша <Ctrl>. Здесь используется операция двоичного И, то есть соответствующие биты
объединяются вместе. Не путайте это с операцией логического И (& &), которая не
делает того, что нужно здесь.
Аргументы, переданные двум другим обработчикам сообщений, те же самые, что
и у функции OnLButtonDown (); код, сгенерированный для них, выглядит следующим
образом:
void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point)
// TODO: добавьте сюда код обработчика сообщений и/или вызов обработчика
/ / по умолчанию
CView::OnLButtonUp(nFlags, point);
void CSketcherView::OnMouseMove(UINT nFlags, CPoint point)
// TODO: добавьте сюда код обработчика сообщений и/или вызов обработчика
//по умолчанию
CView::OnMouseMove(nFlags, point);
Помимо имен функций, скелетный код здесь один и тот же.
Если вы взглянете в конец кода определения класса CSketcherView, то увидите,
что добавлены объявления этих трех функций:
// Сгенерированные функции отображения сообщений
protectedz
DECLARE_MESSAGE_MAP()
public:
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnLButtonUp(UINT nFlags, CPoint point);
afx msg void OnMouseMove(UINT nFlags, CPoint point);
730 Глава 14
Это идентифицирует функции, которые добавлены в качестве обработчиков со-
общений.
Теперь, получив представление об информации, передаваемой созданным обра-
ботчикам сообщений, вы можете начать добавление своего собственного кода, чтобы
заставить их делать то, Что запланировано.
Рисование с помощью мыши
Для сообщения WM_LBUTTONDOWN необходимо записывать позицию курсора в
первой точке, определяющей элемент. Нужно также записывать позицию курсо-
ра после перемещения мыши. Очевидное место для хранения этой информации —
класс CSketcherView, так что вы можете добавить для этого в класс данные-члены.
Щелкните правой кнопкой мыши на имени класса CSketcherView в Class View и
выберите из контекстного меню пункт Ad d^ Add Variable (Добавить1^Добавить пере-
менную). Затем вы сможете добавить детали для дополнительной переменной-члена
класса, как показано на рис. 14.12.
Рис. 14.12. Добавление к классу новой переменнойчлена
анных должен быть protected,
Выпадающий список типов включает только фундаментальные типы, так что для
ввода типа CPoint вы просто высвечиваете отображенный тип двойным щелчком и
затем вводите тип, который хотите. Новый член
чтобы предотвратить непосредственную модификацию его извне класса. Когда вы
щелкаете на кнопке Finish (Готово), будет создана переменная, и ее начальное значе-
ние будет установлено в 0 в списке инициализации конструктора. Но вам нужно на-
значить начальное значение*CPoint (0,0), так что код будет таким:
// Конструирование/уничтожение CSketcherView
CSketcherView::CSketcherView():
Рисование в окне 731
m_FirstPoint(CPoint(0, 0))
// TODO: добавьте сюда код конструирования
Это инициализирует член — объект CPoint в позиции (0, 0). Вы можете те-
перь добавить в класс CSketcherView защищенный член типа CPoint по имени
m_SecondPoint, который будет сохранять следующую точку элемента. Этот член так-
же нужно упомянуть в списке инициализации конструктора: CPoint (0,0).
Теперь вы можете реализовать обработчик WM LBUTTONDOWN следующим образом:
void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point)
// TODO: добавьте сюда код обработчика сообщений и/или
// вызов обработчика по умолчанию
m__FirstPoint = point; // Записать позицию курсора
}
Все, что он делает — записывает координаты, переданные во втором аргументе.
В данной ситуации вы можете вообще проигнорировать первый аргумент.
Пока вы не можете завершить обработчик сообщения WM_MOUSEMOVE, но его на-
бросок может выглядеть так:
void CSketcherView::OnMouseMove(UINT nFlags, CPoint point)
// TODO: добавьте сюда код обработчика сообщений и/или
// вызов обработчика по умолчанию
if(nFlags & MK_LBUTTON) // Проверить, что левая кнопка нажата
m_SecondPoint = point; // Сохранить текущую позицию курсора
// Проверить предыдущий временный элемент
// Мы попадаем сюда, если было предыдущее перемещение мыши,
// поэтому сюда добавить код удаления старого элемента
// Добавить код создания нового элемента
//и вызвать его перерисовку
Важно проверить факт нажатия левой кнопки мыши, поскольку обрабатывать
ремещение мыши нужно только в этом случае. Без этой проверки получится, что
обрабатываете перемещение мыши с нажатой правой кнопкой, либо с вообще не
жатыми кнопками.
ле-
вы
на-
Первое, что делает обработчик после проверки нажатия левой клавиши мыши —
сохраняет позицию курсора. Эта позиция используется в качестве второй точки для
определения элемента. Остальная часть логики, в общем, должна быть ясна, одна-
ко нужно будет установить еще несколько дополнительных моментов, прежде чем
завершать функцию. Вам не просто необходимо определить элемент — вам нужно
определить элемент как объект класса, поэтому некоторые классы уже должны быть
определены. Кроме того, потребуется предусмотреть способ удаления элемента и на-
рисовать его опять при создании нового. Сделаем небольшое отступление.
Перерисовка клиентской области
Рисование и стирание элементов затрагивает всю клиентскую область окна. Как
вы уже знаете, клиентская область перерисовывается в функции-члене OnDraw () клас-
732 Глава 14
са CSketcherView, и эта функция вызывается, когда приложение Sketcher принима-
ет сообщение WM PAINT. Наряду с базовым сообщением для перерисовки клиентской
области Windows передает информацию о части клиентской области, которая должна
быть перерисована. Это может сэкономить массу времени при отображении сложных
сообщений, поскольку в перерисовке нуждается лишь указанная часть области, кото-
рая может составлять малую часть общей области.
Вы можете сообщить Windows, что определенная область должна быть перерисо-
вана, вызывая функцию InvalidateRect (), унаследованную вашим классом представ-
ления от базового класса. Функция принимает два аргумента, первый из которых —
указатель на объект RECT или CRect, определяющий прямоугольник в клиентской
области, который должен быть перерисован. Передача null вместо этого параметра
означает необходимость перерисовки всей клиентской области. Второй параметр —
значение типа BOOL, которое равно TRUE, если фон прямоугольника должен быть
очищен, и FALSE — в противном случае. Этот аргумент имеет значение по умолчанию
TRUE, поскольку обычно перед перерисовкой прямоугольника его нужно очистить,
так что в большинстве случаев вы можете его игнорировать. BOOL — тип Windows API,
представляющий булевские значения, и может принимать значения TRUE или FALSE.
Типичная ситуация, когда может понадобиться перерисовка области — это когда
что-то в ней изменяется, и перемещение какого-то объекта может служить примером.
В данном случае необходимо очистить фон, чтобы удалить старое представление объ-
екта перед рисованием новой версии. Когда вы хотите рисовать поверх существую-
щего фона, вы просто передаете FALSE во втором аргументе InvalidateRect ().
Вызов функции InvalidateRect () не вызывает непосредственной перерисовки
любой части окна; он просто сообщает Windows координаты прямоугольника, кото-
рый вы хотели бы перерисовать в некоторый момент. В Windows поддерживается
область обновления, на самом деле являющуюся прямоугольником, который иден-
тифицирует область окна, подлежащую перерисовке. Область, указанная при вызове
InvalidateRect (), добавляется к текущей области обновления, так что новая об-
ласть обновления включает старый плюс указанный вами прямоугольник. В конечном
итоге окну отправляется сообщение WM_PAINT, и вместе с ним передаются коорди-
наты области, подлежащей обновлению. После завершения обработки сообщения
WM—PAINT область обновления сбрасывается в пустое состояние.
Таким образом, чтобы перерисовать фигуру, потребуется выполнить перечислен-
ные ниже действия.
1. Убедиться, что функция OnDraw О в вашем представлении включает вновь соз-
данный элемент, когда она перерисовывает окно.
2. Вызвать InvalidateRect () с указателем на прямоугольник, описывающий фи-
гуру, подлежащую перерисовке, который передается в первом аргументе.
Аналогично, если вы хотите удалить фигуру из клиентской области окна, то долж-
ны выполнить следующие шаги.
1. Удалить фигуру из перечня элементов, которые функция OnDraw () будет рисо-
вать.
2. Вызвать InvalidateRect () с первым аргументом, указывающим на прямоуголь-
ник, который ограничивает фигуру, подлежащую удалению.
Поскольку фон указанного прямоугольника очищается автоматически, до момента,
когда OnDraw () нарисует фигуру заново, эта фигура исчезает. Конечно, это означает,
что вы должны быть готовы получить прямоугольник, ограничивающий любую созда-
Рисование в окне 733
ваемую вами фигуру, так что для этого вы добавите функцию-член в классы, определя-
ющие элементы, которые сможет рисовать Sketcher.
Определение классов элементов
Немного забегая вперед, отметим, что вам придется каким-то образом сохранять
элементы в документе. Кроме того, должна быть возможность сохранять документ в
файле для последующего извлечения, если рисунок должен обладать хоть каким-то
постоянством. С деталями файловых операций мы разберемся чуть позднее, а пока
достаточно будет знать, что в классе MFC по имени CObj ect предусмотрены необхо-
димые для этого инструменты, так что вы используете CObject в качестве базового
класса для классов элементов.
Еще одна проблема заключается в том, что вы не можете знать заранее, в какой по-
следовательности пользователь будет создавать элементы разных типов. Программа
Sketcher должна уметь обрабатывать любую последовательность элементов. Это
предполагает, что применение указателя базового класса для выбора функции кон-
кретного класса элемента с целью его рисования может несколько упростить задачу.
Например, вам не нужно знать, что это за элемент, чтобы нарисовать его. До тех пор,
пока вы обращаетесь к элементу через указатель его базового класса, вы всегда може-
те заставить его нарисовать себя с помощью виртуальной функции. Это еще один при-
мер полиморфизма, о котором говорилось, когда речь шла о виртуальных функциях.
Все, что необходимо для этого — убедиться, что классы, определяющие конкретные
элементы, разделяют общий базовый класс, и что в этом классе все функции, кото-
рые должны выбираться автоматически во время выполнения, являются виртуаль-
ными. Все это указывает на то, что классы элементов могут быть организованы, как
показано на рис. 14.13.
Рис. 14.13. Диаграмма классов для программы Sketcher
Стрелки на диаграмме в каждом случае указывают на базовый класс. Если вы захо-
тите добавить новый тип элемента, то все, что для этого понадобится — унаследовать
его от CElement. Поскольку эти классы тесно связаны, стоит поместить определения
всех этих классов в один новый файл .h, который назовем Elements .h.
Вы можете создать новый класс CElement, выполнив щелчок правой кнопкой
мыши на Sketcher в Class View и выбрав из контекстного меню пункт Add*^>Class
(Добавить*=>Класс). Выберите в качестве категории класса MFC, а в качестве шабло-
на— MFC Class (Класс MFC). После щелчка на кнопке Add (Добавить) в диалоге по-
734 Глава 14
явится другой диалог, в котором нужно будет специфицировать имя класса, как по-
казано на рис. 14.14.
Рис. 14.14. Добавление нового класса, представляющего элемент
В поле имени класса вписано CElement, а в качестве базового класса выбран
С Object из выпадающего списка. Кроме того, имена исходных файлов исправлены
на Elements. h и Elements. срр, потому что в конечном итоге эти файлы будут со-
держать определения всех классов элементов, которые нам нужны. После щелчка на
кнопке Finish (ГЪтово) генерируется код определения класса:
#pragma once
// CElement - цель команды
class CElement : public CObject
public:
CElement (); virtual ^CElement ();
Единственные члены, которые объявлены здесь и чьи скелетные определения по-
явились в Elements. срр — это конструктор и виртуальный деструктор. Вы можете
видеть, что мастер MFC Class Wizard добавил директиву #pragma once для обеспече-
ния однократного включения содержимого этого файла в файлы . срр. Сохраните все
файлы, чтобы обновилась закладка Class View.
Вы можете добавить другие классы элементов, используя, по сути, тот же самый
процесс. Поскольку другие классы элементов в качестве базового имеют CElement,
а не один из классов MFC, вы должны выбрать в качестве категории класса C++, а в
качестве шаблона класса — C++ Class (Класс C++). Для класса CLine окно мастера C++
Class Wizard должно выглядеть, как показано на рис. 14.15.
Рисование в окне 735
Рис. 14.15. Создание нового класса CLine
Line. h и Line. срр, но вы можете изменить это, указав другие име-
Мастер C++ Class Wizard предлагает имена по умолчанию для заголовочного файла
и файла . срр
на либо воспользовавшись существующими. Чтобы код класса CLine был вставлен в
те же файлы, что и код класса CElement, просто щелкните на кнопке по соседству с
именем файла и выберите соответствующий существующий файл. На рис. 14.14 это
уже было сделано. Вам может понадобиться стереть имя файла по умолчанию, пред-
ложенное в диалоге, и повторно выбрать каталог Sketcher, чтобы отобразить список
файлов. После щелчка на кнопке Finish (Готово) вы увидите запрос подтверждения
вашего намерения включить новый класс в существующий файл. Просто щелкните на
кнопке Yes (Да), чтобы подтвердить это, и сделайте то же самое в ответ на запрос от-
носительно файла Elements. срр. Создав определение класса CLine, повторите то же
самое для CRectangle, CCircle и CCurve. Когда все будет готово, вы должны увидеть
определения всех четырех подклассов CElement в файле Elements .h, каждый со сво-
им конструктором и виртуальным деструктором.
Хранение временного элемента в представлении
Когда я говорил о том, как должны быть нарисованы фигуры, было очевидно, что
при перемещении мыши с нажатой левой кнопкой должна создаваться и отображать-
ся серия объектов. Теперь, когда вы знаете, что базовым классом для всех фигур явля-
ется CElement, можно добавить указатель на класс представления, который будет ис-
пользоваться для хранения адреса временного элемента. Щелкните правой кнопкой
мыши на классе CSketcherView еще раз и еще раз выберите пункт Add^Add Variable
(Добавить1^Добавить переменную) из контекстного меню. Элемент m_pTempElement
должен быть типа CElement* с модификатором доступа protected, как и предыду-
щие две переменные-члены, которые были добавлены ранее (рис. 14.16).
736 Глава 14
Рис. 14.16. Добавление в класс CSketcherView члена m^pTenpElement
Мастер добавления переменных-членов (Add Member Variable Wizard) обеспечива-
ет инициализацию переменной при конструировании объекта представления значе-
нием по умолчанию NULL.
CSketcherView::CSketcherView()
: m_Firs tPoint (CPoint (0,0))
r m_SecondPoint (CPoint (0,0)
, m^TempElament (NULL)
// TODO: добавьте сюда код конструирования
Вы сможете использовать указатель m_pTempElement в обработчике сообщений
WM_MOUSEMOVE в качестве теста для предыдущих временных элементов, поскольку ре-
шено, что он равен null, когда таковые отсутствуют.
Если вы посмотрите, что было добавлено к заголовочному файлу SketcherView. h,
то увидите в его начале строку:
#include "atltypes.h"
Она вставлена потому, что мастер предположил, что CElement — это тип ATL (ac-
tive template library — библиотека активных шаблонов). Вы можете удалить эту стро-
ку, поскольку она не нужна. Чтобы класс CSketcherView корректно компилировался,
сразу за директивой #pragma once потребуется добавить следующий оператор:
class CElement; // Опережающее объявление класса
Это просто укажет, что идентификатор CElement представляет собой имя класса,
определенного где-то в другом месте, так что компилятор обработает его как таковой.
Поскольку вы создаете объекты класса CElement в функциях-членах класса пред-
ставления и ссылаетесь на этот класс при определении члена данных, указывающего
на временный элемент, то должны обеспечить, чтобы определение класса CElement
Рисование в окне 737
было включено до определения класса CSketcherView там, где SketcherView. h
включен в файл . срр. Вы можете сделать это для CSketcherView, добавив директи-
ву #include для Elements.h в файл SketcherView.срр перед директивой #include
для SketcherView.h:
finclude "Elements, h
# include " Ske tcherView. h "
В файле Sketcher .срр также имеется директива #include для SketcherView.h,
так что вы должны добавить #include для Elements .h в этот файл также.
Класс CElement
Теперь можно приступить к наполнению определений классов. Вы будете делать
это инкрементным образом, по мере добавления функциональности к приложению
Sketcher — но что нужно прямо сейчас? Некоторые единицы данных вроде цвета,
очевидно, являются общими для элементов всех типов, так что вы можете поместить
их в класс CElement, чтобы они наследовались каждым из производных классов. Тем
не менее, другие данные-члены в классах, определяющие специфические свойства
элементов, будут достаточно индивидуальными, так что вы объявите эти члены в кон-
кретных производных классах, к которым они относятся.
Таким образом, класс CElement содержит только виртуальные функции, которые
заменяются в производных классах, плюс данные-члены и функции, общие для всех
производных классов. Виртуальные функции — те, что выбираются автоматически
для конкретного объекта через указатель. Сначала вы должны воспользоваться масте-
ром добавления членов, который уже применяли ранее, а затем вручную модифици-
ровать класс. Пока можете модифицировать класс CElement, как показано ниже.
class CElement: public CObject
protected:
COLORREF m__Color; // Цвет элемента
public:
virtual ~CElement();
virtual void Draw (CDC* pDC) {} // Виртуальная операция рисования
CRect GetBoundRect() ; // Получить ограничивающий прямоугольник элемента
protected:
CElement(); // Здесь — для предотвращения вызова
Я изменил доступ к конструктору с public на protected, чтобы предотвратить
его вызов извне класса. В этот момент к членам, подлежащим наследованию в произ-
водных классах, относится определяющая цвет переменная-член m_Color и функция-
член, вычисляющая ограничивающий прямоугольник элемента — GetBoundRect ().
Эта функция возвращает значение типа CRect, описывающее прямоугольник, кото-
рый ограничивает фигуру.
Мы имеем также виртуальную функцию Draw (), реализованную в производных
классах, для рисования конкретного объекта. Функция Draw () нуждается в указателе
на объект С DC, переданном ей для обеспечения доступа к функциям рисования, опи-
санным выше и выполняющим рисование в контексте устройства.
Может возникнуть искушение объявить член Draw () как пустую виртуальную
функцию в классе CElement — в конце концов, она не может иметь осмысленного со-
держимого в этом классе. Кроме того, это сделало бы обязательным ее определение
в каждом производном классе. Обычно вы так и будете поступать, но класс CElement
738 Глава 14
наследует от С Object возможность, называемую сериализацией, к услугам которой
вы прибегнете позже для сохранения объектов в файле, а это потребует возможно-
сти создания экземпляров класса CElement. Класс с пустой виртуальной функцией-
членом является абстрактным, а экземпляры абстрактного класса не могут быть соз-
даны. Если вы хотите использовать возможности сериализации MFC для сохранения
объектов, ваши классы не должны быть абстрактными. Для сериализуемого класса вы
также должны предусмотреть конструктор без аргументов.
У вас также может возникнуть искушение объявить функцию GetBoundRect () как
возвращающую указатель на объект CRect — в конце концов, вы ведь передаете ука-
затель функции-члену InvalidateRect () в классе представления; однако это может
привести к возникновению проблем. Вы создадите объект CRect как локальную пере-
менную функции, так что возвращенный указатель при возврате из GetBoundRect ()
станет указывать на несуществующий объект. Это можно обойти, создав объект CRect
в куче, но тогда придется позаботиться об его удалении после использования; в про-
тивном случае вы наполните кучу объектами CRect — по одному на каждый вызов
GetBoundRect (). Другая возможность заключается в том, чтобы сохранить ограни-
чивающий прямоугольник элемента в виде члена класса и генерировать его во время
создания элемента. Это разумная альтернатива, но если впоследствии вы измените
элемент, скажем, переместив его, то всякий раз должны будете обеспечить повторное
вычисление координат этого прямоугольника.
Класс CLine
Определение класса CLine можно усовершенствовать следующим образом:
class CLine: public CElement
public:
-CLine(void);
virtual void Draw (CDC* pDC); // Функция отображения линии
// Конструктор объекта линии
CLine (CPoint Start, CPoint End, COLORREF aColor);
protected:
CPoint mjStartPoint;
CPoint inJEndPoint;
// Начальная точка линии
// Конечная точка линии
CLine (void); // Конструктор по умолчанию (не должен быть использован)
Переменные-члены, определяющие линию — это m_StartPoint и m_EndPoint, и
обе объявлены как protected. Класс имеет public-конструктор, принимающий па-
раметры значений, определяющих линию, а также конструктор по умолчанию без
аргументов, перемещенный в раздел protected, чтобы предотвратить его внешнее
использование.
Реализация класса CLine
Вы добавляете реализацию функций-членов в файл Elements. срр, который был
сгенерирован мастером при создании класса CElement. Файл stdafx.h был включен
в этот файл, чтобы сделать доступными определения стандартных системных заголо-
вочных файлов. Вам также могут понадобиться директивы #include для файлов, со-
держащих определения сгенерированных мастером MFC Application Wizard классов,
если вы используете любой из них в своем коде.
Рисование в окне 739
4
Конечно, каждое из определений функций-членов придется добавлять вручную,
потому что мастер Class Wizard не участвует в определении этих классов. Теперь вы
готовы добавить конструктор класса CLine в файл Elements. срр.
Конструктор класса CLine
Код конструктора:
// Конструктор класса CLine
CLine::CLine(CPoint Start, CPoint End, COLORREF aColor)
m_StartPoint = Start; // Установить начальную точку линии
m_EndPoint = End; / / Установить конечную точку линии
m Color » aColor; // Установить цвет линии
Первым делом вы сохраняете начальную точку в переменной-члене m_StartPoint,
унаследованной от класса CElement. Позднее вы добавите код для обеспечения пере-
мещения элемента, и это перемещение будет реализовано простым изменением на-
чальной точки, а конечная точка должна быть определена относительно начальной.
Вы сделаете это вычитанием координат х и у для Start из End. Оба члена класса
CPoint, х и у, являются общедоступными, так что вы можете обращаться к ним непо-
средственно. И, наконец, вы сохраняете цвет в переменной-члене m_Color, унаследо-
ванной от класса CElement.
Рисование линии
Функция Draw () класса CLine не так сложна, хотя вы должны принять во внима-
ние цвет, используемый при рисовании линии.
// Рисование объекта CLine
void CLine::Draw(CDC* pDC)
{
// Создать перо для этого объекта и инициализировать
// его цветом объекта и шириной линии в 1 пиксель
СРеп аРеп;
if (!аРеп.CreatePen(PS_SOLID, m_Pen, m_Color))
{
// Создать перо не удалось, прервать программу
AfxMessageBox(_Т("Не удалось создать перо для рисования линии"), МВ_ОК);
AfxAbort ();
СРеп* pOldPen = pDC->SelectObject(&аРеп); // Выбрать перо
// Нарисовать линию
pDC->MoveTo(m_StartPoint);
pDC->LineTo(m_EndPoint);
pDC~>SelectObject(pOldPen); // Восстановить старое перо
Вы создаете перо способом, который видели ранее, но на этот раз убеждаетесь
в его успешности. В том маловероятном случае, когда создать перо не удастся (ско-
рее всего, из-за нехватки памяти), это будет означать серьезную проблему. Это поч-
ти неизбежно вызовет ошибку программы, так что вы должны вызвать функцию
Af xMessageBox (), которая является глобальной и служит для отображения окна со-
общения, а затем вызвать AfxAbort () для прерывания программы. Первый аргумент
AfxMessageBox () специфицирует сообщение, появляющееся в окне, а второй гово-
рит о том, что окно должно иметь кнопку ОК. Вы можете получить подробную ин-
740 Глава 14
формацию о каждой из этих функций, поместив курсор на ее имя в окне редактора и
После выбора пера вы перемещаете текущую позицию в начальную точку линии,
определенную в унаследованной переменной m_S tart Point, а затем рисуете линию
от этой точки до конечной. Наконец, вы восстанавливаете старое перо в контексте
устройства, тем самым завершая работу. Переменная m__Pen
ции CreatePen () — пока не существует; мы добавим ее в класс CElement в этой главе
чуть позже.
второй аргумент функ-
Создание ограничивающих прямоугольников
На первый взгляд получение ограничивающего прямоугольника для фигуры — за-
дача тривиальная. Например, линия всегда направлена по диагонали своего описан-
ного прямоугольника, а окружность определена своим описанным прямоугольником,
однако существует ряд небольших сложностей. Фигура должна находиться полностью
внутри прямоугольника, иначе ее часть может быть не нарисована, так что при вы-
числении ограничивающего прямоугольника необходимо учитывать толщину линий,
которыми нарисована фигура. К тому же, вычисляя поправки координат, определяю-
щих описывающий прямоугольник, следует помнить о режиме отображения и также
принимать его во внимание.
Взгляните на рис. 14.17, который поясняет метод получения ограничивающих пря-
моугольников для линии и окружности.
Рис. 14.17. Получение ограничивающих прямоугольников для линии и окружности
Рисование в окне 741
Я называю прямоугольник, используемый для рисования фигуры, “описанным
прямоугольником”, в то время как прямоугольник, принимающий во внимание ши-
рину пера, “ограничивающим прямоугольником”, дабы отличать их друг от друга. На
рис. 14.17 показаны фигуры с их описанными прямоугольниками, а их ограничиваю-
щие прямоугольники смещены на толщину линии. Очевидно, что здесь это смещение
преувеличено, зато вы наглядно видите, что происходит.
Разница в вычислении координат ограничивающих прямоугольников в разных ре-
жимах отображения касается только координаты у; вычисление координаты х одина-
ково во всех режимах отображения. Чтобы получить углы ограничивающего прямо-
угольника в режиме отображения ММ_ТЕХТ, вычтите толщину линий из координаты
у левого верхнего угла описанного прямоугольника и добавьте ее к правому нижнему.
Однако в режиме ММ LOENGLISH (и всех прочих режимах) ось у направлена противо-
положно , так что вы должны прибавить толщину линии к координате у верхнего ле-
вого угла описанного прямоугольника и отнять ее от координаты правого нижнего
угла. И для всех режимов отображения вы отнимаете толщину линии от координаты
х левого верхнего угла описанного прямоугольника и добавляете ее к координате х
правого нижнего его угла.
Чтобы реализовать в Sketcher типы элементов насколько возможно согласован-
но, вы можете хранить описанный прямоугольник каждой фигуры в переменной-
члене базового класса. Описанный прямоугольник должен быть вычислен при кон-
струировании фигуры. Задача функции GetBoundRect () базового класса состоит в
вычислении ограничивающего прямоугольника добавлением смещения к контуру
описанного прямоугольника, равному толщине линии. Вы можете усовершенствовать
определение класса CElement добавлением двух данных-членов.
class CElement: public CObject
{
protected:
COLORREF m_Color; // Цвет элемента
CRect m__EnclosingRect; // Прямоугольник, описывающий элемент
int m__Pen; // Ширина пера
public:
virtual ~CElement();
virtual void Draw(CDC* pDC) {} // Виртуальная операция рисования
CRect GetBoundRect(); // Получить ограничивающий прямоугольник элемента
protected:
CElement(); // Здесь — для предотвращения вызова
Вы можете добавить их, щелкнув правой кнопкой на имени класса и выбрав пункт
Add Member Variable (Добавить переменную-член) из контекстного меню, либо доба-
вить операторы непосредственно в окне редактора вместе с комментариями.
Вы должны также обновить конструктор CLine, чтобы он получил правильную
ширину пера:
CLine::CLine(CPoint Start, CPoint End, COLORREF aColor)
m_StartPoint = Start; // Установить начальную точку линии
m_EndPoint = End; // Установить конечную точку линии
m_Color = aColor; 11 Установить цвет линии
m_Pen = 1; // Установить ширину пера
Теперь вы можете реализовать функцию-член базового класса GetBoundRect (),
предполагая режим отображения ММ_ТЕХТ:
742 Глава 14
// Получить ограничивающий прямоугольник элемента
CRect CElement::GetBoundRect()
CRect BoundingRect; // Объект для сохранения ограничивающего прямоугольника
BoundingRect = m_EnclosingRect; // Сохранить описанный прямоугольник
// Расширить прямоугольник на ширину пера
BoundingRect.InflateRect(m_Pen, m_Pen);
return BoundingRect; // Возвратить ограничивающий прямоугольник
Эта функция возвращает ограничивающий прямоугольник для объекта любого
производного класса. Вы определяете этот прямоугольник, модифицируя координа-
ты описанного прямоугольника, хранящегося в переменной-члене базового класса,
так что он увеличивается со всех сторон на ширину пера, с использованием метода
Inf lateRect () класса CRect.
Класс CRect предоставляет операцию + для прямоугольников, которую можно
было бы использовать вместо этого. Например, вы могли бы написать оператор,
предшествующий return, следующим образом:
BoundingRect = m_EnclosingRect + CRect(m_Pen, m_Pen, m_Pen, m_Pen);
Точно так же можно было бы просто прибавить (или вычесть) ширину пера для
каждого из значений х и у, описывающих прямоугольник. Для этого следовало бы за-
менить присваивание следующими операторами:
BoundingRect = m^EnclosingRect;
BoundingRect.top -= m_Pen;
BoundingRect.left -= m_Pen;
BoundingRect.bottom += m_Pen;
BoundingRect.right += m_Pen;
Вспомни me, что индивидуальные данные-члены объекта CRect — это левая и верхняя грани-
ца (координаты х и у верхнего левого угла), а также правая и нижняя (координаты нижнего
правого угла). Это общедоступные члены, так что вы можете обращаться к ним непосред-
ственно. Часто допускаемая ошибка (по крайней мере, мною) — запись пар координат как
(у, х) вместо правильного порядка (х, у).
Риск обоих подходов — этого и с использованием Inf lateRect () — заключается
в том, что делается предположение о режиме отображения ММ ТЕХТ, что означает,
что положительное направление оси у — сверху вниз. Если вы измените режим ото-
бражения, оба варианта будут работать неправильно, хотя это и не сразу может быть
заметно.
Нормализованные прямоугольники
Функция Inf lateRect () работает, вычитая переданные ей значения из членов
top и left прямоугольника и добавляя их членам bottom и right. Это значит, что
вы можете обнаружить, что ваш прямоугольник на самом деле уменьшился в размере,
если вы не обеспечите его нормализацию. Нормализованный прямоугольник име-
ет значение left меньше или равным right, а значение top — меньше или равным
bottom. Вы можете обеспечить нормализацию прямоугольника, вызвав функцию-
член объекта NormalizeRect (). Большинство функций-членов для корректной рабо-
ты CRect требуют нормализованного объекта, так что при сохранении описанного
прямоугольника в m_EnclosingRect вы должны обеспечить его нормализацию.
Рисование в окне 743
4
Вычисление описанного прямоугольника для линии
Ниже показано все, что понадобится в коде конструктора линии для вычисления
описанного прямоугольника.
CLine::CLine(CPoint Start, CPoint End, COLORREF aColor)
// Установить
11 Установить
// Установить
начальную точку линии
конечную точку линии
цвет линии
// Установить ширину пера
m_StartPoint = Start;
m_EndPoint = End;
m Color = aColor;
m_Pen = 1;
m_EnclosingRect = CRect(Start, End);
m__EnclosingRect. NormalizeRect ();
Используемые здесь аргументы конструктора CRect описывают начальную и ко-
нечную точки линии. Чтобы гарантировать, что ограничивающий прямоугольник
имеет значение top меньше чем bottom, независимо от относительной позиции на-
чальной и конечной точек линии, вызывается функция NormalizeRect (), член объ-
екта m_EnclosingRect.
Класс CRectangle
Хотя вы определяете объект прямоугольника теми же данными, что служат для
определения линии (начальная точка и конечная точка диагонали прямоугольника),
вы не должны сохранять эти определяющие точки. Описывающий прямоугольник в
данных-членах, унаследованных от базового класса, полностью определяет фигуру,
так что вам не нужны никакие дополнительные данные-члены. Поэтому вы можете
определить класс следующим образом:
// Класс, определяющий объект прямоугольника
class CRectangle: public CElement
public:
-CRectangle(void);
virtual void Draw (CDC* pDC); // Функция для отображения прямоугольника
// Конструктор объекта прямоугольника
CRectangle (CPoint Start, CPoint End, COLORREF aColor);
protected:
CRectangle (void) ; / / Конструктор по умолчанию — не должен использоваться
Конструктор без аргументов теперь объявлен как protected, чтобы предотвра-
тить его использование. Определение прямоугольника становится очень простым —
только конструктор, виртуальная функция Draw () и конструктор без аргументов в
разделе protected класса.
Конструктор класса CRectangle
Код нового конструктора класса CRectangle некоторым образом похож на кон-
структор CLine.
// Конструктор класса CRectangle
CRectangle:: CRectangle(CPoint Start, CPoint End, COLORREF aColor)
m_Color = aColor;
m Pen =1;
// Установить цвет прямоугольника
// Установить ширину пера
744 Глава 14
// Определить описанный прямоугольник
m__EnclosingRect ® CRect (Start, End);
m_EnclosingRect.NormalizeRect();
Если вы модифицируете определение класса CRectangle вручную, не будет ника-
кого скелетного определения конструктора, так что вам нужно просто добавить опре-
деление непосредственно в Elements. срр.
Это очень лаконичный код. Некоторые минимальные изменения подмножества
конструктора CLine, исправленные комментарии — и мы получаем конструктор
CRectangle. Он просто сохраняет цвет и ширину пера, а также вычисляет описан-
ный прямоугольник на основании точек, переданных в виде аргументов.
Рисование прямоугольника
Существует функция-член класса С DC по имени Rectangle (), которая рисует
прямоугольник. Эта функция рисует замкнутую фигуру и закрашивает ее текущей
кистью. Вы можете подумать, что это не совсем то, что вам нужно, поскольку хоти-
те рисовать только контур прямоугольника. Однако если выбрать кисть NULL BRUSH,
то будет нарисовано именно то, что нужно. К тому же, чтобы вы знали, есть еще
функция PolyLine (), рисующая фигуры, состоящие из множества прямолинейных
сегментов, на основе массива точек; в равной степени вы можете вновь воспользо-
ваться LineTo (). Однако простейшим подходом будет все же применение функции
Rectangle().
// Рисование объекта CRectangle
void CRectangle::Draw(CDC* pDC)
// Создать перо для этого объекта и инициализировать
// его цветом объекта и шириной линии в 1 пиксель
СРеп аРеп;
if(iaPen.CreatePen(PS_SOLID, m_Pen, m_Color))
// Создание пера не удалось
AfxMessageBox(__Т("Не удалось создать перо для рисования прямоугольника"),
MBJDK) ;
AfxAbort () ;
// Выбрать перо
СРеп* pOldPen = pDC->SelectObject(&аРеп);
/ / Выбрать кисть
CBrush* pOldBrush = (CBrush*)pDC->SelectStockObject(NULL_BRUSH);
// Теперь нарисовать прямоугольник
pDC->Rectangle(m_EnclosingRect);
pDC->SelectObject(pOldBrush); // Восстановить старую кисть
pDC->SelectObject(pOldPen); // Восстановить старое перо
После установки пера и кисти вы просто передаете весь прямоугольник непосред-
ственно функции Rectangle (), чтобы нарисовать его. Все, что остается сделать — это
очистить и восстановить старое перо и кисть контекста устройства.
Класс CCircle
Интерфейс класса CCircle не отличается от интерфейса класса CRectangle. Вы
можете определить окружность целиком через ее описанный прямоугольник, так что
определение класса будет таким, как показано ниже.
Рисование в окне 745
// Класс, определяющий объект окружности
class CCircle: public CElement
publi c:
~CCircle(void) ;
virtual void Draw (CDC* pDC); // Функция отображения окружности
// Конструктор объекта окружности
CCircle (CPoint Start, CPoint End, COLORREF aColor) ;
protected:
CCircle (void) ; / / Конструктор no умолчанию — не должен использоваться
Вы определили общедоступный конструктор, создающий окружность по двум точ-
кам, а также объявили конструктор без аргументов — опять как protected. Кроме
того, в определение класса добавлено также объявление функции рисования.
Реализация класса CCircle
Как я говорил ранее, когда вы создаете окружность, точка, в которой вы нажимае-
те левую кнопку мыши, становится ее центром, а после перемещения курсора с нажа-
той левой кнопкой точка, в которой кнопка отпущена, находится на линии готовой
окружности. Работа конструктора заключается в том, чтобы преобразовать эти точки
в форму, используемую в классе для определения круга.
Конструктор класса CCircle
Точка, в которой вы отпускаете левую кнопку мыши, может находиться в любом
месте окружности, так что координаты точек, специфицирующих описанный прямо-
угольник, нужно вычислять, как показано на рис. 14.18.
□Sketcher1
Левая кнопка мыши
нажата здесь
Это расстояние составляет 2г
Это расстояние составляет 2г
Левая кнопка мыши
отпущена здесь
Радиус г
Рис. 14.18. Вычисление координат точек, специфицирующих опи-
санный прямоугольник для окружности
На рис. 14.18 видно, как можно вычислить координаты левого верхнего и право-
го нижнего углов описанного прямоугольника по отношению к центру окружности
(xl, yl) — точке, записанной в момент нажатия левой кнопки мыши. Предполагая, что
режимом отображения является ММ ТЕХТ, для левой верхней точки вы просто отни-
маете радиус от каждой из координат центра. Аналогично правая нижняя точка вы-
числяется прибавлением радиуса к координатам х и у центра. Отсюда вы можете за-
кодировать конструктор следующим образом:
746 Глава 14
// Конструктор объекта окружности
CCircle::CCircle(CPoint Start, CPoint End, COLORREF aColor)
// Сначала вычислить радиус.
// Использовать плавающую точку, потому что этого требуют
// библиотечные функции (в math.h) вычисления квадратного корня.
long Radius ® static_cast<long> (sqrt(
static_cast<double>((End.x-Start.x)*(End.x-Start.x)+
(End.y-Start.y)*(End.y-Start.y))));
// Теперь вычислить прямоугольник, описывающий окружность, исходя
//из предположения, что режимом отображения является MMJTEXT
m_EnclosingRect = CRect(Start.x-Radius, Start.y-Radius,
Start.x+Radius, Start.y+Radius);
m_Color = aColor; // Установить цвет окружности
m_Pen - 1; // Установить 1
}
Для использования функции sqrt () вы должны добавить такую строку в начало
файла Elements. срр:
#include <math.h>
Вы можете поместить это после директивы #include для stdafx.h.
Максимальное значение координат составляет 32 бита, и члены х и у из CPoint
объявлены как long, так что вычисление аргументов функции sqrt () может быть
безопасно выполнено в целочисленном виде. Результат вычисления квадратного кор-
ня имеет тип double, поэтому вы приводите его к long, поскольку вам нужен цело-
численный результат.
Рисование окружности
Вы уже видели, как рисовать окружность с помощью функции Аге () класса С DC,
так что здесь воспользуемся функцией Ellipse (). Реализация функции Draw () в
классе CCircle выглядит следующим образом.
// Нарисовать окружность
void CCircle::Draw(CDC* pDC)
{
// Создать объект пера и инициализировать его
// цветом объекта и шириной линии в 1 пиксель
СРеп аРеп;
if(!аРеп.CreatePen(PS_SOLID, m_Pen, m_Color))
{
//Не удалось создать перо
AfxMessageBox(_Т("Создать перо для рисования окружности не удалось"),
МВ_ОК);
AfxAbort();
}
СРеп* pOldPen = pDC->SelectObject(&аРеп); // Выбрать перо
/ / Выбрать нулевую кисть
CBrush* pOldBrush = (CBrush*)pDC->SelectStockObject(NULL_BRUSH);
// Нарисовать окружность
pDC->Ellipse(m_EnclosingRect);
pDC->SelectObject(pOldPen); // Восстановить старое перо
pDC->SelectObject(pOldBrush); // Восстановить старую кисть
Рисование в окне 747
После выбора пера соответствующего цвета и нулевой кисти окружность рисует-
ся вызовом функции Ellipse (). Единственный аргумент — объект CRect, описанный
вокруг рисуемой окружности. Это — другой пример кода, полученного почти “даром”,
поскольку он подобен коду, который был разработан выше для прямоугольников.
Класс CCurve
Класс CCurve отличается от прочих тем, что должен иметь дело с переменным
количеством определенных точек. Это подразумевает поддержку некоего списка, и
поскольку работу со списками произвольной длины мы рассмотрим в следующей гла-
ве, я отложу определение деталей этого класса до того момента. А пока вы можете до-
бавить определение класса, которое включает функции-заглушки, чтобы можно было
откомпилировать и скомпоновать код, содержащий их вызовы. Ниже показа код, ко-
торый должен быть в Element. h.
class CCurve: public CElement
public:
~CCurve(void) ;
virtual void Draw(CDC* pDC); // Функция для отображения кривой
// Конструктор объекта кривой
CCurve(COLORREF aColor);
protected:
CCurve(void); // Конструктор по умолчанию — не должен использоваться
А вот код Element. срр.
// Конструктор объекта кривой
CCurve::CCurve(COLORREF aColor)
m_Color = aColor;
m_EnclosingRect = CRect(0,0,0,0);
m_Pen = 1;
// Нарисовать кривую
void CCurve::Draw(CDC* pDC)
Ни конструктор, ни функция-член Draw () пока не делают ничего полезного, и в
классе не определены данные-члены. Конструктор просто устанавливает цвет, уста-
навливает m_EnclosingRect в пустой прямоугольник и задает ширину пера. В следую-
щей главе мы расширим этот класс до работающей версии.
Завершение обработчиков сообщений мыши
Теперь мы можем вернуться назад к обработчику сообщений WM_MOU S EMOVE и до-
делать детали. Это можно сделать, выбрав CSketcherView в Class View и дважды щел-
кнув на имени обработчика OnMous eMove ().
Этот обработчик рассматривается только для рисования последовательности вре-
менных версий элемента по мере движения курсора, поскольку финальный элемент
создается, когда вы отпускаете левую кнопку мыши. Поэтому вы можете трактовать
рисование временных элементов в процессе движения как полностью локальное для
данной функции, оставляя финальную версию элемента для прорисовки в функции-
члене представления OnDraw (). Этот подход обеспечивает относительно эффектив-
748 Глава 14
ное рисование промежуточных элементов, поскольку не включает вызова функции
OnDraw (), ответственной за рисование полного документа.
Лучше всего вы можете сделать это с помощью члена SetROP2 () класса CDC, кото-
рый особенно эффективен в операциях “эластичного соединения”.
Установка режима рисования
Функция SetROP2 () устанавливает режим рисования для всех последующих опе-
раций вывода в контексте устройства, ассоциированном с объектом С DC. Часть “ROP”
имени функции означает “Raster OPeration” (растровая операция), потому что уста-
новки режимов рисования применяются к растровым дисплеям. Если вы спросите:
“А что в этом случае значит SetROPl () ?”, то ответом будет — ничего. Имя функции
означает “Set Raster OPeration to”!
Режим рисования определяет, как цвет пера, которое вы используете для рисова-
ния, комбинируется с цветом фона, создавая цвет отображаемой сущности. Вы спе-
цифицируете режим рисования единственным аргументом функции, который может
принимать любое из значений, перечисленных в табл. 14.6.
Таблица 14.6. Режим рисования
Режим рисования Эффект
R2_BLACK
Все рисуется черным.
R2_WHITE
Все рисуется белым.
R2_NOP
R2 NOT
R2_COPYPEN
R2_NOTCOPYPEN
R2_MERGEPENNOT
R2_MASKPENNOT
R2_MERGEN0TPEN
R2_MASKNOTPEN
R2_MERGEPEN
R2_NOTMERGEPEN
R2_MASKPEN
R2_NOTMASKPEN
R2_XORPEN
R2_NOTXORPEN
Операция рисования ничего не делает.
Рисование инверсным по отношению к экрану цветом. Это гарантирует, что вывод
всегда будет видимым, поскольку предотвращает рисование цветом, совпадаю-
щим с фоном.
Рисование цветом пера. Это — режим рисования по умолчанию, если не установ-
лен другой.
Рисование цветом, инверсным по отношению к цвету пера.
Рисование цветом, порожденным операцией ИЛИ над цветом пера и инверсией
цвета фона.
Рисование цветом, порожденным операцией И над цветом пера и инверсией цве-
та фона.
Рисование цветом, порожденным операцией ИЛИ над цветом фона и инверсией
цвета пера.
Рисование цветом, порожденным операцией И над цветом фона и инверсией цве-
та пера.
Рисование цветом, порожденным операцией ИЛИ над цветом фона и цветом пера.
Рисование цветом, инверсным по отношению к цвету r2_mergepen.
Рисование цветом, порожденным операцией И над цветом фона и цветом пера.
Рисование цветом, инверсным по отношению к цвету R2_maskpen.
Рисование цветом, полученным в результате операции исключающего ИЛИ над
цветом пера и цветом фона.
Рисование в цвете, инверсном по отношению к R2 xorpen.
Рисование в окне 749
Каждый из этих символов предопределен и соответствует определенному режиму
рисования. Здесь предлагается множество опций, но одна из них может выполнить
для нас некоторую волшебную работу — последняя, то есть R2_NOTXORPEN.
Когда вы устанавливаете режим R2_NOTXORPEN, то первый раз, когда вы рисуете
конкретную фигуру цветом фона по умолчанию, она рисуется нормальным цветом
пера, который вы специфицировали. Если же вы рисуете эту фигуру опять, перекры-
вая первый рисунок, то фигура исчезает, потому что цвет, которым она рисуется, со-
ответствует тому, что получается в результате операции исключающего ИЛИ цвета
пера с ним самим. В результате получается белый цвет рисования. Яснее это можно
увидеть на примере.
Белый цвет формируется из равных пропорций максимальной яркости красного,
синего и зеленого. Для простоты это можно представить, как 1, 1, 1 — три значения,
представляющие компоненты RGB цвета. В той же схеме красный определяется как
1, 0, 0. Это дает комбинации, перечисленные в табл. 14.7.
Таблица 14.7. Комбинации цветов фона и пера при первом рисовании
RGB
Фон — белый 1 1 1
Перо — красное 1 0 0
Объединение по XOR 0 1 1
NOT XOR — дает красный 10 0
Поэтому первый раз вы рисуете красную линию на белом фоне, она остает-
ся красной, как указано в последней строке. Если теперь вы нарисуете ту же ли-
нию второй раз, перекрывая существующую, то пиксели фона перекроют красный.
Результирующие цвета рисования показаны в табл. 14.8.
Таблица 14.7. Комбинации цветов фона и пера при втором рисовании
RGB
Фон — белый 1 0 0
Перо — красное 10 0
Объединение по XOR 0 0 0
NOT XOR - дает белый 1 1 1
Как показано в последней строке, линия рисуется белым, и поскольку фон белый,
линия исчезает из виду.
Здесь вам нужно позаботиться о правильном выборе цвета фона. Вы можете убе-
диться, что рисование белым пером на красном фоне не будет работать так же хоро-
шо, поскольку первый раз вы нарисуете что-то красным, а потому это будет невидимо.
Второй раз оно появится в белом цвете. Если вы станете рисовать на черном фоне,
фигура появится и исчезнет, как это происходит и на белом фоне, но при этом она не
будет рисоваться выбранным цветом пера.
Кодирование обработчика OnMouseMove ()
Начнем с добавления кода, который создает элемент после сообщения о переме-
щении мыши. Поскольку вы собираетесь рисовать элемент в функции-обработчике,
750 Глава 14
понадобится создать объект контекста устройства. Наиболее удобный класс для это-
го — CClientDC, производный от CDC. Как говорилось ранее, выгода от использования
этого класса вместо С DC заключается в том, что он автоматически создает контекст
устройства и уничтожает его, когда необходимость в нем отпадает. Созданный им кон-
текст устройства соответствует клиентской части окна, а это как раз то, что нужно.
Добавьте следующий код к наброску обработчика, который вы определили ранее:
void CSketcherView: :OnMouseMove (UINT nFlags, CPoint point)
// Определение объекта контекста устройства для представления
CClientDC aDC(this); // DC для данного представления
aDC. SetROP2 (R2_NOTXORPEN) ; // Установить режим рисования
if (nFlags & MK__LBUTTON) // Проверить, что левая кнопка нажата
m_SecondPoint = point; // Сохранить текущую позицию курсора
// Проверить предыдущий временный элемент
//Мы попадаем сюда, если было предыдущее перемещение мыши,
// поэтому сюда добавить код удаления старого элемента
// Создать временный элемент типа и цвета, который
// записан в объекте документа, и нарисовать его
m_jpTempE lament = Crea teElemen t (); // Создать новый элемент
m__pTempElement->Draw(&aDC); // Нарисовать элемент}
Первая новая строка кода создает объект CClientDC. Указатель this, передавае-
мый конструктору, идентифицирует текущий объект представления, поэтому объект
CClientDC имеет контекст устройства, соответствующий клиентской области текуще-
го представления. Наряду с упомянутыми мною характеристиками, этот объект обла-
дает всем функциями рисования, которые вам нужны, поскольку наследован от класса
С DC. Первая функция-член, используемая здесь — SetROP2 (), которая устанавливает
режим рисования R2_NOTXORPEN.
Чтобы создать новый элемент, вы сохраняете текущую позицию курсора в пере-
менной-члене m_SecondPoint, а затем вызываете функцию-член представления
CreateElement (). (Функцию CreateElement () мы определим сразу после того,
как покончим с данным обработчиком.) Это функция должна создавать элемент, ис-
пользуя две точки, сохраненные в текущем объекте представления, со специфика-
циями цвета и типа, хранимыми в объекте документа, и возвращать адрес элемента.
Сохраните его в mjpTempElement.
Используя указатель нового элемента, вы вызываете функцию-член Draw (), за-
ставляя объект нарисовать себя. Адрес объекта CClientDC передается как аргумент.
Поскольку вы определили функцию Draw () , как виртуальную в базовом классе
CElement, автоматически выбирается версия этой функции для того типа объекта, на
который указывает m_pTempElement. Новый элемент рисуется нормально в режиме
R2_NOTXORPEN, поскольку вы рисуете его первый раз на белом фоне.
Вы можете использовать указатель m_pTempElement в качестве индикатора суще-
ствования предыдущего временного элемента. Код этой части обработчика показан
ниже.
void CSketcherView::OnMouseMove(UINT nFlags, CPoint point)
/ / Определение объекта контекста устройства для представления
CClientDC aDC(this); // DC для данного представления
Рисование в окне 751
aDC. SetROP2 (R2_NOTXORPEN) ; 11 Установить режим рисования
if(nFlags & MK_LBUTTON) // Проверить, что левая кнопка нажата
m_SecondPoint = point;
if (mjpTempElement)
// Сохранить текущую позицию курсора
// Перерисовать старый элемент, чтобы он исчез из представления
m_pTempElement->Draw(&aDC);
delete m_pTempElement;
m_pTempElement =0;
// Удалить старый элемент
// Сбросить указатель в 0
// Создать временный элемент типа и цвета, который
// записан в объекте документа, и нарисовать его
m_pTempElement = CreateElement();
m_pTempElement->Draw (&aDC);
// Создать новый элемент
// Нарисовать элемент}
Предыдущий временный элемент существует, если указатель m_pTempElement не
равен нулю. Вы должны перерисовать элемент, на который он указывает, чтобы уда-
лить его из клиентской области представления. Затем можно удалить элемент из па-
мяти и сбросить указатель в ноль. После этого создается новый элемент и рисуется
кодом, который вы добавили предварительно. Эта комбинация автоматически “растя-
гивает” создаваемую фигуру до позиции курсора мыши в процессе его перемещения.
Важно не забыть сбросить указатель m_pTempElement в ноль в обработчике события
WM—LBUTTONUP после того, как вы создадите финальную версию элемента.
Создание элемента
Вы должны добавить функцию CreateElement (), как protected-член в раздел
операций класса CSketcherView.
class CSketcherView: public CView
{
// Остальная часть определения класса, как раньше...
// Операции
public:
protected:
CElement* CreateElement (void); // Создает новый элемент в куче
// Остальная часть определения класса - как раньше...
Чтобы сделать это, нужно непосредственно обновить определение класса, добавив
выделенную полужирным строку, или же выполнить щелчок правой кнопкой мыши
на имени класса CSketcherView в Class View и выбрать из контекстного меню пункт
Add=>Add Function (Добавить1^Добавить функцию). Это откроет диалог, показанный
на рис. 14.19.
Добавьте спецификацию функции, как показано, и щелкните на кнопке Finish
(Готово). Объявление функции-члена добавится к определению класса, и вы перей-
дете непосредственно к скелету функции в SketcherView. срр. Если вы вручную до-
бавите объявление к определению класса, то должны будете также добавить полное
определение функции в файл . срр. Оно показано ниже.
// Создать элемент текущего типа
CElement* CSketcherView::CreateElement(void)
// Получить указатель на документ для данного представления
CSketcherDoc* pDoc = GetDocument () ;
ASSERT_VALID(pDoc); // Проверить указатель
752 Глава 14
// Теперь выбрать элемент, используя тип, указанный в документе
switch (pDoc->GetElementType ())
case RECTANGLE:
return new CRectangle (m__FirstPoint, m__SecondPoint,
pDoc-XSetElementColor());
case CIRCLE:
return new CCircle (m__Fir st Point, mJSecondPoint,
pDoc->GetElementColor());
return new CCurve (pDoc->GetEleinentColor ());
case LINE:
return new CLine (m_Fir st Point, m__SecondPoint,
pDoc->GetElementColor());
default:
11 Что-то идет не так
AfxMessageBox (__Т ("Неверный код элемента"), МВ_ОК);
AfxAbort();
return NULL;
14.19. Добавление в класс CSketcherView функции CSketcherView
это те, которые добавляются автома-
Строки, не выделенные полужирным
тически, когда вы добавляете функцию к классу, используя пункт меню Add^Add
Function. Первое, что делается здесь — получение указателя на документ вызовом
Get Document (), как было показано ранее. Для целей безопасности применяется
макрос ASSERT_VALID ()
В отладочной версии MFC, которая применяется с отладочной версией вашего при-
;абы убедиться, что возвращен правильный указатель.
ложения, этот макрос вызывает функцию-член объекта AssertValidO , которая спе-
цифицирована как аргумент макроса. Она проверяет корректность текущего объекта,
Рисование в окне 753
и если переданный указатель равен NULL или объект содержит какие-то дефекты, ото-
бражается сообщение об ошибке. В рабочей версии MFC макрос ASSERT VALID () не
делает ничего.
В операторе switch выбирается создаваемый элемент на основе типа, возвра-
щенного функцией класса документа GetDocumentType (). Другая функция в клас-
се документа используется для получения текущего цвета элемента. Вы можете до-
бавить определения обеих этих функций непосредственно в определение класса
CsketcherDoc, поскольку они очень просты.
class CSketcherDoc: public CDocument
// Остальная часть определения класса - как раньше...
// Операции
public:
unsigned int GetElementType()
{ return m_Element; }
COLORREF GetElementColor ()
// Получить тип элемента
// Получить цвет элемента
{ return m_Color; }
// Остальная часть определения класса - как раньше...
Каждая из функций возвращает значение, хранящееся в соответствующем члене
;анных. Помните, что помещение определения функции в определение класса экви-
валентно объявлению функции как inline (встроенной), поэтому наряду с просто-
той, они получаются еще и быстрыми.
Обработка сообщений wm_lbuttonup
Сообщение WM_LBUTTONUP завершает процесс создания элемента. Функция обра-
ботчика этого сообщения заключается в передаче финальной версии созданного эле-
мента объекту документа с последующей очисткой данных-членов объекта представ-
ления. Вы можете обратиться к коду этого обработчика и редактировать его тем же
способом, как делали это раньше. Добавьте следующие строки к функции:
void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point)
// Проверить, есть ли элемент
if (m_pTempElement)
// Вызвать функцию класса документа для сохранения элемента,
// указанного mjpTempElement в объекте документа
delete m_pTempElement;
mjpTempElement = 0;
// Сбросить указатель элемента
Оператор if проверяет, что m_p Temp Element не равен нулю, перед тем, как его
обработать. Всегда существует возможность, что пользователь нажмет и отпустит ле-
вую кнопку мыши без ее перемещения — в этом случае никакой элемент не создается.
Но если элемент существует, указатель на него передается объекту документа; соответ-,
ствующий код будет добавлен в следующей главе. А пока вы просто удаляете элемент
из памяти, чтобы не переполнять кучу. И, наконец, m__pTempElement сбрасывается в
ноль, что готовит его к началу рисования пользователем следующего элемента.
754 Глава 14
Испытание программы Sketcher
Прежде чем запустить пример с обработчиками событий мыши, вы должны обно-
вить функцию OnDraw () в реализации класса CSketcherView, чтобы избавиться от
старого кода, добавленного ранее.
Дабы убедиться, что функция OnDraw () чиста, перейдите в панель Class View
и выполните двойной щелчок на имени функции, чтобы увидеть ее реализацию в
SketcherView.срр. Удалите любой код, который вы добавили, но оставьте первых
четыре строки, предоставленные мастером для получения указателя на объект доку-
мента. Он понадобится вам позднее, чтобы получить элементы, когда они будут хра-
ниться в документе. Код функции должен быть таким:
void CSketcherView::OnDraw (CDC* pDC)
CSketcherDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if(JpDoc)
return;
Поскольку у вас еще нет никаких элементов в документе, пока не нужно ничего
добавлять в эту функцию. Когда в следующей главе мы начнем сохранять данные в до-
кументе, тогда придется добавить код для рисования элементов в ответ на сообщение
WM__PAINT. А пока, как вы увидите, элементы просто будут исчезать при изменении
размера представления.
Запуск примера
После того, как вы сохраните все исходные файлы, вновь соберите программу.
Если при вводе кода не было допущено ошибок, компиляция и компоновка должны
пройти гладко, так что после этого можно будет сразу запустить программу. Типичное
окно, которое вы при этом увидите, показано на рис. 14.20.
Рис. 14.20. Работа программы Sketcher
Рисование в окне 755
Поэкспериментируйте с пользовательским интерфейсом. Обратите внимание,
что вы можете перемещать окно по экрану, и фигурки остаются в нем до тех пор
до тех пор, пока вы не выдвинете их за пределы границ окна приложения. Если это
сделать, элементы не появятся опять при перемещении окна обратно. Когда клиент-
ская область скрывается и открывается, Windows посылает приложению сообще-
ние WM_PAINT, которое инициирует вызов функции-члена объекта представления
OnDraw (). Как вы знаете, функция представления OnDraw () пока ничего не делает.
Это будет исправлено, когда вы будете использовать документ для хранения нарисо-
ванных элементов.
Когда вы изменяете размеры окна, все фигуры немедленно исчезают, но когда вы
перемещаете представление целиком, они остаются (до тех пор, пока не выходят за
границы окна приложения). Почему так происходит? Дело в том, что когда вы из-
меняете размеры окна, Windows делает недействительной всю клиентскую область и
ожидает, что ваше приложение перерисует ее в ответ на сообщение WM PAINT. Если
вы перемещаете представление целиком, Windows заботится о перемещении клиент-
ской области, как она есть. В этом можно убедиться, переместив представление так,
чтобы фигура была скрыта частично. Когда вы возвращаете ее обратно, то по-преж-
нему видите только ту ее часть, которая оставалась видимой — остальное исчезает.
Если попытаться рисовать в процессе перемещения курсора за пределами клиент-
ской области, то обнаружится необычный эффект. Вне окна представления вы теряе-
те слежение за курсором мыши, что разрушает механизм “растягивания” фигуры. Что
же происходит?
Захват сообщений мыши
Проблема вызвана тем фактом, что Windows посылает сообщения мыши тому окну,
которое находится в данный момент под курсором. Как только курсор покидает кли-
ентскую область окна представления вашего приложения, сообщения WM_MOUSEMOVE
поступают в какое-то другое место. Вы можете исправить это, используя некоторый
унаследованный механизм в классе CSketcherView.
Класс представления наследует функцию SetCapture (), которую вы можете вы-
звать для уведомления Windows, что вы хотите, чтобы ваше окно представления по-
лучало все сообщения мыши до тех пор, пока вы это не отмените вызовом другой уна-
следованной функции класса представления — Releasecapture (). Можно захватить
мышь сразу после нажатия левой кнопки, модифицировав обработчик сообщения
WM_LBUT TON DOWN:
// Обработчик нажатия левой кнопки мыши
void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point)
m_FirstPoint = point;
SetCapture();
// Записать позицию курсора
// Перехватывать все последующие сообщения мыши
После этого вы должны вызвать функцию Releasecapture () в обработчике
WM_LBUTTONUP. Если не сделать этого, другие программы не смогут получить ника-
ких сообщений мыши до тех пор, пока выполняется ваша программа. Конечно, от-
пускать мышь следует только после того, как она ранее была захвачена. Функция
GetCapture (), которую наследует ваш класс представления, возвращает указатель на
окно, захватившее мышь, и это дает вам возможность узнать, были ли захвачены со-
общения мыши. Для этого нужно просто добавить следующий оператор в обработчик
WM LBUTTONUP:
756 Глава 14
void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point)
if (this — GetCapture ())
Releasecapture () ; / / Прекратить захват сообщений мыши
// Убедиться в наличии элемента
if (mjoTempElement)
// Этот код временный
// Сбросить указатель на элемент
// Вызвать функцию класса документа для сохранения элемента,
// указанного m_pTempElement в объекте документа
delete m_pTempElenien t ;
m__pTenrpElement = 0;
Если указатель, возвращенный функцией GetCapture (), эквивалентен указателю
this, значит, мышь захвачена вашим представлением, так что вы можете ее освобо-
дить.
И последнее изменение, которое следует провести — модифицировать обработ-
чик WM MOUSEMOVE, чтобы он имел дело только с сообщениями, захваченными вашим
представлением. Это изменение совсем небольшое:
void CSketcherView::OnMouseMove(UINT nFlags, CPoint point)
// Определить объект контекста устройства для представления
CClientDC aDC(this); // DC для данного представления
aDC.SetROP2(R2_NOTXORPEN); // Установить режим рисования
if((nFlags & MK LBUTTON) && (this == GetCapture()))
m SecondPoint = point;
// Сохранить текущую позицию курсора
III'
// Перерисовать старый элемент, чтобы он исчез из представления
mjpTempElement->Draw (&aDC);
delete m__pTempElement ;
m_pTempElement = 0;
// Удалить старый элемент
// Сбросить указатель в О
// Создать временный элемент типа и цвета, записанного
//в объекте документа, и нарисовать его
m__pTempE lenient = CreateElement();// Создать новый элемент
m_jpTempElenient->Draw(&aDC); // Нарисовать элемент
Этот обработчик теперь обрабатывает сообщения только в том случае, если левая
кнопка нажата и ранее был вызван обработчик нажатия левой кнопки вашего пред-
ставления, так что мышь была захвачена окном вашего представления.
Если вы перекомпилируете программу Sketcher с этими дополнениями, то обна-
ружите, что проблемы, возникавшие ранее при выходе курсора за пределы клиент-
ской области, исчезли.
Резюме
После прочтения этой главы у вас должно появиться четкое понимание того, как
писать обработчики сообщений мыши, и как организовать операции рисования в ва-
ших Windows-программах. Ниже перечислены важнейшие моменты, раскрытые в на-
стоящей главе.
Рисование в окне 757
□ По умолчанию Windows обращается к клиентской области окна, используя кли-
ентскую координатную систему с точкой отсчета, находящейся в левом верх-
нем углу клиентской области. Положительное направление оси х идет слева на-
право, а положительное направление оси у — сверху вниз.
□ Вы можете рисовать в клиентской области окна только с использованием кон-
текста устройства.
□ Для обращения к клиентской области окна контекст устройства предоставляет
набор логических координатных систем, называемых режимами отображения.
□ По умолчанию точка начала координат режима отображения находится в ле-
вом верхнем углу клиентской области. По умолчанию используется режим
отображения ММ_ТЕХТ, который предусматривает измерение координат в пик-
селях. Положительная ось х в этом режиме направлена слева направо, а поло-
жительная ось у — сверху вниз.
□ Ваша программа всегда должна рисовать постоянное содержимое клиентской
области окна в ответ на сообщение WM PAINT, хотя временные сущности могут
быть нарисованы и в другие моменты. Все рисование документа вашего прило-
жения должно управляться функцией-членом OnDraw () класса представления.
Эта функция вызывается в ответ на получение вашим приложением сообщения
WM_PAINT.
□ Вы можете идентифицировать часть клиентской области, которую необходимо
перерисовать, вызвав функцию-член InvalidateRect () вашего класса пред-
ставления. Переданная в качестве аргумента область добавляется Windows к
общей области, подлежащей перерисовке на момент отправки следующего со-
общение WM—PAINT вашему приложению.
□ Windows посылает стандартные сообщения вашему приложению для событий
мыши. Вы можете подготовить обработчики, имеющие дело с этими сообще-
ниями, применив для этого мастер создания классов.
□ Можно перенаправить все сообщения мыши вашему приложению, если вызвать
функцию SetCaptire О класса представления. Завершив работу, вы должны
освободить мышь, вызвав функцию Releasecapture (). Если этого не сделать,
другие приложения не смогут получать сообщения мыши.
□ Можно реализовать “растягивание” фигур при создании геометрических сущ-
ностей, перерисовывая их в обработчике сообщений о перемещении мыши.
□ Функция-член класса С DC под названием SetROP2 () позволяет устанавливать
режимы рисования. Выбор правильного режима рисования существенно упро-
щает реализацию операций “растягивания”.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Добавьте пункт меню и кнопку панели инструментов для элемента типа эллипс,
как в упражнениях к главе 13, и определите класс для поддержки рисования эл-
липсов, заданных по двум точкам противоположных углов описанного прямоу-
гольника.
758 Глава 14
2. Какие функции нужно модифицировать для поддержки рисования эллипса?
Внесите изменения программу, чтобы она могла рисовать эллипсы.
3. Какие функции следует модифицировать в примере из предыдущего упражне-
ния, чтобы первая точка определяла центр эллипса, а текущая позиция курсо-
ра — угол описанного прямоугольника? Модифицируйте пример, чтобы он рабо-
тал подобным образом. (Подсказка: изучите члены класса CPoint в справочной
системе.)
Добавьте новое всплывающее меню IDR__SketcherTYPE для задания стиля пера
(Pen Style), чтобы можно было рисовать линии сплошные, пунктирные, точеч-
ные, штрихпунктирные и штрихпунктирные с двойной точкой.
5. Какие части программы должны быть модифицированы для поддержки опера-
ций меню и рисования элементов линиями этих типов?
6. Реализуйте поддержку нового всплывающего меню и рисование элементов ли-
ниями любых типов.
15
Создание документа
и усовершенствование
представления
В этой главе вы познакомитесь со средствами, предоставляемыми MFC для управ-
ления коллекциями элементов данных. Вы используете их для завершения определе-
ния класса и реализации геометрического элемента — кривой линии, которая оста-
лась недоделанной в предыдущей главе. Вы также расширите приложение Sketcher
для хранения данных в документе, и сделаете представление документа более гибким,
добавив несколько новых приемов в процесс.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
□ Коллекции и работа с ними.
□ Как использовать коллекцию для хранения точки кривой.
□ Как использовать коллекцию для хранения данных документа.
□ Как реализовать рисование документа.
□ Как реализовать прокрутку представления.
□ Как создать контекстное меню в месте положения курсора.
□ Как выделить элемент, ближайший к курсору, чтобы позволить пользователю
перемещать и удалять элементы.
□ Как программировать мышь для перемещения и удаления элементов.
760 Глава 15
Что такое классы коллекций?
В связи с особой природой Windows-приложений вам часто понадобится управ-
лять коллекциями элементов данных в условиях, когда вы заранее не знаете, сколько
будет этих элементов, или даже какого типа они будут» Это отчетливо видно на при-
мере приложения Sketcher. Пользователь может нарисовать произвольное количе-
ство элементов, которые могут быть линиями, прямоугольниками, окружностями и
кривыми, причем в любой последовательности. Библиотека MFC предлагает группу
классов коллекций, предназначенных специально для решения проблем подобного
рода. Коллекция — скопление произвольного количества элементов данных, органи-
зованных определенным образом.
Типы коллекций
MFC предоставляет огромное количество классов коллекций для управления дан-
ными. Вы используете на практике лишь пару из них, но будет полезно иметь пред-
ставление и о других доступных классах коллекций. MFC поддерживает три вида
коллекций, отличающихся способом организации данных. Способ организации кол-
лекции называется формой коллекции. Существуют три таких формы, и они пере-
числены в табл. 15.1.
Таблица 15.1. Формы коллекций
Форма
Массив (array)
Список (list)
Как организована информация
10
Массив в данном контексте похож на массив, который вы видели в языке C++. Это
упорядоченная организация элементов, где любой элемент извлекается по цело-
численному значению индекса. Коллекция-массив может автоматически расти, чтобы
вместить больше элементов данных; однако, другие типы коллекций обычно более
предпочтительны, потому что коллекции-массивы могут оказаться относительно мед-
ленными в работе.
Коллекция-список — упорядоченное множество элементов, в котором каждый эле-
мент имеет два ассоциированных с ним указателя, ссылающихся на следующий и
предыдущий элементы списка. Вы уже сталкивались со связным списком в главе 7,
когда речь шла о структурах. Такой список называется двусвязным списком, потому
что его элементы имеют как прямые, так и обратные ссылки. В нем можно выполнять
поиск в обоих направлениях, и, подобно массиву, коллекция-список при необходи-
мости автоматически растет. Коллекция-список проста в использовании и работает
быстро, когда нужно добавлять элементы. Поиск элемента может быть медленным,
если в списке содержится большое количество элементов данных.
Карта (шар) Карта — это неупорядоченная коллекция элементов данных, в которой каждый эле-
мент ассоциируется с ключом, используемым для извлечения элемента из карты.
Обычно ключом служит строка, но это также может быть числовое значение или объ-
ект любого типа. Карты работают быстро при сохранении элементов данных и при их
поиске, поскольку ключи обеспечивают прямой доступ к нужным элементам. Может
показаться, что карта — идеальный выбор, и часто так оно и есть, но при последова-
тельном доступе массивы работают быстрее. Кроме того, существует проблема вы-
бора уникального ключа для каждого элемента в списке.
Классы коллекций MFC предлагают два подхода к реализации каждого типа кол-
лекций. Один подход основан на использовании шаблонов классов и представляет
безопасную к типам обработку данных в коллекции. Безопасная к типам обработка
Создание документа и усовершенствование представления 761
означает, что данные, передаваемые функции-члену класса коллекции, проверяются
на принадлежность к типу, который может быть обработан функцией.
Другой подход предусматривает использование широкого диапазона классов кол-
лекций (вместо шаблонов), но не обеспечивает никакой проверки данных. Если вы
хотите, чтобы ваши классы коллекций были безопасными к типам, то должны вклю-
чить собственный код для обеспечения этого. Такие классы были доступны в старых
версиях Visual C++ под Windows, а шаблонные классы коллекций — нет. Я сосредоточу
внимание на версиях коллекций на базе шаблонов, поскольку они дают лучшие шан-
сы избежать ошибок в приложении.
Безопасные к типам классы коллекций
Безопасные к типам классы коллекций на базе шаблонов поддерживают коллек-
ции объектов любого типа, а также коллекции указателей на объекты любых ти-
пов. Коллекции объектов поддерживаются шаблонными классами CArray, CList и
СМар, а коллекции указателей на объекты — шаблонными классами CTypedPtrArray,
CTypedPtrList и CTypedPtrMap. Мы не будем погружаться в детали всех этих клас-
сов, а рассмотрим только два, которые будут использованы в программе Sketcher.
Один из них вы используете для хранения объектов, а второй — для хранения указате-
лей на объекты, так что получите представление о коллекциях обоих сортов.
Коллекции объектов
Все шаблонные классы для определения коллекций объектов наследуются от клас-
са MFC CObject. Они определены подобным образом, и поэтому наследуют свойства
класса CObject, что особенно удобно по ряду причин. Сюда входит операции фай-
лового ввода и вывода объектов, обычно называемые сериализацией; вы познакоми-
тесь с ней в главе 17.
Эти шаблонные классы могут хранить и управлять объектами любого рода, вклю-
чая базовые типы C++, плюс любые классы и структуры, которые можете определить
вы или кто-нибудь другой. Поскольку эти классы хранят объекты, то всякий раз, когда
вы добавляете элемент в список, массив или карту, объект шаблонного класса должен
выполнить копирование вашего объекта. Следовательно, любой тип класса, который
вы хотите хранить в любой из этих коллекций, должен иметь конструктор копирова-
ния. Конструктор копирования вашего класса используется для создания дубликата
объекта, подлежащего сохранению в коллекции.
Взглянем на общие свойства каждого шаблонного класса, который предоставляет
безопасное к типам управление объектами. Это не будет исчерпывающим перечнем
всех существующих функций-членов Скорее это нужно только для того, чтобы дать
вам достаточное ощущение того, как они работают, дабы вы могли решить — стоит их
использовать или нет. Вы можете получить информацию обо всех функциях-членах,
используя справочную систему по определению шаблонного класса.
Шаблонный класс CArray
Вы можете применять этот шаблон для хранения объектов любого рода в массиве,
автоматически расширяющемся по мере необходимости. Коллекция-массив показана
на рис. 15.1.
Как и в обычных массивах “родного” C++, элементы коллекции-массива индексиру-
ются, начиная с 0. Объявление коллекции-массива принимает два аргумента. Первый
аргумент — тип хранимого объекта, так что, например, если коллекция-массив долж-
762 Глава 15
на хранить объекты типа CPoint, в качестве первого аргумента вы специфицируете
CPoint. Второй аргумент — это тип, используемый в вызовах функции-члена. Чтобы
избежать накладных расходов, связанных с копированием объектов, переданных по
значению, вторым аргументом обычно бывает ссылка, так что пример коллекции-мас-
сива для хранения объектов CPoint, выглядит следующим образом:
CArraycCPoint, CPoint&> PointArray;
Это определяет объект класса коллекции-массива PointArray, который хранит
объекты CPoint. Когда вызываются функции-члены объекта, аргумент является ссыл-
кой, поэтому, чтобы добавить объект CPoint, вы должны записать:
PointArray.Add(aPoint);
и аргумент aPoint передается по ссылке.
Коллекция-массив: CArray<ObjectType, ObjectType&> anArray
Тип сохраняемого
объекта Тип используемого аргумента
Индекс
_ Возвращаемый объект
GetAt(2)
По индексу -
Возвращаемый индекс
Add (AnObject)
Сохраняет объект
Рис. 15.1. Коллекция-массив
Object 1
Object2
Objects
Object4
Objects
Object6
AnObject I
SetSize(5)
> определяет
начальный размер
Расширение
происходит
автоматически
После объявления коллекции-массива важно вызвать функцию-член SetSize (),
чтобы зафиксировать начальное количество элементов, которое вам понадобится
перед тем, как использовать его. Он будет работать, даже если этого и не сделать,
но начальное размещение элементов и последующие расширения будут малыми, что
снижает эффективность операций и требует частого перераспределения памяти для
массива. Начальное количество элементов, которое вы должны указать, зависит от
типичного размера массива, который, как вы ожидаете, вам понадобится, и от того,
в каких пределах будет изменяться его размер. Если, например, вы ожидаете, что ва-
шей программе понадобится минимум 400-500 элементов, с последующим увеличени-
ем до 700-800, то подойдет начальное значение 600.
Чтобы извлечь содержимое элемента, вы можете применить функцию Get At (),
как показано на рис. 15.1. Чтобы сохранить третий элемент PointArray в перемен-
ной aPoint, вы записываете следующим образом:
aPoint = PointArray.GetAt(2);
Создание документа и усовершенствование представления 763
Класс также перегружает операцию [ ], так что вы можете извлечь третий элемент
PointArray с использованием выражения PointArray [2]. Например, если aPoint —
переменная типа CPoint, вы можете написать так:
aPoint = PointArray[2]; // Сохранить копию третьего элемента
Для коллекций-массивов, объявленных без const, эта нотация также может быть
использована вместо функции Set At (), чтобы устанавливать содержимое существую-
щего элемента. Поэтому следующие два оператора эквивалентны:
PointArray.SetAt(3,NewPoint); // Сохранить NewObject в 4-м элементе
PointArray[3] = NewPoint; // То же, что и предыдущая строка кода
Здесь NewPoint — объект типа, использованного в объявлении массива. В обоих
случаях элемент должен уже существовать. Подобным образом нельзя расширить мас-
сив. Для расширения массива необходимо применять функцию Add (), показанную на
диаграмме, которая добавляет новый элемент в массив. Есть еще функция Append ()
для добавления сразу массива элементов в конец данного массива.
Вспомогательные функции
Всякий раз, когда вы обращаетесь к функции-члену коллекции-массива Set Size (),
вызывается глобальная функция ConstructElements () для выделения памяти под то
количество элементов, которое вы хотите изначально поместить в коллекцию-мас-
сив. Это называется вспомогательной функцией (helper function), поскольку ока-
зывает помощь в процессе установки коллекции-массива. Версия по умолчанию этой
функции устанавливает содержимое распределенной памяти в ноль и не вызывает
конструктора класса вашего объекта, так что вы должны предоставить собственную
версию этой вспомогательной функции, если такое действие не подходит к вашим
объектам. Такой случай может произойти, когда место для данных-членов объекта
вашего класса выделяется динамически, или если требуется какая-то другая инициа-
лизация. ConstructElements () также вызывается функцией-членом InsertAt (), ко-
торая вставляет один или более элементов в определенную позицию индекса внутри
массива.
Члены класса коллекции САггау, удаляющие элементы, вызывают вспомогатель-
ную функцию DestructElements (). Версия по умолчанию не делает ничего, так что
если конструирование вашего объекта включает распределение памяти в куче, вы
должны переопределить эту функцию для правильного освобождения памяти.
Шаблон коллекции CList использует вспомогательную функцию при поиске опре-
деленного объекта в содержимом списка. Мы поговорим об этом в следующем раз-
деле. Другая вспомогательная функция, SerializeElements (), используется класса-
ми коллекций массивов, списков и карт, и об этом речь пойдет во время объяснений
того, как можно записать документ в файл.
Шаблонный класс CList
Шаблон коллекции-списка мы рассмотрим более подробно, потому что позднее он
будет применяться в программе Sketcher. Параметры для шаблона класса коллекции
CList — те же самые, что и для шаблона САггау:
CList<Tnn06beKTa, ТипОбъекта&> aList;
При объявлении коллекции-списка вы должны применить два аргумента к шабло-
ну: тип хранимого в коллекции объекта и способ спецификации объекта в аргументах
функций. Пример показывает, что второй аргумент — ссылка, поскольку так делается
764 Глава 15
наиболее часто. Он не обязательно должен быть ссылкой — вы можете применить и
указатель, или даже просто тип объекта (тогда объекты будут передаваться по ссыл-
ке), но это будет медленнее.
Вы можете воспользоваться списком для управления кривыми линиями в програм-
ме Sketcher. Можно объявить коллекцию-список для хранения точек, описывающих
объект-кривую, в следующем операторе:
CList<CPoint, CPoint&> PointList;
Это объявляет список по имени PointList, хранящий объекты типа CPoint, ко-
торые передаются функциям класса по ссылке. Мы вернемся к этому, когда позднее в
этой главе продолжим совершенствовать программу Sketcher.
Добавление элементов в список
Добавление объектов в начало или конец списка осуществляется с использовани-
ем функций-членов AddHead () или AddTail (), как показано на рис. 15.2.
Коллекция-список: CList<ObjectType, ObjectType&> aList
Тип хранимого
объекта Тип используемого аргумента
Сохраняет объект
AddHead (ThisObject)
ThisObject I
указатель указатель
Objectl
указатель указатель
— Object2 —
указатель указатель
— Objects —
указатель указатель
-J Object4
Расширение
происходит
автоматически
указатель указатель
—* ThatObject П
AddTail (ThatObject)
Сохраняет объект
Рис. 15.2. Коллекция-список
На рис. 15.2 показаны прямые и обратные указатели, которые “склеивают” объ-
екты списка вместе. Это внутренние связи, к которым вы не можете получить доступ
непосредственно, но можете делать почти все, что угодно, используя функции, пред-
ставленные в общедоступном интерфейсе класса.
Для добавления объекта aPoint в хвост списка PointList потребуется записать
так:
PointList.AddTail(aPoint); // Добавить элемент в конец
Когда новые элементы добавляются, размер списка автоматически увеличивается.
Создание документа и усовершенствование представления 765
Обе функции — AddHead () и AddTail () — возвращают значение типа POSITION,
которое специфицирует позицию вставляемого объекта в списке. Способ использова-
ния переменной типа POSITION показан на рис. 15.3.
Коллекция-список: CList<ObjectType, ObjectType&> aList
Тип хранимого
объекта
Тип используемого аргумента
Позиция конкретного
элемента задается
значением типа POSITION
ThisObject
указатель
Возвращает Object 1
Увеличивает aPos
Object2
Objects
• Objectl ! GetNext(aPos) извлекает
элемент в aPos и
устанавливает aPos
на следующий элемент
Object4
указатель
’— ThatObject
Рис. 15.3. Использование переменной типа POSITION
Вы можете использовать тип POSITION для извлечения объекта в заданной пози-
ции списка с помощью функции GetNext (). Обратите внимание, что вы не можете
применять арифметические действия к значениям типа POSITION — вы можете толь-
ко модифицировать значение позиции через функции-члены объекта списка. Более
того, вы не можете установить значение позиции в определенное число. Переменные
POSITION могут устанавливаться только функциями-членами списка.
Наряду с возвратом объекта функция GetNext () увеличивает переменную по-
зиции, переданной ей, так что она после этого указывает на следующий объект в
списке. Вы можете, таким образом, использовать повторные вызовы GetNext () для
пошагового перебора списка — элемент за элементом. Переменная позиции устанав-
ливается в NULL, если вы используете GetNext () для извлечения последнего объекта
в списке, так что вы можете применять это для управления операцией цикла. Всегда
следует убеждаться в том, что вы имеете правильное значение позиции, когда вызы-
ваете функцию-член объекта списка.
Имея значение POSITION, можно вставить элемент в определенную позицию спи-
ска. Чтобы вставить объект ThePoint в список PointList непосредственно перед
элементом в позиции aPosition, используйте следующий оператор:
PointList.InsertBefore(aPosition, ThePoint);
Функция InsertBefore () также возвращает позицию нового объекта. Для вставки
элемента после объекта из заданной позиции предусмотрена функция InsertAfter ().
Эти функции часто применяются со списком, содержащим отображаемые геометри-
ческие элементы. Элементы рисуются на экране в последовательности, в которой вы-
766 Глава 15
полняется проход по списку. Элементы, появившиеся в списке позднее, перекрывают
помещенные ранее элементы, так что порядок элементов определяет, как они пере-
крывают друг друга. Таким образом, вы можете определить, какие из существующих
элементов перекрывают новый элемент, поместив его в соответствующую позицию
списка.
Когда вам нужно установить существующий объект списка с определенное зна-
чение, вы можете использовать функцию Set At (), если знаете значение позиции
объекта:
PointList.SetAt(aPosition, aPoint);
У этой функции нет возвращаемого значения. Вы должны обеспечить правиль-
ность передаваемого ей значения POSITION. Неправильное значение вызовет ошиб-
ку. Поэтому вы должны передавать этой функции только такое значение POSITION,
которое было возвращено одной из прочих функций-членов, и проверять, что оно не
равно NULL.
Итерация по списку
Если вы хотите получить значение POSITION начала или конца списка, то для это-
го класс представляет функции-члены GetHeadPosition () и GetTailPosition ().
Начиная со значения POSITION головы списка, вы можете выполнить итерацию по
всем его элементам, вызывая Get Next () до тех пор, пока не будет получено значение
позиции NULL. Вот типичный код для выполнения этого для списка объектов типа
CPoint, объявленного ранее:
CPoint CurrentPoint(0, 0);
// Получить позицию первого элемента
POSITION aPosition = PointList.GetHeadPosition();
while(aPosition) // Выполнять цикл, пока aPosition не равно NULL
CurrentPoint = PointList.GetNext(aPosition);
// Обработать текущий объект...
Можно пройти этот список и в обратном направлении, используя другую функ-
цию-член, GetPrev (), которая извлекает текущий объект, а затем перемещает инди-
катор позиции на шаг назад. Конечно, в данном случае вы должны начать с вызова
GetTailPosition().
После того, как вы узнали значение позиции объекта в списке, вы можете извлечь
объект функцией-членом Get At (). Вы специфицируете в качестве аргумента значе-
ние позиции, и соответствующий объект возвращается. Неверное значение позиции
вызывает ошибку.
Поиск в списке
Вы можете найти позицию элемента, хранящегося в списке, используя функцию-
член Find():
POSITION aPosition = PointList.Find(ThePoint);
Этот вызов осуществляет поиск объекта, специфицированного в аргументе, вы-
зывая глобальную функцию CompareElements () для сравнения объектов в списке
с аргументом. Это вспомогательная функция — одна из тех, о которых упоминалось
ранее, — предназначенная для обслуживания процесса поиска. Реализация этой функ-
ции по умолчанию сравнивает адрес аргумента с адресом каждого объекта в списке.
Это подразумевает, что для того, чтобы поиск был успешным, аргумент должен быть
Создание документа и усовершенствование представления 767
в действительности элементом списка, а не копией. Если объект найден в списке, воз-
вращается позиция элемента. Если он не найден, возвращается NULL. Вы можете спе-
цифицировать второй аргумент, указывающий значение позиции, с которой следует
начинать поиск.
Если вы хотите искать в списке объект, который эквивалентен другому объекту, то
должны будете реализовать собственную версию CompareElements (), выполняющую
правильное сравнение. Шаблон функции имеет следующую форму:
template<class TYPE, class ARG_TYPE> BOOL CompareElements(
const TYPE* pElementl, const ARG_TYPE* pElement2);
Здесь Elementl и Element2 — это указатели на сравниваемые объекты. Для объ-
екта класса коллекции PointList прототип функции, сгенерированной шаблоном,
должен быть таким:
BOOL CompareElements(CPoint* pPointl, CPoint* pPoint2);
Чтобы сравнить объекты CPoint, вы можете реализовать это следующим обра-
зом:
BOOL CompareElements(CPoint* pPointl, CPoint* pPoint2)
{ return *pPointl == *pPoint2; }
Здесь используется функция operator== () , реализованная в классе CPoint.
Вообще в данном контексте вы должны реализовать функцию operator==() для ва-
шего собственного класса, чтобы потом использовать ее в реализации вспомогатель-
ной функции CompareElements ().
Вы можете также получить позицию элемента в списке, используя значение индек-
са. Индекс работает точно таким же образом, как и с массивом, причем первый эле-
мент находится в позиции с индексом 0, второй — с индексом 1 и так далее. Функция
Findindex () принимает значение индекса типа int в качестве аргумента и возвраща-
ет значение типа POSITION для объекта, находящегося в списке в указанной позиции
индекса. Если вы собираетесь использовать значение индекса, то, скорее всего, захо-
тите узнать количество объектов в списке. Функция GetCount () поможет в этом:
int ObjectCount = PointList.GetCount();
Здесь целочисленное значение счетчика элементов в списке сохраняется в пере-
менной ObjectCount.
Удаление объектов из списка
Вы можете удалить первый элемент списка, используя функцию-член Remove Не ad ().
Эта функция возвратит объект, который станет новым началом списка. Чтобы уда-
лить последний объект, можно использовать функцию RemoveTail (). Обе эти функ-
ции требуют наличия в списке хотя бы одного объекта, поэтому сначала вы должны
использовать функцию IsEmpty () для проверки, что список не пуст. Например:
if(!PointList.IsEmpty())
PointList.RemoveHead();
Функция IsEmpty () возвращает TRUE, если список пуст, и FALSE — в противном
случае.
Если вы знаете значение позиции объекта, который нужно удалить из списка, то
можете сделать это непосредственно:
PointList.RemoveAt(aPosition);
768 Глава 15
У этой функции нет возвращаемого значения. Ответственность за правильность
значения позиции, переданной ей в аргументе, возлагается на вас. Если необходимо
удалить все содержимое списка, используется функция-член RemoveAll ():
PointList .RemoveAll ();
Эта функция также освобождает память, выделенную ранее для элементов списка.
Вспомогательные функции списка
Вы уже видели ранее, как вспомогательная функция CompareElements () использу-
ется функцией Find () для списка. Две глобальные функции — ConstructElements ()
и DestructElements () — также применяются членами шаблонного класса CList. Это
шаблонные функции, объявленные с типом объекта, специфицированным в вашем
объявлении класса CList. Шаблонные прототипы этих функций показаны ниже.
template< class TYPE > void ConstructElements(TYPE* pElements, int nCount);
template< class TYPE > void DestructElements(TYPE* pElements, int nCount);
Чтобы получить функцию, специфичную для вашей коллекции-списка, просто под-
ключите тип хранимого объекта. Например, прототипы для класса PointList будут
такими:
void ConstructElements(CPoint* pPoint, int PointCount);
void DestructElements(CPoint* pPoint, int PointCount);
Обратите внимание, что параметры здесь являются указателями. Ранее упоми-
налось, что аргументы функций-членов PointList должны быть ссылками, но это
не касается вспомогательных функций. Параметры обеих функций одинаковы: пер-
вый — указатель на массив объектов CPoint, а второй — количество объектов в этом
массиве.
Функция ConstructElements () вызывается всякий раз, когда вы вводите объект
в список, а функция DestructElements () вызывается при его удалении. Как и для
шаблонного класса САггау, вы должны реализовать свои версии этих функций, если
операция по умолчанию не подходит для объектов вашего класса.
Шаблонный класс СМар
Благодаря способу их работы, карты особенно подходят для приложений, в кото-
рых объекты имеют уникальные ключи, ассоциированные с ними, такие как класс за-
казчика, в котором каждый заказчик имеет ассоциированный с ним номер, или имя и
класс адреса, где имя может быть использовано в качестве ключа. Организация карты
показана на рис. 15.4.
Карта сохраняет комбинацию объекта и ключа. Ключ используется для определе-
ния, где именно в блоке памяти карты выделено место для хранения объекта. Таким'
образом, ключ предоставляет средство прямого обращения к хранимому объекту, до
тех пор, пока ключ уникален. Процесс преобразования ключа в целое число, кото-
рое может быть использовано для вычисления адреса сущности в карте, называется
хешированием.
Процесс хеширования, примененный к ключу, производит целое число, называе-
мое значением хеша. Это значение хеша обычно используется в качестве смещения
от базового адреса для определения места сохранения ключа и ассоциированного с
ним объекта в карте. Если память, выделенная памяти, расположена по адресу Base,
и каждое вхождение требует Length байт, то это вхождение порождает значение
хеша HashValue, сохраняемое по адресу Base+HashValue*Length.
Создание документа и усовершенствование представления
769
Коллекция-карта: СМар<КеуТуре, КеуТуре&, ObjectType, ObjectType&> аМар
Тип аргумента ключа t \ Тип объекта аргумента
Тип аргумента ключа Тип сохраняемого объекта
Рис. 15.4. Коллекцшисарта
Процесс хеширования может и не произвести уникального значения хеша по клю-
чу, в этом случае элемент — ключ вместе с ассоциированным объектом — вводится и
связывается с элементом или элементами, сохраненными ранее и имеющими то же
значение ключа (часто в виде списка). Конечно, чем меньше сгенерировано уникаль-
ных хеш-значений, тем менее эффективен процесс извлечения из карты, поскольку
поиск требует просмотра всех элементов, имеющих одно и то же значение хеша.
Для объявления карты требуется пять аргументов:
CMap<LONG,. LONG&, CPoint, CPoint&> PointMap;
Первые два специфицируют тип ключа и способ передачи его в аргументе.
Обычно он передается по ссылке. Вторая пара аргументов специфицирует тип объ-
екта и способ его передачи в аргументе, как было показано ранее.
Вы можете сохранить объект в карте с помощью операции [ ] (см. рис. 15.4). Для
сохранения объекта вы можете также воспользоваться функцией-членом Set At (),
передав ей значение ключа и объект в качестве аргументов. Следует отметить, что
вы не можете применять операцию [ ] в правой части операции присваивания для
извлечения объекта, поскольку эта версия операции в классе не реализована.
Чтобы извлечь объект, используйте функцию Lookup (), показанную на рис. 15.4.
Это извлечет объект, соответствующий указанному ключу; функция возвращает TRUE,
если объект найден, и FALSE — в противном случае. Вы можете также выполнить ите-
рацию по всем объектам в карте, применив переменную типа POSITION, хотя после-
довательность извлечения объектов при этом не связана с последовательностью их
добавления в карту. Это объясняется тем, что объекты сохраняются в карте в места,
определяемые значением хеша, а не последовательностью их ввода.
770 Глава 15
Вспомогательные функции, использованные СМар
Наряду с вспомогательными функциями, которые мы обсуждали в контексте масси-
вов и списков, коллекции-карты также используют глобальную функцию HashKey (),
определенную следующим шаблоном:
template<class ARG_KEY>
UINT HashKey (ARGJKEY key);
Эта функция преобразует значение ключа в хеш-значение типа UINT, что эквива-
лентно unsigned int. Версия по умолчанию делает это простым сдвигом значения
ключа вправо на 4 бита. Если операция по умолчанию не отвечает вашим требовани-
ям, вы должны реализовать собственную версию этой функции.
Существуют разные приемы хеширования, которые варьируются в зависимости от
используемого в качестве ключа типа данных, и множество элементов, которые вы,
вероятно, захотите сохранять в карте. Вероятное количество элементов, подлежащих
сохранению, указывает количество уникальных хеш-значений, которые вам понадо-
бятся. Общий метод хеширования значения числового ключа — вычислять хеш-значе-
ние как остаток от деления ключа на N( модуль N), где 7V— количество различных зна-
чений, которые вам нужны. По причинам, которые слишком долго объяснять здесь,
чтобы это хорошо работало, Nдолжно быть простым числом. Не кажется ли вам, что
программа вычисления простых чисел из главы 4 теперь как раз пригодится?
Чтобы объяснить принцип используемого здесь механизма, рассмотрим простой
пример. Предположим, вы собираетесь хранить 100 разных элементов в карте, ис-
пользуя значение ключа Key. Вы можете хешировать ключ следующим оператором:
HashValue = Кеу%101;
В результате значения HashValue будут находиться в пределах между 0 и 100,
что как раз то, что вам нужно для вычисления адреса элемента. Если предположить,
что ваша карта находится в том же месте памяти — Base, а память, необходимая для
хранения объекта вместе с ключом составляет Length байт, то вы можете хранить
элемент, производящий хеш-значение HashValue в месте Base+HashValue*Length.
С процессом хеширования, упомянутым ранее, вы можете разместить до 101 элемен-
тов в уникальных позициях карты.
Когда ключом служит символьная строка, процесс хеширования несколько слож-
нее, в частности, с длинными строками или строками разной длины; однако обыч-
но используемый метод включает применением числовых величин, произведенных
от символов строки. Обычно это предусматривает назначение числового значения
каждому символу, так что если ваши строки состоят из прописных букв и пробелов,
вы можете присвоить каждому символу значение между 0 и 26, причем пробелу будет
соответствовать 0, а— 1, Ь— 2 и так далее. Таким образом, строка может трактовать-
ся как представление числа к некоторой базе, скажем, 32. Числовое значение строки
“fred”, например, будет таким:
6 * 323 + 18 * 322
5*321
4*320
Предполагая, что вы собираетесь хранить 500 строк, хешированное значение клю-
ча можно вычислить следующим образом:
6 * 323 + 18 * 322 + 5 * 321 + 4 * 320 mod 503
Значение 503 для N— минимальное простое число, большее, чем вероятное коли-
чество хранимых элементов. База, выбранная для вычисления хеш-значения строки,
обычно является степенью 2 и соответствует минимальному значению, большему или
Создание документа и усовершенствование представления 771
равному количеству возможных различных символов в строке. Для длинных строк
это может генерировать очень большие числа, так что применяются специальные
приемы для вычисления значения модуля N. Подробное обсуждение этих приемов
выходит за рамки настоящей книги, но вы можете найти множество Web-ссылок, за-
пустив поиск по слову “хеширование”.
Типизированные коллекции указателей
Шаблонные классы типизированных коллекций указателей предназначены для
хранения указателей вместо самих объектов. Это основное отличие между этими
шаблонными классами и шаблонными классами, описанными выше. Рассмотрим
применение шаблонного класса CTypedPtrList, поскольку именно его вы будете ис-
пользовать в качестве основы для управления элементами вашего класса документа
CSketcherDoc.
Шаблонный класс CTypedP trLi s t
Объявить типизированный класс списка можно с помощью оператора следующего
вида:
CTypedPtrList<BaseClass, Туре*> ListName;
Первый аргумент специфицирует базовый класс, который должен быть одним из
классов списков указателей, определенных в MFC — CObList или CPtrList. Выбор
зависит от того, как определен класс ваших объектов. При использовании класса
CObList создается список, поддерживающий указатели на объекты, унаследованные
от CObject, в то время как CPtrList поддерживает указатели void*. Поскольку эле-
менты в примере Sketcher в качестве базового класса имеют CObject, рассмотрим
применение CObList.
Второй аргумент шаблона — тип указателей, которые должны храниться в списке.
В данном примере это будет CElement*, поскольку все ваши фигуры имеют CElement
в качестве базового класса, a CElement — производный от CObject. Таким образом,
объявление класса для хранения фигур будет таким:
CTypedPtrList<CObList, CElement*> m_ElementList;
Можно было бы использовать типы CObList* для хранения указателей на эле-
менты, но тогда список смог бы хранить объекты любых классов, унаследованных от
CObject. Объявление m_ElementList гарантирует, что можно будет хранить только
объекты класса CElement. Это обеспечит программе повышенный уровень безопас-
ности.
Операции CTypedPtrList
Функции, представленные в классах, базирующихся на CTypedPtrList, подоб-
ны тем, что поддерживает CList, конечно, за исключением того, что все операции
выполняются над указателями на объекты, а не над самими объектами, так что их
следует различать. Эти функции можно разделить на две группы: те, что определе-
ны в CTypedPtrList, и те, что унаследованы от базового класса — в данном случае,
CObList.
Функции, определенные в CTypedPtrList, описаны в табл. 15.2.
Функции CTypedPtrList, унаследованные от CObList, перечислены в табл. 15.3.
772 Глава 15
Таблица 15.2. Функции, определенные в CTypedPtrList
Функция Описание
GetHead()
GetTaiK)
RemoveHead()
RemoveTail()
Возвращает указатель на начало списка. Прежде чем вызывать эту функцию, необ-
ходимо с помощью функции isEmpty О убедиться, что список не пуст.
Возвращает указатель на конец списка. Прежде чем вызывать эту функцию, необ-
ходимо с помощью функции IsEmpty () убедиться, что список не пуст.
Удаляет первый указатель в списке. Прежде чем вызывать эту функцию, необходи-
мо с помощью функции IsEmpty () убедиться, что список не пуст.
Удаляет последний указатель в списке. Прежде чем вызывать эту функцию, необхо-
димо с помощью функции IsEmpty () убедиться, что список не пуст.
GetNext () Возвращает указатель на позицию, которая задана переменной типа position,
переданной в аргументе-ссылке. Переменная обновляется для указания на следу-
ющий элемент в списке. Когда достигнут конец списка, переменная позиции уста-
навливается в null. Эта функция может быть использована для итерации в прямом
направлении по всем указателям списка.
GetPrev ()
GetAt()
Возвращает указатель на позицию, которая задана переменной типа position,
переданной в аргументе-ссылке. Переменная обновляется для указания на пред-
ыдущий элемент списка. Когда достигается начало списка, переменная позиции
устанавливается в null. Эта функция может быть использована для итерации в об-
ратном направлении по всем указателям списка.
Возвращает указатель, хранящийся в позиции, которая задана переменной типа
position, переданной в аргументе, причем переменная не изменяется. Поскольку
функция возвращает ссылку, до тех пор, пока список не определен как const, эта
функция может использоваться в левой части операции присваивания для модифи-
кации элемента списка. _____ ____
Таблица 15.3. Функции CTypedPtrList, унаследованные от CQbList
Функция Описание
AddHead () Добавляет указатель, переданный в аргументе, в начало списка и возвращает
значение типа position, соответствующее новому элементу. Существует другая
версия этой функции, добавляющая в начало списка другой список.
AddTail () Добавляет указатель, переданный в аргументе, в конец списка и возвращает зна-
чение типа position, соответствующее новому элементу. Существует другая вер-
сия этой функции, добавляющая в конец списка другой список.
RemoveAll () Удаляет все элементы из списка. Обратите внимание, что это не уничтожает объ-
ектов, на которые указывали элементы списка. Вы должны позаботиться об этом
самостоятельно.
GetHeadPosition()
GetTailPosition()
SetAt()
RemoveAt()
InsertBefore()
Возвращает позицию элемента в начале списка.
Возвращает позицию элемента в конце списка.
Устанавливает указатель, специфицированный вторым аргументом, в позицию списка,
определенную первым аргументом. Неверное значение позиции вызывает ошибку.
Удаляет указатель из позиции списка, специфицированной аргументом типа
position. Неверное значение позиции вызывает ошибку.
Вставляет новый указатель, специфицированный вторым аргументом, перед по-
зицией, специфицированной первым аргументом. Возвращается позиция нового
элемента.
insertAfter () Вставляет новый указатель, специфицированный вторым аргументом, после позиции,
специфицированной первым аргументом. Возвращается позиция нового элемента.
Find () Ищет указатель в списке, идентичный заданному в аргументе. Если найден, воз-
вращает его позицию, в противном случае — null.
FindNext()
GetCount()
IsEmpty ()
Возвращает позицию указателя в списке, специфицированную целочисленным ар-
гументом — индексом, начинающимся с нуля.
Возвращает количество элементов в списке.
Возвращает true, если в списке нет элементов, и false — в противном случае.
Создание документа и усовершенствование представления 773
Некоторые из этих функций-членов вы увидите в действии в настоящей главе
чуть позже — в контексте реализации класса документа для программы Sketcher.
Использование шаблонного класса CList
Вы
можете использовать
:аблон коллекции CList в определении объекта кривой
и
нашего приложения Sketcher. Кривая определяется двумя или более точками, так
что сохранение их в списке должно быть удобным способом управления ими. Сначала
вы должны определить класс коллекции CList как член класса CCurve. Вы исполь-
зуете эту коллекцию для хранения точек. Выше мы с вами уже в некоторых деталях
изучили шаблонный класс CList, так что это должно быть не сложно.
Шаблонный класс CList имеет два параметра, так что общая форма объявления
класса коллекции этого типа выглядит следующим образом:
CList<YourObjectType, FunctionArgType> ClassName;
Первый аргумент, YourObj ectType, специфицирует тип объектов, которые вы со-
бираетесь хранить в списке. Второй аргумент указывает тип аргумента для исполь-
зования в функциях-членах класса коллекции при ссылках на объект. Обычно это —
ссылка на тип объекта, чтобы минимизировать копирование аргументов при вызову
функции. Объявим объект класса коллекции, подходящего для наших нужд, в классе
CCurve:
class CCurve: public CElement
// Остальная часть определения класса...
protected:
CCurve(void); // Конструктор по умолчанию — не должен использоваться
CList<CPoint, CPoint&> m PointList; // Безопасный к типам список указат
Вы можете либо добавить это вручную в определение класса, либо использовать
пункт меню Add^Add Variable (Добавить1^Добавить переменную), находясь на вкладке
Class View (Представление классов). Я пропустил здесь остальную часть определения
класса, потому что сейчас мы ее не рассматриваем. Объявление коллекции выделено
полужирным. Она объявляет коллекцию m_PointList, хранящую объекты CPoint в
списке, и ее функции используют ссылочные аргументы на объекты CPoint.
Класс CPoint не выделяет память динамически, так что вы не должны реализо-
вывать ConstructElements () или DestructElements О , и поскольку вам не нужна
функция Find (), можно также забыть о CompareElements ().
Рисование кривой
Рисование кривой отличается от рисования линии или окружности. В случае ли-
нии или окружности, когда вы перемещаете курсор с нажатой левой кнопкой мыши,
то создаете последовательность разных элементов — линий или окружностей, которые
разделяют общую начальную точку, то есть точку, в которой была нажата левая кнопка
мыши. Но иначе обстоят дела при рисовании кривой, что и показано на рис. 15.5.
Когда вы перемещаете курсор при рисовании кривой, то не создаете последова-
тельность новых кривых, а расширяете одну и ту же кривую, так что каждая после-
дующая точка добавляет к определению кривой новый сегмент. Поэтому вы должны
создать объект кривой, как только получите две точки от сообщений WM_LBUTTONDOWN
и WM MOUSEMOVE.
774 Глава 15
Рисование кривой в режиме отображения ММ_ТЕХТ
Рис. 15.5. Рисование кривой
Точки, определенные последовательностью сообщений о перемещении мыши,
определяют дополнительные сегменты к существующему объекту кривой. Вам пона-
добится добавить функцию AddSegment () к классу С Curve для расширения кривой
после ее создания конструктором.
Следующий момент, который нужно рассмотреть — это каким образом вычислять
описанный прямоугольник. Это определяется получением пары минимальных зна-
чений х и у для левого верхнего угла и пары максимальных значений — для правого
нижнего угла. Это потребует прохода по всем точкам в списке. Таким образом, вы
будете вычислять описывающий прямоугольник инкрементным образом, в функции
AddSegment (), при добавлении точек к кривой.
Определение класса CCurve
С добавленным конструктором и функцией AddSegment () полное определение
класса CCurve будет таким:
class CCurve: public CElement
public:
-CCurve(void);
virtual void Draw(CDC* pDC); // Функция для отображения кривой
// Конструктор объекта кривой
CCurve (CPoint FirstPoint, CPoint SecondPoint, COLORREF aColor) ;
void AddSegment (CPoint& aPoint);
// Добавить сегмент к кривой
protected:
CCurve (void); // Конструктор по умолчанию — не должен использоваться
CList<CPoint, CPoint&> m_PointList; // Безопасный к типам список указателей
Вы должны модифицировать определение класса в Elements .h в соответствии
с приведенным кодом. Конструктор принимает в качестве параметра две точки и
цвет, так что он определяет кривую из одного сегмента. Он вызывается в функции
CreateElement (), вызванной, в свою очередь, из обработчика OnMouseMove () клас-
са представления, когда сообщение WM_MOUSEMOVE впервые принимается для кри-
Создание документа и усовершенствование представления 775
вой, так что не забудьте модифицировать определение функции Cr eat eElement ()
в CSketcherView для вызова конструктора класса CCurve с правильными аргумен-
тами. Оператор, использующий конструктор CCurve в операторе switch функции
CreateElement (), должен быть изменен:
case CURVE:
return new CCurve (m_FirstPoint, m_SecondPoint, pDoc->GetElementColor f));
После вызова конструктора все последующие сообщения WM__MOUSEMOVE должны
инициировать вызов функции AddSegment () для добавления сегмента к существую-
щей кривой, как показано на рис. 15.6.
OnMouseMovef) вызывает
AddSegment() с точками х_,у
OnMouseMovef) вызывает
CreateElement(), которая
вызовет конструктор
с точками х1,у1 и х^
OnLbuttonDownf)
сохраняет точку х, ,у
Рис. 15.6. Последовательность вызовов обработчиков сообщений при рисовании кривой
OnMouseMovef) вызывает
AddSegmentQ с точками х.,у
OnMouseMovef) вызывает
AddSegmentQ с точками x10,y10
Вызывается OnLButtonUpf)
Здесь показана полная последовательность вызовов обработчиков сообщений для
кривой, состоящей из девяти сегментов. Последовательность обозначена пронумеро-
ванными стрелками. Код функции OnMouseMOve () в CSketcherView должен быть об-
новлен следующим образом:
void CSketcherView::OnMouseMove (UINT nFlags, CPoint point)
CClientDC aDC(this); // Контекст устройства для текущего представления
if((nFlags&MK_LBUTTON)&&(this==GetCapture()))
m_SecondPoint « point; // Сохранить текущую позицию курсора
if(m_jpTempElement)
if (CURVE = GetDocument()->GetElementTypef)) // Это кривая?
{ // Рисуем кривую, поэтому добавляем
// сегмент к существующей кривой
static__cast<CCurve*> (mjpTenp&lenient) ->AddSegmant (mjSecondPoint);
m_pTempElement->Draw(&aDC); //Теперь рисуем ""
return; // Готово
aDC. SetROP2 (R2_NOTXORPEN); / / Установить режим рисования
// Перерисовать старый элемента, чтобы он исчез из представления
m__pTempElement->Draw(&aDC);
delete m_pTempElement;
m_pTempElement = 0;
// Удалить старый элемент
// Сбросить указатель в О
776 Глава 15
// Создать элемент по типу и цвету,
// записанному в объекте документа
m_pTempElement = CreateElement();
m_pTempElement->Draw(&aDC);
Вы должны трактовать элемент типа CURVE как специальный случай после его
создания, поскольку при всех последующих вызовах обработчика OnMouseMove () вы
должны вызывать функцию AddSegment () для существующего элемента вместо кон-
струирования нового взамен старого. Вам не нужно устанавливать режим рисования
в этом экземпляре, поскольку нет необходимости каждый раз стирать предыдущую
версию кривой. Поэтому вы перемещаете вызов SetROP2 () в позицию, находящуюся
после кода, обрабатывающего кривую.
Добавление сегмента кривой и рисование расширенной кривой выполняется вну-
три добавленного блока i f. Обратите внимание, что вы должны привести указатель
m_pTempElement к типу CCurve*, чтобы использовать его для вызова AddSegment ()
со старым элементом, потому что AddSegment () — не виртуальная функция. Если вы
не добавите приведение, то получите ошибку, потому что компилятор попытается
разрешить этот вызов статически, как член класса CElement.
Реализация класса CCurve
Напишем код конструктора; он должен быть добавлен к Elements. срр вместо вре-
менного конструктора, использованного в предыдущей главе. Конструктор должен со-
хранять две точки, переданные ему в аргументах, в члене данных m_PointList типа
CList:
CCurve:’.CCurve (CPoint FirstPoint,CPoint SecondPoint, COLORREF aColor)
m_PointList.AddTail(FirstPoint); // Добавить первую точку в список
m_PointList.AddTail(SecondPoint); // Добавить вторую точку в список
m_Color = aColor; // Сохранить цвет
m_Pen = 1; // Установить ширину пера
// Сконструировать описывающий прямоугольник, предполагая режим ММ_ТЕХТ
m_EnclosingRect = CRect(FirstPoint, SecondPoint);
m_EnclosingRect.NormalizeRect();
Точки добавляются к списку m_PointList вызовами функции AddTail () — члена
шаблонного класса CList. Эта функция добавляет копию точки, переданной в виде
аргумента, в конец списка. Описывающий прямоугольник определяется точно таким
же образом, как это делается для линии.
Далее вы можете добавить функцию AddSegment () к Elements. срр. Эта функция
вызывается, когда фиксируются дополнительные точки кривой, после того, как созда-
на первая версия объекта кривой. Эта функция-член очень проста:
void CCurve::AddSegment(CPoint& aPoint)
m_PointList.AddTail(aPoint); // Добавить точку в конец
// Модифицировать описывающий прямоугольник для новой точки
m_EnclosingRect = CRect(min(aPoint.x,
min(aPoint.y,
max(aPoint.x,
max(aPoint.y,
m_EnclosingRect.left),
m_EnclosingRect.top),
m_EnclosingRect.right),
m EnclosingRect.bottom));
Создание документа и усовершенствование представления 777
Используемые здесь функции min () и max () — это стандартные макросы, эквива-
лентные применению условных операций для выбора минимума и максимума из двух
значений. Новая точка добавляется в конец списка точно так же, как в конструкторе.
Важно, чтобы каждая новая точка добавлялась в список способом, согласованным с
конструктором, поскольку вы рисуете сегменты, используя точки в последовательно-
сти — от начала до конца списка. Каждый сегмент линии рисуется от конечной точки
предыдущей линии к новой точке. Если точки располагаются в неправильной после-
довательности, сегменты линий не будут правильно направлены. После добавления
новой точки описанный прямоугольник переопределяется с учетом координат этой
новой точки.
Последняя функция-член, которую потребуется определить для интерфейса класса
CCurve — это Draw ():
void CCurve::Draw(CDC* pDC)
// Создать перо для этого объекта, инициализировать
// его цветов объекта и линией шириной в 1 пиксель
СРеп аРеп;
if(’аРеп.CreatePen(PS_SOLID, m_Pen, m_Color))
// Создать перо не удалось. Закрыть программу
AfxMessageBox(_Т(”Не удалось создать перо для рисования кривой”), МВ_ОК);
AfxAbort ();
СРеп* pOldPen « pDC->SelectObject(&аРеп); // Выбрать перо
// Теперь нарисовать кривую.
/ / Получить позицию первого элемента в списке
POSITION aPosition = m__Point!ist .GetHeadPosition () ;
// Если она в порядке, перейти в эту точку
if(aPosition)
pDC~>MoveTo(m_PointList.GetNext(aPosition));
// Нарисовать сегменты для каждой последующей точки
while(aPosition)
pDC->LineTo(m_PointList.GetNext(aPosition) );
pDC->SelectObject(pOldPen); // Восстановить старое перо
Вы рисуете объект CCurve, выполняя итерацию по всем точкам в списке от нача-
ла, по мере прохода рисуя каждый сегмент. Вы получаете значение POSITION перво-
го элемента, используя функцию GetHeadPosition (), затем вызываете MoveTo () для
установки первой точки в качестве текущей в контексте устройства. Затем вы рисуете
сегменты линии в цикле while — до тех пор, пока aPosition не равно NULL. Вызов
функции GetNext (), который появляется как аргумент функции LineTo (), возвраща-
ет текущую точку с последующим увеличением aPosition для ссылки на следующую
точку в списке.
Испытание класса CCurve
С добавлением описанных изменений к программе Sketcher вы реализуете
весь код, необходимый для рисования элементов-фигур, перечисленных в меню.
Выполните сборку программы Sketcher и запустите ее. Теперь вы сможете рисо-
вать кривые в любом из четырех цветов. Типичное окно приложения показано на
рис. 15.7.
778 Глава 15
Рис, 15.7. Работа программы Sketcher после добавления возмож-
ности рисования кривых
Конечно, подобно другим элементам, которые вы можете нарисовать, кривые
пока еще не будут постоянными. Как только приложению будет отправлено сообще-
ние WM_PAINT, например, по причине изменения размера окна, кривые исчезнут.
После того, как вы сохраните их в объекте документа для приложения, они станут
более постоянными, поэтому перейдем к следующей теме.
Создание документа
Документ в приложении Sketcher должен иметь возможность сохранять соответ-
ствующую коллекцию линий, прямоугольников, окружностей и кривых в любой по-
следовательности, и замечательным средством для этого является список. Поскольку
все классы элементов, определенные в программе, включают возможность объектов
рисовать самих себя, рисование документа легко выполняется проходом по списку.
Использование шаблона CTypedPtrList
Вы можете объявить шаблон CTypedPtrList, хранящий указатели на экземпляры
классов фигур как указатели на CElement. Для этого понадобится только добавить
объявление списка в виде определения нового члена класса CSketcherDoc:
// SketcherDoc. h : интерфейс для класса CSketcherDoc
#pragma once
class CSketcherDoc: public CDocument
protected: // Создается только при сериализации
CSketcherDoc ();
DECLARE_DYNCREATE (CSketcherDoc)
// Остальная часть класса — как раньше...
protected:
COLORREF m_Color;
unsigned int m__Element;
// Текущий класс для рисования
// Тип текущего элемента
Создание документа и усовершенствование представления 779
CTypedPtrList<CObList, CElement*> m ElementList; // Список элементов
// Остальная часть класса — как раньше...
Теперь класс CSketcherDoc ссылается на класс CElement и обычного опережаю-
щего объявления класса CElement перед определением класса CSketcherDoc долж-
но быть достаточно, чтобы приложение Sketcher успешно компилировалось, но не
в этом случае. Компилятору нужно знать о базовом классе класса CElement, чтобы
корректно скомпилировать экземпляр шаблона CTypedPtrList. Подобное возможно,
только если в этом месте будет доступно определение класса CElement. Этого мож-
но достичь двумя способами. Можно обеспечить, чтобы каждой директиве #include
для заголовочного файла SketcherDoc . h предшествовала директива #include для
CElement, или просто добавить директиву #include для Element .h перед определе-
нием класса CSketcherDoc. Последний способ проще и избавляет от необходимости
вылавливать все директивы #include для SketcherDoc.h в исходных файлах.
Вам также понадобится функция-член для добавления элемента в список, и
AddElement () — хорошее, хотя и не оригинальное имя для этого. Вы создаете объек-
ты фигур в куче, поэтому можете просто передать указатель на функцию. Поскольку
все, что она делает — это добавляет элемент, вы можете также поместить реализацию
в определение класса:
class CSketcherDoc: public CDocument
// Остальная часть класса — как раньше..
// Операции
public:
unsigned int GetElementType()
{ return m_Element; }
COLORREF GetElementColor ()
{ return m_Color; }
void AddElement (CElement* pElement)
// Получить тип элемента
// Получить цвет элемента
// Добавить элемент в список
{ m ElementList.AddTail (pElement); }
// Остальная часть класса — как раньше..
Добавление элемента в список потребует единственного оператора, который вы-
зывает функцию-член AddT a i 1 (). Это все, что необходимо для создания документа,
но вы еще должны рассмотреть, что должно случиться, когда документ закрывается.
Нужно гарантировать, чтобы список указателей и все элементы, на которые они ука-
зывают, правильно уничтожались. Для этого потребуется добавить код деструктора
для объектов CSketcherDoc.
Реализация деструктора документа
В деструкторе вы сначала проходите по списку, удаляя элементы, на которые ука-
зывает каждый указатель. После этого вы должны удалить указатели из списка. Код,
который это сделает, выглядит следующим образом:
CSketcherDoc:: ^CSketcherDoc (void)
// Получить позицию начала списка
POSITION aPosition = m_ElementList.GetHeadPosition();
// Удалить элементы, на которые указывает каждое вхождение списка
while(aPosition)
delete m__E lenient Li st .GetNext (aPosition);
m ElementList .RemoveAll (); // И, наконец, удалить все указатели
780 Глава 15
С помощью функции GetHeadPosition () вы получаете значение позиции вхожде-
ния начала списка, и затем этим значением инициализируете переменную aPosition.
Затем вы используете aPosition в цикле while для прохода по списку и удаления
объекта, на который указывает каждый элемент списка. Функция GetNext () возвра-
щает текущий элемент-указатель и обновляет переменную aPosition, чтобы она ука-
зывала на следующий элемент. Когда получен последний элемент списка, aPosition
устанавливается в NULL функцией GetNext (), и цикл завершается. После того, как
вы удалили все объекты, на которые указывали указатели из списка, можно удалить и
сами указатели. Все они удаляются одним вызовом функции RemoveAll () для объекта
списка.
Этот код вы должны добавить в определение деструктора в SketcherDoc. срр.
Перейти сразу к коду деструктора можно через вкладку Class View.
Рисование документа
Поскольку документ владеет списком элементов и этот список является защищен-
ным (protected), вы не можете использовать его непосредственно из представления.
Функция-член представления OnDraw () не должна вызывать функцию-член Draw ()
для каждого элемента в списке, так что нужно подумать, как это сделать лучше всего.
Ниже предложены варианты.
□ Вы можете сделать список public, но это не даст объекту поддерживать
protected-члены класса документа, потому что он показывает все функции-
члены объекта списка.
□ Вы можете добавить функцию-член, возвращающую указатель на список, но
это, по сути, сделает список public и также повлечет за собой накладные рас-
ходы на доступ к ним.
□ Вы можете добавить publ i с-функцию к документу, которая вызовет функции-
члены Draw () каждого элемента. Вы можете затем вызвать этот член через
функцию OnDraw () класса представления. Это будет неплохим решением, по-
скольку обеспечит то, что вы хотите, и при этом сохранит приватность списка.
Единственный аргумент против заключается в том, что функция нуждается в
доступе к контексту устройства, а это — принадлежность представления.
□ Вы можете добавить функцию, предоставляющую значение POSITION первого
элемента, и второй член — для итерации по элементам. Это не откроет списка,
но сделает доступными содержащиеся в нем указатели.
Последний вариант кажется наилучшим выбором, поэтому на нем и остановимся.
Вы можете расширить определение класса документа следующим образом:
class CsketcherDoc: public CDocument
// Остальная часть класса — как раньше...
// Операции
public:
unsigned int GetElementType()
{ return m_Element; }
COLORREF GetElementColor()
// Получить тип элемента
// Получить цвет элемента
{ return m_Color; }
void AddElement(CElement* pElement) // Добавить элемент в список
{ m_ElementList.AddTail(pElement); }
POSITION GetListHeadPosition() // Возвратить значение POSITION начала списка
{ return m ElementList. GetHeadPosition ();
Создание документа и усовершенствование представления 781
{ return m_ElementList. GetNext (aPos); }
// Остальная часть класса — как раньше...
Используя эти две дополнительные функции класса документа, функция OnDraw ()
представления получает возможность выполнить итерацию по списку, вызывая
Draw () для каждого элемента. Ниже показана реализация OnDraw (), которая необхо-
дима для этого.
void CSketcherView: :OnDraw(CDC* pDC)
CSketcherDoc* pDoc = GetDocument () ;
ASSERT_VALID(pDoc);
if(IpDoc)
return;
POSITION aPos = pDoc->GetListHeadPosition();
while(aPos) // Продолжать цикл, пока aPos не null
{
pDoc~>GetNext(aPos)->Draw(pDC) ; // Рисовать текущий элемент
}
}
Эта реализация функции OnDraw () всегда рисует все элементы, содержащиеся в
документе. Оператор в цикле while сначала получает указатель на элемент из доку-
мента выражением pDoc->GetNext (). Возвращенный им указатель используется для
вызова функции Draw () для этого элемента. Оператор работает подобным образом
без скобок, благодаря ассоциативности слева направо операции Цикл while про-
ходит список от начала до конца. Хотя, вы можете реализовать его лучше, и тем са-
мым сделать программу более эффективной.
Нередко когда программе посылается сообщение WM PAINT, должна перерисовы-
ваться лишь часть окна. Когда Windows посылает окну сообщение WM_PAINT, также
определяется область клиентской части окна, и только эта часть должна быть перери-
сована. Класс С DC предлагает функцию-член Re с t Visible (), которая проверяет, пере-
крывается ли прямоугольник, переданный в аргументе, с областью, которую Windows
требует перерисовать, тем самым повышая производительность приложения.
void CSketcherView::OnDraw(CDC* pDC)
CSketcherDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if(’pDoc)
return;
POSITION aPos = pDoc->GetListHeadPosition();
CElement* pElement =0; // Хранилище указателя на элемент
while (aPos) // Выполнять цикл, пока aPos — не null
pElement = pDoc->GetNext (aPos); // Получить указатель на текущий элемент
/ / Если элемент видимый...
if(pDC->RectVisible(pElement->GetBoundRect()))
pElement->Draw(pDC); // ...нарисовать его
Вы получаете позицию первого элемента в списке и сохраняете ее в aPos. Затем
значение, записанное в aPos, используется для управления циклом while, который
извлекает указатель на каждый элемент по очереди, так что цикл продолжается до
782 Глава 15
тех пор, пока aPos не примет значения NULL. Вы извлекаете ограничивающий пря-
моугольник для каждого элемента, вызывая функцию-член GetBoundRect () объекта,
и передаете его функции RectVisible () в операторе if. В результате перерисовы-
ваются только элементы, перекрывающие область, которую Windows идентифициру-
ет как недействительную. Рисование на экране — относительно дорогая операция в
смысле времени, поэтому выборочная перерисовка только тех элементов, которые
нужно перерисовать, вместо рисования каждый раз всего существенно повышает про-
изводительность.
Добавление элемента в документ
Последнее, что вы должны сделать, чтобы получить работающий документ в ва-
шей программе — это добавить в обработчик OnLButtonUp () класса CSketcherView
код, который будет добавлять временный элемент к документу:
void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point)
if(this == GetCapture())
Releasecapture (); // Прекратить захват сообщений мыши
// Если есть элемент, добавить его к документу
if(m_pTempElement)
GetDocument()->AddElement(m_pTempElement);
InvalidateRect(0); // Перерисовать текущее окно
m__pTempElement = 0; // Сбросить указатель элемента
Конечно, вы должны проверить, действительно ли есть элемент, прежде чем
добавлять его к документу. Пользователь может просто щелкнуть кнопкой, не
передвигая мышь. После добавления элемента к списку в документе вызывается
InvalidateRect (), чтобы инициировать перерисовку клиентской области текущего
представления.
Аргумент 0 объявляет недействительной всю клиентскую область представления.
Из-за особенностей работы процесса “растяжения” некоторые элементы могут не ото-
бражаться правильно, если этого не сделать. Если вы рисуете горизонтальную линию,
например, а затем растягиваете прямоугольник в том же цвете так, что его верхняя
или нижняя грань перекрывают линию, перекрытая часть линии исчезнет. Это по-
тому, что грань прямоугольника рисуется операцией XOR с лежащей ниже линией,
так что вы получаете цвет фона. Вы также сбрасываете указатель m_pTempElement в
ноль, дабы избежать путаницы при создании другого элемента.
Испытание документа
После сохранения всех модифицированных файлов можно выполнить сборку по-
следней версии программы Sketcher и запустить ее. Теперь вы можете нарисовать
картинку “счастливый программист”, показанную на рис. 15.8.
Теперь программа работает более реалистично. Она сохраняет указатель на каж-
дый элемент в объекте документа, так что все они автоматически перерисовываются
при необходимости. Программа также выполняет корректную очистку данных доку-
мента, когда он удаляется.
Однако в программе все еще присутствует ряд ограничений, над которыми вы мо-
жете поработать. Эти ограничения перечислены ниже.
Создание документа и усовершенствование представления
783
Sketch© -Sketch© 1
—————.в»—-Miжм———мн» аК
File Edit vie-. Element color Windee help
Ready
Puc. 15.8, Работа программы Sketcher с документом
□ Можно открыть другое окно представления, используя пункт меню Windows
New Window (Окно1^Новое окно) программы. Эта возможность встроена в при-
ложение MDI и открывает новое представление существующего документа, а
не новый документ. Однако если вы рисуете в одном окне, то элементы не ри-
суются в другом окне. Элементы никогда не появляются в окне, отличном от
того, в котором были нарисованы, если только место, которое они занимают,
не должно быть перерисовано по какой-то другой причине.
□ Вы можете рисовать только в той клиентской области, которую видите. Было
бы неплохо иметь возможность прокручивать представление и рисовать в боль-
шей области.
□ Никак невозможно удалить элемент, так что если вы допустили ошибку, то при-
дется либо мириться с ней, либо начинать новый документ.
Все это довольно-таки серьезные недостатки, которые существенно снижают прак-
тическую полезность программы. До конца настоящей главы мы их устраним.
Усовершенствование представления
Первое, что вы можете попытаться исправить — это обновление всего окна доку-
мента, отображающегося при рисовании документа. Проблемы возникают из-за того,
что о новом элементе известно только представлению, в котором рисуется элемент.
Каждое представления работает независимо от других, и между ними нет никакого
взаимодействия. Вы должны заставить любое представление, которое добавляет эле-
мент к документу, каким-то образом известить об этом другие представления, чтобы
они предприняли соответствующее действие.
Обновление множественных представлений
Класс документа оснащен функцией UpdateAllViews О , которая помогает спра-
виться с этой конкретной проблемой. Эта функция, по сути, предоставляет для до-
кумента средство отправки сообщений всем его представлениям. Вам просто нужно
784 Глава 15
вызвать ее из функции OnLButtonUp () в классе CSketcherView, всякий раз, когда вы
добавляете новый элемент к документу:
void CSketcherView::OnLButtonUp (UINT nFlags, CPoint point)
if (this == GetCapture ())
Releasecapture(); // Прекратить перехват сообщений мыши
// Если есть элемент, добавить его к документу
if(mjpTempElement)
GetDocument()->AddElement(m_pTempElement);
GetDocument()->UpdateAllViews(0,O,m_pTempElement);
// Сообщить всем
// представлениям
m_pTempElement =0; // Сбросить указатель элемента
Когда указатель m_pTempElement не равен NULL, то специфическое действие функ-
ции расширено вызовом функции UpdateAllViews () — члена вашего класса доку-
мента. Эта функция взаимодействует с представлениями, инициируя вызов функции
OnUpdate () в каждом представлении. Три аргумента UpdateAllViews () описаны на
рис. 15.9.
Первый аргумент вызова функции UpdateAllViews О — часто указатель this
текущего представления. Это подавляет вызов функции OnUpdate () для текущего
представления. Это удобное средство, когда текущее представление уже обновлено.
В случае программы Sketcher, поскольку вы занимаетесь “растягиванием” в процессе
создания элемента, то хотите также заставить перерисовываться и текущее представ-
ление, поэтому, специфицируя первый аргумент как 0, вы инициируете вызов функ-
ции OnUpdate () для всех представлений, включая текущее. Это исключает необходи-
мость в вызове InvalidateRect (), как делалось раньше.
Здесь вы не используете второй аргумент UpdateAllViews (), поскольку переда-
ете указатель на новый элемент в третьем аргументе. Передача указателя на новый
элемент позволяет представлениям определить, какая часть их клиентской области
должна быть перерисована.
Этот аргумент—указатель
на текущее представление.
Он подавляет вызов
функции-члена OnUpdate()
для представления.
LPARAM — это 32-битный
тип Windows, который может
использоваться для передачи
информации об обновляемом
регионе клиентской области.
Этот аргумент — указатель
на объект, который может
предоставить информацию
о части области, подлежащей
обновлению в рамках
клиентской области.
void UpdateAIIView( CView* pSender, LPARAM IHint = OL, CObject* pHint = NULL);
Эти два значения аргументов
передаются функциям OnUpdate()
в представлениях
Рис, 15,9, Аргументы функции UpdateAllViews ()
Создание документа и усовершенствование представления
785
Чтобы перехватить информацию, переданную функции UpdateAllViews (), вы
добавляете в класс представления функцию-член OnUpdate (). Это можно сделать в
мастере создания классов (Class Wizard), в свойствах класса CSketcherView» Вы долж-
ны помнить, что для того, чтобы увидеть свойства класса, следует выполнить щел-
чок левой кнопкой мыши на имени класса и выбрать из контекстного меню пункт
Properties (Свойства). Если вы щелкнете на кнопке Overrides (Переопределения) в
окне Properties (Свойства), то сможете выбрать OnUpdate в списке функций, которые
можно переопределить. Щелкните на имени функции, затем выберите опцию <Add>
OnUpdate, показанную в выпадающем списке в соседней колонке. Когда вы закрое-
те окно Properties, то сможете отредактировать код переопределения OnUpdate () в
панели редактора. Вам потребуется добавить в определение функции следующий вы-
деленный полужирным код:
void CSketcherView::OnUpdate(CView* pSender, LPARAM IHint, CObject* pHint)
// Объявить недействительной область, соответствующую указанному элементу,
// если он есть, иначе объявить недействительной всю клиентскую область
if (pHint)
InvalidateRect(((CElement*)pHint)->GetBoundRect());
else
InvalidateRect(0);
}
Обратите внимание, что нужно убрать комментарий с имен параметров в сгене-
рированной версии функции; в противном случае функция с добавленным кодом не
скомпилируется. Три аргумента, передаваемые функции OnUpdate () класса пред-
ставления, соответствуют аргументам, которые вы передаете при вызове функции
UpdateAllViews (). То есть, pHint содержит адрес нового элемента. Однако вы не
можете предположить, что это всегда будет так. Функция OnUpdate () также вызыва-
ется при первоначальном создании представления, но при этом третьим аргументом
передается указатель NULL. Таким образом, функция проверяет указатель pHint на
неравенство NULL, и только тогда получает ограничивающий прямоугольник в кли-
ентской области, передавая прямоугольник функции InvalidateRect (). Эта область
перерисовывается функцией OnDraw () представления, когда ему посылается сообще-
ние WM PAINT. Если указатель pHint равен NULL, недействительной объявляется вся
клиентская область.
Может возникнуть соблазн рассмотреть перерисовку нового элемента в функции
OnUpdate (). Это нехорошая идея. Вы должны выполнять постоянное рисование
только в ответ на сообщение WM PAINT. То есть функция представления OnDraw ()
должна быть единственным местом, которое инициирует любые операции рисования
данных документа. Это гарантирует корректную перерисовку представления всякий
раз, когда Windows сочтет ее необходимой.
Если вы соберете и запустите программу Sketcher с включенными новыми моди-
фикациями, то обнаружите, что все представления обновляются синхронно, отражая
текущее содержимое документа.
Прокрутка представлений
Добавление возможности прокрутки к представлению выглядит на первый взгляд
исключительно простой задачей; но на самом деле все гораздо сложнее, чем кажется.
Тем не менее, займемся этим. Первое, что потребуется сделать — заменить базовый
класс для CSketcherView с CView на CScrollView. Этот новый базовый класс облада-
786 Глава 15
ет встроенной функциональностью прокрутки, так что вы можете изменить опреде-
ление класса CSketcherView следующим образом:
class CSketcherView: public CScrollView
// Определение класса — как раньше...
};
которые ссылаются на базовый класс для CSketcherView:
IMPLEMENTJ3YNCREATE (CSketcherView, CScrollView)
BEGIN__MESSAGE_MAP (CSketcherView, CScrollView)
Однако этого все еще недостаточно. Новая версия класса представления долж-
на кое-что знать о рисуемой области, например, размер и насколько далеко должна
выполняться прокрутка представления, когда вы используете ползунок прокрутки.
Эта информация должна применяться перед первым рисованием представления. Вы
можете поместить код для выполнения этой задачи в функцию OnlnitialUpdate О
класса представления.
Необходимую информацию вы устанавливаете вызовом функции, унаследованной
от класса CScrollView — SetScrollSizes (). Аргументы этой функции объясняются
на рис. 15.10.
Это определяет горизонтальное (сх)
и вертикальное (су) расстояние прокрутки
страницы. Может быть определен как
CSize Pagefcx, су);
По умолчанию принята 1/10 часть
всей области.
Это определяет горизонтальное (сх)
и вертикальное (су) расстояние прокрутки
строки. Может быть определен как
CSize Linefcx, су);
По умолчанию принята 1/10 часть
всей области.
void SetScrollSizes(
int MapMode, SIZE Total, const SIZE& Page = sizeDefault, const SIZE& Line = sizeDefault
Может быть любым из:
М MText
MM_LOENGLISH
ММ LOMETRIC
MM.TWIPS
MM.HEINGLISH
MM HIMETRIC
Общая область рисования, может быть
определена как CSize Total(cx,cy);
где сх — горизонтальное приращение,
а су — вертикальное приращение
в логических единицах.
Рис. 15.10. Аргументы функции SetScrollSizes ()
Создание документа и усовершенствование представления 787
Прокрутка на одну строку происходит, когда вы щелкаете на верхней или ниж-
ней стрелке линейки прокрутки, а прокрутка на страницу происходит при щелчке
на теле самой линейки. Здесь вы имеете возможность изменить режим отображения.
MM_LOENGLISH будет хорошим выбором для приложения Sketcher, но сначала обе-
спечим работу прокрутки в режиме отображения ММ_ТЕХТ, поскольку нужно будет
преодолеть некоторые трудности.
Чтобы добавить код для вызова SetScrollSizes (), вы должны переопределить
версию по умолчанию функции OnlnitialUpdate () представления. Вы обращаетесь
к ней точно так же, как делали это с переопределением функции OnUpdate () — через
окно Properties для класса CSketcherView. После добавления переопределения про-
сто добавьте код, помеченный комментариями:
void CSketcherView::OnlnitialUpdate ()
CScrollView::OnlnitialUpdate ();
// Определить размер документа
CSize DocSize (20000,20000) ;
// Установить режим отображения и размер документа.
SetScrollSizes(MMJTEXT,DocSize);
}
Это устанавливает режим отображения ММ ТЕХТ и определяет общий размер про-
странства, в котором вы можете рисовать, в 20 000 пикселей по каждому измерению.
Проделанного достаточно, чтобы получить наглядно работающий механизм про-
крутки. Выполните сборку программы и запустите ее с этими дополнениями. Вы смо-
жете нарисовать несколько элементов, а затем прокрутить представление. Однако,
хотя прокрутка окна работает нормально, если вы попытаетесь нарисовать что-либо,
когда представление прокручено, окажется, что все работает не так, как должно.
Элементы появляются не в той позиции, где вы их рисуете, к тому же отображаются
неправильно. Что происходит?
Логические координаты и клиентские координаты
Проблема кроется в используемых координатных системах — и эта множествен-
ность не случайна. В действительности во всех рассмотренных примерах мы исполь-
зовали две координатные системы, хотя вы могли этого и не заметить. Как было
показано в предыдущей главе, при вызове такой функции, как LineTo (), предпола-
гается применение логических координат. Функция — член класса С DC, определяю-
щего контекст устройства, а контекст устройства имеет собственную систему логиче-
ских координат. Режим отображения, являющийся свойством контекста устройства,
определяет единицу измерения координат при рисовании чего-либо.
С другой стороны, данные координат, которые вы принимаете вместе с сообще-
ниями мыши, никак не связаны с контекстом устройства или объектом С DC — и вне
контекста устройства логические координаты неприменимы. Точки, передаваемые
обработчикам OnLButtonDown () и OnMouseMove (), имеют координаты, всегда вы-
раженные в единицах устройства, то есть в пикселях, и измеряются относительно
верхнего левого угла клиентской области. Их называют клиентскими координатами.
Аналогично, когда вы вызываете InvalidateRect (), предполагается, что прямоу-
гольник определен в терминах клиентских координат.
В режиме ММ_ТЕХТ клиентские координаты и логические координаты контек-
ста устройства совпадают
измеряются в пикселях, а потому являются одними
и теми же — до тех пор, пока вы не прокручиваете окно. Во всех предыдущих примерах
не было никакой прокрутки, поэтому все работало без проблем. В последней версии
788 Глава 15
правой стороне показано, где именно в действительности рисуется линия.
Sketcher все работает отлично, пока вы не прокручиваете представление, причем
точка начала логических координат (точка 0, 0) перемещается механизмом прокрут-
ки, так что уже не находится в том же месте, что и начало клиентских координат.
Единицы измерения логических и клиентских координат здесь совпадают, но началь-
ные точки этих координатных систем отличаются. Эта ситуация проиллюстрирована
на рис. 15.11.
В левой части показано положение клиентской области, в которой вы рисуете, и
точки — положения мыши, определяющие линию. Они записаны в клиентских коор-
динатах.
Рисование выполняется в логических координатах, но вы используете значения кли-
ентских координат. В случае прокрученного окна линия появляется в неправильном
месте из-за смещения логических координат.
Это значит, что для определения элементов в программе S ke t che г применяются не-
верные значения, и когда части клиентской области объявляются недействительными,
чтобы обеспечить их перерисовку, то прямоугольники, передаваемые функции, также
неправильны — отсюда загадочное поведение программы. С другими режимами отобра-
жения все еще хуже, поскольку отличаются не только единицы измерения двух коорди-
натных систем, но к тому же направления оси у могут быть противоположными!
Рисование в непрокручиваемом окне
Логические координаты
Левая кнопка
нажата
Левая кнопка
отпущена
Линия рисуется здесь
в клиентских координатах
в логических координатах
Рисование в прокручиваемом окне
Логические координаты
0,0
X 4------------------
г------------------------
Представление прокручено,
поэтому начальная
точка теперь здесь
Левая кнопка нажата Левая кнопка отпущена
Линия рисуется здесь
в клиентских координатах
Линия появляется здесь
в логических координатах
Рис. 15.11. Рисование в непрокручиваемом и прокручиваемом окнах
Создание документа и усовершенствование представления 789
Работа с клиентскими координатами
Рассмотрим, что можно сделать для решения проблемы.
□ Преобразовать клиентские координаты, полученные в сообщениях мыши, в ло-
гические, а затем использовать их для создания элементов.
□ Преобразовать ограничивающий прямоугольник, созданный в логических ко-
ординатах, обратно в клиентские координаты, если необходимо использовать
их для вызова InvalidateRect ().
В результате гарантируется, что при работе с функциями контекста устройства
всегда используются логические координаты, а для всего прочего взаимодействия
в окне всегда применяются клиентские координаты. Функции, которые должны ис-
пользоваться для выполнения необходимых преобразований, ассоциированы с кон-
текстом устройства, поэтому вы должны получать этот контекст устройства всякий
раз, когда хотите преобразовать логические координаты в клиентские и наоборот.
Для выполнения работы можно применять функции преобразования координат клас-
са CDC, унаследованного от CClientDC.
Ниже показана новая версия обработчика OnLButtonDown (), включающая преоб-
разования.
// Обработчик сообщения нажатия левой кнопки мыши
void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point)
CClientDC aDC(this); // Создать контекст устройства
OnPrepareDC(&aDC); // Получить уточненную начальную точку
aDC.DPtoLP(&point); // Преобразовать точку в логические координаты
m_FirstPoint = point; // Записать позицию курсора
SetCapture(); // Захватить последующие сообщения мыши
Вы получаете контекст устройства текущего представления, создавая объект
CClientDC и передавая указатель this конструктору. Преимущество от CClientDC со-
стоит в том, что он автоматически освобождает контекст устройства, когда объект
выходит из области видимости. Важно, чтобы контекст устройства не оставался за-
нятым без необходимости, поскольку в Windows доступно ограниченное их количе-
ство и очень легко исчерпать их все. Применяя CClientDC, вы всегда находитесь в
безопасности.
Используя CScrollView, унаследованная функция OnPrepareDC () должна быть
вызвана для установки начальной точки системы логических координат в контексте
устройства, чтобы соответствовать прокрученной позиции. После установки этим
вызовом начальной точки осуществляется вызов функции DPtoLP (), которая пре-
образует точки устройства в логические точки (Device Points в Logical Points), пре-
образуя значение point, переданное обработчику, в логические координаты. Затем
преобразованная точка, готовая для создания элемента, сохраняется в обработчике
OnMouseMove().
Новый код обработчика OnMouseMove () будет выглядеть следующим образом:
void CSketcherView::OnMouseMove (UINT nFlags, CPoint point)
CClientDC aDC(this); // Контекст устройства для текущего представления
OnPrepareDC(&aDC); // Получить уточненную начальную точку
if((nFlags&MK_LBUTTON)&&(this==GetCapture()))
aDC.DPtoLP(&point); // преобразовать точку в логические координаты
790 Глава 15
m SecondPoint = point; // Сохранить текущую позицию курсора
// Остаток функции — как раньше...
Код преобразования значения точки, переданной обработчику, по сути, тот
же самый, что и в предыдущем обработчике, и это — все, что необходимо на дан-
ный момент. Легко догадаться, какую последнюю функцию вы должны изменить —
OnUpdate () в классе представления. Она должна приобрести такой вид:
void CSketcherView::OnUpdate(CView* pSender, LPARAM IHint, CObject* pHint)
// Объявить недействительной область, соответствующую указанному элементу,
// если он есть, иначе объявить недействительной всю клиентскую область
if(pHint)
CClientDC aDC (this) ; // Создать контекст устройства
OnPrepareDC (&aDC); // Получить уточненную начальную точку
//Получить ограничивающий прямоугольник и преобразовать в клиентские координаты
CRect aRect= ((CElement*) pHint) -XSetBoundRect ();
aDC.LPtoDP(aRect);
InvalidateRect (aRect); // Инициировать перерисовку области
}
else
InvalidateRect (0); // Объявить недействительной всю клиентскую область
)
Модификация здесь заключается в создании объекта CClientDC и применении
функции LPtoDP () для преобразования прямоугольника, подлежащего перерисовке,
в клиентские координаты.
Теперь, если скомпилировать и запустить программу Sketcher с последними мо-
дификациями, описанными выше, она должна будет работать корректно вне зависи-
мости от позиции прокрутки.
Использование режима отображения mm_loenglish
Теперь посмотрим, что вам понадобится сделать для использования режима ото-
бражения MM_LOENGLISH. Он предусматривает рисование в логических единицах раз-
мером в 0,01 дюйма, а также гарантирует, что размер рисования будет одинаков на
дисплеях с разными разрешениями. Это обеспечит гораздо лучшее впечатление от
программы с точки зрения пользователя.
Вы можете установить режим отображения в вызове SetScrollSizes (), выпол-
ненном из функции OnlnitialUpdate () класса представления. Также вам нужно спе-
цифицировать общую область рисования, так что если вы определите ее размер в
3000 на 3000, это даст площадь рисования в 30 на 30 дюймов, чего должно быть доста-
точно. Значения шагов прокрутки на строку и страницу удовлетворительны, так что
их специфицировать не нужно. Воспользуйтесь вкладкой Class View, чтобы попасть в
функцию OnlnitialUpdate () и затем изменить ее следующим образом:
void CSketcherView: :OnlnitialUpdate (void)
CScrollView::OnlnitialUpdate();
// Определить размер документа 30x30 дюймов в MM_LOENGLISH
CSize DocSize(3000,3000);
// Установить режим отображения и размер документа.
SetScrollSizes(ММ LOENGLISH, DocSize);
Создание документа и усовершенствование представления 791
Вы просто изменяете аргументы в вызове SetScrollSizes () для нужного режима
отображения и размера документа. Это все, что необходимо, дабы позволить пред-
ставлению работать в режиме ММ LOENGLISH, однако вам все равно придется исправ-
лять работу с прямоугольниками.
Следует отметить, что вы не ограничены установкой режима отображения раз и
навсегда. В любое время можно изменять режим отображения в контексте устрой-
ства и рисовать разные части отображаемого образа, используя разные режимы ото-
бражения. Для этого применяется функция SetMapMode (), но я не хочу здесь погру-
жаться в детали. Вы можете заставить свое приложение работать только в режиме
MM LOENGLISH. Всякий раз, когда вы создаете объект CClientDC для представления
и вызываете OnPrepareDC (), контекст устройства, которым он владеет, имеет режим
отображения, установленный функцией OnlnitialUpdate ().
Проблема с прямоугольниками связана с тем, что все классы элементов предпола-
гают, что установлен режим отображения ММ_ТЕХТ, и в ММ LOENGLISH они получаются
перевернутыми вверх ногами из-за обратного направления оси у. Когда вы применяе-
те LPtoDP () к прямоугольнику, предполагается, что он ориентирован правильно — в
соответствии с осями MM_LOENGLISH. Поскольку это не так, функция выполняет зер-
кальное отображение прямоугольников по оси х. Это создает проблему, когда вызы-
вается InvalidateRect () для пометки недействительной области представления,
поскольку зеркально отображенный прямоугольник в координатах устройства не рас-
познается Windows как расположенный внутри видимой клиентской области.
Есть два варианта решения этой проблемы. Можно модифицировать классы
элементов так, чтобы ограничивающие прямоугольники правильно вычислялись в
MM_LOENGLISH, либо повторно нормализовать прямоугольник, который вы намерены
передать функции InvalidateRect (). Последний способ проще, поскольку нужно
модифицировать только один член в классе представления — функцию OnUpdate ().
void CSketcherView::OnUpdate (CView* pSender, LPARAM IHint, CObject* pHint)
// Объявить недействительной область, соответствующую указанному элементу,
// если он есть, иначе объявить недействительной всю клиентскую область
if(pHint)
CClientDC aDC(this); 11 Создать контекст устройства
OnPrepareDC(&aDC); // Получить уточненную начальную точку
// Получить ограничивающий прямоугольник и преобразовать в клиентские координаты
CRect aRect=((CElement*)pHint)->GetBoundRect();
aDC.LPtoDP(aRect);
aRect .NorznalizeRect ();
InvalidateRect(aRect); // Инициировать перерисовку области
else
InvalidateRect(0); // Объявить недействительной всю клиентскую область
Это должно сработать для программы в том виде, в котором она есть. Если вы
заново соберете Sketcher, то получите работающую прокрутку и поддержку множе-
ственных представлений. Следует только не забыть заново нормализовать любой пря-
моугольник, преобразованный в координаты устройства, для последующего использо-
вания в InvalidateRect (). Это также касается любых обратных преобразований.
792 Глава 15
Удаление и перемещение фигур
Возможность удаления фигур — фундаментальное требование к программе рисова-
ния. Единственный вопрос, связанный с этим: как вы собираетесь выбирать элемент,
подлежащий удалению. Конечно, после того, как вы решите, как выбрать элемент,
это в равной мере можно использовать и в случае, если понадобится перемещать эле-
мент, так что вы можете трактовать перемещение и удаление элементов как взаимос-
вязанные проблемы. Однако сначала посмотрим, как добавить в программу операции
перемещения и удаления.
Изящный способ выполнения функций перемещения и удаления предполага-
ет появление всплывающего контекстного меню в позиции курсора при щелч-
ке правой кнопкой мыши. Затем вы можете поместить в это меню элементы Move
(Переместить) и Delete (Удалить). Всплывающее меню, работающее подобным обра-
зом — очень удобная возможность, которую можно использовать во множестве раз-
ных ситуаций.
Как должно использоваться всплывающее меню? Стандартный способ работы кон-
текстного меню состоит в том, что пользователь перемещает курсор мыши на опреде-
ленный объект и выполняет на нем щелчок правой кнопкой мыши. Это позволяет
выбрать объект и отобразить меню, содержащее список элементов, которые соответ-
ствуют действиям, выполняемым над этим объектом. Это значит, что разные объекты
могут иметь разные меню. Вы можете увидеть это в действии в самой среде Developer
Studio. Выполняя щелчок правой кнопкой на пиктограмме класса в Class View, вы по-
лучаете меню, отличающееся от того, которое вы получите, щелкнув правой кнопкой
на пиктограмме функции-члена. Появляющееся меню чувствительно к контексту кур-
сора— отсюда и термин “контекстное меню”. В программе Sketcher присутствуют
два контекста, которые нужно рассмотреть. Вы можете щелкнуть правой кнопкой
мыши, когда курсор находится на элементе, или же щелкнуть правой кнопкой мыши
на свободном поле, когда под курсором нет никакого элемента.
Так как же реализовать упомянутую функциональность в приложении Sketcher?
Это можно сделать, создав два меню: одно на случай наличия элемента под курсором,
а другое — на случай, когда элемента там нет. Можно выполнить проверку, имеется ли
элемент под курсором, когда пользователь щелкает правой кнопкой мыши. Если эле-
мент есть, вы подсвечиваете его, чтобы пользователь точно знал, к какому элементу7
относится всплывающее контекстное меню.
Посмотрим, как создать всплывающее меню в точке курсора, а когда это получит-
ся, вернемся к деталям реализации операций перемещения и удаления.
Реализация контекстного меню
Первый шаг предполагает создание меню, содержащего два набора пунктов:
один с пунктами Move и Delete, а другой — с комбинацией пунктов из меню Element
(Элемент) и Color (Цвет). Для этого перейдите в представление ресурсов (Resource
View) и разверните список ресурсов. Правый щелчок по папке Menu (Меню) вызо-
вет появление контекстного меню — еще одной демонстрации того, что вы собирае-
тесь создать в приложении Sketcher. Выберите пункт Insert Menu (Вставить меню)
для создания нового меню. Новое меню появится с идентификатором по умолчанию
IDR_MENU1, но вы можете это изменить. Выберите имя нового меню в Resource View
и отобразите окно Properties (Свойства) ресурса, нажав <Alt+Enter> (это сокраще-
ние для пункта View^Other Windows1^Properties Window (Вид1^Другие окна^Окно
Создание документа и усовершенствование представления 793
свойств) главного меню). Затем можете отредактировать ID ресурса в окне Properties,
щелкнув на этом значении ID. Имеет смысл изменить его в правой колонке на нечто
более осмысленное вроде IDR_CURSOR_MENU. Обратите внимание, что имя ресурса
меню должно начинаться с IDR. Нажатие клавиши <Enter> сохраняет новое имя.
Теперь можете создать два новых элемента в панели меню. Они могут иметь любые
старые заголовки, поскольку не будут видимы пользователю, а будут представлять два
контекстных меню, которые вы отобразите в Sketcher, так что вы можете назвать их
element и no element
будет использоваться. После этого вы можете добавить к всплывающему меню пун-
кты Move и Delete. Идентификаторы ID ELEMENT MOVE и ID ELEMENT DELETE впол-
не подойд
отдельности. На рис. 15.12 показано, как выглядят новое меню element.
в соответствие с ситуациями, в которых контекстное меню
, но при желании вы можете изменить их в окне Properties — каждый по
Sketcher. rc (ID...OR_MEIW - Menu)
element no element | Type Here
Move
Delete
Рис. 15.12. Новое меню element
Второе меню содержит список доступных типов элементов и цветов, идентичный
содержимым подменю Element и Color в панели главного меню, которое отделено
друг от друга разделителем. Идентификаторы, используемые для этих пунктов меню,
должны совпадать с теми, что были указаны в меню IDR_SketcherTYPE. Это связа-
но с тем, что обработчик меню ассоциирован с его идентификатором. Пункты меню
с одинаковыми идентификаторами используют одни и те же обработчики, поэтому
при выборе пункта меню Line (Линия) будет вызываться один и тот же обработчик,
независимо от того, находится Line в подменю главного меню или же в контекстном
меню.
Имеется удобное сокращение, избавляющее вас от необходимости создавать все
пункты меню по одному. Если вы отобразите меню IDR_SketcherTYPE и разверне-
те меню Element, то сможете выбрать все пункты меню, щелкнув на первом из них,
а затем на последнем при нажатой клавише <Shift>. После этого можно выполнить
щелчок правой кнопкой мыши на выделенной области, и выбрать из всплывающего
меню пункт Сору (Копировать), или же просто нажать <Ctrl+C>. Если затем вернуть-
ся к IDR_CURSOR_MENU и щелкнуть правой кнопкой мыши на первом пункте в меню
no element, то можно вставить полное содержимое меню Element, выбрав пункт
Paste (Вставить) из контекстного меню, либо нажав <Ctrl+V>. Скопированные пункты
меню будут иметь те же идентификаторы, что и оригиналы. Чтобы вставить раздели-
тель, просто щелкните правой кнопкой мыши на пустом пункте меню и выберите из
всплывающего меню Insert Separator (Вставить разделитель). Повторите процесс с
пунктами меню Color, и все будет готово (почти). Совместное размещение пунктов
меню Element и Color создает конфликт (пункты Rectangle (Прямоугольник) и Red
(Красный) разделяют одну и
это, и неплохо было бы для целей согласованности также изменить IDRSketcherTYPE.
Вы делаете это, редактируя свойство Caption (Заголовок) пункта меню. Готовое меню
должно выглядеть, как показано на рис. 15.13.
же горячую клавишу). Замена &Red на Re&d исправит
794 Глава 15
Рис. 15.13. Новое меню по element
это определение меню в ресурсном файле. Оно никак не связано с кодом
Закройте окно свойств и сохраните ресурсный файл. В этот момент все, что вы
имеете
программы Sketcher. Теперь необходимо ассоциировать эти меню и их идентифи-
катор IDR_CURSOR_MENU с классом представления. Вы также должны создать обра-
ботчики команд пунктов всплывающего меню, соответствующих идентификаторам
ID MOVE И ID DELETE.
Ассоциирование меню с классом
Чтобы ассоциировать контекстное меню с классом представления в програм-
ме Sketcher, обратитесь к панели Class View и отобразите окно Properties для
CSketcherView, щелкнув правой кнопкой мыши на имени класса и выбрав Properties
из контекстного меню. Если вы щелкнете на кнопке Messages (Сообщения) в окне
Properties, то сможете добавить обработчик сообщения WM_CONTEXTMENU, выбрав
<Add> OnContextMenu в соседней ячейке правой колонки. Затем добавьте в обработ-
чик приведенный ниже код.
void CSketcherView::OnContextMenu (CWnd* pWnd, CPoint point)
CMenu menu;
И
И
И
menu.LoadMenu(IDR_CURSOR_MENU);
CMenu* pPopup s menu.GetSubMenu(0);
ASSERT(pPopup !=NULL);
// Отобразить всплывающее меню
pPopup->TrackPopupMenu (TPM__LEFTALIGN |
Т PM_RIGHTBUTTON,
Загрузить контекстное меню
Получить первое меню
Убедиться, что оно есть
point.х, point.у, this);
Пока обработчик произвольно отображает первое из двух контекстных меню.
Теперь необходимо найти способ определения того, находится ли курсор над одним
из нарисованных элементов, чтобы решить, какое меню отображать, но мы обратим-
ся к этому немного позже. Вызов метода LoadMenu () объекта menu загружает ресурс
меню, соответствующий указанному в аргументе идентификатору, и прикрепляет его
к объекту меню CMenu. Функция GetSubMenu () возвращает указатель на всплываю-
щее меню, которое соответствует целочисленному аргументу, указывающему позицию
всплывающего меню, причем 0 — первое всплывающее меню, 1 — второе и так далее.
После того, как вы убедитесь, что возвращенный GetSubMenu () указатель не равен
NULL, всплывающее меню отображается вызовом TrackPopupMenu ().
Создание документа и усовершенствование представления 795
Первый аргумент функции TrackPopupMenu () состоит из двух флагов, объединен-
ных операцией ИЛИ. Один из них специфицирует, как всплывающее меню должно
позиционироваться, и может принимать значения, перечисленные в табл. 15.4.
Таблица 15.4. Значения первого аргумента функции TrackPopupMenu ()
Флаг
TPM_CENTERALIGN
TPM_LEFTALIGN
TPM_RIGHTALIGN
Описание
Центрирует всплывающее меню по горизонтали, относительно координаты
х, указанной в качестве второго аргумента функции.
Позиционирует всплывающее меню так, что левая сторона его выравнива-
ется по координате х, переданной во втором аргументе функции.
Позиционирует всплывающее меню так, что правая сторона его выравнива-
ется по координате х, переданной во втором аргументе функции.
Второй флаг специфицирует кнопку мыши и может принимать одно из значений,
описанных в табл. 15.5.
Таблица 15.5. Значения второго аргумента функции TrackPopupMenu ()
Флаг
Описание
tpm leftmousebutton Специфицирует, что всплывающее меню отслеживает левую кнопку мыши.
TPM RiGHTMOUSEBUTTON Специфицирует, что всплывающее меню отслеживает правую кнопку мыши.
Следующие два аргумента функции TrackPopupMenu () специфицируют соответ-
ственно координаты хну всплывающего меню на экране. Координата у определяет
позицию вершины меню. Четвертый аргумент специфицирует окно, которое владеет
меню, и которое должно принимать от него сообщения WM_COMMAND.
Теперь вы можете добавить обработчики пунктов элементов всплывающего меню.
Вернитесь в Resource View и выполните двойной щелчок на idr cursor menu.
После этого сделайте правый щелчок на пункте меню Move (Переместить) с после-
дующим выбором Add Event Handler (Добавить обработчик событий) из контекстно-
го меню. Затем в диалоговом окне мастера создания обработчиков событий (Event
Handler Wizard) можно специфицировать обработчик для пункта меню, как показано
на рис. 15.14.
Это обработчик COMMAND, и он должен быть создан в классе CSketcherView. Щелк-
ните на кнопке Add and Edit (Добавить и редактировать), чтобы создать функцию об-
работчика. Ту же процедуру потребуется проделать и для создания обработчика пун-
кта меню Delete (Удалить).
Вам ничего не нужно делать для второго контекстного меню, поскольку обработ-
чики для них уже написаны в классе документа, и они автоматически позаботятся о
сообщениях от всплывающего меню.
Выбор контекстного меню
На данный момент обработчик OnContextMenu () отображает только первое кон-
текстное всплывающее меню — независимо от того, где именно в представлении был
выполнен щелчок. Это не то, что нужно. Первое контекстное меню применимо к
элементу, в то время как второе — к представлению в целом. Необходимо отображать
первое меню, если под курсором находится элемент, а в противном случае должно
отображаться второе меню.
796 Глава 15
Event Handler Wizard - Sketcher
Welcome to the Event Handler Wizard
л -
,ID ELEMENT MOVE
Message type:
COMMAND
UPDATE _COMMAND_UI
Function handler name:
OnElementMove
C[ass list:
.Called after menu item or command button has been choosen
Puc. 15.14. Создание нового обработчика событий для пункта меню Move
Чтобы добиться этого, понадобятся две вещи. Во-первых, нужен механизм для
нахождения элемента (если он есть), находящегося в позиции курсора, и, во-вто-
рых — где-то сохранять адрес этого элемента, чтобы можно было его использовать в
обработчике OnContextMenu (). Сначала разберемся с сохранением адреса элемента,
поскольку это немного проще.
После нахождения элемента под курсором его адрес сохраняется в переменной-
члене m pSelected вашего класса представления. Это доступно обработчику правой
кнопки мыши, поскольку он находится в том* же классе. Вы можете добавить объявле-
ние переменной в раздел protected класса CSketcherView:
class CSketcherView: public CScrollView
// Остальная часть класса — как раньше...
protected:
CPoint m_FirstPoint;
CPoint m_SecondPoint;
CElement* m_pTempElement;
CElement* mjpSelected;
// Первая точка, записанная для элемента
// Вторая точка, записанная для элемента
// Указатель на временный элемент
// Текущий выбранный элемент
Вы можете добавить показанный код вручную или, в качестве альтернативы, щел-
кнуть правой кнопкой мыши на имени класса и выбрать пункт Add1^ Add Variable
(Добавить1^ Добавить переменную) из контекстного меню, чтобы открыть диалог добав-
ления члена данных. Если вы добавите m_pSelected вручную, вам также понадобится
инициализировать этот элемент в конструкторе класса с помощью следующего кода:
CSketcherView: : CSketcherView ()
: m_FirstPoint(CPoint(0,0))
, m_SecondPoint(CPoint(0,0))
, m_pTempElement(NULL)
Создание документа и усовершенствование представления 797
, mjpSelected(NULL)
// TODO: добавьте сюда код конструирования
Сейчас вы узнаете, как определить факт наличия элемента под курсором, а пока
можете использовать член m_pSelected представления в реализации обработчика
OnContextMenu():
void CSketcherView::OnContextMenu(CWnd* pWnd, CPoint point)
CMenu menu;
menu.LoadMenu(IDR_CURSOR_MENU);
CMenux pPopup = menu.GetSubMenu(m_pSelected == 0 ? 1 : 0);
ASSERT(pPopup !=NULL);
pPopup->TrackPopupMenu(TPM_LEFTALIGN |
TPM_RIGHTBUTTON, point.x, point.y, this);
Выражение m_pSelected == 0 ? 1 : 0 дает в результате 1, когда указатель равен
null, и 0 — в противном случае, поэтому вы выберете первое всплывающее меню, со-
держащее пункты Move и Delete, когда m pSelected не равен null, а второе — когда
равен.
Идентификация выбранного элемента
Для отслеживания, какой элемент находится под курсором, можно добавить со*
ответствующий код к обработчику OnMouseMove () в классе CSketcherView. Этот об*
работчик вызывается каждый раз, когда перемещается курсор мыши, так что все, что
вы должны добавить — это код проверки наличия элемента под курсором, устанавли-
вающий соответствующее значение m_pSelected. Проверка наличия элемента под
курсором проста; если позиция курсора находится внутри ограничивающего прямо-
угольника элемента, значит, этот элемент расположен под курсором. Вот как можно
модифицировать обработчик OnMouseMove () для проверки элемента под курсором.
void CSketcherView::OnMouseMove (UINT nFlags, CPoint point)
/ / Определить объект контекста устройства для текущего представления
CClientDC aDC(this); // Контекст устройства для текущего представления
OnPrepareDC(&aDC);
CSketcherDoc* pDoc=GetDocument();
CElement* pElement = 0;
CRect aRect (0,0,0,0);
POSITION aPos = pDoc->GetListHeadPosition(); // Получить позицию
// первого элемента
// Получить уточненную начальную точку
// Получить указатель на документ
// Указатель элемента
mjpSelected = 0;
while(aPos)
Итерация по списку
pElement = pDoc->GetNext(aPos);
aRect
aDC.LPtoDP(aRect);
aRect.NormalizeRect();
if(aRect.PtlnRect(point))
pElement-XSetBoundRect ();
// Преобразование к координатам устройства
// Нормализовать прямоугольник повторно
// Есть ли элемент под курсором?
mjpSelected = pElement;
break;
798 Глава 15
aDC.SetR0P2(R2_N0TX0RPEN); // Установить режим рисования
if((nFlags&MK_LBUTTON) && (this==GetCapture ()))
aDC.DPtoLP(&point); // Преобразовать указатель к логическим координатам
m_SecondPoint = point; // Сохранить текущую позицию курсора
if(m_pTempElement)
if(CURVE == GetDocument()->GetElementType()) // Это кривая?
{ // Рисуем кривую, поэтому добавить
// сегмент к существующей кривой
static_cast<CCurve*>(m_pTempElement)->AddSegment(m_SecondPoint);
m_pTempElement->Draw(&aDC); // Теперь нарисовать ее
return; // Готово
aDC.SetROP2(R2_NOTXORPEN); // Установить режим рисования
// Перерисовать старый элемент, чтобы он исчез из представления
m_jpTempElement->Draw(&aDC);
delete m_pTempElement; // Удалить старый элемент
m_pTempElement = 0; 11 Сбросить указатель в 0
// Создать временный элемент для типа и цвета,
/ / записанных в объекте документа и нарисовать его
m_pTempElement = CreateElement(); // Создать новый элемент
m_pTempElement->Draw(&aDC); // Нарисовать элемент
Новый код выглядит объемным, тем не менее, он очень прост. Его задача — прове-
рить, что курсор лежит внутри ограничивающего прямоугольника каждого элемента по
очереди, и сохраняет адрес первого элемента, для которого это так, в m_pSelected.
Если курсор не лежит внутри ни одного из прямоугольников, элемент m_pSelected
содержит 0. Обратите внимание на то, как преобразуется каждый ограничивающий
прямоугольник в координаты устройства и повторно нормализуется перед тем, как
выполнить проверку. Без этого прямоугольник окажется в неправильном месте, по-
скольку установлен режим отображения MM_LOENGLISH.
После всего этого код готов к тестированию контекстных меню.
Испытание всплывающих меню
Вы добавили весь код, необходимый для работы всплывающих меню, так что мо-
жете выполнить сборку и запустить программу Sketcher, дабы испытать их. Если под
курсором нет элементов, появляется второе всплывающее меню, позволяя изменить
тип и цвет элемента. Эти опции работают, поскольку они генерируют точно те же со-
общения, что и соответствующие пункты главного меню, а также потому, что вы уже
написали обработчики для них.
Если под курсором есть элемент, появляется первое контекстное меню с пунктами
Move и Delete в нем. Пока они ничего не делают, потому что вам еще только пред-
стоит реализовать обработчики для генерируемых ими сообщений. Попробуйте по-
щелкать правой кнопкой мыши вне окна представления. Сообщения об этом не пере-
даются в окно представления документа вашего приложения, поэтому всплывающие
меню не отображаются.
Обратите внимание, что контекстные меню для выбора элемента и цвета работа-
ют не совсем правильно — они устанавливают правильный тип и цвет в классе, но не
меняют меток во всплывающем меню. Класс документа обрабатывает сообщения от
Создание документа и усовершенствование представления
799
меню, но сообщения UPDATE_COMMAND_UI в контекстном меню не появляются — они
работают только в меню IDR_SketcherTYPE. Читайте дальше о том, как это можно
исправить.
Пометка пунктов контекстного меню
Пометка элементов меню element должна выполняться в функции OnContextMenu ()
в классе CSketcherView перед отображением контекстного меню. Класс CMenu вклю-
чает функцию, специально предназначенную для этой цели. Вот ее прототип:
UINT CheckMenuItem(UINT nlDCheckltem, UINT nCheck);
Эта функция помечает и снимает отметку с любого пункта контекстного меню.
Первый параметр выбирает, какую позицию контекстного всплывающего меню не-
обходимо пометить, или снять с нее отметку; второй параметр — это комбинация двух
флагов, один из которых определяет, как первый параметр специфицирует требуе-
мый пункт, а второй указывает, нужно ли отметку поставить либо же снять. Поскольку
каждый флаг — это отдельный бит в значении UINT, они комбинируются операцией
двоичного ИЛИ.
Флаг, указывающий способ идентификации пункта, может принимать два возмож-
ных значения, описанные в табл. 15.6.
Таблица 15.6. Флаг, указывающий способ идентификации пункта меню
Значение Описание
mf byposition Первый параметр — индекс, где 0 специфицирует первый пункт, 1 — второй и т. д.
mf bycommand Первый параметр — идентификатор меню.
Используйте MF_BYCOMMAND, дабы не заботиться о последовательности появления
пунктов во всплывающем меню или даже в его подменю.
Допустимыми значениями флага для установки и снятия отметки с пункта меню
являются MF_CHECKED и MF_UNCHECKED, соответственно.
Код установки и снятия отметки с пункта меню, по сути, одинаков д^я 'всех пун-
ктов второго всплывающего меню. Посмотрим, как правильно устанавливать отметку
на пункте меню Black (Черный). Первый аргумент функции CheckMenuItem () — это
идентификатор меню ID_COLOR_BLACK. Второй аргумент — комбинация команды
MF_BYCOMMAND либо с MF_CHECKED, либо с MF_UNCHECKED, в зависимости от выбора
текущего цвета. Текущий цвет можно получить из документа, используя функцию
GetElementColor () в следующем операторе:
COLORREF Color = GetDocument () ->GetElementColor ();
Вы можете использовать переменную Color для выбора соответствующего флага,
используя условную операцию, и затем комбинируя результат с флагом MF_BYCOMMAND
[ля получения второго аргумента функции CheckMenuItem (), так что оператор для
установки метки пункта будет таким:
menu.CheckMenuItem(ID_COLOR_BLACK,
(BLACK==Color?MF_CHECKED:MF_UNCHECKED)|MF_BYCOMMAND);
Вам не нужно здесь специфицировать подменю, поскольку пункт меню уникально
идентифицируется в меню значением его ID. Вам нужно просто изменить ID и значе-
ние цвета в операторе, чтобы соответствующим образом установить флаги для каждо-
го из прочих пунктов меню цвета.
800 Глава 15
Пометка пунктов меню элементов, по сути, выполняется так же. Для пометки пун-
кта меню Line можно записать следующим образом:
unsigned int ElementType = GetDocument()->GetElementType();
menu.CheckMenuItem(ID_ELEMENT_LINE,
(LINE==ElementType?MF_CHECKED:MF_UNCHECKED)|MF_BYCOMMAND);
Ниже представлен полный код обработчика OnContextMenu ().
void CSketcherView::OnContextMenu(CWnd* pWnd, CPoint point)
CMenu menu;
menu.LoadMenu(IDR_CURSOR_MENU);
// Установить отметки, если это меню "no element"
if (m pSelected = 0)
// Пометить пункты меню цвета
COLORREF Color = GetDocument () ->GetElementColor () ;
menu. CheckMenuItem (ID_jCOLOR_BLACK,
(BLACK=Color?MF_CHECKED:MF_UNCHECKED) |MF_BYCOMMAND) ;
menu. CheckMenuItem (ID__COLOR_RED,
(RED=Color?MF_CHECKED:MF_UNCHECKED) | MF_BYCOMMAND) ;
menu. CheckMenuItem (ID_jCOLOR_GREEN,
(GREEN=Color?MF_CHECKED :MF_UNCHECKED) |MF_BYCOMMAND) ;
menu. CheckMenuItem (ID__COLOR_BLUE,
(BLUE==Color?MF_CHECKED:MF_UNCHECKED) | MF_BYCOMMAND) ;
// Пометить пункты меню "element"
unsigned int ElementType = GetDocument () ->GetElementType () ;
menu. CheckMenuI tern (ID__ELEMENT__LINE,
(LINE=ElementType?MF_CHECKED:MF_UNCHECKED) | MF_BYCOMMAND) ;
menu.CheckMenuItern(ID_ELEMENT_RECTANGLE,
(RECTANGLE—ElementType ?MF_CHECKED:MF__UNCHECKED) | MF_B YCOMMAND) ;
menu. CheckMenuI tern (ID_JELEMENT_CIRCLE,
(CIRCLE—ElementType?MF_CHE CKED: MF_UNCHECKED) | MF_BYCOMMAND) ;
menu. CheckMenuI tern (ID__ELEMENT_CURVE,
(CURVE=E 1 eme nt Type ?MF CHECKED:MF UNCHECKED) |MF BYCOMMAND) ;
Ofenu* pPopup = menu.GetSubMenu(m_pSelected = 0 ? 1 : 0) ;
ASSERT (pPopup ! = NULL) ;
pPopup->TrackPopupMenu(TPM_LEFTALIGN |
TPM RIGHTBUTTON, point.x, point.y, this);
После этого изменения пункты контекстного меню должны помечаться коррек-
тно, когда вы заново выполните сборку и запустите программу Sketcher.
Подсветка элементов
В идеале пользователь захочет знать, какой элемент находится под курсором, перед
тем, как выполнять щелчок правой кнопкой мыши для вызова контекстного меню.
Когда нужно удалить элемент, вы должны знать, какой именно элемент будет удален.
Аналогично, когда необходимо использовать второе контекстное меню, например,
чтобы изменить цвет, нужно быть уверенным, что под курсором нет элемента. Чтобы
точно показать, какой элемент находится под курсором, его следует каким-то образом
выделить перед нажатием правой кнопки мыши.
Это можно сделать в функции-члене элемента Draw (). Все, что для этого пона-
добится — передать аргумент функции Draw (), чтобы указать, когда элемент должен
Создание документа и усовершенствование представления 801
быть подсвечен. Если передать функции Draw () адрес текущего выбранного элемен-
та, который можно получить от члена m_pSelected представления, то можно срав-
нить его с указателем this, чтобы узнать, текущий ли это элемент.
Подсветка элемента везде работает одинаково, так что возьмем для примера класс
CLine. Вы можете добавить аналогичный код в каждый из классов, описывающих
типы элементов. Прежде чем начать изменять CLine, сначала придется расширить
определение базового класса CElement.
class CElement : public CObject
protected:
COLORREF m_Color;
CRect m__EnclosingRect;
int m Pen;
// Цвет элемента
// Прямоугольник, описывающий элемент
// Ширина пера
public:
virtual ~CElement(void);
// Виртуальная операция рисования
virtual void Draw (CDC* pDC, CElement* pElement=0) {}
CRect GetBoundRect(); // Получить ограничивающий прямоугольник элемента
protected:
CElement(void); / / Для предотвращения вызова конструктора по умолчанию
Изменение состоит в добавлении второго параметра к виртуальной функции
Draw (). Это — указатель на элемент. Причина инициализации второго параметра ну-
лем связана с тем, чтобы позволить использование этой функции только с одним ар-
гументом; второй будет установлен в 0 по умолчанию.
Затем потребуется аналогично модифицировать объявление функции Draw () в
каждом из классов-наследников CElement. Например, вы должны изменить определе-
ние класса CLine следующим образом:
class CLine :
public CElement
public:
-CLine(void);
// Функция для отображения линии
virtual void Draw(CDC* pDC, CElement* pElement=0);
// Конструктор объекта линии
CLine(CPoint Start, CPoint End, COLORREF aColor);
protected:
CPoint m_StartPoint; // Начальная точка линии
CPoint m_EndPoint; // Конечная точка линии
CLine(void); // Конструктор по умолчанию — не должен использоваться
Реализация каждой функции Draw () для класса-наследника CElement должна быть
расширена таким же способом. Вот как будет выглядеть эта функция в классе CLine:
void CLine::Draw(CDC* pDC, CElement* pElement)
11 Создать перо для этого объекта и инициализировать
// его цветом и шириной линии в 1 пиксель
СРеп аРеп;
COLORREF aColor = m_Color; // Инициализировать цв<
if (this = pElement) // Элемент выбран?
aColor = SELECT COLOR;
// Инициализировать цветом элемента
// Элемент выбран?
// Цвет подсветки
802 Глава 15
if(!aPen.CreatePen(PS SOLID, m Pent aColor))
// Создание пера не удалось. Прервать программу
AfxMessageBox(_Т("Не удалось создать перо для рисования линии"), МВ_ОК);
AfxAbort ();
СРеп* pOldPen = pDC->SelectObject(&аРеп); 11 Выбрать перо
// Нарисовать линию
pDC->MoveTo(m_StartPoint);
pDC->LineTo(m_EndPoint);
pDC->SelectObject(pOldPen); // Восстановить старое перо
Это очень простое изменение. Вы устанавливаете новую локальную переменную
aColor в значение текущего цвета, хранящегося в m_Color, а оператор if сбросит
значение aColor в SELECT_COLOR, когда pElement равен this, что бывает, когда
текущий элемент совпадает с выбранным. Вы также должны добавить определение
SELECT_COLOR в файл OurConstants .h.
// Определения констант
#pragma once
// Определения типов элементов.
// Каждое значение типа должно быть уникальным
const unsigned int LINE = 101U;
const unsigned int RECTANGLE = 102U;
const unsigned int CIRCLE = 103U;
const unsigned int CURVE = 104U;
///////////////////////////////////
// Значения цветов для рисования
const COLORREF BLACK = RGB(0,0,0);
const COLORREF RED = RGB(255,0,0);
const COLORREF GREEN = RGB(0,255,0);
const COLORREF BLUE = RGB(0,0,255);
const COLORREF SELECT_COLOR = RGB(255,0,180) ;
///////////////////////////////////
Теперь вы должны добавить директиву #include для OurConstants . h в файл
CElements .срр, чтобы сделать доступным определение SELECT_COLOR. На этом вы
практически полностью реализовали подсветку. Производные от CElement классы те-
перь способны рисовать себя как выбранные — необходим только механизм для опре-
деления элемента, который должен быть выбранным. Где же это сделать? Вы опреде-
ляете элемент (если таковой имеется), находящийся под курсором, в обработчике
OnMouseMove () класса CSketcherView, поэтому очевидно, что это как раз то место,
куда нужно отправить подсветку.
Ниже показаны необходимые дополнения обработчика OnMouseMove ().
void CSketcherView::OnMouseMove(UINT nFlags, CPoint point)
// Определить объект контекста устройства для представления
CClientDC aDC(this); // Контекст устройства для этого представления
OnPrepareDC(&aDC); // Получить уточненную начальную точку
aDC.SetROP2(R2_NOTXORPEN); // Установить режим рисования
if((nFlags&MK_LBUTTON) && (this==GetCapture()))
aDC.DPtoLP(&point); // Преобразовать точку в логические координаты
m SecondPoint = point; // Сохранить текущую позицию курсора
Создание документа и усовершенствование представления 803
if(m_pTempElement)
if(CURVE == GetDocument()->GetElementType()) // Это кривая?
{ // Рисуем кривую, поэтому добавить
/ / сегмент к существующей кривой
static_cast<CCurve*>(m_pTempElement)->AddSegment(m_SecondPoint);
m_pTempElement->Draw(&aDC); // Теперь нарисовать ее
return; // Готово
aDC.SetROP2(R2_NOTXORPEN); // Установить режим рисования
// Перерисовать старый элемент, чтобы он исчез из представления
m_pTempElement->Draw(&aDC);
delete m_pTempElement; // Удалить старый элемент
m_pTempElement =0; // Сбросить указатель в 0
// Создать временный элемент для типа и цвета,
// записанных в объекте документа и нарисовать его
m_pTempElement ® CreateElementО;// Создать новый элемент
m_pTempElement->Draw(&aDC); // Нарисовать элемент
else
{ // Мы не рисуем элемент, поэтому выполним подсветку...
CSketcherDoc* pDoc=GetDocument (); // Получить указатель на документ
CElement* pElement = 0; // Сохранить указатель на элемент
CRect aRect(0,0,0,0); // Сохранить прямоугольник
POSITION aPos = pDoc->GetListHeadPosition(); // Получить позицию
// первого элемента
CElement* pOldSelection = mjpSelected; //Сохранить старый выбранный элемент
mjpSelected = 0;
while (aPos) // Итерация по списку
pElement = pDoc->GetNext(aPos);
aRect = pElement-XSetBoundRect ();
aDC.LPtoDP(aRect);
aRect.NormalizeRect();
// Выбрать первый элемент, находящийся под курсором
if (aRect. PtlnRect (point))
mjpSelected = pElement;
break;
завершаем
if (mjpSelected = pOldSelection)
return;
// Снять старую подсветку, если она была
if(pOldSelection != 0) // Проверить, была ли
aRect = pOldSelection-ХЗеtBoundRect ();
aDC. LPtoDP (aRect); // Преобразовать в координаты устройства
aRect. NormalizeRect (); // Нормализовать
InvalidateRect(aRect, FALSE); // Сделать область недействительной
// Подсветить новый выбор, если он есть
if (m_pSelected != 0) // Проверить, есть ли
aRect = mjpSelected-XSetBoundRect ();
aDC.LPtoDP(aRect) ; // Преобразовать в координаты устройства
804 Глава 15
aRect. NormalizeRect(); // Нормализовать
InvalidateRect(aRect, FALSE); // Сделать область недействительной
}
Когда вы не пребываете в процессе создания нового элемента, то хотите иметь
дело только с подсвеченными элементами. Таким образом, весь код подсветки может
быть добавлен в новой конструкции else главного if. Это потребует перемещения
введенного ранее кода для определения элемента, расположенного под курсором, в
конструкцию else.
Вы должны отслеживать ранее подсвеченный элемент, потому что если есть но-
вый, со старого придется снять подсветку. Чтобы сделать это, вы сохраняете значе-
ние m_pSelected в pOldSelection. Затем вы ищете элемент под курсором, и если он
есть, сохраняете его адрес в m__pSelected.
Если pOldSelection и m_pSelected совпадают, значит, либо они оба содержат
адрес одного и того же элемента, либо оба равны нулю. Если они равны друг другу
и не равны нулю, то, что уже было подсвечено, таковым должно оставаться, так что
ничего не нужно делать. Если они оба равны нулю, ничего не было подсвечено, и ни-
чего не должно быть подсвечено, так что в этом случае тоже ничего не нужно делать.
В обоих случаях просто выполняется возврат из функции. Если же они не равны, с
ними обоими потребуется что-то сделать.
Если pOldSelection не null, то вы можете снять подсветку со старого элемента.
Механизм тот же, что и раньше — получить ограничивающий прямоугольник в коор-
динатах устройства и передать его функции InvalidateRect () для контекста устрой-
ства. Затем вы проверяете m_pSelected, и если он не равен null, значит, неоьходи-
мо подсветить элемент, адрес которого он содержит. Это снова включает получение
ограничивающего прямоугольника в координатах устройства и передачу его функции
InvalidateRect().
Рисование подсвеченных элементов
Но вам все еще нужно обеспечить, чтобы подсвеченный элемент действительно
рисовался подсвеченным. Указатель m_pSelected где-то должен быть передан функ-
ции рисования каждого элемента. Единственное место для этого — функция OnDraw ()
представления.
void CSketcherView::OnDraw (CDC* pDC)
CSketcherDoc* pDoc = GetDocument ();
ASSERT_VALID(pDoc);
if(!pDoc)
return;
POSITION aPos = pDoc->GetListHeadPosition();
CElement* pElement =0; // Хранилище указателя на элемент
while (aPos) // Выполнять цикл, пока aPos — не null
pElement = pDoc->GetNext (aPos); 11 Получить указатель на текущий элемент
// Если элемент видимый...
if(pDC->RectVisible(pElement->GetBoundRect О))
pElement->Draw (pDC , mjpSelected);
// ...нарисовать его
}
Вы должны изменить всего одну строку. Функция Draw () элемента теперь прини-
мает второй аргумент, добавленный для передачи адреса подсвеченного элемента.
Создание документа и усовершенствование представления 805
Испытание подсветки
Вот и все, что необходимо для того, чтобы подсветка работала все время. Это
было не тривиально, но с другой стороны, не так уж и страшно. Вы можете выпол-
нить сборку и запустить программу Sketcher, чтобы испытать ее. Всякий раз, когда
под курсором находится элемент, он рисуется в цвете magenta. Это наглядно показы-
вает, к какому элементу будет относиться контекстное меню, прежде чем вы щелкнете
правой кнопкой мыши, и означает, что вы заранее знаете, какое именно контекстное
меню будет отображено.
Обработка сообщений меню
Следующий шаг состоит в том, чтобы предусмотреть код в теле обработчиков для
добавленных ранее пунктов меню Move и Delete. Сначала вы можете добавить код
для Delete, поскольку он проще.
Удаление элемента
Код, который необходим в обработчике OnElementDelete () класса CSketcherView
для удаления текущего выбранного элемента, достаточно прост:
void CSketcherView::OnElementDelete()
if(m_pSelected)
CSketcherDoc* pDoc = GetDocument();
pDoc->DeleteElement(m_pSelected);
pDoc->UpdateAllViews(0);
m pSelected =0; // C6p<
//
11
//
// Сбросить
Получить указатель на документ
Удалить элемент
Перерисовать все представления
указатель на выбранный элемент
Код удаления элемента выполняется, только если m__pSelected содержит правиль-
ный адрес, а это говорит о том, что существует элемент, подлежащий удалению. Вы
получаете указатель на документ и вызываете функцию De let eElement () для объек-
та документа; эту функцию-член мы добавим в класс CsketcherDoc чуть позже. Когда
элемент удален из документа, вызывается UpdateAllViews (), чтобы заставить все
представления перерисовываться без удаленного элемента. И, наконец, вы устанавли-
ваете rrpjpSelected в ноль, чтобы указать, что теперь выделенного элемента нет.
Добавим объявление DeleteElement () как public-член класса CsketcherDoc:
class CSketcherDoc : public CDocument
protected: // Создавать только сериализацией
CsketcherDoc () ;
DECLARE_DYNCREATE (CsketcherDoc)
// Атрибуты
public:
// Операции
public:
void DeleteElement (CElement* pElement); // Удалить элемент
unsigned int GetElementType() // Получить тип элемента
{ return m_Element; }
// Остальная часть класса — как раньше...
806 Глава 15
Она принимает указатель на элемент, подлежащий удалению, в качестве аргумента
и не возвращает ничего. Вы можете реализовать ее в SketcherDoc. срр следующим
образом:
void CSketcherDoc::DeleteElement(CElement* pElement)
if(pElement)
// Если указатель на элемент правильный,
// найти его в списке и удалить
POSITION aPosition = m_ElementList.Find(pElement);
m_ElementList.RemoveAt(aPosition);
delete pElement; // Удалить элемент из кучи
У вас не должно возникать проблем с пониманием работы этой функции. После
того, как вы убедились, что указатель ненулевой, вы находите значение POSITION ука-
зателя в списке, используя функцию-член Find () объекта списка. Затем вы применяе-
те его с функцией-членом RemoveAt (), чтобы удалить указатель из списка, после чего
удаляете элемент, на который установлен указатель mElement, из кучи.
Это все, что необходимо сделать для удаления элементов. Теперь вы имеете про-
грамму Sketcher, в которой можно рисовать во множественных прокручиваемых
представлениях и удалять любой из элементов вашего рисунка в любом из представ-
лений.
Перемещение элемента
Перемещение выделенного элемента несколько сложнее. Так как элемент дол-
жен перемещаться вместе с курсором мыши, вы должны добавить код к методу
OnMous eMove () для обеспечения такого поведения. Поскольку эта функция также
используется для рисования элементов, необходим механизм обозначения того, ког-
да вы находитесь в режиме “перемещения”. Простейший путь сделать это — завести
флаг в классе представления, который можно назвать m MoveMode. Если сделать его
типа BOOL, то значение TRUE может означать, что режим перемещения включен, а
FALSE — отключен. Конечно, вы можете также определить его как принадлежащий к
фундаментальному типу bool, принимающему значения true и false.
Вы также должны отслеживать курсор во время перемещения, так что для это-
го понадобится еще один член данных представления. Вы можете назвать его
m__CursorPos, и выбрать CPoint в качестве его типа. Еще одна возможность, которую
потребуется предусмотреть — возможность прервать перемещение. Чтобы сделать
это, вы должны запомнить первую позицию курсора при запуске операции перемеще-
ния, дабы при необходимости можно было вернуть элемент обратно. Это будет еще
один член типа CPoint, назовем его m_FirstPos. Добавьте три новых члена в раздел
protected класса представления:
class CSketcherView: public CScrollView
// Остальная часть класса -
protected:
CPoint m_FirstPoint;
CPoint m_SecondPoint;
CElement* m_pTempElement;
CElement* m_pSelected;
BOOL m MoveMode;
как раньше. ..
// Первая точка, записанная для элемента
// Вторая точка, записанная для элемента
// Указатель на временный элемент
// Текущий выбранный элемент
// Флаг перемещения элемента
Создание документа и усовершенствование представления 807
CPoint mjCursorPos; // Позиция курсора
CPoint m^FirstPos; // Исходная позиция з перемещении
// Остальная часть класса
II
Новые члены также должны быть инициализированы в конструкторе CSketcherView,
поэтому модифицируйте его:
CSketcherView:: CSketcherView ()
: m_FirstPoint(CPoint(0,0))
r m_SecondPoint(CPoint(0,0))
f m_pTempElement(NULL)
r m_pSelected(NULL)
, m_MoveMode(FALSE)
, m__CursorPos(CPoint(0,0))
, m FirstPos(CPoint(0,0))
// TODO: добавить сюда код конструирования
Процесс перемещения элемента начинается, когда выбирается пункт Move из кон-
текстного меню. Теперь вы можете добавить код обработчика сообщения от пункта
меню Move, чтобы установить условия, необходимые для выполнения операции.
void CSketcherView: :OnElementMove ()
CClientDC aDC(this);
OnPrepareDC (&aDC); / / Установить контекст устройства
GetCursorPos (SmjCursorPos); // Получить позицию курсора в экранных координатах
ScreenToClient(&m_CursorPos) ;// Преобразовать в клиентские координаты
aDC.DPtoLP(&m_CursorPos);
m_FirstPos = mjCursorPos;
m MoveMode = TRUE;
// Преобразовать в логические координаты
// Запустить режим перемещения
В обработчике происходят четыре вещи, которые перечислены ниже.
1. Получение координат текущей позиции курсора, поскольку операция переме-
щения начинается с этой точки.
2. Преобразование позиции курсора в логические координаты, поскольку ваши
элементы определены в логических координатах.
3. Запоминание начальной позиции курсора на случай, если пользователь позднее
захочет прервать перемещение.
4. Установка режима перемещения как флага для OnMouseMove ().
Функция Windows API по имени GetCursorPosition () сохраняет текущую пози-
цию курсора в m_CursorPos. Обратите внимание, что этой функции вы передаете ука-
затель. Позиция курсора измеряется в экранных координатах (то есть, координатах
относительно левого верхнего угла экрана). Все операции с курсором осуществляются
в экранных координатах. Вам нужна позиция в логических координатах, поэтому вы
должны выполнить преобразование за два шага. Функция ScreentoClient () (унасле-
дованный член класса представления) преобразует экранные координаты в клиент-
ские, а затем к результату преобразования применяется функция DPtoLP () объекта
aDC, чтобы осуществить преобразование в логические координаты.
Имея установленный флаг перемещения, можно обновить обработчик сообщения
о движении мыши, чтобы обрабатывать перемещение элемента.
808 Глава 15
Модификация обработчика wm_mousemove
Перемещение элемента происходит, когда включен режим перемещения и курсор
мыши движется. Таким образом, все, что вам необходимо сделать в OnMouseMove () —
это добавить код для обработки движения элемента в блоке, который выполняется
только при условии, что m_MoveMode равно TRUE. Ниже показан новый код для вы-
полнения этого.
void CSketcherView::OnMouseMove(UINT nFlags, CPoint point)
CClientDC aDC(this); // Контекст устройства для этого представления
OnPrepareDC(&aDC); // Получить уточненную начальную точку
// Если включен режим перемещения, передвинуть выбранный элемент
//и вернуть управление
aDC.DPtoLP(&point);
MoveElement(aDC, point);
return;
// Преобразовать в логические координаты
// Переместить элемент
// Остальная часть обработчика движения мыши — как раньше...
Это дополнение не требует пояснений, не правда ли? Оператор if проверяет, что
вы находитесь в режиме перемещения, и затем вызывает функцию MoveElement (),
которая делает все необходимое для перемещения элемента. Все, что остается — реа-
лизовать эту функцию.
Добавьте объявление MoveElement () как protected-член класса CSketcherView,
добавив следующий фрагмент в соответствующую точку определения класса:
void MoveElement(CClientDC& aDC, CPoint& point); // Перемещение элемента
Как всегда, если хотите, можете для этого выполнить щелчок правой кнопкой
мыши на имени класса в Class View. Функции необходим доступ к объекту, инкапсу-
лирующему контекст устройства для представления, aDC, и текущая позиция курсора,
point, поэтому оба они передаются как ссылочные параметры. Реализация функции
в файле SketcherView.срр выглядит следующим образом.
void CSketcherView::MoveElement (CClientDC& aDC, CPoint& point)
CSize Distance = point - m_CursorPos; // Получить расстояние перемещения
m_CursorPos = point; // Установить текущую точку как 1-ю для следующего раза
// Если есть выбранный элемент, двигать его
if(m_pSelected)
aDC.SetROP2(R2_NOTXORPEN);
m_pSelected->Draw(&aDC,m_pSelected);
m_pSelected->Move(Distance);
m_pSelected->Draw(&aDC,m_pSelected);
//
//
//
//
Нарисовать элемент,
чтобы стереть его
Переместить элемент
Нарисовать передвинутый элемент
Расстояние для перемещения текущего выбранного элемента сохраняется локаль-
но в форме объекта Distance типа CSize. Класс CSize специально предназначен для
представления относительной координатной позиции и включает в себя два члена
данных, сх и су, который соответствуют приращениями хи у, Они вычисляются как
разница между текущим положением курсора, записанным в point, и предыдущей по-
Создание документа и усовершенствование представления
809
зицией курсора, сохраненной в m_CursorPos. Для этого применяется операция вы-
читания, перегруженная в классе CPoint. Используемая здесь версия возвращает объ-
ект CSize, но существует также версия, возвращающая объект CPoint. Обычно вы
можете оперировать комбинацией объектов CSize и CPoint. Вы сохраняете текущую
позицию курсора в m_CursorPos для использования при следующем вызове функции,
который происходит, когда поступает новое сообщение о движении мыши в процессе
текущей операции перемещения.
Вы будете реализовывать перемещение элемента в представлении в режиме рисо-
вания R2_NOTXORPEN, поскольку это легко и быстро. Это то же самое, что вы исполь-
зовали при создании элемента. Вы перерисовываете выбранный элемент в текущем
цвете (выбранном), чтобы сбросить его в цвет фона, а затем вызываете функцию
Move () для перемещения элемента на расстояние, заданное Distance. Очень ско-
ро мы добавим эту функцию к классу элемента. Когда элемент перемещает себя, вы
просто используете функцию Draw () еще раз, чтобы отобразить его с подсветкой
в новой позиции. Цвет элемента вернется к нормальному по завершении операции
перемещения, когда обработчик OnLButtonUp () нормально перерисует все окно, об-
ратившись к UpdateAllViews().
Как заставить элементы перемещать себя
Добавим функцию Move () как виртуальный член базового класса CElement.
Модифицируйте определение класса следующим образом:
class CElement:public CObject
protected:
COLORREF m_Color; // Цвет элемента
CRect m_EnclosingRect; // Описанный вокруг элемента прямоугольник
int m__Pen; // Ширина пера
public:
virtual ~CElement(void); // Виртуальный деструктор
// Виртуальная операция рисования
virtual void Draw(CDC* pDC, BOOL Select=FALSE){}
virtual void Move (CSize& aSize) {} // Переместить элемент
CRect GetBoundRect(); // Получить ограничивающий прямоугольник элемента
protected:
CElement(void); // Здесь — для предотвращения вызова
Как говорилось ранее, когда речь шла о члене Draw (), хотя реализация функции
Move () здесь ничего не делает, вы не можете сделать ее пустой виртуальной из-за
требований сериализации.
Теперь вы можете добавить объявление функции Move () в виде public-члена в
каждый класс, производный от CElement. Она будет одинаковой везде:
// Функция для перемещения элемента
virtual void Move(CSize& aSize);
Далее вы можете реализовать функцию Move () в классе CLine:
void CLine::Move(CSize& aSize)
m_StartPoint += aSize;
m_EndPoint += aSize;
m EnclosingRect += aSize;
// Переместить начальную точку
//и конечную
// Переместить описанный прямоугольник
810 Глава 15
Это просто, благодаря перегруженным операциям +« в классах CPoint и CRect.
Все они работают с объектами CSize, поэтому вы просто добавляете относительное
расстояние, специфицированное в aSize, к начальной и конечной точкам линии и
описанного прямоугольника.
Перемещение объекта CRectangle еще проще:
void CRectangle::Move(CSize& aSize)
m EnclosingRect+= aSize; // Переместить прямоугольник
Поскольку прямоугольник определен членом m_EnclosingRect, это все, что не-
обходимо для его перемещения.
Член Move () класса CCircle идентичен:
void CCircle::Move(CSize& aSize)
m EnclosingRect+= aSize; // Переместить прямоугольник, определяющий окружность
Перемещение объекта CCurve несколько сложнее, так как он определен произ-
вольным числом точек. Вы можете реализовать функцию следующим образом:
void CCurve::Move(CSize& aSize)
m_EnclosingRect += aSize; // Переместить прямоугольник
// Получить позицию первого элемента
POSITION aPosition = m_PointList.GetHeadPosition();
while(aPosition)
m_PointList.GetNext(aPosition) += aSize; // Переместить каждую точку
//в списке
Здесь тоже работы не так уж много. В начале описанный прямоугольник, храня-
щийся в m EnclosingRect, перемещается с применением для этого перегруженной
операции += для объектов CRect. Затем выполняется итерация по всем точкам, опре-
деляющим кривую, с передвижением каждой из них с помощью перегруженной опе-
рации += класса CPoint.
Сброс элемента
Все, что теперь осталось — это сбросить элемент в новую позицию по завершении
перемещения его пользователем или же полностью отменить перемещение. Чтобы
сбросить элемент в новую позицию, пользователь щелкает левой кнопкой мыши, так
что вы можете реализовать эту операцию в обработчике OnLButtonDown (). Чтобы
прервать операцию, пользователь щелкает правой кнопкой мыши, поэтому вы може-
те добавить обработчик OnRButtonDown (), чтобы справиться с этим.
Сначала позаботимся о левой кнопке мыши. Для этого нужно будет предпринять
специальное действие, когда включен режим перемещения. В следующем коде необ-
ходимые изменения выделены полужирным.
void CSketcherView::OnLButtonDown (UINT nFlags, CPoint point)
CClientDC aDC(this);
OnPrepareDC(&aDC);
aDC.DPtoLP(&point);
/ / Создать контекст устройства
// Исправить начальную точку
// Преобразовать точку в логические координаты
Создание документа и усовершенствование представления 811
1 f (m_MoveM6de)
//В режиме перемещения, поэтому сбросить элемент
m_MoveMode = FALSE; // Прекратить режим перемещения
m_pSelected = 0; // Снять выбор с элемента
GetDocument () ->UpdateAllViews (0); // Перерисовать представления
else
m_FirstPoint = point; // Записать текущую позицию
SetCapture (); // Перехватывать последующие сообщения мыши
Как видите, код замечательно прост. Сначала вы убеждаетесь, что режим переме-
щения включен. Если это так, вы просто возвращаете флаг режима в FALSE и сни-
маете выделение с элемента. Это все, что требуется, поскольку положение элемента
отслеживалось вместе с мышью, так что он уже находится в правильном месте. И, на-
конец, чтобы привести в порядок все представления документа, вызывается функция
документа UpdateAllViews (), что приводит к перерисовке всех представлений.
Добавьте в класс CSketcherView обработчик для сообщений WM RBUTTONDOWN, ис-
пользуя окно свойств этого класса. Его реализация должна решать две задачи: пере-
мещать элемент в исходное положение и отключать режим перемещения. Ниже по-
казан необходимый для этого код.
void CSketcherView:: OnRBut ton Down (UINT nFlags, CPoint point)
i f(m_MoveMode)
// В режиме перемещения, поэтому отбросить элемент в исходную позицию
CClientDC aDC(this);
OnPrepareDC(&aDC); 11 Исправить начальную точку
MoveElement(aDC, m_FirstPos); // Переместить элемент в исходную позицию
m_MoveMode = FALSE; // Отменить режим перемещения
m_pSelected = 0; // Снять выделение элемента
GetDocument()->UpdateAllViews(0); // Перерисовать все представления
return; // Завершено
Сначала вы создаете объект CClientDC для использования в функции MoveElement ().
Затем вы можете вызвать функцию MoveElement () для перемещения текущего вы-
бранного элемента на расстояние от текущей позиции курсора в его исходную по-
зицию, сохраненную в m_FirstPos. После того, как элемент возвращен обратно, вы
просто отключаете режим перемещения, снимаете выделение с элемента и иниции-
руете перерисовку всех представлений.
Испытание приложения
Теперь все готово для работы контекстного меню. Если вы снова соберете и запу-
стите программу Sketcher, то сможете выбирать тип и цвет элемента в контекстном
меню, или же, если курсор находится над элементом — сможете двигать или удалять
элемент, пользуясь вторым контекстным меню.
812 Глава 15
Работа с маскированными элементами
Остается еще одно ограничение, которое имеет смысл ликвидировать. Если эле-
мент, который вы хотите переместить или удалить, закрыт прямоугольником другого
элемента, нарисованного позже данного, то вы не сможете выделить его, поскольку
Sketcher всегда находит первым самый верхний элемент. Этот верхний элемент пол-
ностью маскирует элементы, которые он покрывает. Это результат последовательно-
го размещения элементов в списке. Вы можете исправить это, добавив в контекстное
меню пункт Send to Back (На задний план), чтобы можно было переместить элемент
в начало списка.
Добавьте разделитель и новый пункт в ресурс меню IDR_CURSOR_MENU, как показа-
но на рис. 15.15.
Sketcher. г с (ID...OR_MHW - Menu)*
element no element I Tvoe Here
Move
Delete
Send to Back
Pwc. 15Л5. Добавление пункта Send to Back (На задний план)
Обработчик этого пункта меню можно добавить в класс представления через
окно Properties класса CSketcherView. Лучше обработать его в представлении, по-
скольку именно там запоминается выбранный элемент. Выберите кнопку Messages
(Сообщения) панели инструментов в окне Properties класса и дважды щелкните на
идентификаторе сообщения ID_SEGMENT_SENDTOBACK. После этого вы сможете вы-
брать ниже COMMAND и <Add> OnElementSendtoback в правой колонке. Обработчик
можно реализовать так, как показано ниже.
void CSketcherView:: OnElementSendtoback ()
GetDocument ()->SendToBack (injoSelected) ; // Передвинуть элемент в списке
Вы заставляете документ выполнить эту работу, передавая указатель на текущий
выбранный элемент общедоступной (public) функции SendToBackO , которую вы
реализуете в классе CSketcherDoc. Добавьте ее в определение класса с типом возвра-
та void и параметром типа CElement*. Эту функцию можно реализовать следующим
образом.
void CSketcherDoc: :SendToBack(CElement* pElement)
if(pElement)
/ / Если указатель элемента правильный,
// найти его и исключить из списка
POSITION aPosition = m_ElementList.Find(pElement);
m_E1ementList.RemoveAt(aPosition);
m_ElementList.AddTail(pElement); // Поместить его обратно в конец списка
}
Создание документа и усовершенствование представления
813
Получив значение POSITION, соответствующее элементу, вы удаляете элемент из
списка вызовом RemoveAt (). Конечно, это не удалит элемент из памяти, это удалит
лишь указатель из списка. Затем вы вставляете указатель на элемент в конец списка,
применив для этого функцию AddTail ().
Когда элемент передвинут в конец списка, он не может маскировать никакой дру-
гой элемент, потому что поиск всегда начинается с начала. Вы всегда найдете снача-
ла какой-то другой элемент, если соответствующий ограничивающий прямоугольник
решить любую проблему с маскированием элементов в представлении.
Резюме
В этой главе было показано, как применять классы коллекций MFC к решению
проблем управления наборами объектов и указателей на объекты. Коллекции — ис-
тинное сокровище в программировании для Windows, потому что данные приложе-
ния, которые вы сохраняете в документе, часто поступают в неструктурированном и
непредсказуемом виде, и вам необходимо иметь возможность проходить по данным
всякий раз, когда представление требует обновления.
Вы также узнали, как создавать данные документа и управлять ими в списке указа-
телей документа, а в контексте приложения Sketcher —
как взаимодействуют между
собой документ и его представления.
Возможности представлений в программе Sketcher были усовершенствованы в
нескольких отношениях. Была добавлена прокрутка с использованием класса MFC по
имени CScrollView, было введено всплывающее меню в месте нахождения курсора
для перемещения и удаления элементов. Вы также реализовали средство подсветки
элемента, чтобы помочь пользователю увидеть элемент, выбранный для перемеще-
ния или удаления.
В этой главе было затронуто немало фундаментальных тем, и особого внимания
заслуживают следующие важные моменты.
Если вам нужен класс коллекции для управления объектами или указателями, то
лучшим выбором будет один из шаблонных классов коллекций, поскольку в боль-
шинстве случаев они обеспечивают безопасные к типам операции с данными.
□ Когда вы рисуете в контексте устройства, координаты считаются в логиче-
ских единицах измерения, зависящих от установленного режима отображения.
Точки в окне, поступающие от Windows вместе с сообщениями мыши, пред-
ставлены клиентскими координатами. Обычно упомянутые две координатных
системы не совпадают.
□ Координаты, определяющие положение курсора, относятся к экранным коор-
динатам, измеряемым в пикселях относительно левого верхнего угла экрана.
□ Функции преобразования между клиентскими и логическими координатами до-
ступны в классе CDC.
□ Windows запрашивает перерисовку представления, отправляя сообщение
WM PAINT вашему приложению. Это инициирует вызов функции-члена пред-
ставления OnDraw().
□ Все постоянное рисование документа должно выполняться в функции OnDraw ()
класса представления. Это гарантирует, что окно будет правильно нарисовано,
когда того потребует Windows.
814 Глава 15
□ Вы можете сделать свою реализацию функции OnDraw () более эффективной,
вызывая функцию-член класса CDC по имени RectVisible (), чтобы проверить,
какая именно часть сущности должна быть нарисована.
□ Чтобы обеспечить синхронное обновление множества представлений при
обновлении документа, вы можете вызвать функцию-член объекта документа
UpdateAllViews (). Это вызовет член OnUpdate () каждого представления.
□ Вы можете передать информацию функции UpdateAllViews (), указывающую,
какая часть представления должна быть перерисована. Это ускоряет перери-
совку представлений.
□ Можно отобразить контекстное меню в позиции курсора в ответ на щелчок
правой кнопкой мыши. Это меню создается как обычное всплывающее меню.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Реализуйте класс CCurve так, чтобы указатели добавлялись в начало списка, а
не в конец.
2. Реализуйте класс CCurve в программе Sketcher, используя для представления
кривой линии типизированный список указателей вместо списка объектов.
3. Почитайте о шаблонном классе коллекций САггау в справочной системе и вос-
пользуйтесь им для хранения точек класса CCurve программы Sketcher.
16
Работа с диалогами
и элементами управления
Диалоги и элементы управления — это основные инструменты взаимодействия
пользователя со средой Windows. В настоящей главе вы изучите реализацию диалогов
и элементов управления, применяя их для расширения программы Sketcher.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
□ Диалоги и создание диалоговых ресурсов.
□ Элементы управления и способы добавления их к диалогам.
□ Базовые варианты доступных элементов управления.
□ Создание класса диалога для управления диалогом.
□ Как запрограммировать создание диалогового окна и как получить информа-
цию от его элементов управления.
□ Модальные и немодальные диалоги.
□ Как реализовать и использовать прямой обмен данными и верификацию в эле-
ментах управления.
□ Как добавить к приложению панель состояния.
Понятие диалогов
Конечно, диалоговые окна не являются для вас чем-то новым. Большинство
Windows-программ широко используют диалоги для управления вводом данных. Вы
щелкаете на элементе меню, и “всплывает” диалоговое окно с различными элемента-
ми управления, которые служат для ввода информации. Почти все, что появляется в
диалоговом окне, является элементами управления. Диалог — это в действительности
окно, и фактически каждый элемент управления в диалоговом окне также является
816 Глава 16
специализированным окном. Подумайте о том, что большинство вещей, которые вы
видите на экране под Windows, являются окнами.
Хотя элементы управления имеют определенную ассоциацию с диалоговыми
окнами, вы также можете при желании создавать и использовать их в других окнах.
Типичное диалоговое окно показано на рис. 16.1.
Окна списка
Кнопки
Окно списка Кнопки
Комбинированный список
Кнопки панели
инструментов
; 16.1. Типичное диалоговое окно
Это диалоговое окно, которое вызывается в Visual C++ 2005 при выборе пункта
меню FileOpen (Файл^Открыть). Аннотации показывают разнообразные элемен-
ты управления, из которых собирается интуитивно понятный интерфейс для выбора
файла, который требуется открыть. Это делает диалог легким в использовании, даже
несмотря на очень широкий диапазон его возможностей.
Для создания и отображения диалогового окна в программе MFC требуются две
вещи: физическое появление диалогового окна, которое определено в ресурсном
файле, и объект класса диалога, используемый для управления операциями диалога
и его элементов управления. MFC предоставляет вам класс по имени CDialog для ис-
пользования после того, как вы определяете ваш ресурс диалога.
Что такое элементы управления?
В Windows вам доступно множество различных элементов управления, и в боль-
шинстве случаев система обеспечивает высокую степень гибкости в отношении их
внешнего вида и поведения. Большинство из этих элементов попадает в одну из ше-
сти категорий, перечисленных в табл. 16.1.
Работа с диалогами и элементами управления 817
Таблица 16.1. Категории элементов правления
Тип элемента управления
Что делает
Статические элементы управления
Эти элементы используются для отображения заголовков или опи-
сательной информации.
Кнопочные элементы управления
Кнопки представляют механизм ввода “одним щелчком”.
Существуют три разновидности кнопочных элементов управления:
простые нажимаемые кнопки, кнопки-переключатели, из группы
которых в каждый отдельный момент времени может быть выбрана
только одна, и кнопки-флажки, которые могут пребывать в выбран-
ном состоянии, независимо от соседних флажков в той же группе.
Линейки прокрутки
Окна списков
Элементы редактирования
Линейки прокрутки обычно используются для прокрутки текста или
графических изображений, горизонтально или вертикально, внутри
другого элемента управления.
Предоставляют списки выбора, в которых одновременно можно
выделить один или более вариантов.
Элементы редактирования, принимающие текстовый ввод или по-
зволяющие редактировать отображаемый текст.
Комбинированные списки
Комбинированные списки предоставляют списки выбора, из кото-
рых можно выбирать один вариант, комбинированные с возмож-
ностью самостоятельного ввода текста.
На рис. 16.2 показаны некоторые примеры различных типов элементов управле-
ния.
Статические элементы управления
предоставляют статическую информацию,
такую как заголовки или инструкции,
либо просто представляют декорации
в диалоге в форме пиктограммы
или закрашенного прямоугольника
Окно списка представляет предопределенный
список элементов, из которого вы можете
осуществлять выбор. Линейка прокрутки
необходима, когда окно не умещает все элементы
сразу Список также может иметь несколько
колонок и прокручиваться в горизонтальном
направлении. Доступна также версия окна списка,
отображающая пиктограммы наряду с текстом.
Переключатели обычно
группируются так,
что только один из них
может быть помечен
(нажат) в каждый момент
времени, а другие — нет.
Флажки помечаются
индивидуально, и в каждый
момент времени может
быть помечено более
одного флажка группы.
Кнопки могут иметь метки
на своей поверхности,
равно как и пиктограммы.
s Examples of Controls
This is a static control
I Г* Д radio button
A check box
A button
A Scroll Bar
A List Box
I _ . T
Sample
List box
Items
Choose
A Text Box
You can edit this
A Combob ж
Это текстовое окно
представляет собой
простейшую форму
редактирующего
элемента, который
позволяет вводить
и/или редактировать
одну строку текста.
Более сложные элементы
редактирования могут
отображать множество
строк текста и поддерживать
прокрутку текста.
Вы уже видели линейки прокрутки,
прикрепленные к клиентской
области окна Sketcher. Линейки
прокрутки также могут размещаться
свободно
Комбинированные списки сочетают возможности
выпадающего списка, из которого вы можете
выбирать, с возможностью самостоятельного
ввода текста. Диалог Save As... (Сохранить как)
использует комбинированный список, чтобы дать
возможность ввести имя файла
Рис. 16.2. Примеры различных типов элементов управления
818 Глава 16
Элемент управления может быть или не быть ассоциирован с объектом класса.
Статические элементы управления ничего не делают непосредственно, поэтому в
ассоциированном классе могут показаться излишними; однако в MFC предусмотрен
класс CStatic, который представляет функции, позволяющие вам изменить внешний
вид статических элементов управления. Кнопочные элементы управления также во
многих случаях могут управляться объектами диалога, но опять-таки MFC предостав-
ляет класс С But ton для использования в ситуациях, когда нужен объект класса для
управления элементом управления. MFC также предлагает полный набор классов для
поддержки всех прочих элементов управления. Поскольку элементы управления явля-
ются окнами, все они унаследованы от CWnd.
Общие элементы управления
Набор стандартных элементов управления, поддерживаемый MFC и редактором
ресурсов, называется общими элементами управления. Общие элементы управления
включают все, что вы уже видели, а также ряд других более сложных элементов, та-
ких, например, как анимированные элементы управления, которые обладают способ-
ностью воспроизводить файлы AVI (Audio Video Interleaved), и древовидные элемен-
ты, которые могут отображать древовидную иерархию элементов.
Еще один удобный элемент управления в наборе общих элементов — кнопки счет-
чика (spin button). Вы можете использовать их для инкремента или декремента зна-
чений, ассоциированных с элементом редактирования. Полный перечень всех воз-
можных элементов управления, которые вы можете использовать, выходит за рамки
этой книги, поэтому я просто приведу несколько наглядных примеров (включая при-
мер, использующий кнопки счетчика) и реализую их в программе Sketcher.
Создание ресурса диалога
Рассмотрим конкретный пример. Добавим диалог в приложение Sketcher, чтобы
предоставить возможность выбора ширины пера для отображаемых элементов. Это
в конечном итоге включит модификацию текущей ширины пера в документе вместе
с классом CElement и добавление либо модификацию функций, работающих с шири-
ной пера. Все это станет возможным после того, как вы соберете диалог.
Отобразите панель Resource View (Представление ресурсов), разверните дерево
ресурсов для Sketcher и щелкните правой кнопкой мыши на папке Dialog (Диалог)
в этом дереве; затем выберите пункт Insert Dialog (Вставить диалог) во всплывающем
меню, чтобы добавить новый ресурс диалога к Sketcher. В результате будет запущен
редактор диалогов, который отобразит панель редактирования и панель инстру-
ментов. В этом редакторе будет отображен список элементов управления, которые
вы можете добавлять. Диалог уже имеет кнопки ОК и Cancel (Отмена). Добавление
дополнительных элементов в диалог предельно просто; вы можете просто брать
и перетаскивать их из палитры в позицию, где хотите их разместить в диалоге.
Альтернативно вы можете щелкнуть на элементе управления из списка, чтобы вы-
брать его, и затем щелкнуть на поле диалога, куда хотите его поместить. Когда эле-
мент появится там, вы сможете перемещать его, устанавливая точное положение, а
также изменять его размеры, перетаскивая грани и углы.
Диалог имеет присвоенный ему идентификатор по умолчанию — ID_DIALOG1, но
было бы лучше иметь несколько более осмысленный ID. Отредактировать ID можно,
щелкнув правой кнопкой на имени диалога в панели Resource View и выбрав пункт
Работа с диалогами и элементами управления 819
Properties (Свойства) из контекстного меню. Вы можете также отобразить свойства
налога, щелкнув правой кнопкой мыши в панели редактора диалогов и выбрав соот-
ветствующий пункт из контекстного меню. Измените ID на нечто, имеющее отноше-
ние к назначению диалога, например, IDD_PENWIDTH_DLG. В то же время вы можете
также изменить свойство Caption (Заголовок) на Set Pen Width (Установка ширины
пера).
Добавление элементов управления в диалоговое окно
Чтобы предоставить механизм для ввода ширины пера, вы можете добавлять эле-
менты управления к базовому диалогу, отображенному изначально, пока он не примет
вид, показанный на рис. 16.8.
Рис. 163. Окончательный вид диалога установки ширины пера
На рис. 16.3 можно видеть сетку (grid), которая помогает позиционировать эле-
менты управления. Если сетка не отображается, можно выбрать соответствующую
кнопку в панели инструментов для ее отображения — эта кнопка включает и отключа-
ет сетку. В качестве альтернативы можно отобразить линейки сбоку и сверху диалога,
которые можно применить для создания направляющих, как показано на рис. 16.4.
1оризонтальная сетка создается щелчком на соответствующей линейке. Вы може-
те позиционировать направляющую, перетаскивая стрелку по линейке, при этом для
позиционирования элемента управления можно применять одну или более направля-
ющих.
820 Глава 16
Диалог содержит шесть переключателей, обеспечивающих выбор ширины пера.
Они заключены в рамку группы под заголовком Pen Widths (Ширины пера). Рамка
группы заключает в себе переключатели и заставляет их вести себя как взаимосвя*
занная группа, в которой выбор (пометка) одного элемента отменяет выбор всех про-
чих — в каждый момент времени выбран только один член группы. Каждый переклю-
чатель снабжен соответствующей меткой, идентифицирующей ширину пера, которую
он устанавливает в случае выбора. Есть также кнопка по умолчанию ОК и кнопка
Cancel, закрывающие диалоговое окно. Каждый из элементов управления в диалоге
имеет свой собственный набор свойств, к которым вы можете обратиться и модифи-
цировать таким же способом, как и свойства самого диалогового окна.
Следующий шаг — добавление рамки группы. Как уже говорилось, рамка группы
служит средством ассоциирования переключателей с группой с точки зрения функци-
онирования, и предусматривает заголовок и границы для группы кнопок. Когда при-
сутствует более одного набор переключателей, группы необходимы для их правиль-
ной работы. Вы можете щелчком выбрать кнопку, соответствующую рамке группы из
палитры общих элементов управления; затем щелкните в предположительной пози-
ции в диалоговом окне, где должен располагаться центр рамки группы. В результате
в диалог будет помещена рамка группы с размерами по умолчанию. Вы можете затем
перетаскивать грани этой рамки таким образом, чтобы в ней уместились шесть добав-
ленных ранее переключателей. Чтобы установить заголовок рамки группы, наберите
нужный заголовок (в данном случае — Pen Widths).
Последний шаг состоит в добавлении переключателей. Выберите элемент управ-
ления — переключатель, щелкнув на нем и затем щелкнув на позиции в диалоговом
окне, куда хотите его поместить в пределах рамки группы. Повторите то же самое
со всеми шестью переключателями. Каждый переключатель можно выбрать щелчком
на нем, после этого введите необходимый заголовок. Вы можете также при необхо-
димости перетаскивать границы кнопки, изменяя ее размер. Чтобы отобразить окно
свойств элемента управления, щелкните на нем правой кнопкой мыши и выберите
пункт Properties из контекстного меню. Вы можете изменить ID каждого переключа-
теля в окне свойств для элемента управления на нечто, соответствующее его назна-
чению: IDC_PENWIDTHO
шириной 0,01 дюйма, IDC__PENWIDTH2 — для пера шириной в 0,02 дюйма и так далее.
Вы можете позиционировать индивидуальные элементы управления, перетаскивая
их мышкой. Вы также можете выбрать группу элементов управления, последовательно
щелкая на элементах с нажатой клавишей <Shift> либо перетаскивая курсор с нажатой
левой кнопкой мыши для создания охватывающей их прямоугольной области. Чтобы
выровнять группу элементов управления, выберите соответствующую кнопку в панели
инструментов редактора диалогов (Dialog Editor), которая показана на рис. 16.5.
[ля пера шириной в 1 пиксель, IDC_PENWIDTH1 — для пера
Рис. 16.5. Панель инструментов редактора диалогов
иг
[елкнув правой кнопкой мыши в области
Панель инструментов показана здесь в нестыкованном состоянии, то есть, пере-
несенной за пределы области панели инструментов окна. Если требуемая панель ин-
струментов не видна, ее можно отобразить
панелей инструментов и выбрав ее из отображенного списка панелей инструментов.
Вы также можете выровнять элементы управления в диалоге с помощью меню Format
(Формат).
Работа с диалогами и элементами управления 821
Тестирование диалога
Ресурс диалога готов. Вы можете протестировать его, выбрав кнопку, которая на-
ходится в левой части панели инструментов, показанной на рис. 16.5, либо нажав
<Ctrl+T>. Это отобразит диалоговое окно с доступными базовыми операциями над
элементами управления, так что вы сможете пощелкать на созданных переключате-
лях. Когда у вас есть группа переключателей, только один из них может быть выбран-
ным. Выбирая один, вы сбрасываете (отменяете выбор) любого другого, который был
выбран ранее. Щелчок на кнопках ОК или Cancel либо даже на пиктограмме Close
(Закрыть) в панели заголовка диалога завершает его тестирование. После сохране-
ния диалогового ресурса вы готовы добавить к нему некоторый код поддержки.
Программирование для диалога
Программирование для диалога преследует две цели: заставить его отображаться
и обработать взаимодействие пользователя с его элементами управления. Прежде чем
вы отобразите диалог, соответствующий созданному вами ресурсу, вы должны сначала
определить для него класс диалога,
классов.
этом поможет интерактивный мастер создания
Добавление класса диалога
Выполните щелчок правой кнопкой мыши на диалоговом окне, созданном в редак-
торе диалогов, и выберите пункт Add Class (Добавить класс) из контекстного меню,
после чего на экране появится диалоговое окно MFC Class Wizard (Мастер создания
классов MFC). Вы должны определить новый класс диалога, унаследованный от MFC-
класса CDialog, поэтому выберите имя этого класса из выпадающего окна списка Base
Class*. (Базовый класс:). Введите имя класса CPenDialogB поле редактирования Class
name: (Имя класса:). Диалоговое окно мастера MFC Class Wizard показано на рис. 16.6.
Рис. 16.6. Диалоговое окно мастера MFC Class Wizard
822 Глава 16
Щелкните на кнопке Finish (Готово) для создания нового класса.
Класс CDialog — это класс окна (унаследованный от класса MFC CWnd), специаль-
но предназначенный для отображения и управления диалогами. Ресурс диалога, кото-
рый вы создали, автоматически ассоциируется с объектом типа CPenDialog, посколь-
ку член класса IDD инициализируется идентификатором диалогового ресурса:
class CPenDialog : public CDialog
DECLARE_DYNAMIC(CPenDialog)
public:
CPenDialog(CWnd* pParent = NULL); // Стандартный конструктор
virtual ~CPenDialog();
// Данные диалога
enum { IDD = IDD_PENWIDTH_DLG } ;
protected:
virtual void DoDataExchange (CDataExchange* pDX) ; // Поддержка DDX/DDV
DECLARE_MESSAGE_MAP ()
};
Выделенный полужирным оператор определяет IDD как символическое имя для
ID диалога в перечислении.
Кстати, использование перечисления — единственный способ поместить инициа-
лизированный член данных в определение класса. Если вы попытаетесь добавить на-
чальное значение в объявление любого обычного члена данных класса, оно не ском-
пилируется. Вы получите сообщение об ошибке, в котором будет указано на неверный
синтаксис. Здесь это работает потому, что enum определяет символическое имя для
значения типа int. Это не является строго необходимым, поскольку инициализация
IDD может быть выполнена в конструкторе, но так работает мастер MFC Class Wizard.
Эта техника чаще используется при определении символа для размерности массива
(члена класса), и в этом случае использование перечисления — единственно возмож-
ный выбор.
Наличие вашего собственного диалогового класса, унаследованного от CDialog,
означает, что вы получаете всю функциональность, которую обеспечивает этот класс.
Вы можете также настроить класс диалога, добавив данные-члены и функции, удо-
влетворяющие вашим конкретным потребностям. Часто вы захотите обрабатывать
сообщения от элементов управления внутри класса диалога, хотя вы можете также
делать это в представлении или в классе документа, если это более удобно.
Модальные и немодальные диалоги
Существуют два типа диалогов: модальные и немодальные, и работают они со-
вершенно по-разному. Когда на экране открыт модальный диалог, все операции в дру-
гих окнах приостанавливаются до тех пор, пока это диалоговое окно не будет закры-
то — обычно щелчком на кнопке ОК или Cancel. В случае немодального диалога вы
можете перемещать фокус туда и обратно между этим диалоговым окном и другими
окнами вашего приложения простым щелчком мыши, и продолжать пользоваться ди-
алоговым окном до тех пор, пока не закроете его. Мастер MFC Class Wizard — пример
модального диалога, а окно Properties (Свойства) — немодального.
Немодальное диалоговое окно создается вызовом функции Create (), определен-
ной в классе CDialog, но поскольку в примере Sketcher вы будете использовать толь-
ко модальные диалоги, то, как вы вскоре убедитесь, вам придется вызывать функцию
DoModal () для объекта диалога.
Работа с диалогами:
элементами управления
823
Отображение диалога
Куда поместить код отображения диалога в вашей программе — зависит от прило-
жения. В программе Sketcher удобно добавить пункт меню, при выборе которого бу-
дет отображаться диалоговое окно установки ширины пера. Вы поместите это в меню
IDRjSketcherTYPE. Поскольку и ширина, и цвет ассоциируются с пером, вы можете
переименовать меню Color (Цвет) в Реп (Перо). Это делается двойным щелчком на
пункте меню Color в панели Resource View и открытием окна Properties, в котором
значение свойства Caption меняется на &Реп. После закрытия окна изменения всту-
пают в силу.
Когда вы добавляете пункт Width (Ширина) в меню Реп, то должны отделить его
от цветов. Для этого потребуется добавить разделитель после последнего пункта
меню, относящегося к цвету, щелкнув правой кнопкой мыши на пустом пункте меню и
выбрав пункт Insert Separator (Вставить разделитель) из контекстного меню. Новый
пункт меню Width добавляется непосредственно после разделителя. Пункт меню за-
канчивается многоточием, чтобы указать, что он отобразит диалог; это — стандарт-
ное соглашение Windows. Двойной щелчок на меню отображает свойства меню для
модификации, как показано на рис. 16.7.
Укажите ID_PENWIDTH в качестве идентификатора пункта меню, как показано на
рис. 16.7. Вы можете также добавить сообщение для панели состояния, и посколь-
ку вы также добавляете кнопку панели инструментов, то можете включить текст
всплывающей подсказки. Помните, что текст всплывающей подсказки нужно поме-
щать сразу за текстом сообщения для панели состояния, отделяя его \п. Вот как будет
выглядеть значение свойства Prompt: Change pen width\nShow pen width options
(Изменить ширину пера\пПоказать опции ширины пера). Меню будет выглядеть, как
показано на рис. 16.8.
Properties
Menu Editor TMenuEd
Я I
—* T
El Appearance
Caption
Checked
Enabled
Grayed
Popup
El Behavior
Break
Right Justify
Right Order
|| El Misc
&Width...
False
True
False
False
None
False
False
Help False
ID ID_PEM_WIDTH
Prompt ge pen width\nShow pen width options]
Separator False
Prompt
In an MFC application, specifies the text that will appear in
the status bar when the menu item is selected.
Puc. 16.7. Окно свойств меню
824 Глава 16
Рис. 16.8. Окончательный вид меню Реп
Чтобы добавить кнопку в панель инструментов, откройте ресурс панели инстру-
ментов, раскрыв папку Toolbar (Панель инструментов) в Resource View и дважды
щелкнув на IDR__MAINFRAME. Создайте изображение для кнопки панели инструментов,
представляющей ширину пера, как показано на рис. 16.9.
Sketcher.rc (IDR...HFRAME - Toolbar) v X 1
Рис. 16.9. Изображение для кнопки панели инструмен-
тов, представляющей ширину пера
Чтобы ассоциировать новую кнопку с добавленным пунктом меню, откройте окно
свойств кнопки и укажите в качестве ее идентификатора ID PENWIDTH — то же значе-
ние, что и у соответствующего пункта меню.
Код отображения диалога
Код отображения диалога будет находиться в обработчике пункта меню Ре n*=>Width
(Перо*=>Ширина), но в каком классе следует реализовать этот обработчик? Класс
представления — подходящий кандидат на то, чтобы иметь дело с шириной пера, но
следуя предыдущей логике с цветами и элементами имело бы смысл поместить вы-
бор текущей ширины пера в документ, поэтому обработчик должен находиться в
классе CSketcherDoc. Щелкните правой кнопкой мыши на пункте меню Width в па-
нели Resource View для меню iD SketcherTYPE и выберите пункт Add Event Handler
(добавить обработчик событий) из контекстного меню. Затем создайте в классе
CsketcherDoc функцию для обработчика сообщений COMMAND, соответствующего
ID PENWIDTH. Отредактируйте этот обработчик, введя следующий код:
// Обработчик пункта меню установки ширины пера
void CsketcherDoc::OnPenwidth()
Работа с диалогами и элементами управления 825
CPenDialog aDlg; // Создать локальный объект диалога
// Отобразить его в модальном режиме
aDlg.DoModal();
Как видите, в этом обработчике пока что находятся только два оператора. Первый
оператор создает диалоговый объект, автоматически ассоциированный с вашим ре-
сурсом диалога. Затем он отображается с помощью вызова функции DoModal () объ-
екта aDlg.
Поскольку обработчик объявляет объект CPenDialog, вы должны добавить опера-
тор #include для PenDialog.h в начало SketcherDoc.срр (после директив #include
для stdafx.h и Sketcher .h), иначе при сборке программы возникнут ошибки компи-
ляции. Сделав все это, вы можете собрать Sketcher и испытать новый диалог. Он
должен появиться в результате щелчка на кнопке панели инструментов или выбора
пункта меню Pen^Width. Конечно, если диалог должен делать что-то полезное, по-
требуется еще добавить код для поддержки операций с элементами управления; для
закрытия диалога можно использовать любую из его кнопок либо пиктограмму закры-
тия в панели заголовка.
Код закрытия диалога
Кнопки ОК и Cancel (а также пиктограмма закрытия в панели заголовка) всегда
закрывают диалог. Обработчики события BN CLICKED кнопок ОК и Cancel должны
быть реализованы вами самостоятельно. Однако полезно знать, как действие по за-
крытию диалога реализуется в случае, когда вы хотите сделать нечто перед оконча-
тельным закрытием или когда вы работаете с немодальным диалогом.
Класс CDialog определяет метод ОпОК (), вызываемый при щелчке на кнопке по
умолчанию ОК, идентификатор (ID) которой равен IDOK. Эта функция закрывает диа-
лог и заставляет метод DoModal () вернуть ID кнопки по умолчанию ОК, а именно —
IDOK. Функция OnCancel () вызывается при щелчке на кнопке по умолчанию Cancel в
диалоге, что закрывает диалог и вынуждает DoModal () вернуть IDCANCEL. При жела-
нии вы можете переопределить каждую из этих функций в классе диалога. При этом
необходимо только обеспечить вызов соответствующей функции базового класса в кон-
це вашей реализации функции. Возможно, вы помните, что можете добавить переопре-
деления класса, выбрав кнопку Overrides (Переопределения) в окне свойств Класса.
Например, вы можете реализовать переопределение функции ОпОК () примерно
так:
void CPenDialog::ОпОК()
// Ваш код верификации или других действий. . .
CDialog::ОпОК(); // Закрыть диалог
В сложном диалоге может возникнуть необходимость убедиться в корректности
выбранных опций или введенных данных. Необходимый для этого код можно поме-
стить здесь, чтобы проверить состояние диалога и исправить данные или даже оста-
вить диалог открытым в случае обнаружения проблем.
Вызов ОпОК (), определенный в базовом классе, закрывает диалог и заставляет
функцию DoModal () вернуть IDOK. Таким образом, вы можете использовать значе-
ние, возвращенное из DoModal (), для обнаружения случая закрытия диалога в резуль-
тате щелчка на кнопке ОК.
826 Глава 16
Как уже говорилось, вы можете переопределить функцию OnCancel () анало-
гичным образом, если хотите выполнить дополнительные операции очистки перед
закрытием диалога. Не забудьте вызвать метод базового класса в конце реализации
функции.
Используя немодальный диалог, вы должны реализовать переопределения функций
ОпОК () и OnCancel () так, чтобы они вызывали унаследованную DestroyWindow ()
для прерывания диалога. В этом случае не следует вызывать функции класса ОпОК ()
и OnCancel (), поскольку они не уничтожают диалоговое окно, а просто делают его
невидимым.
Поддержка диалоговых элементов управления
В диалоге установки ширины пера выбранная ширина пера сохраняется в пере-
менной— члене данных m_PenWidth класса CPenDialog. Добавить член данных мож-
но либо правым щелчком на имени класса CPenDialog и выбором из контекстного
меню соответствующего пункта, либо непосредственно в определение класса, как по-
казано ниже:
class CPenDialog : public CDialog
// Конструкция
public:
CPenDialog(CWnd* pParent = NULL); // стандартный конструктор
// Данные диалога
enum { IDD = IDD_PENWIDTH_DLG };
// Данные, сохраненные в диалоге
public:
int m_PenWidth; // Запись ширины пера
// Плюс остальная часть реализации класса....
Если вы используете контекстное меню для добавления в класс m_PenWidth, не забудьте до-
бавить комментарий к определению класса. Это полезная привычка, даже если имена членов
кажутся достаточно очевидными.
Член данных m_PenWidth используется для установки переключателя, соответ-
ствующего текущей ширине пера в проверяемом документе. Потребуется также обе-
спечить сохранение в этом члене ширины пера, выбранной в диалоге, чтобы ее мож-
но было извлечь оттуда после закрытия диалога. Пока вы можете инициализировать
m_PenWidth значением 0 в конструкторе класса.
Инициализация элементов управления
Инициализировать переключатели можно, переопределив функцию OnlnitDialog (),
определенную в базовом классе CDialog. Эта функция вызывается в ответ на со-
общение WM INITDIALOG, которое отправляется во время выполнения DoModal (),
непосредственно перед отображением диалога на экране. Вы можете добавить эту
функцию в класс CPenDialog, выбрав OnlnitDialog в списке переопределений окна
свойств Properties класса CPenDialog, как показано на рис. 16.10.
Работа с диалогами и элементами управления 827
I Properties
СРепDialog VCCode Class
OnlnitDialog
get_acdValue
GetlnterfaceHook
GetScrollBarCtrl
HtmlHelp
IsInvokeAllowed
OnAmbientProperty
OnCancel
OnChildNotify
OnCmdMsg
OnCommand
OnCreateAggregates
OnFinalRelease
OnlnitDia-: <
Onhlotify
OnOK
OnToolHitTest
OnWndMsg
PostNcDestroy
Pre Create Wi nd ow
PrelnitDialog
Pre S и bclasSWi ndo w
PreT ranslate Message
Serialize
WindowProc
. WinHelo _____________
OnlnitDialog
Override to augment dialog-box initialization
Puc. 16.10. Выбор функции OnlnitDialog в списке
переопределений окна свойств Properties
Реализация новой версии OnlnitDialog () показана ниже.
BOOL CPenDialog::OnlnitDialog ()
CDialog::OnlnitDialog();
// Пометить переключатель,
switch (m_PenWidth)
case 1:
CheckDlgButton(IDC_PENWIDTH1,1);
break;
case 2:
CheckDlgButton(IDC_PENWIDTH2, 1) ;
break;
case 3:
CheckDlgButton(IDC_PENWIDTH3, 1) ;
break;
case 4:
CheckDlgButton(IDC_PENWIDTH4, 1) ;
break;
case 5:
CheckDlgButton (IDC_PENWIDTH5, 1) ;
break;
default:
CheckDlgButton(IDC_PENWIDTHO,1);
}
return TRUE; //возвратить TRUE, пока не установлен фокус на элементе управления
// ИСКЛЮЧЕНИЕ: страницы свойств OCX должны возвращать FALSE
828 Глава 16
Вы должны оставить здесь вызов функции базового класса, поскольку она осу-
ществляет некоторые важные установки для диалога. Оператор switch помечает
один из переключателей в зависимости от значения, установленного в переменной
m_PenWidth. Это подразумевает установку m PenWidth в подходящее значение перед
выполнением DoModal (), поскольку функция DoModal () приводит к отправке сооб-
щения WM INITDIALOG, в результате чего вызывается ваша версия OnlnitDialog ().
Функция CheckDlgButton () унаследована непосредственно от CWnd через
CDialog. Если второй аргумент равен 1, она помечает кнопку, соответствующую
идентификатору, специфицированному в первом аргументе. Если второй аргумент
равен 0, пометка с кнопки снимается. Это работает как с флажками, так и с переклю-
чателями .
Обработка сообщений переключателей
После отображения диалога при каждом щелчке на одном из переключателей со-
общение генерируется и отправляется приложению. Чтобы работать с этими сооб-
щениями, вы можете добавить обработчики в класс CPenDialog. Щелкните правой
кнопкой мыши на каждом из переключателей и выберите пункт Add Event Handler
(Добавить обработчик событий) из контекстного меню, чтобы создать обработчик
для сообщения BN_CLICKED. На рис. 16.11 показано диалоговое окно обработчика
события для переключателя, имеющего в качестве идентификатора IDC_PENWIDTH.
Обратите внимание, что имя обработчика отредактировано, поскольку имя по умол-
чанию выглядит несколько неуклюже.
Реализации обработчиков событий BN_CLICKED для всех этих переключателей по-
хожи, поскольку каждый из них просто устанавливает ширину пера в диалоговом объ-
екте. В качестве примера ниже показан обработчик для IDC PENWIDTHO.
Event Handler Wizard - Sketcher
Welcome to the Event Handler Wizard
IDC PENWIDTHO
Class list
BN_CLICKED
BN_DOUBLECLICKED
BN KILLFOCUS
Function handler name:
On^enwidthD
CLine
CMainFrame
CPenDialog
□Rectangle
CSketcherApp
CsketcherDoc
CSketcherView
Indicate? the user clicked a button
Add and Edit
Cancel
Jf
Puc. 16.11. Диалоговое окно обработчика события для переключателя IDC_PENWIDTH
Работа с диалогами и элементами управления 829
void CPenDialog::OnPenwidthO()
m_PenWidth = 0;
Вы должны добавить код для всех шести обработчиков реализации класса
CPenDialog, устанавливая значение m_PenWidth равным 1 в OnPenWidthl (), 2 — в
0nPenWidth2 () и так далее.
Завершение операций диалога
Теперь вы должны модифицировать обработчик OnPenWidth () в CSketcherDoc,
чтобы заставить работать диалог. Добавьте в функцию следующий код:
// Обработчик пункта меню установки ширины пера
void CSketcherDoc:: OnPenwidth ()
CPenDialog aDlg; // Создать локальный диалоговый объект
// Установить ширину пера в диалоге в значение, сохраненное в документе
aDlg.m_PenWidth e m__PenWidth;
// Отобразить диалог в модальном режиме.
/ / При закрытии по щелчку на кнопке ОК получить ширину пера
if(aDlg.DoModal() == IDOK)
m_PenWidth = aDlg.m_PenWidth;
i
j
Член m__PenWidth объекта aDlg получает ширину пера, сохраненную в пере-
менной-члене m PenWidth объекта документа; вы должны добавить этот член к
CSketcherDoc. Вызов функции DoModal () теперь происходит в условии оператора if,
которое истинно, если DoModal () вернет IDOK. В этом случае вы извлекаете ширину
пера из объекта aDlg и сохраняете ее в члене m_PenWidth документа. Если диалоговое
окно закрывается кнопкой Cancel или пиктограммой закрытия, то IDOK не будет воз-
вращено DoModal (), и значение m_PenWidth документа останется неизменным.
Следует отметить, что даже после закрытия диалогового окна, когда DoModal ()
возвращает значение, объект aDlg продолжает существовать, поэтому вы можете вы-
зывать его функции-члены без каких-либо проблем. Объект aDlg уничтожается авто-
матически при возврате из OnPenwidth ().
Все, что остается сделать для поддержки изменяемой ширины пера в вашем при-
ложении— это обновить затронутые классы — CSketcherDoc, CElement — и четыре
класса форм, унаследованных от CElement.
Добавление ширины пера к документу
Вы должны добавить член m PenWidth в класс документа, а также функцию
Ge t Ре nW i dth (), чтобы обеспечить доступ извне к сохраненному в ней значению. Для
этого потребуется добавить к определению класса CSketcherDoc следующие операто-
ры, выделенные полужирным:
class CSketcherDoc : public CDocument
// Остальной код — как раньше. . .
protected:
// Остальной код — как раньше.. .
int m PenWidth; // Текут
ширина пера
830 Глава 16
// Операции
public:
// Остальной код — как раньше. . .
int GetPenWidth () // Получить текущую ширину пера
{ return m_PenWidth; }
// Остальной код — как раныпе...
Поскольку это тривиально, вы можете определить функцию GetPenWidth ()
прямо в определении класса и воспользоваться преимуществами того, что она бу-
дет неявно встроенной. Вы по-прежнему нуждаетесь в добавлении инициализации
m PenWidth в конструктор CSketcherDoc, поэтому модифицируйте конструктор в
CSketcherDoc. срр, добавив строку, выделенную полужирным:
CSketcherDoc:: CSketcherDoc ()
: m_Element(LINE)r m_Color(BLACK)
tm_PenWidth (0) // Перо шириной 1 пиксель
// TODO: добавить сюда код одноразового конструирования объекта
Добавление ширины пера к элементам
В классе CElement и унаследованных от него классах форм придется сделать не-
много больше. У вас уже есть член ш_Реп в классе CElement, хранящий ширину, ис-
пользуемую при рисовании элемента, и вы должны расширить каждый конструктор
элементов для приема ширины пера в качестве аргумента, устанавливая член класса
соответственно. Функция GetBoundRect () в CElement должна быть изменена так,
чтобы иметь дело с нулевой
и
ириной пера. Сначала вы можете заняться классом
CElement. Новая версия функции GetBoundRect () класса CElement будет такой:
// Получить ограничивающий прямоугольник элемента
CRect CElement::GetBoundRect()
CRect BoundingRect; //Объект для сохранения ограничивающего прямоугольника
BoundingRect = m_EnclosingRect; //Инициализировать описанным прямоугольником
// Увеличить ограничивающий прямоугольник на ширину пера
int Offset = xn_Pen = 0? l:m_Pen; // Ширина должна быть н
BoundingRect.InflateRect(Offset, Offset);
II
return BoundingRect;
Вы используете локальную переменную Offset, чтобы гарантировать передачу
функции InterSect О значения 1, если ширина пера равна нулю (перо шириной 0
всегда рисует линию шириной в один пиксель), и действительной ширину пера во
всех остальных случаях.
Каждый из конструкторов CLine, CRectangle, CCirsle и CCurve должен быть мо-
дифицирован для приема ширины пера в качестве аргумента и сохранения его в уна-
следованном члене m_Pen класса. Объявление конструктора в каждом определении
класса также должно быть модифицировано добавлением дополнительного параме-
тра. Например, в классе CLine объявление конструктора станет таким, как показано
ниже.
CLine (CPoint Start, CPoint End, COLORREF aColor, int PenWidth);
Реализация конструктора должна быть модифицирована следующим образом:
Работа с диалогами и элементами управления 831
CLine::CLine(CPoint Start, CPoint End, COLORREF aColor, int PenWidth)
: mJEndPoint(CPoint(0,0))
m_StartPoint = Start;
m_EndPoint = End;
m Color = aColor;
m_Pen = PenWidth;
// Установить начальную точку линии
// Установить конечную точку линии
// Установить цвет линии
// Установить ширину линии
// Определить включающий прямоугольник
m_EnclosingRect = CRect(Start, End);
m_EnclosingRect.NormalizeRect();
Точно так же вы можете модифицировать каждое из определений классов и кон-
структоры для всех остальных фигур, чтобы каждый из них инициализировал m_Pen
значением, переданным в последнем аргументе.
Создание элементов в представлении
Последнее изменение вы должны внести в член CreateElement () класса
CSketcherView. Поскольку была добавлена ширина пера как аргумент конструктора
каждой из фигур, вы должны обновить и все вызовы этих конструкторов. Измените
определение CSketcherView::CreateElement () следующим образом:
CElement* CSketcherView: :CreateElement ()
I / Получить указатель на документ в этом представлении
CSketcherDoc* pDoc = GetDocument ();
ASSERT_VALID(pDoc); // Проверить указатель
// Выбрать элемент, используя тип, сохраненный в документе
switch(pDoc->GetElementType())
case RECTANGLE:
return new
case CIRCLE:
return new
CRectangle(m_FirstPoint, m_SecondPoint,
pDoc->GetElementColor(), pDoc->GetPenWidth());
CCircle(m_FirstPoint, m_SecondPoint,
pDoc->GetE lenient Col or(), pDoc->GetPenWidth());
case CURVE:
return new
CCurve(m_FirstPoint, mJSecondPoint,
pDoc-XSetElementColor(), pDoc->GetPenWidth());
case LINE: //По умолчанию — всегда линия
return new CLine(m_FirstPoint, m_SecondPoint,
pDoc->GetElementColor () , pDoc-XSetPenWidth ()) ;
default: // Что-то не так
AfxMessageBox("Недопустимый код элемента", МВ_ОК) ;
AfxAbortO ;
Каждый вызов конструктора теперь передает в аргументе ширину пера. Она из-
влекается из документа с помощью функции GetPenWidth (), которую вы добавили в
класс документа.
Испытание диалога
Теперь вы можете собрать и запустить последнюю версию Sketcher, чтобы посмо-
треть, как работает диалог установки ширины пера. Выбрав пункт меню Pen^Width
832 Глава 16
(Перо ^Ширина) или нажав ассоциированную с ним кнопку панели инструментов,
вы отобразите окно диалога, в котором можно будет выбрать ширину пера. Экран на
рис. 16.12 показывает, что вы увидите при выполнении программы Sketcher.
Рис. 16.12. Работа программы Sketcher после добавления диалога изменения ширины пера
Обратите внимание, что диалог представляет собой совершенно отдельное окно.
Вы можете перетаскивать его в любое место экрана. Вы можете даже перетащить его
за пределы окна приложения Sketcher.
Использование кнопки счетчика
Теперь посмотрим, как может пригодиться приложению Sketcher кнопка счетчи-
ка (spin button). Кнопки счетчика особенно удобны, когда вы хотите ограничить ввод
определенным целочисленным диапазоном. Обычно они применяются в ассоциации
с другим элементом управления, называемым дружественным элементом управле-
ния (buddy control), который отображает значение, модифицируемое кнопкой счет-
чика. Ассоциированным элементом управления обычно служит редактирующий эле-
мент, хотя это и не обязательно.
В приложении Sketcher было бы неплохо иметь возможность рисовать в различ-
ном масштабе. Если будет возможность изменять масштаб рисования, вы сможете
увеличивать рисунок в тех местах, где требуется уточнить мелкие детали вашего ше-
девра, и возвращаться к исходному масштабу, работая с полной перспективой. Вы мо-
жете применять кнопку счетчика для управления масштабированием представления
документа. Масштаб рисования должен быть свойством, специфичным для представ-
ления, а функции рисования должны принимать во внимание текущий масштаб этого
представления. Изменение существующего кода для работы с масштабированием тре-
бует относительно большую работу, нежели настройка элемента управления, поэтому
сначала мы посмотрим, как можно создать кнопку счетчика и заставить ее работать.
Работа с диалогами и элементами управления 833
Добавление пункта меню и кнопки панели
инструментов для функции масштабирования
Начнем с обеспечения средств для отображения диалога масштабирования.
Перейдите к Resource View и откройте меню !DR_SketcherTYPE. Вам потребуется
добавить пункт меню Scale (Масштаб) в конец меню View (Вид). Введите заголовок
для неиспользованного пункта меню — as Scale.... Этот пункт вызовет диалог мас-
штабирования, поэтому пункт завершается многоточием, указывая на то, что в резуль-
тате его выбора откроется диалог. Затем можно добавить разделитель, предшеству-
ющий новому пункту меню, щелкнув на нем правой кнопкой мыши и выбрав пункт
Insert Separator (Вставить разделитель) из контекстного меню. Сверьте установлен-
ные свойства пункта меню с теми, что показаны на рис. 16.13.
properties ▼ -= X
Menu Editor IMenuEd
l+|“= A I
- 4
I □ Appearance
Caption S&cale...
Checked False
Enabled True
Grayed False
Popup False
El Behavior
Break None
Right Justify False
Right Order False
□ M'kc
Help False
ID ID_VIEW_SCALE
Prompt
Separator False
ID
Specifies the identifier of the menu item or menu
resource.
Puc. 16. /5. Свойства пункта меню Scale
Вы можете также добавить кнопку в панель инструментов для этого пункта меню.
Делая это, убедитесь, что идентификатор кнопки будет установлен таким же, как и у
пункта меню — ID_VIEW_SCALE.
Создание кнопки счетчика
Итак, пункт меню у вас есть, теперь нужен диалог, который будет им вызываться.
Находясь в панели Resource View, добавьте новый диалог щелчком правой кнопкой
мыши на папке Dialog (Диалог) и выбором пункта Insert Dialog (Вставить диалог) из
контекстного меню. Измените идентификатор диалога на IDD SCALE DLG, а заголо-
вок— на Set Drawing Scale (Установка масштаба рисования).
834 Глава 16
Щелкните на элементе управления счетчика в палитре, после чего щелкните в по-
зиции диалога, куда вы хотите его поместить. Затем щелкните правой кнопкой мыши
на этом элементе, чтобы отобразить его свойства. Измените идентификатор по умол-
чанию на что-то более осмысленное вроде IDC_SPIN_SCALE. После этого взгляните
на свойства кнопки счетчика. Они показаны на рис. 16.14.
Меню должно выглядеть так, как показано на рис. 16.15.
[Properties ▼ 1 X
ID-C_SPDI_SCALE (Spin Control) ISpinEditor
Iда- I a I i| ==i zZ I
El Appearance
Alignment
Arrow Keys
Client Edge
Modal Frame
No Thousands
Orientation
Static Edge
T ransparent
Wrap
El Behavior
Accept Files
Auto Buddy
Disabled
Help ID
Hot T rack
Set Buddy Integer
Visible
El Mtsc
Group
Tabstop
Right Align *
True
False
False
False
Vertical
False
False
False
False
True
False
False
False
True
True
False
IDC_SPIN_SCALE
False
Alignment
One of: Unattached, Left, or Right Align.
Pwc. 16.14. Свойства кнопки счетчика IDC_SPIN_SCALE
Sketcher.rc i'IDR...herTYPE - Menu)*
File Edit View Element Pen Window Help
Toolbar
Status Bar
Type Here
Type Here
Puc. 16.15. Новое меню View (Вид)
Работа с диалогами и
элементами управления 835
Свойство Arrow Keys (Клавиши со стрелками) всегда установлено в True, что по-
зволяет вам оперировать кнопкой счетчика клавишами со стрелками на клавиатуре.
Вы также должны установить в True значение Set buddy integer (Установка друже-
ственного элемента управления как целого числа), специфицирующее значение дру-
жественного элемента управления как целое число. Значение свойства Auto buddy
(Автоматический выбор дружественного элемента управления), предназначенное для
автоматического выбора дружественного элемента управления, также следует устано-
вить в True. В результате этого элементом управления, выбранным в качестве дру-
жественного, автоматически станет предыдущий элемент, определенный в диалоге.
В данный момент это кнопка Cancel, что не то, что нужно, но очень скоро вы увиди-
те, как это исправить. Свойство Alignment (Выравнивание) определяет, как кнопка
счетчика будет отображаться относительно ее “друга”. Вы должны установить ее в
Right Align (Выравнивание вправо), чтобы кнопка счетчика была присоединена к
правому краю ее дружественного элемента управления.
Затем добавьте элемент редактирования рядом с кнопкой счетчика, выбрав его
из списка в панели инструментов и щелкнув в месте диалога для его размещения.
Измените идентификатор этого элемента редактирования на IDC_SCALE.
Чтобы сделать содержимое элемента редактирования достаточно ясным, вы долж-
ны добавить статический элемент управления непосредственно слева от элемента ре-
дактирования в палитре и ввести View Scale: (Масштаб просмотра:) в качестве его
заголовка. Вы можете выбрать три элемента управления, щелкая на них при нажатой
клавише <Shift>. Нажатие функциональной клавиши <F9> аккуратно выровняет эти
элементы, либо для этого можно воспользоваться меню Format (Формат).
Последовательность табуляции элементов управления
Элементы управления в диалоге имеют последовательность обхода при нажа-
тии клавиши табуляции. Это последовательность, в которой перемещается фокус от
одного элемента к другому, изначально определяемая последовательностью добавле-
ния элементов в диалог. Вы можете увидеть последовательность табуляции, выбрав
пункт Format^Tab Order (Формат^Порядок обхода по клавише табуляции) из главно-
го меню, либо нажав <Ctrl+D>; диалог будет аннотирован, как показано на рис. 16.16.
Рис. 16.16. Отображение номеров в последо-
вательности обхода по клавише табуляции
Порядок табуляции показан на рис. 16.16 последовательностью цифр. Поскольку
кнопка Cancel непосредственно предшествует кнопке счетчика в этой последова-
тельности, свойство Auto Buddy кнопки счетчика выбирает ее как дружественный
элемент управления. Но на самом деле вы хотите, чтобы элемент редактирования
предшествовал кнопке счетчика в последовательности табуляции, поэтому вы долж-
ны выбрать эти элементы, щелкнув на них в такой последовательности: кнопка ОК,
836 Глава 16
руга для
кнопка Cancel, элемент редактирования, кнопка счетчика и, наконец, статический
элемент управления. Теперь элемент редактирования выбран в качестве
кнопки счетчика.
;иалога переменную, которая будет хранить значе-
Генерация класса диалога масштабирования
После сохранения ресурсного файла вы можете щелкнуть правой кнопкой мыши в
диалоге и выбрать Add Class (Добавить класс) из контекстного меню. Затем вы сможете
определить новый класс, ассоциированный с только что созданным ресурсом диалога.
Назовите класс CScaleDialog и выберите в качестве базового для него класс CDialog.
После щелчка на кнопке Finish (Готово) класс будет добавлен к проекту Sketcher.
Вам нужно добавить в класс
ние, возвращенное элементом редактирования, поэтому щелкните на имени класса
CScaleDialog в Class View и выберите Add^Add Variable (Добавить1^Добавить пере-
менную) из контекстного меню. Новый член данных класса относится к специальной
разновидности, называемой переменной элемента управления, поэтому сначала от-
метьте флажок Control Variable (Переменная элемента управления) в окне мастера
Add Member Variable (Добавить переменную-член). Выберите IDC_SCALE в качестве
идентификатора в выпадающем списке Control ID: (Идентификатор элемента управ-
ления:) и Value (Значение) в выпадающем списке Category: (Категория:). Введите в
качестве имени переменной m_Scale. Вы будете хранить целочисленное значение
масштаба, поэтому укажите int для типа переменной. Мастер Add Member Variable
отображает поля редактирования, в которых вы можете задать максимальное и мини-
мальное значения переменной m Scale. Для нашего приложения минимумом будет
1, а максимумом — 8. Обратите внимание, что это ограничение касается только поля
редактирования; элемент счетчика независим от этого. На рис. 16.17 показано, как в
конечном итоге должно выглядеть окно мастера Add Member Variable.
Рис. 16.17. Окно мастера Add Member Variable для переменной m_Scale
Работа с диалогами и элементами управления 837
4
После щелчка на кнопке Finish мастер позаботится о генерации кода, необходимо-
го для поддержки вашей новой переменной элемента управления. После добавления
мастером нового члена определение класса будет выглядеть так:
class CScaleDialog : public CDialog
DECLARE_DYNAMIC(CScaleDialog)
public:
CScaleDialog(CWnd* pParent = NULL); // Стандартный конструктор
virtual -CScaleDialog();
// Данные диалога
enum { IDD = IDDJSCALEJDLG } ;
protected:
virtual void DoDataExchange (CDataExchange* pDX) ; // Поддержка DDX/DDV
DECLARE_MESSAGE_MAP()
public:
// Сохраняет текущий масштаб рисования
int m_Scale;
Интересные части определения класса выделены полужирным. Класс ассоцииро-
ван с диалоговым ресурсом через оператор enum, инициализирующий IDD идентифи-
катором ресурса. Он содержит переменную m Scale, которая специфицирована как
public-член класса, поэтому вы можете устанавливать и извлекать его значение в
объекте CScaleDialog непосредственно. Есть также в реализации класса некоторый
специальный код, имеющий дело с этим новым членом m_Scale.
Обмен и верификация данных диалога
Виртуальная функция по имени DoDataExchange () включена в класс интерактив-
ным мастером Class Wizard. Если вы заглянете в файл ScaleDialog.срр, то найдете
там реализацию вроде следующей:
void CScaleDialog::DoDataExchange(CDataExchange* pDX)
CDialog::DoDataExchange(pDX);
DDX_Text(pDX, IDC_SCALE, m_Scale);
DDV_MinMaxInt(pDX, m_Scale, 1, 8);
Эта функция вызывается каркасом для обслуживания обмена данными между пере-
менными диалога и его элементами управления. Этот механизм называется обменом
данными диалога — Dialog Data Exchange (DDX). Это мощный механизм, в большин-
стве случаев обеспечивающий автоматическую передачу информации между диало-
гом и его элементами управления, избавляя вас от необходимости самостоятельного
программирования обмена данных, как вы делали это с переключателями в диалоге
установки ширины пера.
В диалоге масштабирования DDX обрабатывает обмен данными между элемен-
том редактирования и переменной m Scale класса CScaleDialog. Переменная pDX,
переданная функции DoDataExchange (), управляет направлением передачи данных.
После вызова функции DoDataExchange () базового класса вызывается функция
DDX Text (), которая действительно перемещает данные между переменной m Scale
и элементом редактирования.
Вызов функции DDV MinMaxInt () контролирует нахождение переданного значе-
ния в указанных пределах. Этот механизм называется верификацией данных диало-
838 Глава 16
га — Dialog Data Validation (DDV). Функция DoDataExchange () вызывается автомати-
чески перед отображением диалога для передачи значения, сохраненного в m_Scale,
в элемент редактирования. Вам потребуется лишь обеспечить сохранение правильно-
го значения в m_Scale перед отображением диалогового окна и получить результат
при закрытии диалога.
Инициализация диалога
Вы используете функцию OnlnitDialog () для инициализации диалога — так же,
как вы делали это в диалоге установки ширину пера. На этот раз вы используете ее
для установки кнопки счетчика. Вы инициализируете переменную-член m_Scale чуть
позже, когда создадите диалог в обработчике пункта меню Scale (Масштаб), посколь-
ку она должна быть установлена в значение масштаба, сохраненного в представлении.
А пока добавьте в класс CScaleDialog переопределение функции OnlnitDialog (),
используя тот же механизм, что и в предыдущем диалоге, и добавьте код инициализа-
ции элемента счетчика, как показано ниже:
BOOL CScaleDialog::OnlnitDialog()
CDialog::OnlnitDialog();
// Сначала получить указатель на элемент счетчика
CSpinButtonCtr 1 * pSpin;
pSpin = (CSpinButtonCtrl*) Ge tDlgl tern (IDC_SPIN_SCALE);
// Если вы не помечаете опцию auto buddy в свойствах элемента
// счетчика, установите дружественный элемент управления здесь
// Установить диапазон изменения значений элемента счетчика
pSpin->SetRange(1, 8);
return TRUE;//Возвратить TRUE, пока не установлен фокус на элементе управления
// ИСКЛЮЧЕНИЕ: страницы свойств OCX должны возвращать FALSE
Наряду с четырьмя строками комментариев необходимо добавить только три
строки кода.
Первая строка кода создает указатель на объект MFC-класса CSpinButtonCtrl.
Этот класс специально предназначен для управления кнопками счетчика и инициа-
лизируется в следующем операторе указателем на соответствующий элемент управ-
ления нашего диалога. Функция GetDlgltemO унаследована от CWnd через CDialog,
и извлекает адрес любого элемента управления по идентификатору, переданному в
аргументе. Как было показано ранее, элемент управления — это просто специализи-
рованное окно, поэтому возвращаемый указатель имеет тип CWnd*; потому вы долж-
ны непосредственно привести его к типу элемента счетчика. Если вы не ограничи-
те здесь значения для элемента счетчика, то сможете через него ввести значения в
элемент редактирования, выходящие за допустимые для него пределы, что приведет
к сообщению об ошибке от элемента редактирования. Это можно продемонстриро-
вать, если закомментировать здесь оператор, вызывающий SetRange (), и запустить
программу Sketcher без него.
Если вы хотите установить дружественный элемент управления с помощью кода, а
не установки свойства Auto buddy кнопки счетчика в True, то класс CSpinButtonCtrl
предусматривает для этого специальную функцию. В место, указанное комментарием,
необходимо добавить следующий оператор:
pSpin->SetBuddy(GetDlgltern(IDC_SCALE));
Работа с диалогами и элементами управления 839
Отображение кнопки счетчика
Диалог должен отображаться, когда выбрана опция меню Scale (или ассоции-
рованная с ним кнопка панели инструментов), поэтому вам придется добавить об-
работчик событий COMMAND в класс CSketcherView, соответствующий событию
ID_VIEW_SCALE. Для этого применяется окно Properties класса. Затем можно доба-
вить показанный ниже код.
void CSketcherView: :OnViewScale ()
CScaleDialog aDlg;
aDlg.m_Scale = m_Scale;
if (aDlg. DoModal () = IDOK)
m_Scale = aDlg.m_Scale;
InvalidateRect(0) ;
}
// Создать объект диалога
Передать масштаб представления диалогу
// Получить новый масштаб
// Пометить все окно как недействительное
Вы создаете диалог, как модальный — точно так же, как делали это с диалогом уста-
новки ширины пера. Перед его отображением с помощью функции DoModal () вы со-
храняете значение масштаба, представленное членом m Scale класса CSketcherView
в одноименном члене класса диалога; это обеспечивает отображение в диалоге теку-
щего значения масштаба при его открытии. Если диалог закрывается кнопкой ОК, вы
сохраняете новый масштаб из переменной m Scale объекта диалога в одноименной
переменной представления. Поскольку вы изменили значение масштаба представле-
ния, окно необходимо перерисовать, чтобы применить это новое значение. Вызов
InvalidateRect () обеспечивает это.
Конечно, вы не должны забыть добавить член данных m_Scale в определение
CSketcherView, поэтому вставьте следующую строку в конец списка членов-данных
определения класса:
int m_Scale; // Текущий масштаб представления
Также вы должны модифицировать конструктор CSketcherView, чтобы инициа-
лизировать m Scale значением 1. В результате представление всегда будет начинать
работу с масштаба 1. Примечание: если вы забудете сделать это, вряд ли ваша программа
станет работать правильно.
Поскольку вы ссылаетесь на класс CScaleDialog в реализации класса CSketcherView,
то должны добавить директиву #include для ScaleDialog.h в начало файла
CSketcherView. срр. Это все, что потребуется сделать для работы диалога установ-
ки масштаба и его элемента счетчика. Выполните сборку и запустите приложение
Sketcher, чтобы проверить работу элементов, прежде чем добавлять код для исполь-
зования показателя масштаба отображения в процессе рисования.
Использование показателя масштаба
Масштабирование в Windows обычно предусматривает использование одного
из масштабируемых режимов отображения — MM_ISOTROPIC или MM_ANISOTROPIC.
Используя один из этих режимов отображения, вы можете заставить Windows вы-
полнять большую часть работы. К сожалению, для этого недостаточно просто сме-
нить режим отображения, поскольку ни один из этих режимов не поддерживается
CScr oil View. Однако если это удастся обойти, вы будете в целости и сохранности.
840 Глава 16
Вы используете MM_ANISOTROPIC по причине, которая объясняется ниже, но сначала
давайте разберемся, что необходимо для применения режима отображения.
Масштабируемые режимы отображения
Как я уже говорил, существуют два режима отображения, позволяющие отображать
логические координаты на координаты устройства, и эти режимы — MM ISOTROPIC
или MM_ANISOTROPIC. Режим MM_ISOTROPIC имеет такое свойство, что Windows при-
меняет одинаковый фактор масштабирования для осей х и у, а это дает то преимуще-
ство, что окружности всегда будут круглыми. Недостаток же состоит в том, что вы не
можете отобразить документ так, чтобы он целиком заполнил любой прямоугольник.
С другой стороны, режим MM ANISOTROPIC позволяет выполнять масштабирование
по каждой оси независимо. Поскольку этот режим более гибкий из двух, мы использу-
ем его для операций масштабирования в Sketcher.
Способ трансформации логических координат в координаты устройства зависит
от ряда параметров, которые вы можете установить (табл. 16.2).
Таблица 16.2. Параметры, влияющие на трансформацию логических координат
в координаты устройства
Параметр Описание
Window Origin Логические координаты левого верхнего угла окна. Устанавливается вызовом
функции CDC:: SetWindowOrg ().
Window Extent Размер окна, заданный в логических координатах. Устанавливается вызовом
функции CDC:: SetWindowExt ().
Viewport Origin Координаты левого верхнего окна в системе координат устройства (в пиксе-
лях). Устанавливается ВЫЗОВОМ CDC: :SetViewportOrg ().
viewport Extent Размер окна в координатах устройства (в пикселях). Устанавливается вызовом
CDC::SetViewportExt().
Упомянутое здесь понятие viewport (область просмотра), не имеет физического
значения само по себе; оно служит только в качестве параметра для определения
трансформации координат из логических в координаты устройства.
Ниже перечислены моменты, которые следует хорошо запомнить.
□ Логические координаты (также известные, как страничные координа-
ты) определяются режимом отображения. Например, режим отображения
MM LOENGLISH имеет логические координаты с единицей измерения в 0,01
дюйма, с началом координат в левом верхнем углу клиентской области и по-
ложительным направлением оси у снизу вверх. Они используются функциями
рисования контекста устройства.
Координаты устройства (также известные, как клиентские координаты в
окне) измеряются в пикселях в случае окна, с началом в верхнем левом углу
клиентской области и положительным направлением оси у сверху вниз. Они
используются вне контекста устройства, например, для определения позиции
курсора в обработчике событий мыши.
□ Экранные координаты измеряются в пикселях и имеют начало в верхнем ле-
вом углу экрана с положительным направлением оси у сверху вниз. Они исполь-
зуются при установке или получении позиции курсора.
Работа с диалогами и элементами управления 841
Преобразование логических координат Windows в координаты устройства выра-
жается следующими формулами:
xDevice = (xLogical - xWindowOrg)★(xViewportExt/xWindowExt)+ xViewportOrg
уDevice = (yLogical - yWindowOrg) * (yViewportExt/yWindowExt) + yViewportOrg
В координатных системах, отличных от тех, что предоставляются режимами ото-
бражения MM_ISOTROPIC и MM_ANISOTROPIC, величина окна и величина области про-
смотра фиксированы режимом отображения, и вы не можете их изменить. Вызов
функций SetWindowExt () или SetViewportExt () в объекте CDC для их изменения не
дает никакого эффекта, хотя вы все-таки можете переместить позицию (0, 0) в ваше
логическое обрамляющее окно вызовом SetWindowOrg () или SetViewportOrg ().
Однако для данного размера документа, выраженного размером окна в единицах ло-
гических координат, вы можете просто настроить масштаб отображения элементов,
установив соответствующим образом размер области отображения. Используя и уста-
навливая размеры окна и области отображения, вы можете получить автоматическое
масштабирование.
Установка размера документа
Вы должны поддерживать размер документа в логических единицах в объекте до-
кумента. Вы можете добавить в определении класса CSketcherDoc член данных pro-
tected по имени m_DocSize для хранения размера документа:
CSize m_DocSize; // Размер документа
Вам также понадобится обращаться к членам-данным из класса представления, по-
этому добавьте public-функцию к определению класса CSketcherDoc, как показано
ниже:
CSize GetDocSizeO
{ return m_DocSize; } // Извлечь размер документа
Вы должны инициализировать член m_DocSize в конструкторе документа, поэто-
му модифицируйте реализацию CSketcherDoc () следующим образом:
CSketcherDoc:: CSketcherDoc ()
: m_Element(LINE)
, m_Color(BLACK)
, m_PenWidth(0)
, m_DocSize(CSize (3000,3000))
If TODO: добавить сюда одноразовый код конструирования
Вы используете национальные координаты MM_LOENGLISH, поэтому можете трак-
товать логические единицы как 0,01 дюйма, и такая установка даст вам область для
рисования в 30 квадратных дюймов.
Установка режима отображения
Установить режим отображения MM_ANISOTROPIC можно в переопределенной
функции OnPrepareDC () класса CSketcherView. Эта функция всегда вызывается для
любого сообщения WM PAINT, и вы будете обращаться к ней при рисовании времен-
ных объектов в обработчике сообщений мыши; однако потребуется предпринять
нечто большее, чем просто установить режим отображения. Вы должны создать
842 Глава 16
переопределенную функцию с CSketcherView, прежде чем добавлять код. Просто
откройте окно свойств класса CSketcherView и щелкните на кнопке Overrides
(Переопределения) в панели инструментов. Затем вы можете добавить переопреде-
ление, выбрав OnPrepareDC из списка и щелкнув на <Add> OnPrepareDC в соседней
колонке. Затем вы сможете ввести код непосредственно в панели редактирования.
Ниже показана реализация OnPrepareDC ().
void CSketcherView: :OnPrepareDC (CDC* pDC,
CScrollView::OnPrepareDC(pDC, plnfo);
CsketcherDoc* pDoc = GetDocument ();
pDC->SetMapMode(MM_ANISOTROPIC);
CSize DocSize = pDoc->GetDocSize() ;
CPrintlnfo* plnfo)
// Установить режим отображения
// Получить размер документа
//Размер у должен быть отрицательным, поскольку хотим использовать MM__LOENGLISH
DocSize.су = -DocSize.су; // Сменить знак у
pDC->SetWindowExt(DocSize); // Теперь установить размер окна
// Получить количество пикселей на дюйм в х и у
int xLogPixels = pDC->GetDeviceCaps (LOGPIXELSX) ;
int yLogPixels = pDC->GetDeviceCaps(LOGPIXELSY);
// Вычислить размер области отображения по х и у
long xExtent = static__cast<long> (DocSize. ex) *m_Scale*xLogPixels/100L;
long yExtent = static_cast <long>(DocSize.cy)*m_Scale*yLogPixels/100L;
pDC->SetViewportExt(static_cast<int>(xExtent),
static_cast<int>(-yExtent)); // Установить размер
// области отображения
Переопределение функции базового класса здесь несколько необычно, поскольку
вы должны оставить вызов CscrollView: : OnPrepareDC () и добавить модифици-
рованный код после него, а не в том месте, где рекомендует комментарий в коде по
умолчанию. Если класс унаследован от С View, вы должны заменить вызов версии ба-
зового класса, поскольку она ничего не делает, но не в случае CScrollView. Вам нуж-
на функция базового класса для установки некоторых атрибутов, прежде чем устанав-
ливать режим отображения. Не делайте ошибки, вызывая функцию базового класса в
конце переопределенной версии — если вы это сделаете, масштабирование работать
не будет.
После установки режима отображения и получения размера документа, вы уста-
навливаете размер окна с отрицательным у. Это просто должно быть согласовано с
режимом MM LOENGLISH, который вы применяли ранее — помните, что начало коор-
динат находится вверху, поэтому значения у при таком режиме отображения в кли-
ентской области являются отрицательными.
Функция-член CDC по имени GetDeviceCaps () применяет информацию об устрой-
стве, с которым ассоциирован контекст устройства. В зависимости от аргументов,
переданных функции, можно получать различную информацию об устройстве. В дан-
ном случае аргументы LOGPIXELSX и LOGPIXELSY возвращают количество пикселей
на логический дюйм по осям х и у. Эти значения эквивалентны 100 единицам ваших
логических координат.
Вы используете эти значения при вычислении значений х и у для размера области
отображения, которые затем сохраняете в переменных xExtent и yExtent. Размер
документа по осям в логических единицах, деленный на 100, дает размер документа в
дюймах. Если его увеличить на количество логических пикселей на дюйм для устрой-
ства, получится эквивалентное количество пикселей на размер. Если затем исполь-
Работа с диалогами и элементами управления 843
зовать это значение в качестве размера области отображения, получатся элементы,
отображенные в масштабе 1:1. Если упростить уравнения преобразования между ко-
ординатами устройства и логическими координатами, исходя из предположения, что
как точка начала координат окна, так и точка начала координат области отображе-
ния, расположены в (0, 0), то они принимают следующий вид:
xDevice = xLogical *(xViewportExt/xWindowExt)
yDevice = yLogical *(yViewportExt/yWindowExt)
Если умножить значения размеров области отображения на масштаб (хранящийся
в m_Scale), то элементы отображаются в соответствии со значением m_Scale. Эта ло-
гика явно представлена в вашем коде выражениями для размеров х и у. Упрощенные
уравнения с включенным масштабом выглядят так:
xDevice = xLogical *(xViewportExt*m_Scale/xWindowExt)
yDevice = yLogical ★ (yViewportExt*m_Scale/yWindowExt)
Здесь видно, что данная пара координат устройства варьируется пропорциональ-
но значению масштаба. Координаты в масштабе 3 втрое больше координат в масшта-
бе 1. Конечно, по мере роста размеров элементов, масштаб также удаляет их от точки
начала координат.
Это и все, что вам понадобится для масштабирования представления. Но, к сожа-
лению, в момент прокрутки масштабирование не работает, поэтому посмотрим, что
можно с этим сделать.
Реализация прокрутки с масштабированием
Класс CScrollView не работает с режимом отображения MM_ANISOTROPIC, поэто-
му ясно, что для настройки линеек прокрутки вы должны использовать другой режим
отображения. Легче всего это сделать, применив ММ_ТЕХТ, поскольку в этом случае
логические координаты совпадают с клиентскими координатами — то есть, пиксе-
лями. Поэтому все, что нужно сделать — это определить, сколько пикселей эквива-
лентно логическому размеру документа для того масштаба, в котором выполняется
рисование, и это проще, чем вы можете подумать. Вы можете добавить функцию к
CSketcherView, чтобы она позаботилась о линейках прокрутки и реализовала все,
что нужно. Щелкните правой кнопкой мыши на имени класса CSketcherView в Class
View и добавьте public-функцию ResetScrollSizes () с типом возврата void и без
параметров. Добавьте приведенный ниже код в реализацию.
void CSketcherView::ResetScrollSizes (void)
CClientDC aDC(this);
OnPrepareDC(&aDC);
// Установить контекст устройства
CSize DocSize = GetDocument()->GetDocSize(); // Получить размер документа
aDC.LPtoDP(&DocSize);
SetScrollSizes(MM_TEXT, DocSize);
// Получить размер в пикселях
// Настроить линейки прокрутки
После создания локального объекта CClientDC для представления вызывается
OnPrepareDC () с целью установки режима отображения MM_ANISOTROPIC. Поскольку
здесь учитывается масштабирование, член LPtoDP () объекта aDC преобразует раз-
мер документа, сохраненный в локальной переменной DocSize, в корректное чис-
ло пикселей для текущего логического размера документа и текущего масштаба.
Общий размер документа в пикселях определяет, насколько большими должны быть
линейки прокрутки в режиме ММ_ТЕХТ, а вы должны вспомнить, что логические
844 Глава 16
координаты ММ ТЕХТ измеряются в пикселях. Затем вы можете воспользоваться
SetScrollSizes (), функцией-членом CScrollView, для настройки линеек прокрут-
ки на основе этого, специфицируя в качестве режима отображения ММ_ТЕХТ.
Может показаться странной возможность изменения режима отображения подоб-
ным образом, но следует помнить, что режим отображения — это не что иное, как
определение того, как логические координаты преобразуются в координаты устрой-
ства. Любой режим (а, следовательно, и алгоритм преобразования координат), уста-
навливаемый вами, применяется ко всем последующим функциям контекста устрой-
ства, пока вы не измените его, а изменить вы можете в любой момент. Когда вы
устанавливаете новый режим, все последующие вызовы функций контекста устрой-
ства просто используют алгоритм преобразования, определенный новым режимом.
Вы определяете, насколько велик документ в пикселях с MM_ANISOTROPIC, поскольку
это единственный способ применить масштабирование к процессу, и переключаетесь
в ММ_ТЕХТ для установки линеек прокрутки, поскольку вам нужны размеры в пикселях
для их правильной работы. Если разобраться, то все просто.
Настройка линеек прокрутки
Вы должны изначально настроить линейки прокрутки для представления в чле-
не OnlnitialUpdate () класса CSketcherView. Измените предыдущую реализацию
функции следующим образом:
void CSketcherView: :OnlnitialUpdate ()
ResetScrollSizes () ; // Установить линейки прокрутки
CScrollView: : OnlnitialUpdate () ;
Все, что потребуется сделать — вызвать функцию ResetScrollSizes (), которая
только что была добавлена к представлению. Она позаботится обо всем, ну, почти
обо всем. Объект CScrollView нуждается в правильной установке начального раз-
мера, чтобы правильно работала OnPrepareDC (), поэтому вам нужно добавить один
оператор в конструктор CSketcherView:
CSketcherView:: CSketcherView ()
: m_FirstPoint(CPoint(0, 0))
, m_SecondPoint(CPoint(0, 0))
, m_pTempElement(NULL)
, m_pSelected(NULL)
, m_MoveMode(FALSE)
, m_CursorPos(CPoint(0, 0) )
, m_FirstPos(CPoint(0, 0) )
, m Scale(1)
1-ю записанную точку в 0,0
2-ю записанную точку в 0,0
указатель на временный элемент в 0
нет выбранного элемента
Установить
Установить
Установить
Изначально
Отключить режим перемещения
Инициализировать нулем
Инициализировать нулем
Установить масштаб 1:1
SetScrollSizes(ММ TEXT
CSize(0,0)); // Установить произвольные значения
Дополнительный оператор просто вызывает SetScrollSizes () с произвольным
размером, чтобы инициализировать линейки прокрутки перед рисованием представ-
ления. Когда представление рисуется впервые, вызов функции ResetScrollSizes ()
в OnlnitialUpdate () правильно устанавливает линейки прокрутки.
Конечно, при каждом изменении масштаба представления вы должны обновлять
линейки прокрутки перед перерисовкой представления. Вы можете делать это в об-
работчике OnViewScale (), в классе CSketcherView:
Работа с диалогами и элементами управления 845
void CSketcherView: :OnViewScale ()
CScaleDialog aDlg; // Создать объект диалога
aDlg.m_Scale = m_Scale; // Передать масштаб представления диалогу
if(aDlg.DoModal () == IDOK)
m_Scale = aDlg.m_Scale; // Получить новый масштаб
ResetScrollSizes (); //Исправить прокрутку в соответствии с новым масштабом
InvalidateRect(0) ; // Пометить все окно как недействительное
С помощью функции ResetScrollSizes () позаботиться о линейках прокрутки
несложно. Вы убедитесь, что линейки прокрутки будут работать именно так, как надо.
Обратите внимание, что каждое представление поддерживает собственный фактор
масштабирования, не зависящий от других представлений.
Работа с панелями состояния
Независимое масштабирование каждого представления требует некоторой инди-
кации текущего масштаба активного представления. Удобнее всего это сделать, ото-
бражая масштаб в панели состояния, которая создается приложением Sketcher по
умолчанию. По умолчанию панель состояния появляется в нижней части окна при-
ложения под горизонтальной линейкой прокрутки, хотя вы можете поместить ее и в
верхнюю часть клиентской области. Панель состояния разделена на сегменты, назы-
ваемые областями; панель состояния в Sketcher имеет четыре области. Область сле-
ва содержит текст Ready (Готово), а остальные три немного утопленные области ис-
пользуются для индикации активности клавиш CAPS lock, NUM lock и SCROLL lock.
В панель состояния можно вывести то, что мастер Application Wizard предлагает по
умолчанию, но вам нужен доступ к переменной m_wndStatusBar объекта CMainFrame,
которая представляет эту панель. Поскольку это защищенный (protected) член клас-
са, вы должны добавить общедоступную (public) функцию-член для модификации па-
нели состояния извне класса. Вы можете добавить следующую функцию-член public
в класс CMainFrame:
void CMainFrame::SetPaneText(int Pane, LPCTSTR Text)
m_wndStatusBar.SetPaneText(Pane, Text);
Реализация обращается в файл . срр и вы должны добавить объявление функции
в определение класса. Функция SetPaneText () устанавливает текст, специфициро-
ванный вторым параметром — Text, в область, идентифицированную первым пара-
метром — Рапе, внутри объекта панели состояния, представленного m wndStatusBar.
Области панели состояния индексируются слева направо, начиная с 0. Теперь с по-
мощью этой функции вы сможете выводить информацию в панель состояния извне
класса CMainFrame, например:
CMainFrame* pFrame « (CMainFrame*) AfxGetApp () ->m_jpMainWnd;
pFrame->SetPaneText (0, ’’Прощай, жестокий мир") ;
Этот фрагмент кода получает указатель на главное окно приложения и выводит
текстовую строку, которую вы видите в крайней левой области панели состояния. Все
прекрасно, но главное окно приложения — это не то место, где нужно видеть масштаб
846 Глава 16
отображения. В Sketcher может быть открыто несколько представлений, поэтому
на самом деле вы захотите ассоциировать вывод текущего масштаба с каждым пред-
ставлением. Лучший подход состоит в том, чтобы в каждом клиентском окне была
своя панель состояния. m_wndStatusBar в CMainFrame является экземпляром класса
CStatusBar. Вы можете использовать тот же класс для реализации вашей собствен-
ной панели состояния.
Добавление панели состояния в обрамляющее окно
Класс CStatusBar определяет управляющую панель с множеством областей, в
которых вы можете отображать информацию. Объекты типа CStatusBar могут
предоставлять ту же функциональность, что и общий элемент — панель состояния
Windows, через функцию-член GetStatusBarCtrl (). Существует классы MFC, кото-
рые специально инкапсулирует каждый из обычных элементов управления Windows.
Тот, что ассоциирован с панелью состояния, называется CStatusBarCtrl. Однако
использование их напрямую требует определенной работы по интеграции с другими
классами MFC, поскольку “сырые” элементы управления Windows не подключены к
MFC. Использование CStatusBar в нашей программе Sketcher проще и безопаснее.
Функция GetStatusBarCtrl () возвращает ссылку на объект CStatusBarCtrl, ко-
торый обеспечивает всю функциональность общего элемента управления, а объект
CStatusBar обеспечивает взаимодействие с остальной частью MFC.
Первый шаг состоит в добавлении члена данных для панели состояния к опреде-
лению CChildFrame — обрамляющему окну для представления, поэтому добавьте сле-
дующее объявление в раздел public класса:
CStatusBar m_StatusBar; // Объект панели состояния
Здесь может понадобиться небольшое уточнение. Панели состояния должны быть частью
обрамляющего окна, а не представления. Вам не нужно прокручивать панели состояния
или рисовать поверх них. Они должны всегда оставаться прикрепленными к нижней ча-
сти окна. Если вы добавите панель состояния к представлению, она появится внутри ли-
неек прокрутки и станет прокручиваться вместе с этим представлением. Любое рисование
внутри представления, содержащего панель состояния, вызовет перерисовку и самой панели
состояния, сопровождающуюся нежелательным мерцанием. Помещение панели состояния в
обрамляющее окно позволяет избежать этих проблем.
Член данных m_StatusBar следует инициализировать непосредственно перед тем,
как отобразится окно видимого представления. Поэтому с помощью окна свойств
класса CChildFrame добавьте в класс функцию, которая будет вызываться в ответ на
сообщение WM CREATE, отправленное приложению при создании окна. Добавьте в об-
работчик OnCreate () приведенный ниже код.
int CChildFrame::OnCreate(LPCREATESTRUCT IpCreateStruct)
if(CMDIChildWnd::OnCreate(IpCreateStruct) == -1)
return -1;
// Создать панель состояния
m_StatusBar.Create(this);
// Вычислить ширину отображаемого текста
CRect textRect;
CClientDC aDC(&m_StatusBar);
aDC.Selectobject(m_StatusBar.GetFont());
aDC.DrawText(_T("Масштаб отображения:99")
DT SINGLELINE IDT CALCRECT);
-1, textRect,
// Настроить размер части панели состояния, достаточную для принятия текста
int width = textRect. Width () ;
m_StatusBar.GetStatusBarCtrl().SetParts(1, &width);
// Инициализировать текст панели состояния
m__StatusBar. GetStatusBarCtrl () . SetText (_T (’’Масштаб отображения: 1"), 0, 0);
return 0;
}
Сгенерированный код не отмечен полужирным. Имеется вызов версии функции
OnCreate () базового класса, которая занимается созданием определения окна пред-
ставления. Важно не удалять этот вызов функции, ибо окно создаваться не будет.
Функция Create () в объекте CStatusBar создает панель состояния. Указатель
this для текущего объекта CCildFrame передается функции Create (), устанавливая
связь между панелью состояния и содержащим ее окном. Взглянем на то, что проис-
ходит в коде, добавленном в функцию OnCreate ()
Определение частей панели состояния
Объект CStatusBar имеет ассоциированный с ним объект CStatusBarCtrl с
одной или более частями. Части и области в контексте панелей состояния — терми-
ны эквивалентные. CStatusBar ссылается на области, a CStatusBarCtrl — на части.
Вы можете отображать отдельные элементы информации в каждой части.
Количество частей и их ширины можно определить вызовом функции-члена объ-
екта CStatusBarCtrl по имени SetParts (). Эта функция принимает два аргумента.
Первый — количество частей панели состояния, а второй — массив, специфицирую-
щий правую грань каждой части в клиентских координатах. Если пропустить вызов
SetParts (), то по умолчанию панель состояния будет иметь единственную часть,
растянутую на всю ширину; вы можете использовать ее, хотя выглядит она неаккурат-
но. Лучший подход — установить размер каждой части так, чтобы подлежащий ото-
бражению текст точно в нее помещался, и именно это вы сделаете в Sketcher.
Первое, что потребуется сделать в функции OnCreate () — создать временный объ-
ект CRect, в котором вы сохраните описывающий прямоугольник для текста, кото-
рый хотите отобразить. Затем вы создадите объект CClientDC, содержащий контекст
устройства с тем же размером, что у панели состояния. Это возможно, потому что
панель состояния, как и все прочие элементы управления — это просто окно.
Затем шрифт, используемый в той же панели состояния (установленный как
часть свойств рабочего стола) выбирается в контексте устройства вызовом функции
Selectobject (). Член GetFont () объекта m StatusBar возвращает указатель на
объект CPoint, представляющий текущий шрифт. Очевидно, что размеры отображае-
мого текста будут зависеть от конкретного шрифта.
Вы вызываете функцию-член DrawText () объекта CClientDC для вычисления опи-
сывающего прямоугольника текста, который вы хотите отобразить. Эта функция при-
нимает четыре аргумента, перечисленные ниже.
□ Текстовая строка, подлежащая перерисовке. Вы будете передавать строку, со-
держащую максимальное количество символов, которые хотите отобразить —
"Масштаб отображения: 99".
□ Количество символов в строке. Вы специфицируете его как -1, указывая, что
применяется строка, ограниченная нулевым байтом. В этом случае функция са-
мостоятельно подсчитает необходимое количество символов.
□ Ваш прямоугольник — textRect. Описанный прямоугольник для текста сохра-
няется здесь в логических координатах.
□ Один или более флагов, управляющих работой функции.
848 Глава 16
Мы специфицировали комбинацию из двух флагов: DT SINGLELINE указывает на
то, что текст должен размещаться в одной строке, a DT CALCRECT — что необходима
функция для вычисления размера прямоугольника, необходимого для отображения
строки и сохранения ее в прямоугольнике, на который указывает третий аргумент.
Фикция DrawText () обычно используется для действительного рисования строки.
Существует множество других флагов, которые вы можете использовать в этой функ-
ции; за подробностями обращайтесь в справочную систему.
Следующий оператор настраивает части панели состояния:
m_StatusBar.GetStatusBarCtrl().SetParts(l, &width);
Выражение m StatusBar. GetStatusBarCtrl () возвращает ссылку на объект
CStatusBarCtrl, относящийся к m_StatusBar. Возвращенная ссылка используется
для вызова функции SetParts () объекта. Ее первый аргумент определяет количе-
ство частей для панели состояния, в данном случае — 1. Второй аргумент — обычно
адрес массива типа int, содержащего координаты х правой грани каждой части в
клиентских координатах. Массив имеет по одному элементу для каждой части панели
состояния. Поскольку у нас только одна часть, мы передаем адрес отдельной перемен-
ной — width, содержащей ширину прямоугольника, сохраненного в textRect. Это
клиентские координаты, поскольку контекст устройства использует по умолчанию
ММ_ТЕХТ.
И, наконец, мы устанавливаем начальный текст в панели состояния вызовом функ-
ции SetText () — члена CStatusBarCtrl. Первый аргумент — выводимая текстовая
строка, второй — индекс позиции части, содержащей текстовую строку, а третий спе-
цифицирует внешний вид части экрана. Третий аргумент может принимать значения,
перечисленные в табл. 16.3.
Таблица 16.3. Стили панели состояния
Код стиля
SBT_NOBORDERS
SBT_OWNERDRAM
SBT-POPOUT
Внешний вид
Текст имеет границу, которая выглядит “утопленной” в панели состояния.
Текст отображается без границ.
Текст отображается родительским окном.
Текст имеет границу, выглядящую “выступающей” над поверхностью
панели состояния.
В коде вы специфицируете текст с границей, “утопленной” в панели состояния.
Можете попробовать другие опции, чтобы посмотреть, как они выглядят.
Обновление панели состояния
Если теперь вы соберете и запустите код, то панели состояния появятся, но они
будут показывать значение масштаба 1, независимо от реально установленного факто-
ра масштабирования, что не особенно полезно. Поэтому нужно куда-то добавить код,
изменяющий текст при каждом выборе нового масштаба. Это означает модификацию
обработчика OnViewScale () в классе CSketcherView для изменения панели состоя-
ния обрамляющего окна. Потребуется всего четыре дополнительных строки кода:
void CSketcherView: :OnViewScale ()
CScaleDialog aDlg; // Создать объект диалога
aDlg.m_Scale = m_Scale; // Передать диалогу масштаб представления
Работа с диалогами и элементами управления 849
if(aDlg.DoModal() == IDOK)
m_Scale = aDlg.m_Scale; // Получить новый масштаб
// Получить обрамляющее окно для данного представления
CChildFrame* viewFrame
static cast<CChildFrame*>(GetParentFrame());
// Построить строку сообщения
CString StatusMsg("Масштаб отображения:")
StatusMsg += static_cast<char> ('O' + m_Scale);
// Вывести сообщение в панель состояния
viewFrame~>m_S tatusBar.GetStatusBarCtrl().SetText(StatusMsg, 0, 0);
ResetScrollSizes (); // Исправить прокрутку по новому масштабу
InvalidateRect(0); // Пометить все окно как недействительное
Поскольку вы обращаетесь здесь к объекту CChildFrame, вы должны добавить
директиву #include для ChildFrm.h в начало файла SketcherView.срр после всех
остальных директив #include.
Первая строка вызывает функцию GetParentFrame () — член класса CSketcherView,
унаследованного от CScrollView. Она возвращает указатель на объект CFrameWnd, со-
ответствующий рамочному окну, поэтому он должен быть приведен к CChildFrame*,
чтобы вы могли его использовать.
Следующие две строки строят сообщение, отображаемое в панели состояния.
Класс CString применяется лишь потому, что он более гибок, чем простой массив
char. Мы поговорим подробнее об объекте CString, когда добавим новый тип эле-
мента к Sketcher. Здесь мы получаем символ для показа шаблона, добавляя значение
m Scale (которое может меняться от 1 до 8) к символу ‘О’. Таким образом, генериру-
ются символы от ‘Г до ‘8’.
И, наконец, вы используете этот указатель на дочерний фрейм, чтобы получить
член m_StatusBar, добавленный ранее. Затем вы можете получить его элемент управ-
ления — панель состояния и использовать его функцию-член SetText () для измене-
ния отображаемого текста. Остаток функции OnViewScale () не изменился.
Это и все, что вам нужно для панели состояния. Если вы вновь соберете Sketcher,
то должны получить множественные прокручиваемые окна, каждое со своим соб-
ственным масштабом, отображаемым в панели состояния каждого представления.
Использование окна списка
Конечно, вы не обязаны использовать кнопки прокрутки для установки масштаба.
С тем же успехом можно, к примеру, применить окно списка. Логика обработки фак-
тора масштабирования будет точно такой же, отличаться будут только окно диалога и
код получения значения масштаба. Если вы хотите попробовать это, не теряя имею-
щегося варианта разработки программы Sketcher, скопируйте проект Sketcher це-
ликом в другую папку и внесите модификации в копию. Удаление части программы,
управляемое мастером Class Wizard, может поначалу показаться несколько запутан-
ным, поэтому опыт подобного рода вам пригодится.
850 Глава 16
Удаление диалога масштаба
Сначала вы должны удалить определение и реализацию CScaleDialog из нового
проекта Sketcher, а также ресурс диалога масштабирования. Чтобы сделать это, обра-
титесь к панели Solution Explorer (Проводник решений), выберите ScaleDialog. срр
и нажмите клавишу <Delete>; затем выберите ScaleDialog.h и нажмите <Delete>,
чтобы исключить его из проекта. В каждом случае вы увидите диалог, который предо-
ставит выбор: только исключить файлы из проекта либо окончательно стереть их;
щелкните на кнопке Delete (Удалить), если сохранить код не требуется. Затем обрати-
тесь к Resource View, раскройте папку Dialog, щелкните на IDD_SCALE_DLG и нажми-
те клавишу <Delete>, чтобы удалить ресурс диалога. Удалите директиву #include для
ScalingDialog.h из файла SketcherView. срр. На этом этапе все ссылки на исхо-
дный класс диалога из проекта исключены. Все ли сделано? Почти. Идентификаторы
ресурсов должны были быть удалены. Чтобы проверить это, щелкните правой кноп-
кой по Sketcher. гс в Resource View и выберите пункт Resource Symbols (Символы
ресурсов) из контекстного меню; вы можете отметить, что IDC SCALE и IDC_SPIN_
SCALE в списке более не содержатся. Конечно, обработчик OnViewScale () в классе
CSketcherView все еще ссылается на CScaleDialog, поэтому проект Sketcher пока
не полон. Вы исправите это, когда добавите элемент управления — окно списка.
Выберите пункт меню Builds Clean Solution (Сборка=> Очистить решение) для
удаления промежуточных файлов из проекта, которые могут иметь ссылки на
CScaleDialog. Когда это будет сделано, вы можете пересоздать диалоговый ресурс
для ввода значения масштаба.
Создание элемента управления — окна списка
Щелкните правой кнопкой мыши на Dialog в Resource View и добавьте новый диа-
лог с подходящим идентификатором и заголовком. Вы можете использовать тот же
идентификатор, что и ранее — IDD_SCALE_DLG.
Выберите кнопку окна списка в списке элементов управления и щелкните в ме-
сте диалогового окна, куда хотите поместить это окно списка. Вы можете увеличить
окно списка и уточнить его положение в диалоговом окне, перетащив мышью, куда
нужно. Щелкните правой кнопкой на окне списка и выберите Properties из контекст-
ного меню. Вы можете установить значение ID в нечто более подходящее, например,
IDC SCALELIST, как показано на рис. 16.18.
Свойство Sort по умолчанию примет значение True, поэтому не забудьте изме-
нить его на False. Это означает, что строки, которые вы добавляете в окно списка,
не будут автоматически сортироваться. Вместо этого они будут просто добавлять-
ся в конец списка и отображаться в том порядке, в котором были введены вами.
Поскольку вы используете позицию выбранного элемента в списке для указания мас-
штаба, важно не менять последовательность. Окно списка по умолчанию снабжено
вертикальной линейкой прокрутки, и вы можете принять значения по умолчанию
также и для других свойств. Если вы хотите исследовать эффект от других свойств,
то можете щелкнуть на каждом из них по очереди, чтобы отобразить текст в нижней
части окна Properties, описывающий, что именно делает данное свойство.
Теперь, когда диалог готов, сохраните его и приступайте к созданию класса для
этого диалога.
Работа с диалогами и элементами управле:
Properties ▼
IDC SCALELISI (Listbox Control) UBEditor
Notify True
Right Align Те control Events6
Right To Left Reading Or False
False
False
True
T ransparent
Vertical Scrollbar
В Behavior
Accept Files
Disabled
Has Strings
Help ID
Multicolumn
No Data
Owner Draw
Selection
Sort
Use Tabstops
Visible
Want Key Input
El Mtsc
False
False
False
False
False
False
No
Single
False
True
Group
Т abstop
1Г< ‘CALELIST dstb
False
IDC_SCALELIST
True
Sort
Automatically sorts strings added to the list box.
Puc. 16.18. Доступные свойства окна списка
Создание класса диалога
Щелкните на диалоге правой кнопкой мыши и выберите пункт Add Class (Добавить
класс) из контекстного меню. Откроется диалог создания нового класса. Присвойте
классу подходящее имя, вроде того, что использовали ранее — CScaleDialog, и вы-
берите CDialog в качестве базового класса. Если после щелчка на кнопке Finish вы
получите окно сообщения, говорящее о том, что ScaleDialog.cpp уже существует,
значит, вы забыли удалить файлы . h и . срр. Вернитесь и сделайте это сейчас. После
этого все должно заработать. Теперь остается добавить в класс public-переменную
элемента управления по имени m__Scale, соответствующего идентификатору окна
списка — IDCJSCALELIST. Ее типом должен быть int, а предельными значениями — О
и 7. Не забудьте установить в качестве Category вариант Value, иначе вы не сможете
ввести эти ограничения. Поскольку m_Scale создана как управляющая переменная,
для нее реализуется DDX, и вы используете эту переменную для сохранения начинаю-
щегося с нуля индекса одного из восьми вхождений окна списка.
Окно списка должно быть инициализировано в обработчике OnlnitDialogO
класса CScaleDialog, поэтому добавьте переопределение этой функции, используя
окно свойств класса. Соответствующий код показан ниже.
BOOL CScaleDialog::OnInitDialog()
CDialog::OnlnitDialogO ;
CListBox* pListBox = static__cast<CListBox*>(GetDlgItem(IDC_SCALELIST)) ;
pListBox->AddString (_T ("Scale 1"));
pListBox->AddString(_T("Scale 2”));
pListBox->AddString (_T ("Scale 3"));
pListBox->AddString( T("Scale 4"));
852 Глава 16
pListBox->AddString(_Т("Scale 5"));
pListBox->AddString(_T("Scale 6")) ;
pListBox->AddString(_T("Scale 7 ")) ;
pListBox->AddString(_T("Scale 8")) ;
pListBox->SetCurSel(m_Scale-l);
return TRUE; //Возвратить TRUE, пока не установлен фокус на элементе управления
//ИСКЛЮЧЕНИЕ: страницы свойств OCX должны возвращать FALSE
Первая из добавленных строк получает указатель на элемент управления — окно
списка, вызывая функцию-член GetGldltem () класса диалога. Она унаследована от
класса MFC CWnd. Эта функция возвращает указатель на тип CWnd*, поэтому вы приво-
дите его к типу CListBox* — указателю на класс MFC, определяющий окно списка.
Используя указатель на объект CListBox диалога, вы затем многократно вызыва-
ете функцию-член AddString () для добавления строк, определяющих список значе-
ний масштаба. Они появляются в окне списка в том порядке, в котором вы вводите
их, поэтому диалог отображается так, как показано на рис. 16.19.
Г
Set the Drawing Scale
Scale 1
Scale 2
Scale 3
Scale 4
Cancel
Puc. 16.19. Диалог с окном списка выбора масштаба
Каждое вхождение списка ассоциировано со значением начинающегося с нуля
значения индекса, которое автоматически сохраняется в члене m_Scale класса
CScaleDialog через механизм DDX. Таким образом, если вы выберете третий эле-
мент в списке, в m Scale будет установлено значение 2.
Отображение диалога
Диалог отображается обработчиком OnViewScale (), который вы добавили к
CSketcherView в предыдущей версии Sketcher. Вам нужно внести лишь неболь-
шие исправления, чтобы иметь дело с новым диалогом, использующим окно списка.
Необходимый для этого код показан ниже.
void CSketcherView::OnViewScale ()
{
CScaleDialog aDlg; 11 Создать объект диалога
aDlg.m_Scale = m_Scale; // Передать масштаб представления диалогу
if (aDlg. DoModal () == IDOK)
m_Scale = 1 + aDlg.m_Scale; // Получить новый мае
н
// Получить фрейм-обертку данного представления
CChildFrame* childFrame = static_cast<CChildFrame*>(GetParentFrame());
// Собрать строку сообщения
CString StatusMsg("View Scale:”);
StatusMsg += static_cast<char>(’1' + m_Scale - 1) ;
/1 Установить панель состояния
childFrame->m StatusBar.GetStatusBarCtrl().SetText(StatusMsg, 0, 0);
Работа с диалогами и элементами управления 853
ResetScrollSizes(); //Уточнить прокрутку в соответствии с новым масштабом
InvalidateRect(0); //Пометить все окно как недействительное
Поскольку значения индекса для выбранного из списка вхождения начинаются
с нуля, вам нужно просто прибавить к нему единицу, чтобы получить реальное зна-
чение масштаба, которое нужно сохранить в представлении. Код для отображения
этого значения в панели состояния представления остается таким же, как раньше.
Остальная часть кода для обработки факторов масштабирования уже готова и не тре-
бует изменений. После добавления директивы #include для ScaleDialog.h можно
заново собрать и выполнить новую версию Sketcher, чтобы увидеть окно списка в
действии.
Использование элемента
управления — поля редактирования
Поле редактирования может служить для добавления аннотаций в Sketcher. Вам
понадобится новый тип элемента — CText, соответствующий текстовой строке, а так-
же дополнительный пункт меню для установки режима TEXT (Текст) для создания эле-
ментов.
Поскольку текстовому элементу нужна только одна точка ссылки, вы можете соз-
дать его в обработчике OnLButtonDown () класса представления. Вам также понадо-
бится новый пункт в меню Element для установки режима TEXT. Эта возможность до-
бавляется в Sketcher в описанной ниже последовательности.
1. Создать ресурс окна диалога и ассоциированный с ним класс.
2. Добавить новый пункт в меню.
3. Добавить код открытия диалога для создания элемента.
4. Добавить поддержку класса CText.
Создание ресурса поля редактирования
Находясь в Resource View, создайте новый диалог щелчком правой кнопкой мыши
на папке Dialog и выбором Insert Dialog (Вставить диалог) из контекстного меню.
Измените идентификатор (ID) нового диалога на IDD TEXT DLG, а его заголовок
(Caption) — на Enter Text (Введите текст).
Чтобы добавить поле редактирования, выберите соответствующую пиктограмму
из палитры списка элементов управления, затем щелкните в позиции внутри диалога,
куда хотите поместить его. Вы можете настроить размер поля редактирования, пере-
таскивая его границы, а позицию — перетаскивая его целиком. Отобразите свойства
поля редактирования, щелкнув на нем правой кнопкой мыши и выбрав из контекст-
ного меню пункт Properties. Для начала измените ID на IDC_EDITTEXT, как показано
на рис. 16.20.
Некоторые из свойств этого элемента управления интересуют в первую очередь.
Во-первых, выберите свойство Multiline (Многострочное). Установка его значения в
True создает многострочное поле редактирования, в котором вводимый вами текст мо-
жет распространяться на более чем одну строку. Это позволяет вводить длинные стро-
ки текста, которые остаются видимыми целиком в пределах поля редактирования.
854 Глава 16
Свойство Align text (Выравнивать текст) определяет то, как текст будет позици-
онироваться в пределах многострочного поля редактирования. Здесь подойдет зна-
чение Left (Влево), поскольку вы все равно будете отображать текст как одну строку,
тем не менее, есть также выбор установить Center (По центру) или Right (Вправо).
Если вы хотите изменить значение свойства Want return (Необходим возврат ка-
ретки) в True, то нажатие клавиши <Enter> в процессе ввода текста в элементе управ-
ления вставит символ возврата каретки в текстовую строку. Это позволит вам само-
стоятельно разбивать текст для отображения во множестве строк. Если вам не нужен
этот эффект, оставьте в качестве значения этого свойства False. В этом состоянии
нажатие <Enter> даст эффект выбора элемента управления по умолчанию (которым
является кнопка ОК), поэтому нажатие <Enter> закроет диалог.
Если вы установите значение свойства Auto HScroll (Автоматическая горизон-
тальная прокрутка) в False, то при достижении правой границы во время ввода тек-
ста автоматический переход на новую строку не произойдет. Однако это нужно лишь
для видимости текста в пределах поля редактирования — оно никак не отображается
на содержимом строки. Вы можете также установить в True свойство Auto VScroll
(Автоматическая вертикальная прокрутка), чтобы позволить тексту продолжаться за
пределы количества строк, видимых в элементе управления.
Properties
IDC_EDITTEXT (Edit Control) lEdBoxEditor
Accept Files
Align Text
Auto HScroll
Auto VScroll
Border
Client Edge
Disabled
Group
Help ID
Horizontal Scroll
ID
Left Scrollbar
Lowercase
Modal Frame
Multiline
No Hide Selection
Number
OEM Convert
False
Left
False
True
True
False
False
False
False
False
IDC_EDITTEXT
False
False
False
True
False
False
False
Read Only False
Right Align Text False
Right To Left Reading Or False
Static Edge False
JTabstoo- _ True
Auto VScroll
Automatically scrolls text up when the user presses
ENTER on the last line.
Puc. 16.20. Установка ID в IDC_EDITTEXT
Завершив настройку свойств поля редактирования, закройте окно Properties.
Убедитесь, что поле редактирование является первым в порядке обхода по клавише
табуляции, выбрав пункт меню Format^Tab Order (Формат^Порядок обхода по кла-
више табуляции) либо нажав <Ctrl+D>. Вы можете затем протестировать диалог, вы-
брав пункт меню Test Dialog (Протестировать диалог) либо нажав <Ctrl+T>. Диалог
показан на рис. 16.21.
Работа с диалогами и элементами управления 855
Рис. 16.21. Диалог Enter Text (Введите текст)
В тестовом режиме можно даже вводить текст в диалоге, чтобы посмотреть, как
это работает. Щелкните на кнопке ОК или Cancel, чтобы закрыть диалог.
Создание класса диалога
После сохранения ресурса диалога вы можете создать подходящий класс диалога,
соответствующий этому ресурсу, который можно назвать CTextDialog. Щелкните
правой кнопкой мыши на диалоге в Resource View и выберите Add Class (добавить
класс) из контекстного меню. Базовым классом должен быть CDialog. Затем вы мо-
жете добавить переменную элемента управления, щелкнув правой кнопкой в Class
View и выбрав Add^Add Variable (Добавить1^Добавить переменную) из контекстного
меню. Выберите IDC_EDITTEXT в качестве идентификатора элемента управления и
Value — в качестве категории. Назовите новую переменную mJTextString и оставьте
ее типом CString — мы рассмотрим этот класс после того, как завершим с классом
диалога. Можно также специфицировать максимальную длину в поле редактирования
Max chars: (Максимальное количество символов), как показано на рис. 16.22.
Add Member Variable Wizard - Sketcher
Welcome to the Add Member Variable Wizard
Access:
public
И Control variable
Variable type:
Control ID:
IDC EDITTEXT
Variable name:
Control
Category:
Value
Маи chars:
m_TextString
IDO
- e:
Comment (// notation not required):
Store a text string entered in the edit box in the text dialog!
Cancel
Puc. 16.22. Создание новой переменной m__TextString в классе CTextDialog
856 Глава 16
Длина в 100 символов более чем достаточна для ваших нужд. Добавленная здесь
переменная автоматически обновляется по данным, введенным в элементе управле-
ния, благодаря механизму DDX. Щелкните на кнопке Finish для создания переменной
в классе CTextDialog и закрыть мастер Add Member Variable.
Класс CString
Класс CString предоставляет очень удобный и легкий в использовании механизм
для обработки строк, который вы можете применять почти везде, где необходимы
строки. Точнее говоря, вы можете использовать объекты CString вместо строк типа
const char*, обычно применяемого типа символьных строк в “родном” C++, либо
вместо типа LPCTSTR — часто используемого строкового типа в функциях Windows
API.
Класс CString предлагает несколько перегруженных операций, которые облегча-
ют обработку строк, как показано в табл. 16.4.
Таблица 16.4. Операции класса CString
Операция Применение
= Копирует одну строку в другую, как показано ниже:
Strl = Str2; // Копирует содержимое Str2 в Strl
Strl = "Нормальная строка"; // Копирует строку RHS в Strl
+ Сцепляет две или более строк вместе, как показано ниже:
Strl = Str2+Str3+" еще"; // Формирует строку Strl из трех строк
+= Добавляет строку к существующему объекту CString.
== Сравнивает две строки на эквивалентность, как показано ниже:
if (Strl == Str2)
// делать что-то...
< Проверяет, меньше ли одна строка, чем другая.
< = Проверяет, меньше или равна одна строка другой.
> Проверяет, больше ли одна строка, чем другая.
> = Проверяет, больше или равна одна строка другой.
Переменные Strl и Stг2 в приведенной выше таблице являются объектами CString.
Объекты CString автоматически растут по мере необходимости, например, когда вы
добавляете дополнительную строку в конец существующего объекта. Рассмотрим не-
большой фрагмент кода.
CString Str = "Строка начинается здесь...
Str += "а продолжается здесь.";
Первый оператор объявляет и инициализирует объект Str. Второй оператор до-
бавляет дополнительную строку к Str, поэтому длина Str автоматически увеличива-
ется.
Обычно, насколько это возможно, лучше избегать создания объектов CString в куче.
Управление памятью, необходимое для их роста, означает, что операция будет медленной.
Работа с диалогами и элементами управления 857
Добавление пункта меню Text
Добавление нового пункта меню теперь должно быть легким. Нужно только
двойным щелчком в Resource View открыть ресурс меню с идентификатором, рав-
ным IDR SketcherTYPE, и добавить в меню Elements новый пункт — Text (Текст).
Идентификатором по умолчанию будет ID_ELEMENT_TEXT, который появится в окне
Properties; его можно не изменять. Вы можете добавить сообщение для отображения
в панели состояния, соответствующее пункту меню, и поскольку вы также захотите
добавить дополнительную кнопку в панель инструментов, соответствующую этому но-
вому пункту меню, можете добавить также всплывающую подсказку для этой кнопки в
конец строки сообщения, используя символ \п в качестве разделителя.
Не забудьте о контекстном меню. Вы можете скопировать пункт меню из
IDR SketcherTYPE. Щелкните правой кнопкой мыши на пункте меню Text и выберите
пункт Сору (Копировать) из контекстного меню. Откройте меню IDR__CURSOR_MENY,
разверните меню no elements, щелкните правой кнопкой мыши на пустом элементе
внизу и выберите пункт Paste (Вставить) из контекстного меню. Все, что останется
сделать — это перетащить элемент в соответствующую позицию над разделителем и
сохранить ресурсный файл.
Добавьте кнопку в панель инструментов IDR_MAINFRAME и установите ее иден-
тификатор (ID) таким же, как у пункта меню, то есть ID_ELEMENT_TEXT. Вы можете
перетащить новую кнопку так, чтобы позиционировать ее в конце блока, определя-
ющего прочие типы элементов. Сохранив ресурс, добавьте обработчик событий для
нового пункта меню.
Находясь в панели Class View, щелкните правой кнопкой мыши на CSketcherDoc
и отобразите его окно свойств Properties. Добавьте обработчик COMMAND для
ID ELEMENT TEXT и поместите в него следующий код:
void CSketcherDoc: :OnElementText ()
m Element
Для установки типа элемента TEXT в документе необходима только одна строка
кода.
Также потребуется добавить функцию для отметки пункта меню текущего режима,
поэтому добавьте обработчик UPDATE COMMAND UI, соответствующий ID ELEMENT
TEXT, и реализуйте его код следующим образом:
void CSketcherDoc: :OnUpdateElementText (CCmdUI* pCmdUI)
11 Установить метку, если текущий элемент
pCmdUI->SetCheck (m Element = TEXT);
Это работает точно так же, как и другие пункты всплывающего меню Element.
Вы также должны добавить следующую строку в заголовочный файл OurConstants. h:
const unsigned int TEXT = 105U;
Можете добавить этот оператор в заголовочный файл, в конец списка определе-
ний других типов элементов. Следующий шаг — определение класса CText для объ-
екта типа TEXT.
858 Глава 16
Определение текстового элемента
Вы можете унаследовать класс CText от класса CElement следующим образом:
// Определение класса для объекта текст
class CText: public CElement
public:
// Функция отображения текстового элемента
virtual void Draw(CDC* pDC, CElement* pElement=O);
/ / Конструктор текстового элемента
CText(CPoint Start, CPoint End, CString aString, COLORREF aColor);
virtual void Move(CSize& aSize); // Переместить текстовый элемент
protected:
CPoint m_StartPoint; // Позиция текстового элемента
CString m_String; // Текст, подлежащий отображению
CText(){} // Конструктор по умолчанию
Я добавил все это вручную, но то, как поступите вы — значения не имеет. Это
определение должно попасть в конец файла Elements .h, вслед за всеми прочими ти-
пами элементов. Это определение класса объявляет виртуальные функции Draw () и
Move (), как и классы прочих элементов. Член данных m_String типа CString хранит
отображаемый текст, a m StartPoint специфицирует позицию строки в клиентской
области представления.
Взглянем внимательнее на объявление конструктора. Конструктор CText прини-
мает четыре параметра, обеспечивающие его всей важной информацией (табл. 16.5).
Таблица 16.5. Параметры конструктора CText
Параметр Что определяет
CPoint start Позицию текста в логических координатах.
CPoint End Угол, противоположный start, определяющий прямоугольник, описывающий текст.
CString aString Отображаемая текстовая строка как объект CString.
COLORREP aColor Цвет текста.
Ширина пера к текстовому элементу не применяется, поскольку его внешний
вид определяется шрифтом. Хотя вы и не должны передавать ширину пера в каче-
стве аргумента конструктора, все же конструктор должен инициализировать член
m_PenWidth, унаследованный от базового класса, поскольку он используется в вычис-
лении описывающего текст прямоугольника.
Реализация класса CText
В классе CText должны быть реализованы три функции.
□ Конструктор объекта CText.
□ Виртуальная функция Draw () для его отображения.
□ Функция Move () для поддержки перемещения текста при перетаскивании его
мышью.
Все это было добавлено в файл Elements. срр.
Работа с диалогами и элементами управления 859
Конструктор CText
Конструктор объекта CText необходим для инициализации класса и членов его
базового класса:
CText::CText(CPoint Start, CPoint End, CString aString, COLORREF aColor)
m_Pen » 1; // Установить ширину пера
m_Color = aColor; // Установить цвет текста
m_String = aString; // Создать копию сроки
m_StartPoint = Start; // Начальная точка строки
m_EnclosingRect = CRect(Start, End);
m_EnclosingRect.NormalizeRect();
Это все — стандартные вещи вроде тех, что вы видели в других элементах.
Рисование объекта CText
Рисование текста в контексте устройства отличается от рисования геометриче-
ской фигуры. Реализация функции Draw () объекта CText выглядит так:
void CText::Draw(CDC* pDC, CElement* pElement)
COLORREF Color(m_Color); // Инициализировать цветом элемента
if(this==pElement)
Color = SELECT_COLOR; // Установить текущий цвет
// Установить цвет текста и вывести текст
pDC->SetTextColor(Color);
pDC->TextOut(m_StartPoint.х, m_StartPoint.у, m_String);
Для отображения текста перо вам не понадобится. Вы должны только специфи-
цировать цвет текста, используя функцию SetTextColor () — член объекта CDC, и за-
тем с помощью члена TextOut () вывести строку. Это отобразит строку шрифтом по
умолчанию.
Поскольку функция TextOut () не использует перо, оно не затрагивается установ-
кой режима отображения контекста устройства. Это значит, что метод растровой
операции (raster operation — ROP), используемый вами для перемещения элементов,
оставляет временный след, если его применять к тексту. Напомним, что вы исполь-
зовали функцию SetROP2 () для указания способа логической комбинации пера с
фоном. Выбирая R2_NOTXORPEN в качестве режима рисования, вы можете заставить
ранее нарисованный элемент исчезнуть, перерисовывая его — он будет отображен в
цвете фона и, таким образом, станет невидимым. При рисовании шрифтов перо не
используется, потому это не работает с текстовыми элементами. В следующей главе
будет показано, как решить эту проблему.
Перемещение объекта CText
Функция Move () для объекта CText очень проста:
void CText::Move(CSize& aSize)
m_StartPoint += aSize; // Переместить начальную точку
m_EnclosingRect += aSize; // Переместить прямоугольник
860 Глава 16
Все, что потребуется сделать — изменить точку, определяющую позицию строки,
а также член данных, определяющий описывающий прямоугольник, на расстояние,
указанное параметром aSize.
Создание текстового элемента
После того, как тип элемента установлен в TEXT, в позиции курсора должен соз-
даваться текстовый объект при каждом щелчке левой кнопки мыши, а в нем вводить-
ся текст, который необходимо отобразить. Поэтому вам нужно открыть диалоговое
окно, позволяющее ввести текст, в обработчике OnLButtonDown (). Добавьте следую-
щий код в этот обработчик в классе CSketcherView:
void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point)
CClientDC aDC(this);
OnPrepareDC(&aDC);
aDC.DPtoLP(&point);
// Создать контекст устройства
// Исправить начало координат
// преобразовать точку в логические координаты
//В режиме перемещения
if(m MoveMode)
сбросить элемент
m_MoveMode = FALSE;
m_pSelected =0;
GetDocument()->UpdateAllViews(0);
Прекратить режим перемещения
Отменить выбор элемента
Перерисовать все представления
CSketcherDoc* pDoc = GetDocument ();
Получить указатель на документ
CTextDialog aDlg;
if (aDlg. DoModal () = IDOK)
// Выход no щелчку на OK, поэтому создать текстовый элемент
CSketcherDoc* pDoc = GetDocument () ;
CSize TextExtent = aDC.GetTextExtent(aDlg.m_TextString) ;
//Получить нижний правый угол прямоугольника текста - MM_LOENGLISH
CPoint BottomRt(point.x+TextExtent.ex, point.у-TextExtent.cy);
CText* pTextElement = new CText(point, BottomRt,
aDlg.m_TextString, pDoc->GetElementColor());
// Добавить элемент к документу
pDoc->AddElement(pTextElement);
// Обновить все представления
pDoc->UpdateAllViews (0,0, pTextElement);
return;
FirstPoint = point; // Запомнить текущую позицию
// Перехватить последующие сообщения мыши
m
SetCapture ();
Добавленный код выделен полужирным. Он создает объект CTextDialog, а затем
открывает диалог с использованием функции DoModal (). Затем члену m__TextString
объекта aDlg автоматически присваивается строка, введенная в поле редактирова-
ния, так что вы можете использовать этот член данных для передачи введенной стро-
Работа с диалогами и элементами управления 861
ки обратно в конструктор CText, если для закрытия диалога был выполнен щелчок
на кнопке ОК. Цвет и ширина пера получаются из документа с помощью функций-
членов GetElementColor () и GetPenWidth (), которые вы уже применяли ранее.
Положение текста определяется значением point, хранящим текущую позицию кур-
сора, переданную обработчику.
Вам также потребуется вычислить противоположный угол прямоугольника,
ограничивающего текст. Поскольку размер прямоугольника для блока текста за-
висит от шрифта, используемого контекстом устройства, вы используете функцию
GetTextExtent () объекта CClientDC по имени aDC, чтобы инициализировать объ-
ект CSize по имени TextExtent шириной и высотой текстовой строки в логических
координатах.
Подобное вычисление прямоугольника, ограничивающего текст, не совсем “чест-
но” и может вызвать проблемы при сохранении документа в файле, поскольку суще-
ствует возможность чтения документа в среде, где шрифт контекста устройства по
умолчанию окажется больше, чем был на момент вычисления прямоугольника. Это
не должно происходить часто, поэтому мы не станем пока об этом беспокоиться, но в
качестве подсказки отметим, что вы можете применять объект класса CFont в опреде-
лении CText для указания конкретного используемого шрифта. Затем вы сможете ис-
пользовать характеристики шрифта для вычисления ограничивающего прямоуголь-
ника для текстовой строки.
Вы также можете применять CPoint для изменения размера шрифта, чтобы текст
также изменялся в размере при увеличении масштаба; однако вы также должны пред-
усмотреть способ вычисления ограничивающего прямоугольника на основе размера
текущего шрифта, который будет меняться вместе с масштабом представления.
Объект CText создается в куче, поскольку список элементов документа поддержи-
вает только указатели на элементы. Вы добавляете новый элемент в документ вызо-
вом функции AddElement () — члена CsketcherDoc, с указателем на новый текстовый
элемент в качестве аргумента. И, наконец, вызывается UpdateAllViews () с первым
аргументом равным 0, что говорит о том, что все представления должны быть обнов-
лены.
Для успешной компиляции программы вы должны добавить директиву #include
для TextDialog.h в файл SketcherView. срр. Теперь вы сможете создавать анноти-
рованные эскизы, используя множество масштабируемых и прокручиваемых пред-
ставлений, как показано на рис. 16.23.
Резюме
В этой главе вы познакомились с тремя различными диалогами, использующими
разнообразные элементы управления. Хотя вы не создавали диалогов, включающих
много разных элементов управления сразу, механизм их обработки будет таким же,
как было показано здесь, поскольку каждым элементом управления вы можете опери-
ровать независимо от других.
Ниже перечислены наиболее важные моменты, с которыми вы ознакомились в
этой главе.
□ Диалог включает два компонента: ресурс, определяющий диалоговое окно с его
элементами управления, и класс, используемый для отображения и управления
диалогом.
□ Информация может быть извлечена из элементов управления диалога с помо-
щью механизма DDX. Данные могут верифицироваться с помощью механизма
862 Глава 16
DDV. Для применения DDX/DDV нужно использовать опцию Control Variable
(Переменная элемента управления) в интерактивном мастере Add Member
Variable Wizard, чтобы определить переменные в классе диалога, ассоцииро-
ванные с его элементами управления.
Модальный диалог удерживает фокус в приложении до тех пор, пока окно диа-
лога не будет закрыто. Пока отображается модальный диалог, все прочие окна
приложения не активны.
□ Немодальный диалог позволяет переключать фокус с диалогового окна на дру-
гие окна приложения и обратно. Немодальный диалог при необходимости мо-
жет оставаться на экране до тех пор, пока выполняется приложение.
Общие элементы управления (Common Controls) — это набор стандартных эле-
ментов управления Windows, которые поддерживаются MFC и средствами ре-
дактирования ресурсов Developer Studio.
Хотя элементы управления обычно ассоциированы с диалогом, тем не менее
вы можете добавлять их в любые окна.
Рис. 16.23. Версия программы Sketcher с возможностью ввода текста
Работа с диалогами и элементами управления 863
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Реализуйте диалог масштабирования с использованием переключателей.
2. Реализуйте диалог установки ширины пера с использованием окна списка.
> 3. Реализуйте диалог установки ширины пера как комбинированный список с вы-
падающим списком типов, выбранным на вкладке Styles (Стили) окна свойств
(этот выпадающий список позволяет выбирать из фиксированного перечня, но
не помещать альтернативные вхождения в список).
17
Сохранение и печать
документов
С тем, что вы до сих пор реализовали в программе Sketcher, можно создавать
достаточно развитые документы с представлениями в разных масштабах, но инфор-
мация этих документов временна, поскольку нет средств их сохранения. В этой главе
речь пойдет о том, как можно сохранять документы на диске. Кроме того, вы узнаете,
как выводить документ на принтер.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
□ Что такое сериализация и как она работает.
□ Как сделать объекты класса сериализуемыми.
□ Роль объекта CArchive в сериализации.
□ Как реализовать сериализацию в ваших собственных классах.
□ Как реализовать сериализацию в приложении Sketcher.
□ Как работает печать в MFC.
□ Как можно использовать функции класса представления для подцержки печати.
□ Как реализовать многостраничную печать в приложении Sketcher.
Что такое сериализация?
Документ основанной на MFC программы — не простая сущность, это объект клас-
са, который может оказаться очень сложным. Обычно он содержит широкое разноо-
бразие объектов, каждый из которых может содержать другие объекты, а те, в свою
очередь, могут содержать в себе еще объекты... и такая структура может распростра-
няться на множество уровней.
866 Глава 17
Вам нужно иметь возможность сохранять документ в файле, но запись объекта
класса в файл представляет собой некоторую проблему, поскольку это не то же самое,
что базовый элемент данных вроде целого числа или символьной строки. Базовый
элемент данных состоит из известного количества байт, поэтому чтобы записать его
в файл, необходимо записать это определенное количество байт. И наоборот, если
вы знаете, что в файл было записано значение типа int, то чтобы получить его об-
ратно, необходимо просто прочесть соответствующее количество байт.
С записью объекта дела обстоят иначе. Даже если вы запишете все данные-члены
объекта, этого недостаточно для того, чтобы получить обратно исходный объект.
Объекты класса содержат функции-члены наряду с данными-членами, и все члены —
как данные, так и функции — имеют спецификаторы доступа; таким образом, чтобы со-
хранить объекты во внешнем файле, информация, записываемая в этот файл, должна
содержать полные спецификации всех участвующих структур классов. Процесс чтения
также должен быть достаточно интеллектуальным, чтобы полностью синтезировать
исходные объекты на основе данных файла. Библиотека MFC поддерживает механизм,
называемый сериализациеи, который помогает реализовать ввод с диска и вывод на
диск объектов ваших классов с минимальными затратами времени и усилий.
Основная идея, лежащая в основе сериализации, заключается в том, что любой
сериализуемый класс самостоятельно заботится о сохранении и восстановлении себя.
Это значит, что для того, чтобы ваши классы были сериализуемыми — в случае прило-
жения Sketcher речь идет о классе класс CElement и классах фигур, унаследованных
от него — они должны уметь записывать себя в файл. Это подразумевает, что для того,
чтобы класс был сериализуемым, все типы данных, используемые в его объявлении,
также должны быть сериализуемыми.
Сериализация документа
Все это звучит довольно замысловато, но основная возможность сериализации
вашего документа изначально встраивается в приложение мастером Application
Wizard. Обработчики для пунктов меню File^Save (Файл =>Сохранить), File^Open
(Файл*=>Открыть) предполагают, что будет реализована сериализация ваших докумен-
тов, и уже содержат код, необходимый для ее поддержки. Взгляните на части опреде-
ления и реализации CSketcherDoc, относящиеся к созданию документов с использо-
ванием сериализации.
Сериализация в определении класса документа
Код для определения CsketcherDoc, обеспечивающий сериализацию объекта до-
кумента, в следующем фрагменте выделен полужирным.
class CsketcherDoc : public CDocument
protected: // Создается только сериализацией
CSketcherDoc();
DECLAREJDYNCREATE(CsketcherDoc)
// Остальная часть класса...
// Переопределения
public:
virtual BOOL OnNewDocument();
virtual void Serialize (CArchivefi ar) ;
// Остальная часть класса...
Сохранение и печать документов
867
Здесь представлены три вещи, имеющие отношение к сериализации объекта до-
кумента.
1. Макрос DECLARE—DYNCRE АТЕ ().
2. Функция-член Serialize ().
3. Конструктор класса по умолчанию.
DECLARE_DYNCREATE () — это макрос, позволяющий динамически создавать объек-
ты класса CSketcherDoc каркасом приложения во время сериализационного процесса
ввода. В реализации класса его дополняет другой макрос — IMPLEMENT—DYNCREATE ().
Этот макрос применяется только к классам, унаследованным от CObject, но как вы
вскоре увидите, это не единственная пара макросов, которые могут использоваться в
этом контексте. Для любого класса, который вы хотите сериализовать, CObject дол-
жен быть прямым или непрямым базовым классом, поскольку он добавляет функци-
ональность, которая позволяет работать сериализации. Вот почему класс CElement
унаследован от CObject. Почти все классы MFC унаследованы от CObject, и как тако-
вые, являются сериализуемыми.
В иерархической диаграмме в справочнике Microsoft Foundation Class Reference для Visual C++
2005 показаны me классы, которые не унаследованы от CObj ect. Обратите внимание, что
С Ar chive входят в этот список»
Определение класса также включает объявление виртуальной функции SerializeO.
Каждый сериализуемый класс должен содержать эту функцию. Она вызывается для
выполнения операций сериализации — как ввода, так и вывода — с членами данных
класса. Объект типа С Ar chive, передаваемый в качестве аргумента этой функции,
определяет, какая операция выполняется — ввода или вывода. Вы узнаете об этом под-
робнее, когда будете рассматривать реализацию сериализации класса документа.
Обратите внимание, что класс явно определяет конструктор по умолчанию. Это
также существенно для правильной работы сериализации, поскольку конструктор по
умолчанию используется каркасом для синтеза объекта при чтении из файла, и син-
тезируемый объект затем заполняется данными из файла, устанавливая значения дан-
ных-членов объекта.
Сериализация в реализации класса документа
Есть две части файла, содержащие реализацию CSketcherDoc, которые касаются
сериализации. Первая — это макрос IMPLEMENT—DYNCRETE (), дополняющий макрос
DECLARE_DYNCREATE ():
// SketcherDoc. срр : реализация класса CSketcherDoc
//
#include "stdafx.h"
#include "Sketcher.h"
#include "PenDialog.h"
#include "SketcherDoc.h"
#ifdef -DEBUG
#define new DEBUG_NEW
#endif
// CSketcherDoc
IMPLEMENT—DYNCREATE (CSketcherDoc, CDocument)
// Карты сообщений и остальная часть файла...
868 Глава 17
Все, что делает макрос — это определение класса С Do cume nt в качестве базового
для CsketcherDoc. Это требуется для правильного динамического создания объекта
CsketcherDoc, включая члены, унаследованные от базового класса.
Функция Serialize ()
Реализация класса также включает определение функции Serialize ():
void CsketcherDoc:: Serialize (CArchive& ar)
if (ar.IsStoring())
// TODO: добавить сюда код сохранения
else
// TODO: добавить сюда код загрузки
Функция сериализует данные-члены класса. Аргумент, передаваемый функции —
это ссылка на объект аг класса CArchive. Функция-член IsStoring () объекта этого
класса возвращает TRUE, если нужно выполнить операцию сохранения данных-членов
в файле, и FALSE — если необходимо читать данные-члены из ранее сохраненного до-
кумента.
Поскольку мастер Application Wizard не имеет понятия о том, какие данные со-
держит ваш
документ, процесс записи и чтения этой информации находится в вашей
компетенции, что указано в комментариях. Чтобы понять, как это делается, взгляни-
те внимательнее на класс CArchive.
Класс CArchive
Класс CArchive — это механизм, управляющий сериализацией. Он предоставляет
MFC-эквивалент потоковых операций C++, которые вы использовали для чтения с
клавиатуры и записи на экран в примерах консольных программ. Объект класса MFC
CArchive предоставляет механизм для направления ваших объектов в файл либо за-
писи их из входного потока, автоматически реконструируя объекты вашего класса в
процессе.
Объект CArchive включает в себя ассоциированный с ним объект CFile, обеспе-
чивающий возможности ввода-вывода для двоичных файлов, а также действитель-
ное подключение к конкретному физическому файлу. Во время сериализации объ-
ект CFile заботится обо всех деталях операций файлового ввода-вывода, а объект
CArchive имеет дело с логикой структурирования данных объекта, подлежащих за-
писи, или реконструкции объектов на основе прочитанной информации. Вам при-
дется заботиться о деталях ассоциированного объекта CFile только в том случае,
если вы хотите сконструировать свой собственный объект CArchive. Для докумен-
тов в Sketcher каркас уже обеспечит все необходимое и передаст объект аг типа
CArchive, который будет им построен, функции Serialize () из CsketcherDoc. Вы
сможете использовать один и тот же объект в каждой функции Serialize (), кото-
рые добавите в классы фигур при реализации их сериализации.
Класс CArchive перегружает операции вставки и извлечения (» и «), необходи-
мые для ввода и вывода объектов классов, унаследованных от CObject, плюс диапа-
зон базовых типов данных. Эти перегруженные операции работают с перечисленны-
ми в табл. 17.1 объектными и примитивными типами.
Сохранение и печать документов
869
Таблица 17.1. Типы, с которыми работают операции вставки и извлечения,
перегруженные в классе CArchive
Тип Определение
bool Булевские значения — true или false.
float Стандартный тип с плавающей точкой одинарной точности.
double Стандартный тип с плавающей точкой двойной точности.
byte 8-битовое целое без знака.
char 8-битовый символ.
wchar_t
int и short
LONG И long
LONGLONG
ULONGLONG
WORD и unsigned int
DWORD и unsigned int
CObject *
CString
SIZE И CSize
POINT И CPoint
RECT И CRect
CObject *
16-битовый символ.
16-битовое целое со знаком.
32-битовое целое co знаком.
64-битовое целое со знаком.
64-битовое целое без знака.
16-битовое целое без знака.
32-битовое целое без знака.
Указатель на CObject.
Объект cstring, определяющий строку.
Объект, определяющий размер, как пару сх, су.
Объект, определяющий точку, как пару сх, су.
Объект, определяющий прямоугольник по его левому верхнему
и правому нижнему углам.
Указатель на CObject.
Для базовых типов данных в ваших объектах вы используете операции вставки и
извлечения для сериализации данных. Чтобы прочесть или записать объект сериа-
лизуемого класса, который вы наследуете от CObject, вы можете либо вызвать функ-
цию Serialize () для объекта, либо воспользоваться операциями извлечения или
вставки. Какой бы путь вы ни выбрали, следует обеспечить согласованность между
вводом и выводом, поэтому вы не должны выводить объект с использованием опера-
ции вставки, а затем читать его обратно функцией Serialize (), или наоборот.
Когда вам не известен тип объекта при чтении, как, например, в случае указателей
в списке фигур документа, вы должны использовать только функцию Serialize().
Это вводит в действие механизм виртуальных функций, поэтому каждый раз вызыва-
ется функция Serialize (), соответствующая типу объекта, с которым она вызвана,
что определяется во время выполнения.
Объект CArchive конструируется либо для сохранения объектов, либо для их
извлечения. Функция IsStoring () класса CArchive возвращает TRUE, если объект
предназначен для вывода, и FALSE — если для ввода. Вы видели это в операторе if в
тексте функции Serialize () класса CSketcherDoc.
Существует множество других функций-членов класса CArchive, которые касают-
ся детальных механизмов процесса сериализации, но обычно для применения сериа-
лизации в своих программах знать их не обязательно.
870 Глава 17
Функциональность классов, базирующихся на CObject
Есть три уровня функциональности, доступных вашим классам, если они наследу-
ются от класса MFC CObject. Конкретный уровень, который вы получаете в вашем
классе, определяется тем, какой из трех макросов вы используете в определении ва-
шего класса (табл. 17.2).
Таблица 17.2. Макросы, определяющие уровень функциональности
Макрос
Функциональность
declare dynamic () Поддержка информации о классе во время выполнения.
declare_dyncreate () Поддержка информации о классе во время выполнения и динамическое
создание объектов.
DECLARE_SERIAL()
Поддержка информации о классе во время выполнения и динамическое
создание объектов и сериализации объектов.
Каждый из этих макросов требует дополняющего макроса, именованного с пре-
фиксом IMPLEMENT— вместо DECLARE—, помещенного в файл, содержащий реализа-
цию класса. Как следует из табл. 17.2, эти макросы обеспечивает нарастающий объем
функциональности, поэтому сосредоточим внимание на третьем из них — DE CLARE-
SERIAL (), поскольку он обеспечивает все то, что предшествующие ему, и даже более.
Этот макрос следует применять, чтобы обеспечить возможность сериализации ваших
собственных классов. Он требует добавления макроса IMPLEMENT—SERIAL () к файлу,
содержащему реализацию класса.
Возможно, вас удивит — почему класс документа использует DECLARE—DYNCREATE (),
а не DECLARE—SERIAL (). Макрос DECLARE—DYNCREATE () обеспечивает возможность
сериализации класса плюс динамическое создание объектов класса, таким образом,
заключая в себе эффект от DECLARE—DYNCREATE (). Ваш класс документа не нуждается
в сериализации, поскольку каркас должен только синтезировать объект документа и
затем восстанавливать значения данных-членов; однако данные-члены документа долж-
ны быть сериализуемыми, поскольку этот процесс применяется для их сохранения и
извлечения.
Макрос, добавляющий сериализацию к классу
С макросом DECLARE—SERIAL () в определении вашего основанного на CObject
класса, вы получаете доступ к поддержке сериализации, обеспечиваемой CObject.
Это включает специальные операции new и delete, отвечающие за обнаружение уте-
чек памяти в отладочном режиме. Вам не нужно делать ничего особенного, чтобы ис-
пользовать это, поскольку все оно работает автоматически.
Макрос требует спецификации имени класса в качестве аргумента, поэтому для
сериализации класса CElement понадобится добавить следующую строку к определе-
нию класса:
DECLARE—SERIAL(CElement)
Здесь точка с запятой не требуется, поскольку это - вызов макрос, а не оператор C++.
Неважно, куда именно вы поместите макрос в пределах определения класса, но
если вы всегда будете помещать его в первой строке, то всегда сможете убедиться в
его присутствии, даже когда определение класса состоит из множества строк кода.
Сохранение и печать документов 871
Макрос IMPLEMENT_SERIAL (), который вы помещаете в файл реализации класса,
принимает три аргумента. Первый аргумент — имя класса, второй — имя непосред-
ственного базового класса, а третий аргумент — беззнаковое 32-битное целое, иденти-
фицирующее номер схемы, или номер версии вашей программы. Этот номер схемы
позволяет защитить процесс сериализации от проблем, которые могут возникнуть,
если вы пишете объекты в одной версии программы, а читаете в другой, в которой
классы могут отличаться.
Например, вы можете добавить в реализацию класса CElement такую строку кода:
IMPLEMENT_SERIAL(CElement, CObject, 1)
Если вы последовательно модифицируете определение класса, то должны при
этом изменять номер схемы на что-то другое, например, на 2. Если программа по-
пытается прочесть данные, которые были записаны с номером схемы, отличным от
номера текущей активной программы, возбуждается исключение. Лучше всего поме-
стить этот макрос в первой строке, следующей за директивами #include и всеми на-
чальными комментариями в файле .срр.
Когда CObject является непрямым базовым классом, как, например, в случае
CLine, для корректной работы сериализации каждый класс в иерархии должен иметь
добавленный макрос сериализации в классе верхнего уровня. Чтобы работала сериа-
лизация CLine, макрос также должен быть добавлен к CElement.
Как работает сериализация
Общий процесс сериализации документа в упрощенном виде показан на рис. 17.1.
Вывод документа с применением сериализации
Serialize()
Объект
документа
SerializeQ
Внутренний
объект
Внутренний ]
объект
Файл
Рис. 17.1, Общий процесс сериализации документа
872 Глава 17
Функция Serialize () в объекте документа вызывает функцию Serialize () (или
использует перегруженную операцию вставки) для каждого из членов данных. Когда
член является объектом класса, функция Serialize () для этого объекта сериализи-
рует каждый из членов данных по очереди, пока в конечном счете все базовые типы
данных будут записаны в файл. Поскольку большинство классов MFC в конечном ито-
ге унаследованы от CObject, они содержат поддержку сериализации, так что вы мо-
жете почти всегда сериализовать объекты классов MFC.
Когда вы наследуете класс CObject через множество уровней, функция Serialize ()
вашего класса и объект документа приложения в каждом случае должна вызывать
член Serialize () его непосредственного предка. Обратите внимание, что сериали-
зация не поддерживает множественное наследование, поэтому должен существовать
только один базовый класс для каждого класса, определенного в иерархии.
Как реализовать сериализацию класса
На основе предшествующей дискуссии можно подытожить шаги, которые необхо-
димо предпринять для добавления в класс возможностей сериализации.
1. Убедитесь, что класс унаследован прямо или непрямо от CObject.
2. Добавьте макрос DECLARE_SERIAL () в определение класса (и в непосредствен-
ный базовый класс, если это не CObject).
3. Реализуйте в своем классе функцию Serialize ().
Теперь посмотрим, как можно реализовать сериализацию документов в программе
Sketcher.
Применение сериализации
Чтобы реализовать сериализацию в приложении Sketcher, вы должны реализо-
вать функцию Serialize () в CsketcherDoc, так чтобы она обрабатывала все данные-
члены класса. Вам понадобится добавить сериализацию к каждому классу, который
специфицирует объекты, включаемые в документ. Прежде чем вы начнете добавлять
сериализацию к классам приложения, нужно будет внести некоторые небольшие
изменения в программу, чтобы фиксировать факт внесения изменений в документ-
эскиз. Это не является абсолютно необходимым, но весьма желательно, поскольку по-
зволяет программе предотвращать закрытие документа без сохранения изменений.
Запись изменений в документе
У нас уже есть механизм для фиксации факта изменения документа, он использу-
ет унаследованный член CSketcherDoc — SetModif iedFlag (). Вызывая эту функцию
согласованно при каждом изменении документа, вы фиксируете факт такого измене-
ния в члене данных объекта класса документа. Это вызывает автоматический запрос
при попытке выхода из приложения без сохранения модифицированного документа.
Аргумент функции SetModif iedFlag () имеет тип BOOL и значение по умолчанию
TRUE. Если вы имеете возможность отметить, что документ не был изменен, то мо-
жете вызвать эту функцию с аргументом FALSE, хотя такая необходимость может воз-
никнуть редко.
Существуют только три случая, когда вы изменяете объект документа.
Сохранение и печать документов
873
□ При вызове члена AddElement () класса CSketcherDoc для добавления нового
элемента.
□ При вызове члена DeleteElement () класса CSketcherDoc для удаления эле-
мента.
□ При перемещении элемента.
Вы легко можете справиться с этими тремя ситуациями. Все, что необходимо
делать — это добавить вызов SetModif iedFlag () в каждую функцию, задействован-
ную в этих операциях. Определение AddElement () появляется в определении класса
CSketcherDoc. Вы можете расширить это следующим образом:
void AddElement(CElement* pElement)
m_ElementList.AddTail(pElement);
SetModifiedFlag();
/ / Добавить элемент в список
// Установить флаг модификации
Вы можете получить определение DeleteElement () в CSketcherDoc, щелкнув на
имени функции в панели Class View (Представление классов). К нему потребуется до-
бавить одну строку, как показано ниже.
void CSketcherDoc::DeleteElement (CElement* pElement)
if(pElement)
// Если указатель на элемент корректен,
/ / найти его в списке и удалить
SetModifiedFlag (); // Установить флаг модификации
POSITION aPosition = m_ElementList.Find(pElement);
m_ElementList.RemoveAt(aPosition);
delete pElement; // Удалить элемент из кучи
Обратите внимание, что вы должны установить флаг, если pElement не равен
null, поэтому вы не можете просто поместить вызов функции, где заблагорассудится.
В объекте представления перемещение элемента в члене MoveElement (), вызван-
ном обработчиком сообщения WM_MOUSEMOVE, но вы изменяете документ только тог-
да, когда нажата левая кнопка мыши. Если нажата правая кнопка, элемент возвращает-
ся на свою исходную позицию, поэтому вы должны только добавлять вызов функции
SetModif iedFlag () документа в функцию OnLButtonDown (), как показано ниже:
void CSketcherView: :OnLButtonDown (UINT nFlags, CPoint point)
CClientDC aDC(this);
OnPrepareDC(&aDC);
aDC.DPtoLP(&point);
if(m_MoveMode)
// Создать контекст устройства
// Получить исправленное начало координат
// Преобразовать точку к логическим координатам
//В режиме перемещения, поэтому сбросить элемент
m_MoveMode = FALSE;
m_pSelected =0;
GetDocument()->UpdateAllViews(0);
GetDocument () ->SetModifiedFlag () ;
// Отключить режим перемещения
// Отменить выделение элемента
// Перерисовать все представления
// Установить флаг модификации
н
874 Глава 17
Вы вызываете унаследованный член GetDocument () класса представления, чтобы
получить доступ к указателю на объект документа, и затем используете этот указатель
для вызова функции SetModifiedFlag(). Таким образом, покрываются все места, где
документ может изменяться.
Если вы соберете и запустите проект Sketcher и затем модифицируете документ
или станете добавлять к нему элементы, то перед выходом из программы получите
приглашение на сохранение документа. Конечно, опция меню File^Save (Файл=>
Сохранить) пока ничего не делает, за исключением очистки флага модификации и
сохранения пустого файла на диск. Вы должны реализовать сериализацию для пра-
вильной записи документа на диск, и это будет следующим шагом.
Сериализация документа
Первый шаг предусматривает реализацию функции Serialize () в классе
CSketcherDoc. Внутри этой функции вы должны добавить код сериализации данных-
членов CSketcherDoc. Данные-члены, объявленные в классе, выглядят так:
class CSketcherDoc : public CDocument
// Создается только сериализацией
protected:
CSketcherDoc () ;
DECLARE_DYNCREATE (CSketcherDoc)
// Атрибуты
public:
protected:
COLORREF m_Color; // Текущий цвет рисования
unsigned int m_Element; // Текущий тип элемента
CTypedPtrList<CObList, CElement*> m_ElementList; // Список элементов
int m_PenWidth; // Текущая ширина пера
CSize m_DocSize; // Размер документа
// Остальная часть класса. . .
Обратите внимание, что вы не должны добавлять никакого приведенного кода,
поскольку он уже есть. Все, что потребуется вставить — это операторы для сохране-
ния и извлечения этих пяти данных-членов в члене Serialize () класса. Вы можете
сделать это следующим образом:
void CSketcherDoc::Serialize(CArchive& ar)
m_ElementList.Serialize(ar); // Сериализация списка элементов
if (ar.IsStoring())
« m PenWidth
m_Element
m PenWidth
текущий тип элемента,
текущую ширину пера
и текущий размер документа
else
Восстановить текущий цвет,
текущий тип элемента,
текущую ширину пера
и текущий размер документа
Сохранение и печать документов
875
Для четырех из данных-членов вы просто используете операции извлечения и
вставки, которые в классе CArchive перегружены. Это работает с членом m_Color,
даже несмотря на то, что его типом является COLORREF, поскольку тип COLORREF —
это тоже самое, что тип long. Вы не можете использовать операции извлечения и
вставки для m_ElementList, поскольку его тип не поддерживается операциями, но
до тех пор, пока CTypedPtrList определен из шаблона класса коллекции с использо-
ванием CObList, как вы делали в объявлении m_ElementList, класс автоматически
поддерживает сериализацию. Поэтому вы можете просто вызвать функцию объекта
Serialize().
Вам не нужно помещать вызовы члена Serialize () объекта m_ElementList в опе-
ратор if-else, поскольку конкретный вид выполняемой операции определяется авто-
матически аргументом CArchive по имени аг. Единственный оператор, вызывающий
член m_ElementList — функцию Serialize () — заботится и о вводе, и о выводе.
Это все, что вам понадобится для сериализации данных-членов класса документа,
однако сериализация списка элементов m_ElementList инициирует вызовы функции
Serialize () классов элементов для того, чтобы элементы сохраняли и извлекали
себя, поэтому вам придется реализовать сериализацию для этих классов.
Сериализация классов элементов
Все классы фигур сериализуемые, поскольку вы унаследовали их от базового клас-
са CElement, который, в свою очередь, унаследован от CObject. Причина выбора
CObject в качестве базового класса для CElement связана исключительно с необхо-
димостью получить поддержку сериализации. Теперь вы можете добавить поддержку
сериализации в каждый из классов фигур, добавив код сериализации данных-членов в
функцию-член Serialize (). Начать можно с базового класса CElement, определение
которого потребуется модифицировать следующим образом:
class CElement: public CObject
// Цвет элемента
// Прямоугольник, описывающий элемент
// Ширина пера
// Виртуальный деструктор
DECLARE_SERIAL(CElement)
protected:
COLORREF mjColor;
CRect m_EnclosingRect;
int m_Pen;
public:
virtual «'CElement () {}
// Виртуальная операция рисования
virtual void Draw (CDC* pDC, CElement* pElement=O) { }
virtual void Mbve(CSize& aSize) {} // Перемещение элемента
CRect GetBoundRect (); // Получить описывающий элемент прямоугольник
virtual void Serialize (CArchivefi ar); // Функция сериализации класса
protected:
CElement(void);
// Чтобы пред отвратить вызов
Здесь добавлен макрос DECLARE_SERIAL (), а также объявление виртуальной функ-
ции Serialize().
У вас уже есть конструктор по умолчанию, созданный мастером Application Wizard.
Вы изменяете его на protected, хотя его спецификатор доступа не имеет значения
до тех пор, пока он не появляется явно в определении класса. Он может быть public,
protected или private, и сериализация все равно будет работать. Однако если вы за-
будете включить конструктор по умолчанию, то получите сообщение об ошибке при
компиляции макроса IMPLEMENT_SERIAL ().
876 Глава 17
Макрос DECLARE—SERIAL () необходимо добавить к каждому из классов-наслед-
ников— CLine, CRectangle, CCircle, CCurve и CText, с соответствующими имена-
ми классов в качестве аргумента. Вы также должны добавить объявление функции
Serialize () как public-члена класса.
В начало Elements. срр следует добавить такой макрос:
IMPLEMENT—SERIAL (CElement, CObject, VERSION_NUMBER)
Вы можете определить константу VERSION—NUMBER в файле OurConstants . h, до-
бавив следующие строки:
// Номер версии программы для использования при сериализации
const UINT VERSION_NUMBER = 1;
Затем вы можете использовать ту же константу при добавлении макроса для каж-
дого из классов фигур. Например, в класс CLine потребуется добавить следующую
строку:
IMPLEMENT_SERIAL(CLine, CElement, VERSION—NUMBER)
Для классов других фигур все аналогично. Когда вы модифицируете любые клас-
сы, относящиеся к документу, то все, что нужно сделать — это изменить определение
VERSION_NUMBER в OurConstants .h и указать новый номер версии. Вы можете при
желании поместить все операторы IMPLEMENT—SERIAL () в начало файла. Ниже по-
казан их полный набор.
IMPLEMENT_SERIAL(CElement, CObject, VERSION—NUMBER)
IMPLEMENT-SERIAL(CLine, CElement, VERSION—NUMBER)
IMPLEMENT-SERIAL(CRectangle, CElement, VERSION—NUMBER)
IMPLEMENTjSERIAL(CCircle, CElement, VERSION—NUMBER)
IMPLEMENT-SERIAL(CCurve, CElement, VERSION—NUMBER)
IMPLEMENT-SERIAL(CText, CElement, VERSION—NUMBER)
Функции Serial! ze() классов фигур
Теперь вы можете реализовать функцию-член Serialize () для каждого из клас-
сов фигур. Начните с класса CElement:
void CElement::Serialize(CArchive& ar)
CObject::Serialize(ar);
if (ar.IsStoring())
ar « m_Color
« m_EnclosingRect
« m_Pen;
else
ar » m_Color
» m_EnclosingRect
» m_Pen;
// Вызвать функцию базового класса
// Сохранить цвет,
// описывающий прямоугольник,
//и ширину пера
// Извлечь цвет,
// описывающий прямоугольник,
//и ширину пера
Эта функция имеет ту же форму, что и ранее добавленная в класс CSketcherDoc.
Все данные-члены, определенные в CElement, поддерживаются перегруженными
операциями вставки и извлечения, и потому все необходимое делается этими one-
Сохранение и печать документов 877
рациями. Обратите внимание, что вы должны вызвать член Serialize () для класса
CObject, чтобы гарантировать сериализацию унаследованных данных-членов.
Для класса CLine эту функцию можно закодировать следующим образом:
void CLine::Serialize(CArchive&
CElement::Serialize(ar);
if (ar.IsStoring())
ar « m_StartPoint
« m_EndPoint;
else
ar » m_StartPoint
» m EndPoint;
// Вызов функции базового класса
// Сохранить начальную точку линии
// и ее конечную точку
// Извлечь начальную точку линии
// и ее конечную точку
Опять-таки все данные-члены поддерживаются операциями извлечением и встав-
ки объекта аг класса CArchive. Вы вызываете член Serialize () базового клас-
са CElement для сериализации его данных-членов, и это приводит к вызову члена
Serialize () класса CObject (). Вы можете видеть, как процесс сериализации про-
ходит по иерархии классов.
Функция-член Serialize () класса CRestangle проста:
void CRectangle::Serialize(CArchive& ar)
CElement::Serialize(ar); // Вызов функции базового класса
Здесь только вызывается функция базового класса, поскольку данный класс не
имеет данных-членов.
Класс CCircle также не имеет дополнительных данных-членов помимо тех, что
унаследованы от CElement, поэтому функция Serialize () также только вызывает
функцию базового класса:
void CCircle::Serialize(CArchive& ar)
CElement::Serialize(ar); // Вызов функции базового класса
Для класса CCurve также понадобится удивительно немного работы. Его функцию
Serialize () можно закодировать следующим образом:
void CCurve:: Serialize (CArchive& ar)
CElement::Serialize(ar); // Вызов функции базового класса
m_PointList.Serialize(ar); // Сериализовать список точек
После вызова функции Serialize () базового класса вы просто можете вызвать
функции Serialize () объекта m_PointList класса CList. Подобным образом могут
быть сериализованы объекты классов CList, С Ar г ay и СМар, поскольку эти классы
унаследованы от COb j ect.
Последний класс, для которого потребуется добавить реализацию Serialize () в
Elements. срр — это CText:
878 Глава 17
void CText::Serialize(CArchive& ar)
CElement:: Serialize (ar); // Вызов функции базового класса
if (ar.IsStoring())
ar « m_StartPoint // Сохранить начальную точку
« m_String; // и текстовую строку
else
ar » m_StartPoint // Извлечь текстовую строку
» m_String; //и текстовую строку
}
После вызова функции базового класса вы сериализуете два члена данных, исполь-
зуя операции вставки и извлечения для аг. Класс CString, хотя он и не унаследован
от CObject, все же полностью поддерживается CArchive через перегруженные опе-
рации.
Испытание сериализации
Это и все, что вам нужно сделать для реализации сохранения и извлечения до-
кументов в программе Sketcher! Опции файлового меню для сохранения и восста-
новления теперь полностью функциональны без добавления какого-либо дополни-
тельного кода. Если вы соберете и запустите программу Sketcher после включения
описанных выше изменений, то сможете сохранять и восстанавливать файлы, и бу-
;ете получать автоматический запрос на сохранение модифицированного документа,
когда попытаетесь закрыть или выйти из программы, как показано на рис. 17.2.
Рис. 17.2. Автоматический запрос на сохранение модифицированного документа
Сохранение и печать документов
879
Запрос работает благодаря добавленным вами вызовам SetModif iedFlag () везде,
где вы обновляете документ. Если вы щелкнете на кнопке Yes (Да) на экране, показан-
ном на рис. 17.2, то увидите диалоговое окно File^Save As (Файл^Сохранить как),
показанное на рис. 17.3.
Это стандартное диалоговое окно для упомянутого пункта меню в Windows. Оно
полностью функционально и поддерживается кодом, предоставленным каркасом.
Имя файла для документа генерируется по назначенному ему при первом открытии
документа, а расширение файла автоматически определяется как . ske. Теперь при-
ложение обладает полной поддержкой файловых операций с документом. Просто, не
правда ли?
Рис. 173. Диалоговое окно Save As
Перемещение текста
Теперь самое время вернуться немного назад и решить проблему, возникшую в
предыдущей главе. Вспомните, что когда вы пытались перемещать текстовый эле-
мент, он оставлял за собой след до тех пор, пока текст не был позиционирован на
новом месте в документе. Это вызвано тем, что мы полагались на рисование ROP в
функции-члене представления MoveElement ():
void CSketcherView: :MoveElement (CClientDC& aDC, CPoint& point)
CSize Distance - point - m_CursorPos; // Получить расстояние перемещения
m_CursorPos = point; // Установить текущую точку как 1-ю для следующего раза
// Если есть выбранный элемент, переместить его
if(m_pSelected)
{
aDC.SetROP2(R2_NOTXORPEN);
m_pSelected->Draw(&aDC,m_pSelected); // Нарисовать элемент, чтобы
// стереть его
880 Глава 17
m__pSelected->Move (Distance) ; // Переместить элемент
m_pSelected->Draw(&aDC,m_pSelected); // Нарисовать перемещенный элемент
Как упоминалось ранее, установка режима рисования контекста устройства в
R2_NOTXORPEN не удаляет след, оставляемый текстом при перемещении. Вы можете
обойти это, используя метод пометки недействительных прямоугольников, которые
затронуты перемещаемыми элементами, так чтобы они перерисовывали себя. Однако
это может привести к некоторому нежелательному мерцанию при быстром переме-
щении элемента. Лучшее решение состоит в использовании метода пометки недей-
ствительными только текстовых элементов и исходного метода ROP для всех прочих
элементов, но как узнать, к какому классу относится выбранный элемент? Это неожи-
данно просто: можно воспользоваться оператором if, как показано ниже:
if (m_pSelected->IsKindOf(RUNTIME_CLASS(CText)))
// Помещенный сюда код выполняется только в случае,
// если выбран элемент класса CText
Здесь применяется макрос RUNTIME—CLASS для получения указателя на объект
типа CRuntimeClass, затем указатель this передается функции IsKindOf () — члену
m_pSelected. Она возвращает ненулевой результат, если m_pSelected относится к
классу CText, и ноль — в противоположном случае. Единственное условие — чтобы
проверяемый класс был объявлен с использованием макроса DECLARE—DYNCREАТЕ
или DECLARE—SERIAL, чем объясняется то, что решение проблемы откладывалось до
этого момента.
Существует и другой способ определения типа класса, использующий средство,
встроенное в ANSI/ISO C++. Операция type id () возвращает ссылку на объект типа
type_inf о, который инкапсулирует указатель на имя типа объекта времени выполне-
ния, либо выражение, которое помещается в скобках. Поскольку вы можете сравни-
вать объекты type_info, используя операцию == (или !=), всегда можно проверить,
относится ли ш_pSelected к типу CText:
if (typeid(m_pSelected) == typid(CText))
// Помещенный сюда код выполняется только в случае,
/ / если выбран элемент класса CText
Если вы хотите использовать операцию typeid (), то должны добавить директиву
flinclude для заголовочного файла ISO/ANSI C++ <typeinfo>. Конечно, в этом слу-
чае не обязательно, чтобы типы были объявлены с макросами MFC, используемыми в
предыдущем методе, хотя вы должны использовать опцию компилятора /GR, которая
включает информацию о типе времени выполнения.
Ниже показан окончательный код MoveElement () с использованием макроса MFC
RUNTIME_CLASS.
void CSketcherView::MoveElement (CClientDC& aDC, CPoint& point)
CSize Distance = point - m_CursorPos; // Получить расстояние перемещения
m CursorPos = point; // Установить текущую точку как 1-ю для следующего раза
// Если есть выбранный элемент, переместить его
if(m_pSelected)
Сохранение и печать документов 881
// Если элементом является текст, использовать этот метод...
if(m_pSelected->IsKindOf(RUNTIME_CLASS(CText)))
CRect 01dRect=m_pSelected->GetBoundRect();// Получить старый
// граничный прямоугольник
m_pSelected->Move(Distance); // Переместить элемент
CRect NewRect=m_pSelected->GetBoundRect();// Получить новый
// граничный прямоугольник
OldRect.UnionRect(&OldRect,&NewRect); //Комбинировать прямоугольники
aDC.LPtoDP(OldRect); // Преобразовать в клиентские координаты
OldRect.NormalizeRect(); // Нормализовать клиентские координаты
InvalidateRect(&OldRect); // Пометить комбинированную область
/ / как недействительную
UpdateWindow(); // Перерисовать немедленно
m_pSelected->Draw(&aDC,m_pSelected); // Нарисовать с подсветкой
return;
// ...иначе использовать этот метод
aDC.SetROP2(R2_NOTXORPEN);
m_pSelected->Draw (&aDC,m_pSelected);//Нарисовать элемент, чтобы стереть его
m_pSelected->Move(Distance); // Переместить элемент
m_pSelected->Draw(&aDC,m__pSelected); // Нарисовать перемещенный элемент
Вы видите, что код пометки прямоугольников недействительными, который вы
должны использовать для перемещения текста, намного менее элегантен, чем код
ROP, используемый с другими элементами. Однако он работает, как вы сами убеди-
тесь, внеся предложенные модификации, а затем собрав и запустив приложение.
Если вы предпочитаете применить механизм type id () для тестирования типа, про-
сто измените условие в операторе if и добавьте директиву #include для <typeinf о>
в SketcherView.срр.
Печать документа
Теперь обратимся к печати документа. У вас уже есть базовые возможности печа-
ти, реализованные в программе Sketcher, благодаря предусмотрительности масте-
ра Application Wizard и каркасу. Пункты меню File^Print (Файл^Печать), File1^Print
Setup (Файл^Параметры страницы) и File1^Print Preview (Файл=> Предварительный
просмотр) уже работают. Выбор пункта File1^ Print Preview отображает окно, показы-
вающее текущий документ Sketcher на странице (рис. 17.4).
Все, что есть в текущем документе, помещается на один лист бумаги в текущем мас-
штабе представления. Если размеры документа выходят за пределы бумажного листа,
то часть документа, выходящая за границы листа, не печатается. Если вы щелкнете
на кнопке Print (Печать), данная страница будет отправлена на ваш принтер.
В качестве базовой возможности, которую вы получаете бесплатно, это достаточ-
но впечатляет, но не подходит для большинства случаев. Типичный документ нашей
программы не будет помещаться на одну страницу, поэтому вы должны будете либо из-
менить масштаб, чтобы все уместилось, либо при необходимости печатать документ
на нескольких страницах. Вы можете добавить собственный код обработки печати,
чтобы расширить возможности, предоставленные каркасом, но сначала вы должны
разобраться, как печать реализована в MFC.
882 Глава 17
Е! Sketched
Two Page
Zoom In
View Scale:l
Рис. 17.4. Окно предварительного просмотра документа
Процесс печати
Печать документа управляется текущим представлением. Этот процесс неизбежно
несколько запутан, поскольку печать — по природе своей запутанный процесс, кото-
рый потенциально вовлекает вас в реализацию собственных версий множества уна-
следованных функций вашего класса представления.
На рис. 17.5 показана логика процесса и участвующие в нем функции.
Цикл, пока остаются страницы
OnPrepareDCO
OnPrint()
OnEndPrintingO
CDC::StartDoc()
Члены представления
• Вычисление количества страниц
* Вызов DoPreparePrintingO
* Выделение ресурсов GDI
* Смена начала координат области
отображения
* Установка атрибутов DC
* Печать нижних и верхних
колонтитулов
* Печать текущей страницы
* Возврат выделенных ресурсов GDI
Рис. 17.5. Логика процесса печати
Сохранение и печать документов
883
На рис. 17.5 видно, как последовательность событий управляется каркасом, и как
печать документа включает вызов пяти унаследованных методов вашего класса пред-
ставления, которые вы должны переопределить. Функции-члены С DC, показанные в
левой части диаграммы, взаимодействуют с драйвером устройства печати и вызыва-
ются каркасом автоматически.
Типичная роль каждой функции в текущем представлении в процессе операции пе-
чати специфицирована в примечаниях, расположенных рядом. Последовательность
их вызовов указана числами на стрелках. На практике вам не обязательно реализо-
вывать все эти функции, а только те, которые нужны для обеспечения ваших кон-
кретных требований к печати. Обычно вам понадобится реализовать, как минимум,
собственные версии OnPreparePrinting (), OnPrepareDC () и OnPrint (). Далее
в этой главе вы увидите пример реализации этих функций в контексте программы
Sketcher.
Вывод данных на принтер выполняется таким же образом, как и на дисплей — че-
рез контекст устройства. Вызовы GDI, которые вы используете для вывода текста или
графики, независимы от устройства, поэтому они работают одинаково хорошо как с
принтером, так и с дисплеем. Единственное отличие — в применяемом объекте С DC.
Функции CDC на рис. 17.5 взаимодействуют с драйвером принтера. Если документ,
подлежащий печати, требует для своего вывода более одной страницы, процесс ци-
клически повторяет вызовы функции OnPrepareDC () для каждой следующей новой
страницы, как определено функцией EndPage ().
Все функции в вашем классе представления, участвующие в процессе печати, по-
лучают в качестве аргумента указатель на объект типа CPrintlnfо. Этот объект обе-
спечивает связь между всеми функциями, управляющими процессом печати, поэтому
рассмотрим класс CPrintlnfо более подробно.
Класс CPrintinfo
Объект CPrintinfo играет основополагающую роль в процессе печати, поскольку
сохраняет информацию о задании печати и подробности о его состоянии в любой мо-
мент времени. Также он обеспечивает функции доступа и манипуляции данными. Этот
объект — средство передачи информации от одной функции представления к другой
во время печати, а также между каркасом и вашими функциями представления.
Объект класса CPrintinfo создается всякий раз, когда вы выбираете пункты меню
File1^ Print и File1^ Print Preview. После использования каждой из функций текущего
представления, которая участвует в процессе печати, он автоматически удаляется по
окончании операции печати.
Все данные-члены класса CPrintinfo общедоступны (public). Их перечень при-
веден в табл. 17.3. Объект CPrintinfo имеет общедоступные функции-члены, пере-
численные в табл. 17.4.
При печати документа, состоящего из нескольких страниц, вы должны опреде-
лить, сколько печатных страниц займет документ, и сохранить эту информацию в
объекте CPrintinfo, чтобы сделать ее доступной каркасу. Это можно сделать в вашей
версии функции-члена текущего представления OnPreparePrinting ().
Для установки номера первой страницы документа потребуется вызвать функцию
SetMinPage () в объекте CPrintinfo, которая принимает номер страницы как аргу-
мент типа UINT. Возвращаемое значение отсутствует. Чтобы установить номер послед-
ней страницы документа, вы вызываете функцию SetMaxPage (), которая также при-
нимает номер страницы как аргумент типа UINT и не возвращает значения. Если вы
позднее пожелаете извлечь эти значения, то можете вызвать функции GetMinPage ()
и GetMaxPage () с объектом CPrintinfo.
884 Глава 17
Таблица 17.3. Данные-члены класса CPrintinfo
Член
m pPD
m_bDirect
m_bPreview
m_bContinuePrinting
Использование
Указатель на объект CPrintDialog, отображающий диалоговое окно печати.
Устанавливается каркасом в true в операции печати, чтобы пропустить диа-
логовое окно печати; в противном случае равно false.
Член типа bool, имеющий значение true при выборе File*^ Print Preview; в
противном случае равно false.
Значение типа bool. Если установлено в true, каркас продолжает цикл пе-
чати, показанный на диаграмме. При установке в false цикл печати завер-
шается. Вы должны установить эту переменную, только если не передаете
счетчик страниц для операции печати объекту CPrintinfo (используя функ-
цию-член SetMaxPage ()). В этом случае вы отвечаете за своевременное из-
вещение об окончании печати установкой этой переменной в false.
m_nCurPage Значение типа UINT, хранящее номер текущей страницы. Обычно страницы
нумеруются, начиная с 1.
m_nNumPreviewPages Значение типа UINT, специфицирующее количество страниц, отображенных
в окне предварительного просмотра печати. Может быть 1 или 2.
rn_lpUserData
m_rectDraw
m_stгPageDesс
Значение типа lpvoid, хранящее указатель на объект, который вы создаете.
Это позволяет вам создать объект для хранения дополнительной информа-
ции об операциях печати и ассоциировать его с объектом CPrintinfo.
Объект CRect, определяющий используемую область страницы в логических
координатах.
Объект CString, содержащий форматную строку, используемую каркасом
для отображения номеров страниц во время предварительного просмотра
печати.
Таблица 17.4. Функции-члены класса CPrintinfo
Функция Описание
SetMinPage(UINT nMinPage)
Аргумент специфицирует номер первой страницы документа. Типа
возврата нет.
SetMaxPage (UINT nMaxPage)
GetMinPage() const
GetMaxPage () const
GetFromPage() const
GetToPage() const
Аргумент специфицирует номер последней страницы документа.
Типа возврата нет.
Возвращает номер первой страницы документа, как тип uint.
Возвращает номер последней страницы документа, как тип uint.
Возвращает номер первой страницы документа, подлежащей печати,
как тип uint. Это значение устанавливается в диалоге печати.
Возвращает номер последней страницы документа, подлежащей пе-
чати, как тип uint. Это значение устанавливается в диалоге печати.
Задаваемые вами номера страниц сохраняются в объекте CPrintDialog, на кото-
рый указывает член CPrintinfo m_pPD, и отображается в диалоговом окне, всплыва-
ющем при выборе пункта меню File1^Print. Пользователь затем получает возможность
специфицировать номера первой и последней печатаемых страниц, которые вы мо-
жете получить вызовом GetFromPage О и GetToPage () объекта CPrintinfo. В каж-
дом случае возвращается значение типа UINT. Диалог автоматически верифицирует
Сохранение и печать документов
885
вхождение номеров первой и последней печатаемых страниц в диапазон, указанный
минимальной и максимальной страницей документа.
Теперь вам известно, какие функции вы можете реализовать для себя в классе
представления с целью управления печатью, оставив большую часть работы каркасу
Вы также знаете, какая информация доступна через объект CPrintlnfо, передавае-
мый функциям, занятым в процессе печати. Вы получите гораздо более ясное пони-
мание подробностей механизма печати, если реализуете базовую возможность много-
страничной печати документов Sketcher.
Реализация многостраничной печати
Вы используете режим отображения MM LOENGLISH в программе Sketcher для
установки всех вещей и затем переключаетесь в MM_ANISOTROPIC. Это означает, что
фигуры и размеры представления измеряются в сотых долях дюйма. Конечно, с фик-
сированными физическими единицами измерения в идеале вы хотите печатать объ-
екты в их действительном размере.
При размере документа, специфицированном как 3000 на 3000 единиц вы можете
создавать документы площадью до 30 квадратных дюймов, которые распространяют-
ся на несколько листов бумаги, если заполнить всю их область. Вычисление количе-
ства страниц, необходимых для печати эскиза, требует немного больше усилий, не-
жели при печати типичного текстового документа, поскольку в большинстве случаев
для печати полного документа-эскиза понадобится двумерный массив страниц.
Чтобы избежать чрезмерного усложнения, предположим, что вы печатаете на
нормальный лист бумаги (формата А4 или размером 8!6 на 11 дюймов) в портретной
ориентации (то есть длинная сторона располагается по вертикали). При любом раз-
мере листа вы печатаете в центральной его части размером 6 на 9 дюймов. При этих
предположениях вы не должны заботиться о реальном размере бумаги; вам нужно
только поделить документ на куски размером 600 на 900 единиц. Документ, занимаю-
щий более одной страницы, придется разбить, как показано в примере на рис. 17.6.
Как видите, страницы будут нумероваться построчно, поэтому в данном случае
страницы от 1 до 4 находятся в первой строке, а страницы с 5 по 8 — во второй.
Количество страниц по ширине = 4
6 дюймов
Рис, 17.6. Печать многостраничного документа
886 Глава 17
Получение полного размера документа
Чтобы узнать, сколько страниц занимает конкретный документ, вам нужно знать,
насколько велик эскиз, а для этого нужно вычислить прямоугольник, который вклю-
чает в себя весь документ. Это легко сделать, добавив функцию GetDocExtent () в
класс документа CsketcherDoc. Добавьте следующее объявление к public-интерфей-
су класса CsketcherDoc:
CRect GetDocExtent (); //Получить ограничивающий прямоугольник для всего документа
Реализация также не составит большой проблемы:
// Получить прямоугольник, ограничивающий весь документ
CRect CsketcherDoc: : GetDocExtent ()
CRect DocExtent(0,0,1,1); // Начальный размер документа
CRect ElementBound(0,0,0,0); // Пространство для ограничивающего
// прямоугольник элемента
POSITION aPosition = m_ElementList.GetHeadPosition();
while(aPosition) // Цикл по всем элементам в списке
// Получить ограничивающий прямоугольник элемента
ElementBound=(m_ElementList.GetNext(aPosition))->GetBoundRect();
// Уточнить координаты поля документа по внешним пределам
DocExtent.UnionRect(DocExtent, ElementBound);
DocExtent.NormalizeRect();
return DocExtent;
Вы можете добавить определение этой функции в файл Sketcher Do с . срр или
просто добавить ее код, применив средство Add^Add Function (Добавить1^Добавить
функцию) из контекстного меню в Class View. Процесс проходит в цикле по каждо-
му элементу в документе, используя переменную aPosition для выполнения шагов
по списку и получения ограничивающего прямоугольника для каждого элемента.
Функция UnionRect (), член класса CRect, вычисляет минимальный прямоугольник,
содержащий два других прямоугольника, переданных ей в качестве аргументов, и по-
мещает его значение в объект CRect, для которого данная функция вызвана. Таким
образом, DocExtent продолжает расти в размере, пока все элементы не поместятся в
него. Обратите внимание, что вы инициализируете DocExtent размером (0, 0, 1, 1),
поскольку функция UnionRect () не работает правильно с прямоугольниками нулевой
длины или ширины.
Сохранение данных печати
Функция OnPreparePrinting () в классе представления вызывается каркасом
приложения для того, чтобы позволить вам инициализировать процесс печати до-
кумента. Требуемая базовая инициализация предназначена для того, чтобы предста-
вить информацию о страницах, требуемых вашим документом, которую вы могли бы
использовать позже, в других функциях представления, участвующих в процессе пе-
чати. Вы создаете ее в функции-члене OnPreparePrinting () класса представления,
сохраняете в объекте вашего собственного класса, который определяете специально
для этой цели, и сохраняете указатель на объект в объекте CPrintlnfo, доступ к кото-
рому обеспечивает каркас. Такой подход предназначен преимущественно для демон-
страции того, как работает данный механизм; в большинстве случаев вы обнаружите,
Сохранение и печать документов 887
что проще сохранить данные в вашем объекте представления — в основном потому,
что это обеспечит более простой способ обращения к данным.
Вы должны сохранить количество страниц, приходящихся на ширину докумен-
та, m__nWidths, и количество строк страниц, приходящихся на длину документа,
ш_nLengths. Вы также сохраните левый верхний угол прямоугольника, включающего
данные документа, как объект CPoint по имени m_DocRef Point, поскольку он будет
использоваться при нахождении позиции страницы, подлежащей печати, по ее но-
меру. Вы можете сохранять имя документа в объекте CString по имени m_DocTitle,
чтобы иметь возможность добавить его в качестве заголовка каждой страницы.
Определение класса, подходящего для этого, может выглядеть так:
#pragma once
class CPrintData
public:
UINT m_nWidths;
UINT m_nLengths;
CPoint m_DocRefPoint;
CString m_DocTitle;
//
//
//
//
Количество страниц на ширину документа
Количество страниц на длину документа
Верхний левый угол содержимого документа
Имя документа
Вы можете добавить к проекту новый заголовочный файл под названием
PrintData. h, выполнив щелчок правой кнопкой мышки на папке Header Files (Заго-
ловочные файлы) в панели Solution Explorer, и затем выбрав пункт Add1^ New Item
(Добавить1^ Новый элемент) из всплывающего меню. После этого вы можете ввести
определение класса в новый файл.
Вам не понадобится отдельный файл реализации этого класса. Конструктор по
умолчанию (генерируемый автоматически) здесь вполне адекватен. Поскольку объ-
ект этого класса существует и используется кратковременно, вам не нужно применять
CObject в качестве базового класса, как не нужны и другие усложнения.
Процесс печати начинается с вызова функции-члена класса представления
OnPreparePrinting (), так что разберем, как вы должны реализовать ее.
Подготовка к печати
Мастер Application Wizard добавляет версии OnPreparePrinting ( ) ,
OnBeginPrinting () и OnEndPrinting () к CSketcherView с самого начала. Базовый
код OnPreparePrinting () вызывает DoPreparePrinting() в операторе return, как
видно в следующем коде:
BOOL CSketcherView: :OnPreparePrinting (CPrintinfo* plnfo)
// Подготовка по умолчанию
return DoPreparePrinting(plnfo);
Функция DoPreparePrinting () отображает диалоговое окно Print, используя
информацию о количестве страниц, подлежащих печати, которая определена в объ-
екте CPrintinfo. Всегда, когда это возможно, вы должны вычислять количество стра-
ниц, подлежащих печати, и сохранять его в объекте CPrintinfo перед этим вызовом.
Конечно, во многих ситуациях вам может понадобиться информация для принтера
из контекста устройства прежде, чем вы сможете сделать это, например, когда печа-
тается документ, в котором количество страниц зависит от размера используемого
шрифта; в таких случаях может оказаться невозможным получить счетчик страниц в
888 Глава 17
члене OnBeginPrinting (), который принимает указатель на контекст устройства в
качестве аргумента. Эта функция вызывается каркасом после OnPreparePrinting (),
поэтому информация, введенная в диалоговом окне Print (Печать), уже доступна. Это
означает, что вы можете также получить выбранный пользователем в диалоговом
окне Print размер листа.
Предположим, что размер листа достаточен, чтобы уместить область 6 на 9 дюй-
мов для отображения данных документа, поэтому вы можете вычислить количество
страниц в OnPreparePrinting (). Ниже приведен необходимый для этого код.
BOOL CSketcherView: :OnPreparePrinting (CPrintInfo* plnfo)
pInfo->m_lpUserData = new CPrintData; // Создать объект печатаемых данных
CSketcherDoc* pDoc = GetDocument(); // Получить указатель на документ
// Получить всю область документа
CRect DocExtent = pDoc->GetDocExtent();
// Сохранить точку ссылки на весь документ
((CPrintData*)(pInfo->m_lpUserData))->m_DocRefPoint =
CPoint(DocExtent.left, DocExtent.bottom);
// Получить имя файла документа и сохранить его
((CPrintData*)(pInfo->m_lpUserData))->m_DocTitle = pDoc->GetTitle();
// Вычислить, сколько печных страниц потребует ширина в 600 единиц
/ / для вмещения ширины документа
((CPrintData*)(pInfo->m_lpUserData))->m_nWidths =
static_cast<UINT>(ceil((static_cast<double>(DocExtent.Width()))/600.0));
// Вычислить, сколько печных страниц потребует длина в 900 единиц
// для вмещения длины документа
((CPrintData*)(pInfo->m_lpUserData))->m_nLengths =
static_cast<UINT>(ceil((static_cast<double>(DocExtent.Height()))/900.0));
// Установить номер первой страницы равным 1 и номер
// последней страницы — равным общему количеству страниц
p!nfo->SetMinPage(1);
pInfo->SetMaxPage((
static_cast<CPrintData*>(pInfo->m_lpUserData))->m_nWidths *
(static_cast<CPrintData*>(pInfo->m_lpUserData))->m_nLengths);
return DoPreparePrinting(plnfo);
Первоначально вы создаете объект CPrintData в куче и сохраняете его адрес в
указателе m lpUserData, принадлежащем объекту CPrintInfo, переданному функции
в виде параметра plnfo. После получения указателя на документ, вы получаете прямо-
угольник, включающий все элементы документа, вызывая функцию GetDocExtent (),
которую вы добавили в класс документа ранее в этой главе. Затем вы сохраняете угол
этого прямоугольника в члене m_DocRefPoint объекта CPrintData и помещаете имя
файла, содержащего документ, в m DocTitle.
Обращение к объекту CPrintData через указатель в объекте CPrintlnfо довольно
громоздко. Это делается выражением plnf o->m_lpUserData, но поскольку указатель
имеет тип void, вы должны добавить приведение к типу CPrintData*, чтобы полу-
чить член m DocRef Point объекта. Полное выражение для доступа к начальной точ-
ке документа выглядит так:
(static_cast<CPrintData*>(pInfo->m_lpUserData))->m_DocRefPoint
Сохранение и печать документов
889
Вам следует использовать этот подход для всех ссылок на члены объекта CPrintData,
поэтому любое выражение, использующее их, будет “украшено” такой нотацией. Если
вы поместите данные в класс представления, то вам понадобится использовать толь-
ко имя члена данных. Не забудьте добавить директиву #include для PrintData.h в
файл SketcherView.срр.
Следующие две строки кода вычисляют количество страниц, приходящееся на ши-
рину документа, а также количество страниц, приходящихся на его длину. Количество
страниц, покрывающее ширину, вычисляется делением ширины документа на ши-
рину печатной области страницы, которая составляет 600 единиц, или 6 дюймов, с
округлением до следующего большего целого с помощью библиотечной функции
cell (), определенной в заголовке <cmath>. Директива #include для этого файла
также должна быть добавлена в SketcherView. срр. Например, cell (2.1) возвраща-
ет 3.0, cell (2.9) также возвращает 3.0, a cell (-2.1) возвращает -2.0. Подобные
вычисления выполняются и для определения количества страниц по длине докумен-
та. Произведение этих двух значений дает общее количество страниц, подлежащих
печати, и именно это значение вы используете в качестве максимального номера
страницы.
Очистка после печати
Поскольку вы создали объект CPrintData в куче, вы должны обеспечить его
удаление по завершении работы с ним. Это делается добавлением в функцию
OnEndPrinting () следующего кода:
void CSketcherView::OnEndPrinting(CDC* /*pDC*/, CPrintlnfo* plnfo)
// Удалить объект печати
delete static_cast<CPrintData*> (pInfo->m__lpUserData) ;
Это все, что необходимо добавить в эту функцию в программе Sketcher, но в не-
которых случаях понадобится сделать несколько больше. Вся окончательная очистка
должна быть помещена сюда. Не забудьте удалить знаки комментария (/ * * /), окружа-
ющие имя второго параметра; в противном случае функция не скомпилируется. В реа-
лизации по умолчанию имена параметров закомментированы, поскольку вам может и
не понадобится обращаться к ним в коде. Поскольку вы использовали параметр pln-
fo, то должны убрать комментарий с него; в противном случае компилятор сообщит,
что он не определен.
Вам ничего не нужно добавлять к функции OnBeginPrinting () в программе
Sketcher, но вы должны добавить код для выделения ресурсов GDI, таких как перья,
если они понадобятся в процессе печати. Затем вы должны удалить все это в процес-
се очистки в OnEndPrinting ().
Подготовка контекста устройства
Пока что программа Sketcher вызывает функцию OnPrepareDC (), которая уста-
навливает в качестве режима отображения MM_ANISOTROPIC, дабы учесть фактор
масштабирования. Вы должны внести некоторые дополнительные изменения, чтобы
контекст устройства был правильно подготовлен в случае печати:
void CSketcherView: :OnPrepareDC (CDC* pDC, CPrintlnfo* plnfo)
int Scale =
1
II
Scale;
// Сохранить масштаб локально
1
4
890 Глава 17
if(pDC->IsPrinting())
Scale = 1; // Если выполи
CScrollView::OnPrepareDC(pDC, plnfo);
CSketcherDoc* pDoc ~ GetDocument ();
pDC->SetMapMode(MM_ANISOTROPIC);
CSize DocSize = pDoc->GetDocSize();
установить масштаб 1
// Установить режим отображения
// Получить размер документа
//Значение у должно быть отрицательным,
pDC->SetWindowExt(DocSize);
поскольку необходим режим MM_LOENGLISH
// Изменить знак у
// Установить размер окна
/ / Получить количество пикселей на дюйм для х и у
int xLogPixels = pDC->GetDeviceCaps(LOGPIXELSX);
int yLogPixels = pDC->GetDeviceCaps(LOGPIXELSY);
// Вычислить размер области отображения по х и у
long xExtent = s tatic__cast<long> (DocSize. сх) *Scale*xLogPixels/100L;
long yExtent = static_cast<long>(DocSize.cy)*Scale*yLogPixels/100L;
pDC->SetViewportExt(static_cast<int>(xExtent),
static_cast<int>(-yExtent)); // Установить размер области отображения
Функция вызывается каркасом для вывода — как на принтер, так и на экран. Вы
должны убедиться, что для установки отображения из логических координат в коор-
динаты устройства при выполнении печати используется масштаб 1. Если вы остави-
те все как есть, то вывод пойдет в текущем масштабе представления, однако вы долж-
ны учитывать масштаб при вычислении необходимого количества страниц, а также
того, как устанавливается начало каждой страницы.
Определить, имеете ли вы дело с контекстом устройства печати, можно вызо-
вом функции-члена IsPrinting () объекта CDC, которая возвращает TRUE, если вы-
полняется печать. Все, что необходимо сделать, когда вы имеете дело с контекстом
устройства принтера — это установить масштаб в 1. Конечно, вы должны изменить
операторы, использующие значение масштаба, чтобы они использовали локальную
переменную Scale вместо члена представления m_Scale. Значения, возвращенные
вызовами GetDeviceCaps () с аргументами LOGPIXELSX и LOGPIXELSY, означают ко-
личество логических точек на дюйм в направлениях х и у для вашего принтера — в
случае печати либо эквивалентные значения для вашего дисплея при рисовании на
экране, потому это автоматически адаптирует размер области отображения для соот-
ветствующего устройства, когда вы посылаете на него вывод.
Печать документа
Записывать данные в контекст устройства принтера можно в функции On Print ().
Она вызывается один раз для каждой печатаемой страницы. Вы должны добавить
переопределение этой функции к CSketcherView, используя окно свойств класса.
Выберите OnPrint () из списка переопределений и щелкните на <Add> OnPrint в
правой колонке.
Вы можете получить номер текущей страницы из члена m_nCurPage объекта
CPrintinfo и использовать это значение для нахождения координат положения до-
кумента, соответствующих левому верхнему углу текущей страницы. Как это делает-
ся, лучше всего пояснить на примере, поэтому предположим, что выполняется пе-
чать страницы под номером семь в восьмистраничном документе, как показано на
рис. 17.7.
Сохранение и печать документов 891
mjiWidths Количество страниц по ширине = 4
m_nCurPage = 2x4 = 8
Страница 1
Страница 2
Страница 3
Страница 4
Doc Def Point
900*(((m_nCurPage-1 )/m_nWidths
900*((7-1)/4) =
900*(6/4) =
900*1 = 900
xOrg.yOrg
Страница 5
Страница 6
Страница 7
Страница 8
xOrg = DocDefPoint х + 1200
yOrg = DocDefPoint.y - 900
о
о
600 единиц
600*((m_nCurPage-1 )%m_nWidths)
600*(6%4) = 600*2 = 1200
Puc. 11. 7. Печать страницы под номером семь в восьмистраничном документе
Вы можете получить индекс горизонтальной позиции страницы, уменьшив номер
страницы на 1 и взяв остаток от деления на количество ширин страниц, приходя-
щихся на ширину печатаемой области документа. Умножение результата на 600 дает
координату х верхнего левого утла страницы относительно верхнего левого угла пря-
моугольника, описывающего все элементы документа. Аналогично вы можете опреде-
лить индекс вертикальной позиции в документе, разделив номер текущей страницы,
уменьшенный на 1, на количество ширин страниц, приходящихся на длину всего до-
кумента. Умножив остаток на 900, вы получаете относительную координату у верхне-
го левого угла страницы. Эти два оператора можно выразить следующим образом:
int xOrg = (static_cast<CPrintData*>(pInfo->m_lpUserData))->
m_DocRefPoint.x + 600*((pInfo->m_nCurPage - 1)%
((static_cast <CPrintData*>(p!nfo->m_lpUserData))->m_nWidths));
int yOrg = (static_cast<CPrintData*>(p!nfo->m_lpUserData))->
m_DocRefPoint.у - 900*( (pinfo->m_nCurPage - 1)/
((static_cast <CPrintData*>(pInfo->m_lpUserData))->m_nWidths));
Эти операторы кажутся сложными, но это в основном из-за необходимости обра-
щения к информации, хранящейся в объекте CPrintData через указатель в объекте
CPrintlnfo.
Было бы неплохо напечатать имя файла документа на вершине каждой страницы,
но вы хотите убедиться, что не напечатаете данные документа поверх имени файла.
Вы также хотите отцентрировать на странице печатную область. Это можно сделать,
переместив начало системы координат в контексте устройства печати после того, как
будет напечатано имя файла. Все это проиллюстрировано на рис. 17.8.
На рис. 17.8 показано соответствие между печатной областью в контексте устрой-
ства и печатаемой страницей в ссылаемой рамке данных документа. Помните, что
все это представлено в логических координатах — эквивалент MM_LOENGLISH в
Sketcher — поэтому у имеет возрастающее отрицательное значение в направлении
сверху вниз. Страница показывает выражения смещений от начала координат стра-
ницы для области 600 на 900, куда мы собираемся печатать страницу. Вы хотите печа-
892 Глава 17
тать информацию документа в заштрихованную область, показанную на странице, по-
этому вам следует отобразить точку xOrg, yOrg в документе на позицию, показанную
на печатаемой странице, которая отображается со смещением xOf fset и yOf fset от
начала координат страницы.
По умолчанию начало системы координат, которую вы используете для опреде-
ления элементов документа, отображается на начало координат контекста устрой-
ства, однако вы можете это изменить. В объекте С DC для этой цели предусмотрена
функция SetWindowOrg (). Это позволяет определить точку в системе логических
координат, которую вы хотите поставить в соответствие началу координат контек-
ста устройства. Важно сохранить исходную начальную точку, возвращенную функци-
ей SetWindowOrg (), как объект CPoint. Вы должны восстановить старую начальную
точку по завершении рисования текущей страницы; в противном случае, когда вы
приступите к печати следующей страницы, член m_rectDraw объекта CPrintlnfo бу-
дет установлен неправильно.
Точка документа, которую вы хотите отобразить на начало координат страницы,
имеет координаты xOrg-xOffset, yOrg+yOffset. Это может быть нелегко визуализи-
ровать, но вспомните, что установкой начальной точки окна вы определяете точку, ото-
Сохранение и печать документов
893
бражающую начало координат представления. Если вы подумаете об этом, то увидите,
что точка xOrg, yOrg в документе — это та точка, которая вам нужна на странице.
Ниже показан полный код печати страницы документа.
// Напечатать страницу документа
void CSketcherView: :OnPrint (CDC* pDC, CPrintinfo* plnfo)
// Вывести имя файла документа
pDC->SetTextAlign (TA_CENTER) ; // Центрировать следующий текст
pDC->TextOut(plnf o->m_rectDraw.right/2, -20,
(static_cast<CPrintData*> (pInfo->m__lpUserData)) ->m_JDocTitle) ;
pDC->SetTextAlign (TA__LEFT) ; // Выравнивать текст влево
II
int xOrg =
(static__cast<CPrintData*> (pInfo->m_lpUserData)) -XnJDocRefPoint.x +
600* ((pInfo->m_nCurPage - 1) %
((static_cast<CPrintData*> (pInfo->m_lpUserData)) ->m__nWidths)) ;
int yOrg =
(static_cast<CPrintData*>(pInfo->m_lpUserData)) ->m_DocRefPoint. у -
900* ((pInfo->m__nCurPage - 1) /
((static__cast<CPrintData*> (pInfo->m_lpUserData)) ->m__nWidths));
// Вычислить смещения центра области рисования как положительные значения
int xOffset = (plnfo->m__rectDraw. right - 600)/2;
int yOffset = - (plnf о->m_r ectDr aw. bottom + 900)/2;
// Сменить начало окна для соответствия текущей странице и сохранить
// старое значение
CPoint OldOrg = pDC->SetWindowOrg(xOrg-xOffset, yOrg+yOffset) ;
// Определить отсекающий прямоугольник по размеру печатаемой области
pDC->IntersectClipRect(xOrg,yOrg,xOrg+600,yOrg-900);
OnDraw (pDC) ; 11 Нарисовать весь документ
pDC->SelectClipRgn (NULL) ; // Удалить отсекающий прямоугольник
pDC->SetWindowOrg (OldOrg); // Восстановить старое начало окна
}
Первый шаг — вывод имени файла, которое вы сохранили в объекте CPrintinfo.
Функция-член SetTextAllign () объекта CDC позволяет определить выравнивание
последующего текстового вывода по отношению к точке привязки текстовой строки
в функции TextOut (). Выравнивание определяется константой, передаваемой в аргу-
менте функции. Доступны три варианта указания выравнивания текста, перечислен-
ные в табл. 17.5.
Таблица 17.5. Выравнивание текста по горизонтали
Константа
Выравнивание
ТА LEFT
Точка находится слева от ограничивающего текст прямоугольника, так что текст
располагается справа от указанной точки. Это — выравнивание по умолчанию.
ta_right Точка находится справа от ограничивающего текст прямоугольника, так что текст
располагается слева от указанной точки.
ТА CENTER
Точка находится в центре ограничивающего текст прямоугольника.
Вы определяете координату х имени файла на странице как половину ее шири-
ны, а координату у — как 20 единиц, что составляет 0,2 дюйма, от вершины страницы.
После вывода имени файла документа в виде центрированного текста выравнивание
894 Глава 17
текста сбрасывается в значение по умолчанию, TA_LEFT, для последующего вывода
текста в документе.
Функция Set Text Align () также позволяет изменить позицию текста по верти-
кали, комбинируя с помощью операции ИЛИ второй флаг с флагом выравнивания.
Второй флаг может быть любым из перечисленных в табл. 17.6.
Таблица 17.6. Выравнивание текста по вертикали
Константа
Выравнивание
ТА_ТОР
ТА BOTTOM
TA_BASELINE
Выровнять вершину прямоугольника, ограничивающего текст, по точке, определяю-
щей положение текста. Установлено по умолчанию.
Выровнять низ прямоугольника, ограничивающего текст, по точке, определяющей
положение текста.
Выровнять базовую линию шрифта, используемого для текста по точке, определяю-
щей позицию текста.
Следующее действие в OnPrint () использует метод, который обсуждался ранее,
для отображения области документа на текущую страницу. Вы получаете документ,
нарисованный на странице, вызывая функцию OnDraw (), которая используется для
отображения документа в представлении. Это потенциально нарисует весь документ,
но вы можете ограничить то, что появляется на странице, определив отсекающий
прямоугольник (clip rectangle). Вывод вне этого прямоугольника подавляется. Также
для этого можно определить и области неправильной формы.
Начальная область отсечения по умолчанию определена в контексте устройства
печати по границам страницы. Вы определяете отсекающий прямоугольник, соответ-
ствующий области 600 на 900 в центре страницы. Это гарантирует, что будет нарисо-
вана только эта область, а имя файла не будет перекрыто.
После рисования текущей страницы вы вызываете SetClipRgn () с аргумен-
том NULL для удаления отсекающего прямоугольника. Если этого не сделать, то вы-
вод заголовка документа будет подавлен на всех страницах, следующих за первой,
поскольку он лежит вне этого прямоугольника, который останется в силе на про-
тяжении всего процесса печати до тех пор, пока не произойдет следующий вызов
IntersectClipRect().
Последнее действие состоит в еще одном вызове SetWindowOrg (), необходимом
для восстановления начальной точки в ее исходном положении, как было сказано ра-
нее в настоящей главе.
Получение печатного вывода документа
Чтобы получить первый печатный документ Sketcher, нужно только собрать про-
ект и выполнить программу (исправив все возможные опечатки). Выбор пункта меню
File1^Print Preview (Файл^предварительный просмотр) должен привести к отображе-
нию окна, подобного показанному на рис. 17.9.
То есть совершенно бесплатно вы получаете полную функциональность предва-
рительного просмотра печати. Каркас использует ваш код для нормальной операции
многостраничной печати, чтобы создать образы страниц в окне предварительного
просмотра. То, что вы видите в окне предварительного просмотра печати, должно
полностью соответствовать тому, что вы получите на печатной странице.
Сохранение и печать документов
895
View Scaeii
Pagts 1-2
Рис. 17.9. Окно предварительного просмотра печатного документа
Резюме
В настоящей главе вы узнали, как получить документ для сохранения на диске, в
форме, позволяющей прочитать и реконструировать составляющие его объекты с по-
мощью процесса сериализации, поддерживаемого MFC. Чтобы реализовать сериали-
зацию для классов, определяющих данные документа, потребуется выполнить пере-
численные ниже действия.
1. Определить класс как прямой или непрямой наследник CObject.
2. Специфицировать макрос DECLARE__SERIAL () в определении класса.
3. Специфицировать макрос IMPLEMENT__SERIAL () в реализации класса.
4. Реализовать конструктор по умолчанию в классе.
5. Объявить в классе функцию Serialize ().
6. Реализовать в классе функцию Serialize () для сериализации всех данных-
членов.
Процесс сериализации использует объект CArchive для выполнения ввода и вы-
вода. Вы используете объект CArchive, переданный функции Serialize () для сери-
ализации данных-членов вашего класса.
Также вы познакомились с поддержкой MFC процесса вывода на печать. Для до-
бавления базовых возможностей печати, предоставляемых по умолчанию, вы можете
реализовать свои собственные версии функций класса представления, участвующие
в процессе печати документа. В табл. 17.7 описаны основные роли каждой из этих
функций.
896 Глава 17
Таблица 17.7. Основные роли функций, связанный с печатью
Функция Роль
OnPreparePrinting()
OnBeginPrinting()
OnPrepareDC()
OnPrint()
OnEndPrinting()
Определяет количество страниц в документе и вызывает член представ-
ления DoPreparePrinting ().
Выделяет ресурсы, необходимые контексту устройства печати на про-
тяжении процесса печати, а также определяет количество страниц в до-
кументе, зависящее от информации из контекста устройства.
Устанавливает атрибуты контекста устройства печати при необходимости.
Печатает документ.
Удаляет все ресурсы GDI, созданные в OnBeginPrinting (), а также вы-
полняет всю прочую необходимую очистку.
Информация, касающаяся процесса печати, сохраняется в объекте типа CPrintInfo,
который создается каркасом. Вы можете сохранить дополнительную информацию в
своем представлении либо в другом вашем собственном объекте. Если вы используете
объект вашего собственного класса, то можете отслеживать его, сохраняя его указа-
тель в объекте CPrint Inf о.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Добавьте некоторый код в функцию OnPrint (), чтобы внизу каждой страницы
документа печатался номер страницы в форме “Page тГ, Если вы используете
средства класса CString, то сможете сделать это всего тремя дополнительными
строками кода!
В качестве дальнейшего усовершенствования класса CText измените реализа-
цию так, чтобы правильно работало масштабирование. (Подсказка: обратитесь
к описанию функции CreatePointFont () в оперативной справочной системе.)
18
Написание собственных
DLL-библиотек
В главе 9 было описано, как библиотеки классов C++/CLI сохраняются в файле
.dll. Динамически подключаемые библиотеки также интенсивно используются при-
ложениями на “родном” C++. Полное описание DLL-библиотек в приложениях на
“родном” C++ выходит за рамки книги для начинающих, тем не менее, вам предлага-
ется глава ознакомительного характера, посвященная этим библиотекам.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
□ Библиотеки DLL и их работа.
□ Когда следует рассматривать возможность реализации DLL.
□ Какие существуют вариации DLL и для чего они используются.
□ Как можно расширить MFC с помощью DLL.
□ Как обращаться к содержимому DLL из ваших программ.
Что такое DLL-библиотека?
Почти все языки программирования поддерживают библиотеки или стандартные
модули кода, включающие часто используемые функции. В “родном” C++ вы использу-
ете множество функций, хранящихся в стандартных библиотеках, таких как функция
cell (), которую вы применяли в предыдущей главе, объявленная в заголовочном фай-
ле <cmath>. Код этой функции сохраняется в библиотечном файле с расширением
. lib, и когда создается исполняемый модуль программы Sketcher, компоновщик из-
влекает код этой стандартной функции из библиотечного файла и интегрирует его ко-
пию в файл . ехе программы Sketcher. Если вы пишете другую программу и использу-
ете ту же функцию, она также получает собственную копию функции cell (). Функция
cell () является статически связываемой с каждым приложением и представляет со-
бой составную часть каждого исполняемого модуля, как показано на рис. 18.1.
898 Глава 18
Статическая библиотека
Копия добавляется в каждую программу во время компоновки
ProgramB.exe
ProgramA.exe
Библиотечная
функция
Библиотечная
функция
Рис. 18.1. Статическая компоновка
Библиотечная
функция
ProgramC.exe
Библиотечная
функция
Хотя это очень удобный способ использования стандартной функции с минималь-
ными усилиями с вашей стороны, с ним связаны и недостатки, поскольку он ведет
к тому, что несколько параллельно выполняющихся программ применяют несколько
копий одной и той же функции в среде Windows. Статически связываемые стандарт-
ные функции, используемые более чем одной программой, дублируются в памяти для
каждой использующей их программы. Это может показаться и несущественным для
функции cell (), но некоторые функции, например, ввода и вывода, неизбежно яв-
ляются общими для большинства программ, и весьма вероятно, что они займут зна-
чительные объемы памяти. Статическая компоновка таких функций была бы чрезвы-
чайно неэффективной.
Другой аргумент против статической компоновки заключается в том, что стан-
дартная функция из статической библиотеки может быть скомпонована в сотни про-
грамм в вашей системе, поэтому идентичные копии их кода будут занимать дисковое
пространство в файле . ехе каждой программы. По этой причине Windows поддер-
живает дополнительную возможность работы с библиотеками стандартных функций.
Она называется динамически подключаемыми или динамически компонуемыми
библиотеками (dynamic link library — DLL). Это позволяет разделять доступ к одной
Написание собственных DLL-библиотек
899
копии функции между несколькими параллельно выполняющимися программами, из-
бегая необходимости помещения копии кода библиотечных функций в исполняемый
модуль использующих их программ.
Как работают DLL-библиотеки
Динамически подключаемая библиотека — это файл, содержащий коллекцию
модулей, которые могут использоваться любым количеством различных программ.
Обычно такой файл имеет расширение .dll, хотя это и не обязательно. Присваивая
имя DLL-библиотеке, вы можете назначить ей любое расширение, которое вам нра-
вится, однако это повлияет на то, как она будет обрабатываться Windows. Windows
автоматически загружает динамически подключаемые библиотеки, файлы которых
имеют расширение .dll. Если они имеют какое-то другое расширение, то вам при-
дется загружать их явно, добавляя для этого специальный код в программу. В самой
Windows некоторые системные DLL-библиотеки имеют расширение . ехе. Наверняка
вы встречали файлы с расширениями . vbx и . осх, которые применяются к DLL, со-
держащим специальные типы элементов управления.
Может показаться, что у вас есть выбор — применять в своих программах динами-
ческие библиотеки или нет, но на самом деле это не так. Программный интерфейс
Win32 API используется любой Windows-программой, и этот API-интерфейс реали-
зован в виде набора DLL-библиотек. Библиотеки DLL — фундаментальная часть про-
граммирования для Windows.
Подключение функции из DLL к программе обеспечивается иначе, чем при под-
ключении статически связываемых библиотек, где код помещается в программу раз
и навсегда во время компоновки исполняемого модуля. Функция в DLL подключается
к программе только во время выполнения, и это делается при каждом ее запуске, как
показано на рис. 18.2.
На рис. 18.2 представлена последовательность событий, происходящих, когда три
программы, использующие функцию из DLL, стартуют последовательно и затем вы-
полняются параллельно. Никакой код из DLL не включается в исполняемые модули
ни одной из программ. Когда выполняется одна из программ, она загружается в па-
мять, и если используемая ею DLL-библиотека еще не находится в памяти, она загру-
жается отдельно. Затем устанавливаются соответствующие связи между программой
и DLL-библиотекой. Если при загрузке программы DLL-библиотека уже загружена, то
все, что потребуется сделать — это связать программу с затребованной функцией из
DLL.
В частности, отметьте, что когда ваша программа вызывает функцию из DLL,
Windows автоматически загружает DLL-библиотеку в память. Любая программа, впо-
следствии загруженная в память и работающая с той же DLL-библиотекой, может ис-
пользовать любые средства, предоставляемые той же копией DLL, поскольку Windows
распознает, что код библиотеки уже находится в памяти, и просто устанавливает свя-
зи между ним и программой. Windows отслеживает количество программ, которые
используют DLL-библиотеку, находящуюся в памяти, так что библиотека остается там
до тех пор, пока хотя бы одна программа все еще работает с ней. Когда DLL уже не
используется ни одной выполняющейся программой, Windows автоматически удаляет
ее из памяти.
MFC поставляется в форме множества DLL, к которым ваша программа может
подключаться динамически, наряду с библиотеками, которые могут компоноваться в
программу статически. По умолчанию мастер Application Wizard генерирует програм-
мы, динамически связываемые с DLL из состава MFC.
900 Глава 18
ций, не затрагивая использующих их программ. До тех пор, пока интерфейс функции
в DLL остается неизменным, программа может работать с новой версией функции
вполне успешно, без необходимости какой-либо перекомпиляции или перекомпо-
новки. К сожалению, это также имеет и свою отрицательную сторону: очень легко в
конечном итоге использовать с программой неправильную версию DLL. Это может
представить проблему для программ, устанавливающим DLL в системную (System)
папку Windows. Некоторые коммерческие приложения произвольным образом пишут
DLL, ассоциированные с программой, в эту папку, несмотря на вероятность того, что
могут перезаписать существующие там одноименные DLL. Это может негативно ска-
заться на других приложениях, которые инсталлированы ранее, и в худшем случае,
нарушить их работоспособность.
Память компьютера
. 18.2. Ранее связывание
Написание собственных DLL-библиотек 901
Динамическое связывание во время выполнения
Библиотеки DLL, которые вы будете создавать в этой главе, автоматически загру-
жаются в память, когда в память для выполнения попадает использующая их програм-
ма. Это известно как динамическое связывание во время загрузки, или раннее свя-
зывание, поскольку связи с используемыми функциями устанавливаются, как только
программа и DLL-библиотека загружаются в память. Операция такого рода проил-
люстрирована на рис. 18.2; однако это не единственно возможный вариант выбора.
Также допускается вызвать загрузку DLL-библиотеки после начала выполнения про-
поздним связыванием. Последовательность операций, осуществляемых при этом,
показана на рис. 18.3.
1. Программа загружается, но
DLL-библиотеки не загружаются.
Программа может использовать
любую из трех DLL.
Library 1 .dll
Library2.dll
Library3.dll
Program.exe
3. Библиотека Library2.dll загружается
по запросу программы.
Программа
2. В этой точке программа определяет,
что ей необходима Library2.dll
и инициирует ее загрузку.
4. Программа получает адрес
функции из DLL и использует
его для вызова функции
Память компьютера
. 18.3. Позднее связывание
902 Глава 18
Динамическое связывание во время выполнения позволяет программе отложить
связывание с DLL-библиотекой до момента, когда потребуется определенная функ-
ция из этой DLL. Это позволяет писать программу, которая может самостоятельно
определять, когда ей необходимо загружать одну или более DLL-библиотек, на основе
полученного ею ввода, так что в память будут загружены только те функции, которые
действительно нужны. В некоторых случаях это может радикально снизить объем па-
мяти, необходимой для выполнения программы.
Программа, реализованная для использования динамического связывания во вре-
мя выполнения, вызывает библиотечную функцию Windows LoadLibraryO, чтобы
загрузить DLL-библиотеку в случае необходимости. Адрес функции внутри DLL может
быть получен с помощью функции GetProcAddress (). Когда программе уже не нужна
DLL-библиотека, она может отключиться от нее вызовом функции FreeLibraryO.
Если никакая другая программа не использует ту же самую DLL, она будет удалена из па-
мяти. Подробности работы этого механизма в настоящей книге не рассматриваются.
Содержимое DLL-библиотеки
Динамически подключаемая библиотека не ограничена хранением кода функций.
Вы можете также размещать в ней ресурсы, включая битовые графические изобра-
жения и шрифты. Игра Solitaire (Пасьянс), поставляемая вместе с Windows, исполь-
зует динамическую библиотеку Cards .dll, содержащую все битовые изображения
карт, наряду с функциями манипуляции ими. Если вы захотите написать собствен-
ную карточную игру, то можете с успехом использовать эту DLL в качестве базы и
не заботиться о создании собственных графических изображений, необходимых для
представления карт на экране. Конечно, потребуется точно знать, какие функции и
ресурсы включены в DLL.
Вы также можете определить в DLL статические глобальные переменные, включая
объекты классов C++, так что они станут доступными программам, использующим ее.
Конструкторы глобальных статических объектов класса вызываются автоматически
при создании этих объектов. Вы должны заметить, что каждая программа, использу-
ющая DLL, получает собственную копию любого статического глобального объекта,
определенного в DLL, даже если он не обязательно применятся в программе. Для гло-
бальных объектов классов это включает накладные расходы на вызов конструктора
каждого из них. Поэтому вы должны избегать включения таких объектов в DLL, если
только это не является абсолютно необходимым.
Интерфейс DLL библиотеки
Вы не можете просто получить доступ ко всему, что содержится в DLL. Внешнему
миру видимы только элементы, специально идентифицированные как экспортиро-
ванные. Функции, классы, глобальные статические переменные и ресурсы — все они
могут экспортироваться из DLL, и вместе они образуют ее интерфейс. Все, что не
экспортировано, остается недоступным извне. Далее в этой главе вы увидите, как
можно экспортировать элементы из DLL.
Функция DllMain ()
Даже несмотря на то, что DLL-библиотека не является исполняемым модулем, бу-
дучи независимой программой, она содержит специальную разновидность функции
main () по имени DllMain (). Эта функция вызывается Windows, когда DLL впервые
загружается в память, чтобы позволить ей выполнить всю необходимую инициали-
Написание собственных DLL-библиотек 903
зацию перед использованием ее содержимого. Windows также вызывает DllMain ()
непосредственно перед удалением DLL из памяти, чтобы дать ей возможность выпол-
нить за собой необходимую очистку. Существуют и другие ситуации, когда вызывается
DllMain (), но они выходят за пределы тем, рассматриваемых в настоящей книге.
Вариации DLL-библиотек
Существуют три разновидности DLL-библиотек, которые вы можете собирать в
Visual C++ 2005 с использованием MFC: DLL расширения MFC, обычную DLL со ста-
тически связанной MFC, а также обычную DLL с динамически связанной MFC.
DLL-библиотека расширения MFC
Сборка DLL этого типа осуществляется, когда в нее планируется включить классы,
унаследованные от MFC. Ваши классы-наследники в DLL, по сути, расширяют MFC.
В среде, где будет применяться ваша DLL, MFC должна присутствовать, поэтому все
классы MFC должны быть доступны вместе с вашими производными классами — отсю-
да и название “DLL расширения MFC”. Однако наследование ваших классов от MFC —
не единственная причина использования DLL такого типа. Если вы пишете DLL, ко-
торая содержит функции, передающие указатели на объекты классов MFC функциям
программы, использующей ее, или же принимающие такие параметры от функций
программы, то вы должны создавать ее как DLL расширения MFC.
Доступ к классам в MFC через DLL расширения всегда разрешается динамиче-
ски путем связывания с разделяемой версией MFC, которая сама реализована в виде
набора DLL. DLL расширения создаются с использованием разделяемой DLL-вер-
сии MFC, поэтому когда вы применяете DLL расширения, должна быть доступной
и разделяемая версия MFC. DLL расширения MFC может использоваться обычным
приложением, сгенерированным мастером Application Wizard. Оно требует выбора
опции Use MFC as Shared Dll (Использовать MFC как разделяемую DLL) в наборе
свойств General (Общие), который доступен через пункт меню Projects Properties
(Проект1^Свойства). Это — выбор по умолчанию для программ, сгенерированных ма-
стером Application Wizard. По причине фундаментальной природы разделяемой вер-
сии MFC в DLL расширениях MFC такие библиотеки не могут использоваться про-
граммами, которые статически связаны с MFC.
Обычные DLL, статически связанные с MFC
Это DLL-библиотеки, которые используют классы MFC, связанные статически.
Применение DLL не требует необходимой доступности MFC в среде во время вы-
полнения, поскольку код всех используемых классов включен в DLL. Это увеличива-
ет размер DLL, но в то же время дает то значительное преимущество, что подобные
DLL могут использоваться любыми программами Win32, независимо от того, приме-
няют ли они MFC.
Обычные DLL, динамически связанные с MFC
Это DLL-библиотеки, использующие динамически связанные классы MFC, но не
добавляющие своих собственных классов. С DLL подобного рода могут работать лю-
бые программы Win32, независимо от того, используют ли они сами по себе MFC,
однако их применение требует доступности MFC в среде выполнения.
Для сборки всех трех типов DLL, использующих MFC, можно применять мастер
Application Wizard. Вы также можете создать проект для DLL, который вообще не-
904 Глава 18
зависим от MFC; для этого создается проект типа Win32 на основе шаблона Win32
Project (Проект Win32) с выбором DLL в настройках приложения проекта.
Что помещать в DLL-библиотеку
Как решить, когда необходима DLL-библиотека? В большинстве случаев использо-
вание DLL представляет собой решение определенного рода программной проблемы,
поэтому если у вас есть такая проблема, DLL может оказаться подходящим ответом.
Общим знаменателем часто является необходимость в разделении кода между рядом
программ, однако встречаются и другие ситуации, когда DLL обеспечивает преиму-
щества. Ситуации, в которых помещение кода или ресурсов в DLL оказывается очень
удобным и эффективным подходом, кратко описаны ниже.
□ У вас есть набор функций или ресурсов, которые вы хотите стандартизовать и
использовать в нескольких различных программах. В частности, DLL будет хо-
рошим решением, когда существует вероятность, что некоторые из ваших про-
грамм, использующих стандартные средства, будут выполняться параллельно.
□ У вас есть сложное приложение, включающее несколько программ и огром-
ный объем кода, но при этом часть функций или ресурсов могут быть разде-
лены между некоторыми программами приложения. Применение DLL-библи-
отеки для хранения общей функциональности или общих ресурсов позволяет
управлять и разрабатывать их со значительной степенью независимости от
программных модулей, использующих их, а также упрощает сопровождение
программ.
□ Вы разработали набор стандартных прикладных классов, унаследованных от
MFC, которые собираетесь применять в нескольких программах. Упаковывая
реализацию этих классов в DLL расширения, вы можете значительно упростить
их использование в нескольких программах, обеспечивая при этом возможность
совершенствования этих классов, не затрагивая конечных приложений.
□ Вы разработали набор функций, предоставляющих легкий в использовании, но
невероятно мощный набор инструментов для прикладной области, который
пригодится многим разработчикам. Вы можете подготовить пакет этих функ-
ций в виде обычной DLL и распространять его в таком виде.
Бывают и другие случаи, когда вы можете предпочесть использовать DLL — ког-
да хотите иметь возможность динамически загружать и выгружать библиотеки либо
выбирать различные модули во время выполнения. Вы даже можете использовать их
для облегчения разработки и обновления своего приложения.
Чтобы лучше всего понять, как надо использовать DLL, давайте попробуем соз-
дать ее.
Написание DLL-библиотек
Мы должны рассмотреть два аспекта написания DLL: как собственно писать DLL,
и как определить, что именно будет доступно в DLL программам, ее использующим.
В качестве практического примера написания DLL создадим DLL расширения, кото-
рая добавит набор классов к MFC. Затем вы расширите эту DLL за счет добавления
переменных, доступных использующим ее программам.
Написание собственных DLL-библиотек 905
Написание и использование DLL расширения
Вы можете создать DLL расширения MFC, которая будет содержать классы фигур
для приложения Sketcher. Хотя это не предоставит особых преимуществ данному
приложению, все же позволит продемонстрировать способ написания DLL расшире-
ния без необходимости написания значительного объема нового кода.
Начальной точкой будет мастер Application Wizard, поэтому создайте новый про-
ект, нажав <Ctrl+Shift+N> и выбрав в качестве типа проекта MFC, а в качестве шабло-
на — MFC DLL, как показано на рис. 18.4.
Такой выбор идентифицирует, что вы создаете проект для основанной на MFC ди-
намической библиотеки по имени ExtDLLExample. Щелкните на кнопке ОК и выбе-
рите Application Settings (Настройки приложения) в следующем окне. Это окно долж-
но выглядеть, как показано на рис. 18.5.
Здесь находятся три переключателя, соответствующие трем типам MFC-ориенти-
рованных DLL, о которых говорилось ранее. Вы должны выбрать третью опцию, как
показано на рисунке.
Два флажка ниже группы переключателей позволяют включить в DLL код под-
держки автоматизации (Automation) и сокетов Windows (Windows sockets). Это до-
полнительные возможности Windows-программ, которые здесь вам не понадобятся.
Автоматизация обеспечивает возможность размещения объектов, созданных и управ-
ляемых одним приложением, внутри другого. Сокеты Windows предоставляют клас-
сы и функции, позволяющие вашей программе взаимодействовать с другими по сети,
но пока вам не понадобится это, к тому же оно выходит за рамки тематики книги.
Щелкните на кнопке Finish (Готово) и завершите создание проекта.
Рис. 18.4. Создание нового проекта для DLL расширения MFC
906 Глава 18
Рис. 18.5. Окно Application Settings
После того, как мастер MFC DLL завершит свою работу, можете заглянуть в сгене-
рированный им код. Если вы посмотрите на содержимое проекта в панели Solution
Explorer (Проводник решений), то увидите, что мастер MFC DLL сгенерировал не-
сколько файлов, включая файл . txt, содержащий описание остальных файлов. Вы
можете все прочитать в этом файле . txt, однако два из сгенерированных файлов ре-
ализации DLL представляют непосредственный интерес (табл. 18.1).
Таблица 18.1. Некоторые файлы, сгенерированные мастером MFC DLL
Имя файла
ExtDLLExample.срр
ExtDLLExample.def
Содержимое
Этот файл содержит функцию DllMain () и является первичным исходным
файлом DLL.
Информация в этом файле используется во время компиляции. Он содер-
жит имя DLL-библиотеки, и вы можете также добавить в него определения
элементов DLL, которые должны быть доступны программе, которая будет
использовать эту DLL. В приведенном далее примере применяется альтер-
нативный и в некоторой степени более простой способ идентификации таких
элементов.
Когда ваша DLL будет загружена в память, то первое, что произойдет — запуск
функции DllMain (), поэтому рассмотрим сначала эту функцию.
Функция DllMain ()
MFC DLL сгенерировал версию DllMain (), которая выглядит следующим образом:
extern "С" int APIENTRY
DllMain(HINSTANCE hlnstance, DWORD dwReason, LPVOID IpReserved)
Написание собственных DLL-библиотек
907
// Удалите это, если используется IpReserved
UNREFERENCED_PARAMETER(IpReserved);
if (dwReason == DLL_PROCESS_ATTACH)
TRACED("EXTDLLEXAMPLE.DLL Initializing’\n”);
// Однократная инициализация DLL расширения
if (.’AfxInitExtensionModule (ExtDLLExampleDLL, hlnstance))
return 0;
// Вставьте эту DLL в цепочку ресурсов
// ПРИМЕЧАНИЕ: если DLL расширения неявно связана
// с обычной MFC DLL (такой как элемент управления ActiveX),
// а не с приложением MFC, вы можете удалить эту строку
//из DllMain и поместить ее в отдельную функцию,
// экспортируемую из этой DLL расширения. Обычная DLL,
// которая использует эту DLL расширения, должна явно вызывать
// эту функцию для инициализации DLL расширения. В противном случае
// объект CDynLinkLibrary не будет присоединен к цепочке ресурсов
// обычной DLL, что приведет к возникновению серьезных проблем.
new CDynLinkLibrary(ExtDLLExampleDLL);
else if (dwReason == DLL_PROCESS_DETACH)
TRACED("EXTDLLEXAMPLE.DLL Terminating!\n”);
// Завершить библиотеку перед вызовами деструкторов
AfxTermExtensionModule(ExtDLLExampleDLL);
return 1; //ok
При вызове DllMain () передаются три аргумента. Первый аргумент, hlnstance —
это дескриптор, созданный Windows для идентификации DLL. Каждая задача под
Windows имеет дескриптор экземпляра, уникально идентифицирующий ее. Второй
аргумент, dwReason, указывает причину вызова DllMain (). Легко заметить, что
этот аргумент проверяется в операторе if. Первый if проверяет его на равенство с
DLL__PROCESS__ATTACH, указывающим, что программа собирается использовать данную ди-
намическую библиотеку, а второй if — на равенство с DLL_PROCESS_DETACH, который
говорит о том, что программа завершает работу с DLL. Третий аргумент — указатель,
зарезервированный для использования Windows, поэтому его можно игнорировать.
Когда DLL используется программой в первый раз, она загружается в память, и
функция DllMain () выполняется с аргументом dwReason, установленным в зна-
чение DLL__PROCESS_ATTACH. Это приводит к вызову функции AfxInitExtension
Module (), необходимой для инициализации DLL и создания в куче объекта класса
CDynLinkLibrary. Windows использует объекты этого класса для управления DLL
расширения. Если вы хотите предусмотреть собственную инициализацию, добавьте
ее в конец этого блока. Любой код очистки, необходимый вашей DLL, можно доба-
вить в блок следующего оператора if.
Добавление классов в DLL расширения
DLL используется для включения реализации классов форм Sketcher, поэтому пе-
реместите файлы Elements .h и Elements.срр из папки, содержащей исходный код
Sketcher, в папку, хранящую DLL. Убедитесь, что вы перемещаете файлы, а не копи-
908 Глава 18
руете их. Поскольку классы фигур для Sketcher должна поддерживать DLL-библиоте-
ка, вам незачем оставлять их в исходных текстах самого приложения Sketcher.
Вам также нужно удалить Elements. срр из проекта Sketcher. Чтобы сделать это,
откройте проект Sketcher, выделите Elements.срр в панели Solution Explorer, затем
нажмите клавишу <Delete>. Если вы не сделаете этого, компилятор решит, что он не
может найти файл при попытке компиляции проекта. Используйте ту же процедуру,
чтобы избавиться от Elements. h в папке Header Files (Заголовочные файлы) панели
Solution Explorer.
Классы фигур используют константы, которые вы определили в файле OurConstants .h,
поэтому скопируйте этот файл из папки проекта Sketcher в папку, содержащую DLL.
Обратите внимание, что переменная VERSION NUMBER используется исключительно
макросом IMPLEMENT SERIAL () в классах фигур, поэтому вы можете удалить ее из
файла OurConstants .h, используемого программой Sketcher.
Теперь потребуется добавить Elements. срр, содержащий реализацию наших клас-
сов фигур, в проект DLL расширения, поэтому откройте проект ExtDLLExample, вы-
берите пункт меню Project^Add Existing Item (Проект1^ Добавить существующий эле-
мент) и укажите файл Elements. срр в окне списка диалогового окна, как показано на
рис. 18.6.
Рис. 18.6. Добавление файла Elements. срр в проект DLL расширения
Проект также должен включать файлы, содержащие определения классов
форм и ваши константы, поэтому повторите процесс для файлов Elements . h и
OurConstants. h, добавив их в проект. Вы можете добавить множество файлов за
один прием, удерживая нажатой клавишу <Ctrl> при выборе файлов из списка в диа-
логовом окне Add Existing Item (Добавление существующего элемента). В конечном
итоге вы должны увидеть все файлы в панели Solution Explorer и все классы — в пане-
ли Class View проекта.
Написание собственных DLL-библиотек 909
1
Экспорт классов из DLL расширения
Имена классов, определенных в DLL, которые должны быть доступны в исполь-
зующей ее программе, должны быть некоторым образом идентифицированы, чтобы
между программой и DLL могли устанавливаться соответствующие связи. Как было
показано ранее, одним из способов сделать это является добавление информации к
файлу . de f для DLL. Это включает добавление того, что называется декорирован-
ными именами (decorated names) к DLL и ассоциацию декорированного имени с
уникальным идентифицирующим числовым значением, называемым порядковым
(ordinal). Декорированное имя объекта — это имя, сгенерированное компилятором,
который добавляет дополнительную строку к имени, которое вы присваиваете объ-
екту. Эта дополнительная строка представляет информацию о типе объекта, или — в
случае функции — информацию о типах параметров функции. Помимо прочего, это
гарантирует уникальность идентификаторов и позволяет компоновщику отличать
перегруженные функции друг от друга.
Получение декорированных имен и присвоение порядковых значений для экспор-
та элементов из DLL требует большого объема работы и является не лучшим и не са-
мым простым подходом в Windows. Намного легче идентифицировать классы, кото-
рые вы хотите экспортировать из DLL, модифицируя их определения в Elements .h
так, чтобы они включали ключевое слово AFX_EXT__CLASS для каждого имени класса,
как показано в следующем фрагменте для класса CLine:
// Класс, определяющий объект - линию
DECLARE_SERIAL (CLine)
public:
virtual void Draw(CDC* pDC, CElement* pElement=0) ; // Функция отображения
virtual void Move (CSize& aSize);
// Конструктор объекта линии
CLine (CPoint Start, CPoint End, COLORREF aColor, int PenWidth);
virtual void Serialize (CArchive& ar) ; // Функция сериализации CLine
protected:
CPoint Ш—StartPoint; // Начальная точка линии
CPoint Ш—Endpoint; // Конечная точка линии
CLine (void); // Конструктор по умолчанию
— не должен использоваться
Ключевое слово AFX EXT CLASS указывает, что класс подлежит экспорту из DLL.
Это делает весь класс доступным любой программе, использующей DLL, и автомати-
чески открывает доступ ко всем данным и функциям в общедоступном (public) ин-
терфейсе класса. Коллекция вещей в DLL, доступных использующим ее программам,
называется интерфейсом DLL. Процесс включения объекта в открытый интерфейс
DLL называется экспортом объекта.
Ключевое слово AFX_EXT__CLASS потребуется добавить ко всем прочим клас-
сам фигур, включая базовый класс CElement. Зачем нужно экспортировать из DLL
CElement? В конце концов, программы создают только объекты классов-наследни-
ков CElement, а не объекты самого класса CElement. Причина в том, что у вас есть
объявленные public-члены CElement, которые являются частью интерфейса про-
изводных классов фигур, и которые почти наверняка потребуются программам, ис-
пользующим DLL. Если вы не экспортируете класс CElement, то такие функции, как
GetBoundRect (), будут недоступными.
910 Глава 18
И последняя необходимая модификация — добавление директивы:
#include <afxtempl.h>
в файл stdafx.h проекта DLL, чтобы сделать доступным определение класса
CList.
Теперь сделано все необходимое для добавления классов фигур к DLL. Все, что
остается — скомпилировать и выполнить сборку проекта, создав DLL.
Сборка DLL-библиотеки
Сборка DLL выполняется точно таким же образом, как и любого другого проек-
та— с использованием пункта меню Build*=> Build Solution (Сборка^Собрать решение)
или щелчком на соответствующей кнопке в панели инструментов. Однако вывод, ко-
торый вы получите при этом, несколько отличается. Вы можете увидеть построенные
файлы во вложенной папке Debug папки проекта при отладочной (Debug) сборке,
или в папке release — при окончательной (Release) сборке. Исполняемый код DLL
содержится в файле ExtDLLExample. dll. Этот файл должен быть доступен при вы-
полнении программ, которые пользуются DLL. ExtDLLExample.dll — импортируе-
мый файл библиотеки, содержащий определения элементов, экспортированных их
DLL, который должен быть доступен компоновщику при сборке программ, использу-
ющих DLL.
Если вы обнаружите, что сборка DLL завершается неудачей из-за того, что El ements. срр
содержит директиву #include для Sketcher. h, удалите ее. В моей системе мастер Class
Wizard добавил эту директиву #include при создании кода класса CElement, хотя это и
не обязательно.
Использование DLL расширения в Sketcher
Теперь у вас нет никакой информации в программе Sketcher о классах фигур, по-
скольку вы переместили файлы, содержащие определения классов и их реализации в
проект DLL. Однако компилятору все равно необходимо знать, откуда возьмутся клас-
сы фигур, чтобы скомпилировать код программы. Программа Sketcher должна вклю-
чать заголовочный файл, определяющий классы, подлежащие импорту из DLL. Она
также должна идентифицировать классы, как внешние по отношению к проекту, ис-
пользуя макрос AFX__EXT_CLASS в определениях классов — точно так же, как и для экс-
порта этих классов из DLL. Поэтому вы можете просто скопировать файл Elements .h
из проекта DLL в папку, содержащую исходные тексты Sketcher, поскольку он со-
держит все подлежащее импорту из DLL в исходный код Sketcher. Вы можете сде-
лать это, изменив его имя на Dlllmports .h; в этом случае нужно будет поправить
директивы #include, которые уже есть в программе Sketcher для Elements .h, дабы
они ссылались на новое имя файла (это касается Sketcher. срр, SketcherDoc. h и
SketcherView.cpp). Вы также должны добавить файл Dlllmports .h к проекту, щел-
кнув правой кнопкой мыши на папке Header Files в панели Solution Explorer и выбрав
Add^ Existing Item (Добавить1^Существующий элемент) из контекстного меню.
Когда вы будете перестраивать приложение Sketcher, компоновщику понадо-
бится указать зависимость проекта от файла ExtDLLExample. lib, поскольку этот
файл содержит информацию о содержимом DLL. Щелкните правой кнопкой мыши
на Sketcher в панели Solution Explorer и выберите пункт Properties (Свойства) из
контекстного меню. Затем введите имя файла . lib как дополнительную зависимость,
что показано на рис. 18.7.
Написание собственных DLL-библиотек 911
Sketcher Property Paes
Comgu rate п:
Actrve(Debug)
Platform:
Actw&(Wn32)
Configuration Manager
® Common Properties
0 Configuration Properties
General
Debugging
S Linker
General
Inpu
anifest File
Debugging
System
Optimization
Embedded IDL
Advanced
Command Line
Manlfes! Tiiol
Resources
51 XML Document Genei
m Browse Info гтайоп
f»i Build Ever its
® Custom BuiEd Step
Additional De lendencies
Ignore All Default Libraries
Ig noire S pecific Lib rary
Modu e Definition File
Add Modute to Assembly
Embed Managed Resource File
Force Symbol References
Delay Loaded DLLs
Assembly Link Resource
s\ExtDLLExam ple\debug\ ExtDLLExample.№* (7.
No
Additions Dep t. .dt ncies
Specifies additional items to add to the link line (ex: kemel32. h b'; configuration specific.
:^rrei
Puc. 18.7. Добавление к проекту дополнительной зависимости
На рис. 18.7 показано вхождение для отладочной версии Sketcher. Файл . lib для
DLL расположен в папке Debug внутри папки проекта DLL. Если вы создаете версию
релиза Sketcher, то вам также понадобится рабочая версия DLL с соответствую-
щим файлом .lib, поэтому вы должны ввести полностью квалифицированное имя
файла . lib для рабочей версии DLL, соответствующее рабочей версии Sketcher.
Файл, к которому применяются свойства, выбирается в выпадающем окне списка
Configuration (Конфигурация) в окне Properties (Свойства). У вас есть только одна
внешняя зависимость, но при необходимости вы можете ввести их несколько, щел-
кнув на кнопке справа от текстового поля ввода. Поскольку здесь вводится полный
путь файла .lib, компоновщик будет знать не только о внешней зависимости от
ExtDLLExample. lib, но и о его местоположении.
Имейте в виду, что если полный путь к файлу . 1 ib содержит пробелы (как в приведенном
примере), то его придется заключить в двойные кавычки, чтобы компоновщик смог правиль-
но его распознать.
Теперь выполните сборку приложения Sketcher еще раз и все должно компили-
роваться и компоноваться, как обычно. Однако если вы попытаетесь запустить про-
грамму, то увидите окно сообщения, показанное на рис. 18.8.
Sketcher.exe - Unable То Locate Component
This application has failed to start because ExtDLLExample.dll was not found. Re-installing the
application may fix this problem.
Puc. 18.8. Сообщение о невозможности нахождения компонента
912 Глава 18
Это одно из наименее загадочных сообщений об ошибке — здесь совершенно ясно
указано, что именно идет не так. Чтобы позволить Windows загрузить DLL-библиоте-
ку для программы, обычно следует поместить DLL в папку \WINNT\System. Если ее
нет в этой папке, то Windows просматривает папку, содержащую исполняемый файл
Sketcher.exe. Если DLL нет и там, вы получаете показанное сообщение об ошибке.
Поскольку вы вряд ли захотите засорять папку \WINNT\System без необходимости,
скопируйте ExtDllExample.dll из папки Debug проекта в папку Debug программы
Sketcher. После этого программа Sketcher должна работать, как раньше, но с тем
отличием, что будет использовать классы фигур из созданной вами DLL-библиотеки.
Файлы, необходимые для использования DLL-библиотеки
На основании всего вышеизложенного в контексте работы с созданной вами DLL
в программе Sketcher вы можете заключить, что для использования DLL в програм-
ме, должны быть доступны файлы, перечисленные в табл. 18.2.
Таблица 18.2. Файлы, необходимые для использования DLL-библиотеки
Расширение Содержимое
♦ h Определяет элементы, экспортируемые из DLL, и позволяет компилятору правильно
справляться со ссылками на эти элементы в исходном коде программы, использующей
DLL. Файл .h должен быть добавлен в исходный код программы, использующей DLL.
Определяет элементы, экспортированные DLL, в форме, позволяющей компоновщику
справляться со ссылками на экспортируемые элементы при компоновке программ, ис-
пользующих DLL.
»dll Содержит исполняемый код DLL, загружаемый Windows при выполнении программы,
использующей эту DLL.
Если вы планируете поставлять программный код в форме DLL для использова-
ния другими программистами, вам нужно поставлять все три файла в пакете. Для
приложений, уже использующих DLL, понадобится только файл .dll наряду с фай-
лом .ехе.
Экспорт переменных и функций из DLL-библиотеки
Вы видели, как можно экспортировать классы из DLL расширения с применением
ключевого слова AFX_EXT_CLASS. Также из DLL любого типа можно экспортировать
и объекты, применяя для их идентификации атрибут dllexport. Благодаря использо-
ванию dllexport для идентификации объектов классов, переменных или функций,
которые должны экспортироваться из DLL, вы избегаете сложностей, связанных с
модификацией файла .def, и, следовательно, определяете интерфейс DLL в более
прямолинейной манере.
Не заблуждайтесь относительно того, что подход, используемый вами для экспор-
та сущностей из DLL, делает метод файлов .def излишним. Подход с файлом .def
более сложен — именно поэтому вы выбираете более простой путь - но в некоторых
ситуациях он обладает существенными преимуществами перед выбранным вами под-
ходом. В частности, это справедливо в контексте продуктов, распространяемых ши-
роко, для которых существует вероятность изменения со временем. Одним из глав-
ных плюсов является то, что файл .def позволяет определять порядковые значения,
соответствующие экспортируемым функциям. Это дает возможность позднее добав-
лять дополнительные экспортируемые функции и присваивать им порядковые значе-
Написание собственных DLL-библиотек 913
ния, причем эти значения для существующих функций остаются неизменными. Это
значит, что некто, применяющий новую версию DLL с программой, собранной для
использования старой версии, не будет вынужден повторно компилировать свои при-
ложения.
Вы должны использовать атрибут dllexport в сочетании с ключевым словом
_declspec, когда идентифицируете подлежащий экспорту элемент. Например, следу-
ющий оператор
declspec(dllexport) double aValue = 1.5;
определяет переменную aValue типа double с начальным значением 1.5 и иденти-
фицирует ее, как переменную, доступную программам, использующим DLL. Чтобы
экспортировать функцию из DLL, вы используете атрибут dllexport в аналогичной
манере. Например:
declspec(dllexport) CString FindWinner(CString* Teams);
Этот оператор экспортирует из DLL функцию FindWinner ().
Чтобы избежать несколько запутанной нотации при спецификации атрибута
dllexport, вы можете упростить ее, применив директиву препроцессора:
#define DllExport declspec(dllexport)
С таким определением два предыдущих примера можно переписать следующим
образом:
DllExport double aValue = 1.5;
DllExport CString FindWinner(CString* Teams);
Такая нотация намного экономнее, а также более читабельна, поэтому вы наверня-
ка будете применять такой подход при кодировании собственных DLL-библиотек.
Очевидно, что только символы, представляющие объекты из глобального контек-
ста, могут экспортироваться из DLL. Переменные и объекты классов, локальные по
отношению к функциям в DLL, перестают существовать по завершении выполнения
функции — точно так же, как это обстоит и с функциями в нормальной программе.
Попытки экспортировать такие символы приводят к ошибкам времени компиляции.
Импорт символов в программу
Атрибут dllexport идентифицирует символы в DLL, формирующие часть ин-
терфейса. Если вы хотите использовать их в программе, то должны обеспечить их
соответствующую идентификацию как импортируемых из DLL. Это обеспечивается
применением ключевого слова dll import в объявлениях импортируемых символов в
файле . h. Вы можете упростить нотацию, применив тот же прием, что был показан
выше с атрибутом dllexport. Определите Dll Import с помощью такой директивы:
#define Dlllmport _declspec(dllimport)
После этого вы сможете импортировать в программу переменную aValue и функ-
цию FincWinner () следующим объявлением:
Dlllmport double aValue;
Dlllmport CString FindWinner(CString* Teams);
Эти операторы должны появиться в файле .h, включаемом в файлы . срр про-
граммы, которые обращаются к этим символам.
914 Глава 18
Реализация экспорта символов из DLL-библиотеки
Вы можете модифицировать DLL расширения для приложения Sketcher, обе-
спечив доступ к символам, определяющим типы фигур и цвета через ее интерфейс.
Чтобы экспортировать типы элементов и цвета, они должны быть глобальными
переменными. Как глобальные переменные, их лучше разместить в файле . срр вме-
сто файла . h, поэтому перенесите их определения из файла OurConstants. h в на-
чало Elements. срр в исходном коде DLL. Затем можно будет применить атрибут
dllexport к их определениям в файле Elements. срр, как показано ниже.
// Определение констант и идентификация символов, подлежащих экспорту
#define DllExport __declspec(dllexport)
// Определения типов элементов
// Значения типов должны быть уникальными
DllExport extern const unsigned int LINE = 101U;
DllExport extern const unsigned int RECTANGLE = 102U;
DllExport extern const unsigned int CIRCLE = 103U;
DllExport extern const unsigned int CURVE = 104U;
DllExport extern const unsigned int TEXT = 105U;
///////////////////////////////////
11 Значения цветов для рисования
DllExport extern const COLORREF BLACK = RGB(0,0,0);
DllExport extern const COLORREF RED = RGB(255,0,0);
DllExport extern const COLORREF GREEN = RGB(0,255,0);
DllExport extern const COLORREF BLUE = RGB(0,0,255);
DllExport extern const COLORREF SELECT__COLOR = RGB (255, 0,180) ;
///////////////////////////////////
Добавьте все это в начало Elements. срр, сразу после директив #include. Сначала
вы определяете символ DllExport для упрощения спецификаций экспортируемых
переменных, как уже видели ранее. Затем вы присваиваете атрибут DllExport каж-
дому типу элемента и цвету.
Обратите внимание, что спецификатор extern также добавляется к определени-
ям этих переменных. Причина этого — в эффекте от модификатора const, который
указывает компилятору, что эти значения являются константами и не должны моди-
фицироваться в программе, что вам и требовалось. Однако по умолчанию ключевое
слово const также указывает на то, что переменные имеют внутреннее связывание,
поэтому они локальны по отношению к файлу, в котором они появляются. Вы хоти-
те экспортировать эти переменные в другую программу, поэтому должны добавить
модификатор extern, чтобы переопределить спецификацию компоновки по умол-
чанию относительно модификатора const и гарантировать их внешнее связывание.
Символы, которым назначено внешнее связывание, являются глобальными, а потому
могут экспортироваться. Конечно, если переменные не имеют модификатора const,
то вам не понадобится и extern, поскольку они являются глобальными автоматиче-
ски, если появляются в глобальном контексте.
Теперь файл OurConstants. h будет содержать только одно определение:
// Определения констант
#pragma once
// Определение номера версии программы для применения в сериализации
UINT VERSION NUMBER = 1;
Написание собственных DLL-библиотек 915
Конечно, это по-прежнему необходимо, поскольку используется в макросе
IMPLEMENT_SERIAL () в Elements. срр. Теперь вы сможете заново выполнить сборку
DLL, так что она будет готова для использования программой Sketcher. Не забудьте
скопировать последнюю версию файла .dll в папку Debug проекта Sketcher.
Использование экспортированных символов
Чтобы сделать экспортированные из DLL символы доступными в программе
Sketcher, вы должны специфицировать их как импортируемые из DLL. Это делается
добавлением идентификации импортированных символов в файл Dlllmport .h, ко-
торый содержит определения импортируемых классов. Таким образом, вы получаете
один файл, специфицирующий все элементы, импортированные из DLL. Операторы,
появляющиеся в этом файле, выглядят так:
// Переменные фигур, определенные в DLL ExtDLLExample.dll
#pragma once
#define Dlllmport __declspec( dllimport )
I/ Импортировать объявления типов элементов
// Каждое значение типа должно быть уникальным
Dlllmport extern const unsigned int LINE;
Dlllmport extern const unsigned int RECTANGLE;
Dlllmport extern const unsigned int CIRCLE;
Dlllmport extern const unsigned int CURVE;
Dlllmport extern const unsigned int TEXT;
///////////////////////////////////
11 Импортировать значения цвета для рисования
Dlllmport extern const COLORREF BLACK;
Dlllmport extern const COLORREF RED;
Dlllmport extern const COLORREF GREEN;
Dlllmport extern const COLORREF BLUE;
Dlllmport extern const COLORREF SELECT—COLOR;
///////////////////////////////////
// Плюс определения классов фигур...
Это определяет и использует символ Dlllmport для упрощения этих объяв-
лений способом, который был продемонстрирован ранее. Это значит, что файл
OurConstants .h в проекте Sketcher теперь лишний, потому вы можете удалить его
вместе с его директивой #include в Sketcher .h и SketcherView.cpp.
Может показаться, что теперь вы сделали все необходимое для использования но-
вой версии DLL в приложении Sketcher, однако это не так. Если вы попытаетесь
перекомпилировать Sketcher, то получите сообщения об ошибках для оператора
switch в члене CreateElement () класса CSketcherView.
Значения в этих операторах case должны быть константами, но хотя вы и при-
своили переменным типов фигур атрибут const, компилятор не имеет доступа к их
значениям, поскольку они определены в DLL, а не в самой программе Sketcher.
Поэтому компилятор не может определить значения констант для конструкций case
и генерирует ошибку. Простейший способ обойти эту проблему — заменить оператор
switch в функции CreateElement () серией операторов if, как показано ниже:
// Создать элемент текущего типа
CElement* CSketcherView::CreateElement ()
// Получить указатель на документ для данного представления
CSketcherDoc* pDoc = GetDocument();
916 Глава 18
ASSERT_VALID(pDoc); // Проверить корректность точки
// Теперь выбрать элемент, используя тип, записанный в документе
unsigned int ElementType = pDoc->GetElementType();
COLORREF Elementcolor = pDoc->GetElementColor() ;
int PenWidth = pDoc->GetPenWidth () ;
if (ElementType == RECTANGLE)
return new CRectangle (m__FirstPoint, mjSecondPoint, Elementcolor, PenWidth);
if (ElementType = CIRCLE)
return new CCircle (m_FirstPoint, mjSecondPoint, Elementcolor, PenWidth) ;
if (ElementType — CURVE)
return new CCurve(m__FirstPoint, m_SecondPoint, Elementcolor, PeriWidth) ;
else
// Всегда по умолчанию - линия
return new CLine(i&jFirstPoint, m_SecondPoint, Elementcolor, PenWidth);
Вы добавили локальные переменные для хранения текущего типа элемента, цвета
и ширины пера, которые извлекаются из объекта документа. Тип элемента сравни-
вается с типами элементов, импортированных из DLL, в серии операторов if. Они
решают ту же задачу, что и оператор switch, но не требуют точной информации о
значениях констант типов элементов. Если вы соберете программу Sketcher с при-
веденными изменениями, она будет выполняться с использованием DLL, применяя
экспортированные символы наряду с экспортированными классами фигур.
Резюме
настоящей главе вы изучили основы конструирования и применения динамиче-
ски подключаемых библиотек. Ниже перечислены наиболее важные моменты в этом
контексте.
□ Динамически подключаемые библиотеки предоставляют средства динамиче-
ского связывания стандартных функций при выполнении программы, вместо
включения их в исполняемый модуль программы.
□ Программы, сгенерированные мастером Application Wizard, по умолчанию свя-
зываются с версией MFC, сохраненной в DLL.
□ Единственная копия DLL в памяти может использоваться несколькими парал-
лельно выполняющимися программами.
□ DLL расширения так называется потому, что расширяет набор классов MFC.
DLL расширения должна использоваться тогда, когда вы хотите экспортиро-
вать из DLL основанные на MFC классы или объекты классов MFC. DLL рас-
ширения также может экспортировать обычные функции и глобальные пере-
менные.
□ Обычная DLL может использоваться, если вы хотите экспортировать только
обычные функции или глобальные переменные, не являющиеся экземплярами
классов MFC.
□ Вы можете экспортировать классы из DLL расширения с помощью ключевого
слова AFX__EXT_CLASS, предваряя им имя класса в DLL.
□ Вы можете экспортировать обычные функции и глобальные переменные из
DLL, присваивая им атрибут dllexport с ключевым словом _de cIspec.
Написание собственных DLL-библиотек
917
□ Вы можете импортировать классы, экспортированные DLL расширения, при-
меняя включение файла . h из DLL, содержащего определения классов с ис-
пользованием ключевого слова AFX EXT CLASS.
□ Вы можете импортировать обычные функции и глобальные переменные, экс-
портированные из DLL, присваивая атрибут dll import к их объявлениям в ва-
шей программе, используя ключевое слово declspec.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Это последний шанс для усовершенствования этой версии программы
Sketcher, так что воспользуйтесь им. Применяя созданную нами DLL, реали-
зуйте браузер документов Sketcher — другими словами, программу, которая
просто открывает документ, созданный программой Sketcher, и отображает
его в окне целиком. Вам не нужно беспокоиться о редактировании, прокрутке
и печати, но потребуется обрабатывать масштабирование, которое необходимо
для помещения большой картинки в маленьком окне!
19
Подключение
к источникам данных
В этой главе будет показано, как можно взаимодействовать с базой данных, исполь-
зуя Visual C++ и MFC для доступа к данным. Это, конечно, не стоит рассматривать как
совершенно исчерпывающую и всестороннюю дискуссию обо всех существующих воз-
можностях, поскольку полное описание разработки приложений баз данных с при-
менением Visual C++ само по себе заняло бы достаточно объемную книгу. Однако в
этой главе вы узнаете, как читать данные из базы, а в следующей главе будут изложе-
ны основы обновления данных в базе. Конечно, вы можете обращаться к источникам
данных в приложениях CLR, и об этом речь пойдет в главе 22.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
□ Язык SQL и его применение.
□ Как извлекать данные оператором SQL SELECT.
□ Какие службы баз данных поддерживает MFC.
□ Что собой представляет объект набора записей и как он связывается с табли-
цей реляционной базы данных.
□ Как объект набора записей может извлекать информацию из базы.
□ Как представление записи может отображать информацию из набора записей.
□ Как создать проект для приложения баз данных.
Q Как добавить наборы записей к вашей программе.
□ Как обрабатывать многострочные представления записей.
Основы баз данных
Это не место для детальной диссертации по технологиям баз данных, но предва-
рительно следует убедиться, что вы обладаете общим пониманием терминологии баз
920 Глава 19
данных. Существует широкое разнообразие баз данных, но в наши дни наибольшее
распространение получили реляционные базы данных. Именно о них и пойдет речь
в настоящей главе.
В реляционной базе ваши данные организованы в одну или более таблиц. Вы мо-
жете воспринимать таблицу базы как электронную таблицу, состоящую из строк и
столбцов. Каждая строка содержит информацию о единственном элементе (записи),
а каждый столбец — информацию о некоторых характеристиках каждого элемента.
Запись базы эквивалентна строке электронной таблицы. Каждая запись состоит
из элементов данных, составляющих ее. Эти элементы данных известны как поля.
Поле — это ячейка таблицы, идентифицируемая заголовком столбца. Термин поле так-
же может означать целый столбец.
Вы можете лучше представить структуру таблицы, взглянув на диаграмму, показан-
ную на рис. 19.1.
Здесь видно, что данная таблица служит для хранения информации о линейке
продуктов. Поэтому не удивительно, что таблица называется таблицей продуктов.
Каждая запись в таблице, представленная строкой на диаграмме, содержит данные
об одном продукте. Описание продуктов разделено на поля таблицы, причем каждое
поле хранит информацию об одном отдельном аспекте продукта: наименование про-
дукта (Product Name), цена за единицу (Unit Price) и так далее.
Хотя поля этой таблицы хранят только относительно простую информацию (сим-
вольные строки или числовые значения), тип данных, который вы выберете для кон-
кретного поля, может представлять почти все, что вы пожелаете. Вы можете хранить
в базе времена, даты, изображения или даже двоичные объекты.
Таблица Products (Продукты)
Каждый столбец таблицы
идентифицирует поле строки
<9
А 10006
10007
молоко
Каждая строка определяет
отношение, состоящее
из набора связанных полей
апельсины
яблоки
кофе
хлеб
10002
10002
10003
10004
10005
1.50
0.50
0.30
1.20
0.05
0.15
0.30
пирожное
чай
Рис. 19.1. Структура таблицы продуктов
Подключение к источникам данных
921
Обычно таблица имеет, по крайней мере, одно поле, которое может использовать-
ся для уникальной идентификации каждой записи, и в приведенном выше примере
таким полем, вероятно, может служить идентификатор продукта. Поле таблицы,
служащее для идентификации каждой записи внутри таблицы, называется ключом;
ключ, уникально идентифицирующий каждую запись таблицы, называется первич-
ным ключом. В некоторых случаях могут использоваться два или более ключевых
поля — в этом случае первичный ключ представляет комбинация этих полей.
Реляционный аспект базы данных и важность ключей проявляются, когда вы со-
храняете реляционную информацию в отдельных таблицах. Вы определяете отно-
шения между таблицами, используя ключи, и применяете эти отношения для нахож-
дения ассоциированной информации, хранящейся в вашей базе данных. Обратите
внимание, что таблицы сами по себе ничего не знают об отношениях, поскольку ни-
как не интерпретируют биты данных, хранящиеся в них. Это задача программы, об-
ращающейся к данным, которая может использовать информацию из таблиц для со-
вместного извлечения взаимосвязанных данных — будь то Access, SQL Server или ваша
собственная программа, написанная на C++. Все вместе это именуется системами
управления реляционными базами данных, или СУРБД (relational database manage-
ment system — RDBMS).
Реальные, хорошо спроектированные реляционные базы данных обычно состо-
ят из большого количества таблиц. Каждая таблица обычно имеет только несколько
полей и множество записей. Причина того, что таблицы ограничиваются лишь не-
сколькими полями, связана с необходимостью повышения производительности. Не
вдаваясь в подробности оптимизации баз данных, скажу лишь, что гораздо быстрее
выполняются запросы ко многим таблицам с небольшим количеством полей, чем за-
просы к единственной таблице с множеством полей.
Давайте расширим представленный выше пример для иллюстрации реляционной
базы данных с двумя таблицами: Products (Продукты) и Categories (Категории) из
базы данных Northwind, как показано на рис. 19.2.
Таблица Products (Продукты)
Таблица Categories (Категории)
Данные в этом поле могут использоваться для
получения имени категории из таблицы Categories
Рис. 19.2. Реляционная база данных с двумя таблицами
922 Глава 19
Как видно на этой диаграмме, поле Category ID служит для связи информации,
хранящейся в двух таблицах. Category ID уникально идентифицирует запись о катего-
рии в таблице Categories, поэтому оно является первичным ключом для этой табли-
цы. В таблице Products поле Category ID используется для связи записи о продукте с
категорией, поэтому упомянутое поле здесь называется внешним ключом; внешние
ключи не обязаны быть уникальными и часто таковыми не являются.
Реляционные базы данных могут создаваться и управляться различными спосо-
бами. На рынке присутствует огромное количество СУРБД, которые предоставляют
широкий набор возможностей для создания и манипуляции информацией баз дан-
ных. Очевидно, что вы можете добавлять и удалять записи в таблице базы, а также
обновлять значения полей в записи, хотя обычно в СУРБД присутствуют средства,
ограничивающие такую деятельность на основе уровня авторизации пользователя.
Наряду с доступом к информации отдельной таблицы базы данных вы можете ком-
бинировать записи из двух и более таблиц в новой таблице, основанной на их отно-
шениях, а также извлекать информацию из них. Подобное комбинирование таблиц
называется соединением таблиц (table join). Чтобы запрограммировать все эти опе-
рации в реляционной базе данных, вы можете использовать язык, известный как SQL
и поддерживаемый большинством СУРБД и сред программирования.
Немного об SQL
SQL означает “Structured Query Language” (язык структурированных запросов).
Это относительно простой язык, предназначенный специально для доступа и моди-
фикации информации в реляционных базах данных. Изначально он был разработан
в компании IBM для сред универсальных вычислительных машин (мэйнфреймов), но
в настоящее время используется в компьютерном мире повсеместно. SQL сам по себе
не существует в виде программного пакета — обычно он “живет” в некоторой среде,
будь то СУРБД либо библиотека, реализованная для языка программирования, такого
как COBOL, С или C++. Среда, включающая в себя SQL, обеспечивает такие глобаль-
ные вещи, как обычный ввод-вывод и общение с операционной системой, в то время
как SQL применяется только для запросов к базе данных.
Поддержка баз данных MFC использует SQL для спецификации запросов и других
операций над таблицами баз данных. Эти операции предоставляются набором специ-
ализированных классов. В примере, который мы разберем далее в настоящей главе,
вы увидите, как используются некоторые из них.
Язык SQL включает в себя операторы для извлечения, сортировки и обновления
записей таблицы, для добавления и удаления записей и полей, для соединения таблиц
и вычисления результатов, наряду со многими другими возможностями создания и
манипулирования таблицами базы. Я не стану погружаться в перечисление всех воз-
можных опций программирования, доступных в SQL, но опишу ряд деталей, доста-
точных для того, чтобы вы могли понимать то, что происходит в примерах, которые
вы напишете, даже если ранее вы никогда не имели дело с SQL.
Когда вы используете SQL в программах на основе MFC, большей частью вам не
придется писать полные операторы SQL, поскольку каркас MFC позаботится о сборке
полного оператора и применении его к используемому вами механизму базы данных.
Тем не менее, я объясню, как пишутся типичные операторы SQL во всей их полноте,
чтобы вы почувствовали, как они структурированы.
Операторы SQL обычно (хотя и не обязательно) пишутся с ограничителем — точ-
кой с запятой (как и операторы C++), и по соглашению ключевые слова языка запи-
Подключение к источникам данных 923
сываются заглавными буквами. Рассмотрим несколько примеров SQL-операторов и
разберем, как они работают.
Извлечение данных с использованием SQL
Чтобы извлечь данные, вы используете оператор SELECT. Фактически вас уди-
вит то, насколько много всего можно сделать в базе данных с помощью оператора
SELECT, оперирующего одной или более таблиц вашей базы данных. Результатом вы-
полнения оператора SELECT всегда является набор записей (recordset) — коллекция
данных, созданная на основе информации таблиц, указанных в аргументах оператора.
Данные в наборе записей организованы в виде таблицы, с именованными столбцами,
которые составлены из таблиц, указанных в операторе SELECT, и строк выбранных за-
писей, на основе условий, указанных в этом операторе. Набор записей, сгенерирован-
ный оператором SELECT, может включать только одну запись, или даже быть пустым.
Возможно, простейшая операция извлечения из базы данных — это обращение ко
всем записям единственной таблицы, поэтому, если предположить, что в базе есть та-
блица по имени Products, вы можете получить все ее записи, применив следующий
оператор SQL:
SELECT * FROM Products;
Символ * говорит о том, что вам нужны все поля таблицы. Параметр, следующий
за ключевым словом FROM, указывает таблицу, из которой эти поля должны быть из-
влечены. Записи, возвращенные оператором SELECT, никак не ограничены, так что
вы получите их все. Чуть позднее вы увидите, как можно ограничить выбранные
записи.
Если вы захотите получить все записи, но при этом ограничиться только опреде-
ленными полями в каждой записи, вы можете специфицировать это, указав имена по-
лей, разделенные запятыми, вместо звездочки из предыдущего примера. Вот пример
оператора, который сделает это:
SELECT ProductID,UnitPrice FROM Products;
Этот оператор выберет все записи таблицы Products, но из каждой записи будут
выбраны только поля ProductID и UnitPrice. Это породит таблицу, состоящую толь-
ко из двух указанных полей.
Имена полей, которые были использованы, не содержат пробелов, однако мо-
гут их содержать. Когда имена содержат пробелы, стандарт SQL требует, чтобы они
были ограничены двойными кавычками. Если бы поля имели имена Product ID и
Unit Price, то вы должны были бы написать оператор SELECT следующим образом:
SELECT "Product ID”,"Unit Price" FROM Products;
Применение двойных кавычек с именами, как показано выше, немного неудобно
в контексте C++, когда вам нужно передавать SQL-операторы базе в виде строк. В C++
двойные кавычки обычно служат ограничителями символьных строк, поэтому возни-
кает путаница, если вы пытаетесь заключить в двойные кавычки имена объектов базы
данных (таблицы или поля). По этой причине при обращении к таблице базы данных
или именам полей, включающим пробелы, в среде Visual C++ вы должны заключать
их в квадратные скобки вместо двойных кавычек. То есть вы должны записать имена
полей для этого примера как [Product ID] и [Unit Price]. Позднее, когда в этой
главе мы напишем программу, работающую с базой данных, вы увидите эту нотацию
в действии.
924 Глава 19
Выбор записей
В отличие от полей, записи таблицы не имеют имен. Единственный способ вы-
брать определенные записи — это применить некоторое условие или ограничение
содержимого одного или более полей в записи, чтобы выбраны были только запи-
си, удовлетворяющие этому условию. Это делается добавлением конструкции WHERE
к оператору SELECT. Параметр, следующий за ключевым словом WHERE, определяет
условие, используемое для выборки записей.
Вы можете выбрать записи таблицы Products, имеющие определенное значение
поля Category ID, применив следующий оператор:
SELECT * FROM Products WHERE [Category ID] = 1;
Это выберет только те записи, у которых поле Category ID имеет значение 1, так
что из приведенной выше таблицы вы получите только записи о кофе, чае и моло-
ке. Обратите внимание, что для спецификации условия проверки эквивалентности в
SQL применяется одиночный знак равенства, а не ==, как в C++.
Также вы можете использовать другие операции сравнения, такие как <, >, <=
и >=, чтобы специфицировать условие в конструкции WHERE. Вы можете также ком-
бинировать логические выражения с использованием операций AND или OR. Чтобы
наложить дополнительные ограничения на записи, выбираемые в данном примере,
вы можете написать:
SELECT * FROM Products WHERE [Category ID] = 1 AND [Unit Price] > 0.5;
В этом случае результирующая таблица содержит только две записи, поскольку мо-
локо исключено из нее по причине низкой цены. Только записи с Category ID, рав-
ным 1, и значением Unit Price больше 0,5, выбираются этим оператором.
Соединение таблиц с помощью SQL
Вы можете также применить оператор SELECT для соединения таблиц, хотя это
немного сложнее, чем вы могли бы себе представить. Предположим, что существуют
две таблицы: Products с тремя записями и тремя полями и Orders — с тремя запися-
ми и четырьмя полями (рис. 19.3).
Таблица Products (Продукты)
Таблица Orders (Заказы)
10001 кофе 1.50
10002 хлеб 0.50
10003 пирожное 0.30
20001 10002 VEAD 50
20002 10003 TOMS 40
20003 10002 VEAD 30
Рис, 19,3, Таблицы Products и Orders
Здесь вы имеете ограниченный набор продуктов в таблице Products, включаю-
щий только кофе, хлеб и пирожное, и имеете три заказа, содержащиеся в таблице
Orders, но ни один из них не оформлен на кофе.
Подключение к источникам данных 925
Вы можете соединить эти таблицы с помощью следующего оператора SELECT:
SELECT * FROM Products, Orders;
Этот оператор создает результирующий набор, используя записи из двух указан-
ных таблиц. Набор записей содержит семь полей — три из таблицы Products и че-
тыре из таблицы Orders, но сколько записей он будет иметь? Ответ можно найти на
рис. 19.4.
Таблица Products (Продукты)
Таблица Orders (Заказы)
10001 кофе 1.50
10002 хлеб 0.50
10003 пирожное 0.30
20001 10002 VEAD 50
20002 10003 TOMS 40
20003 10002 VEAD 30
10001
20001
10002
VEAD
50
кофе
10001
20002
1.50
10003
TOMS
40
кофе
кофе
10001
1.50
20003
10002
VEAD
30
10002
хлеб
0.50
20001
10002
VEAD
50
10002
хлеб
20002
10003
TOMS
40
10002
хлеб
0.50
20003
10002
VEAD
30
10003
пирожное
0.30
20001
10002
VEAD
50
10003
пирожное
0.30
20002
10003
TOMS
40
10003
пирожное
0.30
20003
10002
VEAD
30
Результат операции соединения
Рис. 19.4. Соединение таблиц Products и Orders
Набор записей, произведенный оператором SELECT, содержит девять записей, по-
лученных комбинацией каждой записи из таблицы Products с каждой записью табли-
цы Orders, поэтому в него включены все возможные комбинации. Это может быть
не совсем тем, что вы ожидали. Произвольное включение всех комбинаций записей
из одной таблицы с записями другой таблицы должно как-то ограничиваться. Смысл
записи, содержащей подробности о хлебе с заказом на пирожное, понять трудно. Вы
также можете столкнуться с невероятно большим объемом таблиц в такой ситуации.
926 Глава 19
Если вы скомбинируете таблицу, содержащую 100 продуктов, с таблицей, содержащей
500 заказов, никак не ограничивая операцию соединения, то полученная в результате
таблица будет содержать 50 000 записей!
Для получения осмысленного соединения в операторе SELECT обычно использу-
ется конструкция WHERE. В наших таблицах единственное условие, которое позволит
получить осмысленный результат — это выбрать только записи, у которых значения
Product ID из одной таблицы совпадают со значением того же поля в другой табли-
це. Это условие скомбинирует записи из таблицы Products с теми записями таблицы
Orders, которые имеют отношение к этому продукту. Вот как должен выглядеть соот-
ветствующий оператор:
SELECT * FROM Products,Orders
WHERE Products.[Product ID] = Orders.[Product ID];
Обратите внимание, как здесь идентифицировано отдельное поле конкретной та-
блицы. Вы добавляете имена таблиц в виде префикса и отделяете его от имени поля
точкой. Такая квалификация имени поля важна, когда в обеих таблицах присутствуют
одноименные поля. Без имени таблицы нет способа узнать, какое из двух одноимен-
ных полей вы имеете в виду. Этот оператор SELECT, примененный к содержимому
таблиц, использованному ранее, даст нам набор записей, показанный на рис. 19.5.
Таблица Products (Продукты)
Таблица Orders (Заказы)
10001 кофе 1.50
10002 хлеб 0.50
10003 пирожное 0.30
20001 10002 VEAD 50
20002 10003 TOMS 40
20003 10002 VEAD 30
Результат операции соединения со следующим условием:
Products/Product ID" = Orders."Product ID"
Рис. 19.5. Соединение таблиц с указанием условия
Подключение к источникам данных
927
Конечно, всего этого может оказаться недостаточно, поскольку вы получите два
поля, содержащих идентификатор продукта, но от лишнего легко избавиться, если в
операторе SELECT указать имена требуемых полей вместо символа *. Столбцы с оди-
наковыми именами, однако, можно различить, если квалифицировать их именами
таблиц, к которым они относятся в наборе записей.
Сортировка записей
Когда вы извлекаете данные из базы с использованием оператора SELECT, то часто
хотите, чтобы записи были отсортированы в определенном порядке. В предыдущем
примере показанные таблицы уже отсортированы, но на практике это не обязательно
так. Вам может потребоваться вывод из последнего примера, отсортированный иным
образом, в зависимости от обстоятельств. Один раз вам может оказаться удобным по-
лучить записи, отсортированные по Customer ID, а в другом случае лучше упорядо-
чить их по Quantity (Количество) в пределах Product ID. Конструкция ORDER BY,
добавленная к оператору SELECT, как раз и позволяет этого добиться. Например, вы
можете уточнить предыдущий оператор SELECT, добавив к нему следующую конструк-
цию ORDER BY:
SELECT * FROM Products,Orders
WHERE Products.[Product ID] « Orders.[Product ID]
ORDER BY [Customer ID] ;
В результате вы получите те же записи, что и в предыдущем примере, но при этом
они будут упорядочены по возрастанию значения в поле Customer ID. Поскольку
тип данных, хранящихся в конкретном поле, известен, записи упорядочиваются в со-
ответствии с этим типом данных поля. В данном случае получается алфавитный по-
рядок.
Если вы хотите выполнить сортировку по двум полям, скажем, по Customer ID и
Product ID, причем расположить записи в порядке убывания, то вам следует запи-
сать такой оператор:
SELECT * FROM Products,Orders
WHERE Products.[Product ID] = Orders.[Product ID]
ORDER BY [Customer ID] DESC, Products.[Product ID] DESC;
В конструкции ORDER BY вы обязаны использовать квалифицированное имя
Products. [Product ID], чтобы избежать неоднозначности, как это сделано и в кон-
струкции WHERE. Ключевое слово DESC в конце каждого поля в конструкции ORDER BY
указывает порядок по убыванию для операции сортировки. Существует дополняющее
его ключевое слово ASC для порядка по возрастанию, хотя обычно оно опускается,
поскольку применяется по умолчанию.
Это, конечно, далеко не все, что касается SQL-оператора SELECT, но этого доста-
точно для того, чтобы вы могли написать следующий пример программы, работаю-
щей с базой данных.
Поддержка баз данных в MFC
При написании приложений баз данных с применением MFC у вас есть выбор, по-
скольку здесь поддерживается два принципиальных подхода, описанные в табл. 19.1.
928 Глава 19
Таблица 19.1. Подходы, используемые при написании приложений баз данных,
основанных на MFC
OLE DB Предоставляет способ доступа к локальным и удаленным базам данных с применени-
ем СОМ, также известного, как ActiveX. OLE DB используется технологией ActiveX Data
Objects (ADO), обеспечивающей эффективный способ доступа к локальным и удаленным
базам данных, без дополнительных расходов, накладываемых MFC.
ODBC Open DataBase Connectivity, больше известный как ODBC; определяет стандартный, ори-
ентированный на функции интерфейс доступа к данным, поддерживаемый множеством
поставщиков продуктов баз данных. ODBC применяется для иллюстрации приемов раз-
работки приложений баз данных в настоящей и последующей главах.
Для использования OLE DB и ADO вам понадобятся глубокие знания СОМ
(ActiveX), поэтому я пока сосредоточусь на ODBC, для применения которого нужно
лишь некоторое погружение в SQL. Когда вы познакомитесь с СОМ, вам стоит рас-
смотреть возможности применения ADO в приложениях, поскольку это намного бо-
лее эффективно, чем ODBC.
ODBC — это системно-независимый интерфейс к средам баз данных, который
требует наличия драйвера ODBC для каждой из СУРБД, с которыми вы собираетесь
работать. ODBC определяет набор вызовов функций для операций с базами данных,
которые являются системно-нейтральными, поэтому его применение ориентировано,
по сути, на вызовы функций. Вы можете использовать базу данных с ODBC только в
том случае, если имеете DLL (динамически подключаемую библиотеку), содержащую
драйвер для работы с форматом файлов приложения баз данных. Назначение драйве-
ра — служить интерфейсом для стандартного набора системно-независимых вызовов
для операций с базами данных, которые будут использованы в вашей программе в со-
ответствии со спецификой конкретной реализации базы.
Классы MFC для поддержки ODBC
Поддержка ODBC со стороны MFC реализована в пяти классах, описанных в табл.
19.2.
Таблица 19.2. Классы MFC, реализующие поддержку ODBC
Класс Описание
CDatabase Объект этого класса представляет соединение с вашей базой данных. Это соеди-
нение должно существовать до того, как вы попытаетесь выполнить любую опера-
цию с базой данных.
CRecordset
CRecordView
Объект класса, производного от этого, представляет результат выполнения SQL-
оператора select, то есть набор записей. Этот объект делает доступной по одной
записи за раз и предлагает функции для перемещения по набору вперед и назад.
Объект класса, производного от этого, используется для отображения текущей
информации из ассоциированного объекта набора записей. Представление — это,
по сути, диалоговое окно.
CFieldExchange Этот класс обеспечивает обмен данных между базой и объектом набора записей.
Вы должны использовать этот класс непосредственно только в том случае, если
реализуете обмен данными пользовательских типов.
CDBException
Объекты этого класса представляют исключения, возникающие внутри операций с
базами данных ODBC.
Подключение к источникам данных
929
Чтобы лучше понять, как работают операции баз данных с MFC, необходимо соз-
дать пример приложения. Я объясню, как вы можете применить подход ODBC для
доступа к примерной базе данных под названием Northwind Trades. База данных
Northwind Trades обладает тем достоинством, что с ней просто работать, но при
этом содержит существенное разнообразие таблиц, наполненных реалистичным ко-
личеством записей. Это обеспечивает широкое поле для экспериментирования, на-
ряду с поддержкой восприятия того, насколько хорошо ваш код работает на практи-
ке. Легко впасть в заблуждение относительно безопасности вашей программы, когда
вы запускаете ее на тестовой базе данных, где количество таблиц и записей в этих
таблицах достаточно невелико. Когда же дело дойдет до реальной работы, вы удиви-
тесь, насколько много времени понадобится на выполнение транзакций. Следует упо-
мянуть одно предостережение относительно базы данных Northwind: вы не должны
рассматривать ее как пример правильного дизайна базы данных, в частности, в том,
что касается безопасности. Она полезна лишь в качестве примера, необходимого для
понимания работы механизма доступа к базе данных.
Чтобы разработать и запустить примеры из этой и следующей глав, вам нужно иметь
инсталлированную базу данных Northwind Trades. Поэтому вам понадобится окружение
базы данных, способное поддерживать базу Northwind Trades. На момент написания на-
стоящей книги эта база доступна для SQL Server Express, SQL Server 2000 и Microsoft
Access. Вы можете найти ее на сайте http: //www.microsoft. сот/downloads, запу-
стив поиск по слову “Northwind”. Документацию об инсталляции различных доступ-
ных версий вы можете найти на соответствующей странице загрузки. Наши примеры
показывают применение версии Microsoft Access базы данных Northwind Trades, но
тот же код подойдет для любой другой системы управления баз данных, которую вы
используете.
Создание приложения базы данных
В качестве примера будет показано, как использовать три взаимосвязанных табли-
цы в базе данных Northwind.
На первом шаге вы создадите программу для отображения записей из таблицы
Products базы данных. Затем вы добавите код, который позволит просматривать все
заказы для заданного продукта, используя другие таблицы. И, наконец, вы обратитесь
к таблице Customers за подробностями о заказчиках, оформивших отображаемый за-
каз. Прежде чем начать кодирование, вам понадобится идентифицировать базу дан-
ных для операционной системы.
Регистрация базы данных ODBC
Перед тем как использовать базу данных ODBC, ее необходимо зарегистрировать
в системе. Это делается через панель управления (Control Panel), к которой вы обра-
щаетесь через меню Start (Пуск) в Windows ХР. Выберите в панели управления пикто-
грамму Data Sources (ODBC) (Источники данных (ODBC)), доступную после щелчка
на пиктограмме администрирования. Вы должны увидеть диалоговое окно, показан-
ное на рис. 19.6.
У вас есть возможность зарегистрировать базу как User DSN (Пользовательский
DSN), доступный только для вас, либо как System DSN (Системный DSN), доступный
всем пользователям машины, либо как File DSN (Файловый DSN), который будет до-
ступен вообще, возможно, по сети. Сначала я объясню, как вы можете зарегистриро-
вать базу как User DSN.
930 Глава 19
Рис. 19.6. Диалоговое окно управления источниками данных ODBC
Щелкнув на кнопке Add (Добавить), вы увидите диалоговое окно Create New Data
Source (Создание нового источника данных), показанное на рис. 19.7.
Create New Data Source
Select a driver for which you want to set up a data source.
Name
Л'ЛШЛ
-W-WAWA
и
.‘AVrtV.
WAWA
WAAWA
WAWA
м
-"-W-W-
AFAWA
'.WAW.
I
в
.WZAW
Miaoscft Access Driver i’.mdb)
Microsoft Access-Treiber ’ *.mdb)
Microsoft d Base Driver f.dbf)
Microsoft d Base VFP Driver Г-dbf)
Microsoft dBase-Treiber (*dbf)
Microsoft Excel Driver pjds)
Microsoft Excel-Treiber pjds)
Microsoft FoxPro VFP Driver ^.dbf)
Microsoft ODBC for Oracle
- Г- !"| - lA* II 1
пн
RahI
Finish
Cancel I
Рис. 19.7. Диалоговое окно создания нового источника данных
Здесь потребуется выбрать из списка драйверов ODBC тот, который вы собира-
(*.mdb) (или, если вы
етесь использовать, в данном случае — Microsoft Access Driver (*.mdb) (или, если вы
работаете с SQL Server, то его драйвер). Эти драйверы должны быть инсталлирова-
ны автоматически при типичной установке Windows ХР. Если вы не видите в списке
нужный драйвер, вам следует вернуться к установке Windows и инсталлировать его.
Выбрав драйвер, щелкните на кнопке Finish (Готово). Это откроет еще одно диалого-
вое окно, показанное на рис. 19.8.
Подключение к источникам данных
931
Рис. 19.8. Диалоговое окно настройки драйвера Microsoft Access
Введите имя файла базы данных в поле Data Source Name (Имя источника дан*
ных), которым в этом случае должно быть Northwind. Вы используете это имя
для идентификации базы данных при генерации приложения с помощью мастера
Application Wizard. Затем вы должны щелкнуть на кнопке Select (Выбрать), чтобы
перейти к финальному диалоговому окну, которым должно быть окно Select Database
(Выбор базы данных), где вы сможете выбрать файл в том каталоге, где он хранится.
Последнее диалоговое окно для выбора базы данных показано на рис. 19.9.
I
Select Database
Database Name
Northwind .mdb
.......
sampdata.mdb
technical Jibrary.mdb
technical_library_V7.mc
List Files of Type:
Access Databases *m <
Directories
d:\...Vnodel access db
Beginning Visual C
Model Access D E
Help |
Read Only
Exclusive
Drives:
k=] d: Data
Network...
Puc. 19.9. Диалоговое окно выбора базы данных
В конце последовательно щелкните на трех кнопках ОК, в результате чего ваша
база данных будет зарегистрирована. Если что-то будет выглядеть не так, нужно будет
обратиться к справочной системе или поэкспериментировать с опцией ODBC в пане-
ли управления. Истина где-то рядом.
Когда все получится, вы сможете двигаться дальше в разработке приложения базы
данных и, как всегда, начальной точкой будет выбор пункта меню File^New1^ Project
(Файл^Создать^Проект) в Visual C++ 2005 либо просто нажатие комбинации клавиш
<Ctrl+Shift+N>.
932 Глава 19
вы увидите, основное при-
ержкой). Файловая под-
связанный с базами данных, автоматически позаботится
Генерация программы MFC ODBC
Обычным образом создайте новый проект MFC на основе шаблона MFC Application
(Приложение MFC) и присвойте ему подходящее имя вроде DBSample. После щелчка
на кнопке ОК выберите набор опций Application Туре (Тип приложения) и выберите
интерфейс SDI для поддержки документа, поскольку он наиболее подходит для ваших
нужд. Документ некоторым образом является второстепенным для операций прило-
жения баз данных, поскольку большей частью управление выполняется объектами
наборов данных и объектами представления записей.
менение документа заключается в хранении объектов наборов записей, так что вам
не понадобится более одного документа.
Выберите набор опций Database Support (Поддержка баз данных). У вас есть воз-
можность включить файловую поддержку с помощью переключателя Database view
with file support (Представление баз данных с файловой по,
держка касается сериализации документа, что обычно не является необходимым, по-
скольку любой ввод и выво,
об использовании объектов наборов записей в приложении. Выберите переключа-
тель Database view without file support (Представление баз данных без файловой под-
держки), как показано на рис. 19.10.
Когда вы выбираете любую из опций, связанных с базами данных, активизируются
другие флажки, переключатели и кнопка Data Source (Источник данных). Выберите
переключатель ODBC и затем щелкните на кнопке Data Source для указания базы дан-
ных, которую будет использовать ваше приложение. Это отобразит диалоговое окно
Select Data Source (Выбор источника данных), показанное на рис. 19.11.
Если база данных Northwind зарегистрирована как пользовательская база данных,
то она появится на вкладке Machine Data Source (Источник данных машины), как на
рис. 19.11.
MFC Application Wizard - DBSample
Пт-ТТЛ ,
-Л
Database
Support
Overview
Application Type
Compound Document Support
Document Template Strings
Database Support
User Interface Features
Advanced Features
Generated Classes
Database support:
С ’ None
' Header files only
’) Database view without file support
О Database view with file support
Client type:
Q OLE DB
0 ODBC
Data source:
v* Bind all columns
( । Dynaset
? &0.apshD^
Data Source
< Previous
Next
Finish
Cancel
r -
Г
Puc. 19.10. Установка опции Database view without file support
Подключение к источникам данных
933
Рис. 19.11. Диалоговое окно Select Data Source
После выбора базы данных и щелчка на кнопке ОК отображается диалоговое окно
Login (Вход) для этой базы данных. В нем необходимо ввести регистрационное имя
и пароль, чтобы открыть базу данных. Когда вы щелкнете на кнопке ОК, то увидите
диалоговое окно Select Database Object (Выбор объекта базы данных), показанное
на рис. 19.12, в котором потребуется выбрать объекты базы данных, доступ к кото-
рым нужен.
Рис. 19.12. Диалоговое окно Select Database Object
934 Глава 19
Разверните узел Tables (Таблицы) в этом диалоговом окне и щелкните на табли-
це Products. Можно выбирать столько таблиц, сколько необходимо, щелкая на каж-
дой из них при нажатой клавише <Ctrl>, но пока что понадобится только таблица
Products. Затем щелкните на кнопке ОК для закрытия диалога. Сейчас вы специфи-
цировали операцию выборки для класса набора записей, сгенерированную мастером
Application Wizard следующим образом:
SELECT * FROM Products;
Использование * для всех полей определяется каркасом. Он просто использует
имя таблицы либо имена, которые вы выбрали здесь, чтобы сформировать операцию
SQL, применяемую к набору записей.
Диалоговое окно мастера MFC Application Wizard также предоставляет выбор
между Snapshot (Снимок) и Dynaset (Динамический набор) в качестве типа набора
записей, используемых проектом. Между ними есть существенная разница, которой
посвящается следующий раздел.
Сравнение наборов записей Snapshot и Dynaset
Ваш объект набора записей обеспечит результатом выполнения операции SELECT
в базе данных. В случае набора записей snapshot (снимок) запрос выполняется од-
нократно, и его результат сохраняется в памяти. Ваш объект набора записей затем
может открыть доступ к любой из записей таблицы, полученных по запросу, так что
snapshot по своей природе статичен. Любые изменения, которые могут произойти
в базе данных в результате обновлений другими пользователями, не отображаются
в данных, которые вы получаете в таком наборе записей. Если вам нужно видеть из-
менения, которые могут там произойти, для этого придется перезапустить оператор
SELECT.
При выборе опции dynaset (динамический набор) ваш объект набора записей ав-
томатически обновляет текущую запись из базы данных при перемещении от одной
записи к другой в таблице, сгенерированной запросом получения этого набора.
Имейте в виду, что обновления происходят, только когда ваш объект набора данных
обращается к записи. Если данные в текущей записи модифицированы другим поль-
зователем, это не отражается в вашем наборе, пока вы не переместитесь к другой за-
писи и не вернетесь затем к исходной. Набор dynaset использует индекс при доступе
к таблице базы данных для динамической генерации каждой записи. Поскольку в дан-
ном случае у вас нет других пользователей, работающих с базой данных Northwind,
вы смело можете выбрать опцию Snapshot для вашего примера.
После выбора переключателя Snapshot вы можете щелкнуть на опции Generated
Classes (Сгенерированные классы) для отображения классов вашего приложения.
Диалоговое окно показано на рис. 19.13.
Здесь при желании можно изменить имена классов и соответствующие имена фай-
лов, присвоенные мастером, на что-то более подходящее. В дополнение к изменени-
ям, показанным для классов CDBSampleView и CProductView, а также соответствую-
щим изменениям в именах файлов . h и . срр класса, вы также можете изменить имя
класса CDBSampleSet на CProductset и привести в соответствие с новыми именами
классов имена ассоциированных файлов . h и . срр. После того, как это будет сдела-
но, щелкните на кнопке Finish (Готово) и сгенерируйте проект.
Подключение к источникам данных 935
Рис. 19.13. Классы, сгенерированные мастером для приложения
Структура программы
Базовая структура программы остается такой же, как вы видели ранее — с клас-
сом приложения CSBSampleApp, классом обрамляющего окна СМа in Frame, классом
документа CDBSampleDoc и классом представления CProductView. Объект шабло-
на документа отвечает за создание и установление отношений между объектами об-
рамляющего окна, документа и объекта представления. Это делается стандартным
образом, в члене Initlnstance () объекта приложения. Класс документа стандарт-
ный, за исключением того, что мастер MFC Application Wizard добавляет член дан-
ных m_DBSampleSet — объект типа класса CProductset. Как следствие, объект на-
бора записей создается автоматически при создании объекта документа в функции
Initlnstance () — члене объекта приложения. Существенные отличия от программ,
не связанных с базами данных, проявляются в деталях класса CRec ordSet и класса
CRecordView, поэтому рассмотрим их более внимательно.
Наборы записей
Вы можете по частям рассмотреть определение класса CProductset, сгенериро-
ванного мастером Application Wizard, и увидеть, как эти части работают. Эти фраг-
менты кода выделены полужирным.
Создание набора записей
Ниже показан первый сегмент определения класса, который нас интересует.
class CProductset : public CRecordset
(
public:
CProductset (CDatabase* pDatabase « NULL);
DECLARE DYNAMIC(CProductset)
936 Глава 19
// Плюс прочая часть определения класса...
// Переопределения
// Переопределения сгенерированных мастером виртуальных функции
public:
virtual CS tring GetDefaultConnect (); // Строка подключения по умолчанию
virtual CString GetDefaultSQL(); //SQL-код по умолчанию для набора записей
virtual void DoFieldExchange(CFieldExchange* pFX);// Поддержка RFX
// Плюс некоторая другая стандартная часть
Этот класс имеет в качестве базового CRe cordset и обеспечивает функциональ-
ность извлечения данных из базы. Конструктор класса принимает указатель на объект
CDatabase, по умолчанию установленный в NULL. Параметр конструктора позволяет
объекту CProductset создаваться для уже существующего объекта CDatabase, кото-
рый позволяет использовать существующее соединение с базой данных. Открытие
соединения с базой данных — длительная операция, поэтому когда это возможно, вы-
годно повторно использовать существующие соединения.
Если конструктору не передается никакой указатель, как в случае члена
ш_DBSampleSet класса документа CDBSampleDoc, то каркас автоматически создает для
вас объект CDatabase и вызывает функцию-член CProductset GetDefaultConnect ()
для определения соединения. Мастер Application Wizard предлагает следующую реа-
лизацию этой функции:
CString CProductset::GetDefaultConnect()
return _T(
"DSN=Northwind;
DBQ=D:\\Beg Visual C++ 2005\\Model Access DB\\Northwind.mdb;
Driverld=25;
FIL=MS Access;
MaxBufferSize=2048;
PageTimeout=5;
UID=admin;");
Функция GetDefaultConnect () — пустая виртуальная функция базового класса
CRecordset, а потому должна всегда быть реализована в классе-наследнике набора
записей. Возвращенное функцией значение — это одиночная строка, заключенная в
двойные кавычки, но я показал ее разбитой на несколько строк, чтобы сделать ее со-
держимое более ясным. Реализация, предоставленная мастером Application Wizard,
возвращает показанную строку каркасу. Она идентифицирует базу данных с ее име-
нем и путем, а также значениями прочих параметров, которые вы можете видеть, и
позволяет каркасу создавать объект CDatabase, устанавливающий соединение с ба-
зой данных автоматически. Описание аргументов строки соединения приведено в
табл. 19.3.
На практике обычно для доступа к базе данных также необходимо указывать па-
роль наряду с идентификатором пользователя, однако неразумно помещать пароль в
код в открытой форме. По этой причине мастер Application Wizard вставляет следую-
щую строку, предшествующую определению функции GetDefaultConnect ():
#error Безопасность: строка соединения может содержать пароль
При наличии этой директивы в коде компиляция завершается ошибкой, поэтому
вы должны закомментировать ее либо удалить, чтобы можно было успешно скомпи-
лировать программу.
Подключение к источникам данных
937
Таблица 19.3. Аргументы строки соединения
Аргумент Описание
DSN
DBQ
DriverID
FIL
MaxBufferSize
PageTimeout
DID
Имя источника данных.
Квалификатор базы данных, в данном случае — путь к файлу базы данных Access.
Идентификатор драйвера ODBC для базы данных.
Тип файла базы данных.
Максимальный размер буфера, используемого для обмена данными с базой.
Длительность времени ожидания соединения с базой в секундах. Важно установить
это значение в адекватную величину, чтобы избежать сбоев соединения при досту-
пе к удаленной базе.
Идентификатор пользователя для доступа к базе данных.
Вы можете заставить каркас вывести всплывающее диалоговое окно для пользо-
вателя, чтобы он мог выбрать имя базы данных из списка зарегистрированных ис-
точников данных, переписав оператор return в функции GetDefaultConnection ()
следующим образом:
return _T(”ODBC;");
Также при попытке доступа к базе данных вам будет предложено ввести идентифи-
катор пользователя и пароль.
Выполнение запроса к базе данных
Класс CProductset включает член данных для каждого поля таблицы Products.
Мастер Application Wizard получает имена полей из базы данных и использует их
для именования соответствующих данных-членов класса. Они появляются в блоке
кода, следующем за комментарием Данные полей/параметров в определении класса
CProductset.
class CProductSet : public CRecordset
public:
CProductset(CDatabase* pDatabase = NULL);
DECLARE—DYNAMIC(CProductset)
// Данные полей/параметров
// Строковые типы ниже (если есть) отражают фактические типы данных
// полей базы - CStringA для типов данных ANSI и CStringW для типов
// данных Unicode. Это необходимо для того, чтобы предотвратить выполнение
// драйвером ODBC потенциально ненужных преобразований.
// Если хотите, можете изменить типы этих членов на CString
//и тогда драйвер ODBC будет выполнять все необходимые
// преобразования.
// (Примечание: для поддержки этих, а также Uni code-преобразований необходимо
// использовать драйвер ODBC версии 3.5 и новее.)
long m_ProductID; // Номер, автоматически присваиваемый каждому пре
CStringW m__Produc tName;
long m_SupplierID; 11 Некоторое вхождение в таблицу Suppliers.
long mjCategorylD; / / Некоторое вхождение в таблицу Categories.
CStringW m_QuantityPerUnit; // (например, случаи с упаковкой из 24
double m UnitPrice;
938 Глава 19
int m__UnitsInStock;
int m UnitsOnOrder;
BOOL m Discontinued; //Да означает
// Переопределения
// Переопределения сгенерированных мастером виртуальных функции
public:
virtual CString GetDefaultConnect (); // Строка соединения по умолчанию
virtual CString GetDefaultSQL (); //SQL-код по умолчанию для набора записей
virtual void DoFieldExchange (CFieldExchange* pFX) ; // Поддержка RFX
/1 Реализация
#ifdef _DEBUG
virtual void AssertValidO const;
virtual void Dump(CDumpContext& de) const;
#endif
Тип каждого члена данных устанавливается согласно типу соответствующего поля
в таблице Products. На практике вам могут и не понадобиться все эти поля, но, хоти-
те или нет, вы не должны удалять их из определения класса. Как вы вскоре убедитесь,
ссылки на них появляются и в ряде других мест, так что в этом случае вам придется
удалять их вручную и там. Еще одно предупреждение заключается в том, что вы не
должны удалять первичные ключи. Если это сделать, то набор записей не будет рабо-
тать, так что вы должны выяснить, какие поля служат первичными ключами, прежде
чем нарушать то, что нарушать не желательно.
Обратите внимание, что два поля имеют тип CStringW. Вы еще не сталкивались с
ним ранее, поэтому знайте, что класс CStringW инкапсулирует в себе строку Unicode
вместо строки ASCII. Вам будет удобнее обращаться к полям, используя тип CString,
поэтому измените тип членов m_ProductName и m_QuantityPerUnit на CString. Это
позволит в данном примере обрабатывать строки как ASCII. Ясно, что если вы со-
бираетесь интернационализировать приложения баз данных, то вам придется под-
держивать поля CStringW, поскольку они могут содержать символы, не входящие в
набор символов ASCII.
Операция SQL, применяемая к набору записей для наполнения этих членов дан-
ных, специфицирована в функции Get De fault SQL (). Реализация, предложенная ма-
стером Application Wizard, выглядит так:
CString CProductset::GetDefaultSQL()
return _T (” [Products] ’’);
Возвращаемая строка, очевидно, создана на основе имени выбранной таблицы во
время создания проекта. Квадратные скобки включены для обеспечения возможно-
сти применения пробелов в имени таблицы. Если вы выбираете несколько таблиц в
процессе создания проекта, они должны вставляться здесь и разделяться запятыми,
причем имя каждой таблицы заключается в квадратные скобки.
Функция GetDef aultSQL () вызывается каркасом MFC, когда он конструирует опе-
ратор SQL, применяемый к набору записей. Каркас вставляет строку, возвращенную
этой функцией, в скелетный оператор SQL следующей формы:
SELECT * FROM < Строка, возвращенная GetDefaultSQL() >;
Подключение к источникам данных 939
Это выглядит упрощением, и в самом деле таковым является, но, как вы вскоре
увидите, позднее к операции можно добавить конструкции WHERE и ORDER BY.
Передача данных между базой данных и набором записей
Передача данных от базы к набору записей и обратно обеспечивается членом
класса CProductset — функцией DoFileExchange (). Ее реализация выглядит показа-
на ниже.
void CProductset::DoFieldExchange(CFieldExchange* pFX)
pFX->SetFieldType(CFieldExchange::outputcolumn);
// Такие макросы, как RFX_Text() и RFX_Int(), зависят от типа
// переменной-члена, а не от типа поля таблицы базы данных.
// ODBC попытается автоматически преобразовать значение столбца
// к запрошенному типу.
RFX_Long (pFX, _Т (” [ProductID] ’’) , m_ProductID) ;
RFX_Text(pFX, _T(”[ProductName]”), m_ProductName);
RFX_Long(pFX, _T("[SupplierlD]"), m_SupplierID);
RFX_Long(pFX, _T("[CategoryID]”), m_CategoryID);
RFX_Text(pFX, _T("[QuantityPerUnit]"), m_QuantityPerUnit);
RFX_Double (pFX, _T (" [Unitprice] ’’), m_UnitPrice) ; ‘
RFX_Int(pFX, _T("[UnitsInStock]"), m_UnitsInStock);
RFX_Int(pFX, _T("[UnitsOnOrder]"), m_UnitsOnOrder);
RFX_Int(pFX, _T(”[ReorderLevel]”), m_ReorderLevel);
RFX_Bool(pFX, _T(”[Discontinued]"), m_Discontinued);
Эта функция вызывается автоматически каркасом MFC для сохранения данных и
извлечения их из базы. Она работает подобно функции DoDataExchange (), знакомой
вам по диалоговым элементам управления, в том, что параметр pFX определяет, явля-
ется ли данная операция операцией чтения или записи. При каждом вызове она пере-
мещает единственную запись в объект набора записей или из него.'
Первая вызванная функция — SetFieldType (), которая устанавливает режим для
последующих вызовов PFX_ (). В данном случае режим специфицирован как output-
Column, а это указывает на то, что данные должны быть переданы между полем базы
и соответствующим аргументом, специфицированным в каждом последующем вызове
функций PFX— ().
Существует полный набор функций PFX_ () для разнообразных типов поля базы
данных. Вызов функции для конкретного поля соответствует типу данных данного
поля. Первый аргумент вызова функции PFX_ () — это объект pFX, определяющий на-
правление перемещения данных. Второй аргумент — имя поля таблицы, а третий —
член данных, которой должен быть помещен в соответствующее поле текущей записи.
Представление записей
Назначение класса представления — отображать информацию из объекта набо-
ра записей в окне приложения, поэтому нужно разобраться с тем, как он работает.
Наиболее важные фрагменты определения класса CProductView, которые нас инте-
ресуют, выделены полужирным.
class CProductView : public CRecordView
{
protected: // Создается только сериализацией
CProductView();
DECIARE DYNCREATE(CProductView)
940 Глава 19
public:
enum{ IDD = IDD__DBSAMPLE_FORM };
CProductset* m_pSet;
// Атрибуты
public:
CDBSaxnpleDoc* GetDocument () ;
// Операции
public:
// Переопределения
public:
virtual CRecordset* OnGetRecordset ();
virtual BOOL PreCreateWindow (CREATE STRUCT & cs) ;
protected:
virtual void DoDataExchange (CDataExchange* pDX) ; // Поддержка DDX/DDV
virtual void OnlnitialUpdate(); //Вызывается первой после конструирования
virtual BOOL OnPreparePrinting(CPrintlnfo* plnfo);
virtual void OnBeginPrinting(CDC* pDC, CPrintlnfo* plnfo);
virtual void OnEndPrinting(CDC* pDC, CPrintlnfo* plnfo);
/ / Реализация
public:
virtual ^CProductView();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContexts de) const;
#endif
protected:
// Сгенерированные функции карты сообщений
protected:
DECLARE_MESSAGE_MAP()
Класс представления для набора записей всегда должен наследоваться, поскольку
он должен подстраиваться для отображения конкретных полей из требуемого набо-
ра. Базовый класс CRecordView включает всю функциональность, необходимую для
управления взаимодействием с наборами записей. Все, что вам нужно сделать — это
подогнать класс представления записи под нужды вашего приложения. Вскоре мы
вернемся к этому вопросу.
Обратите внимание, что конструктор является protected. Это связано с тем, что
объекты данного класса создаются только через сериализацию, что является предпо-
сылкой по умолчанию для классов представления записи. Когда вы добавляете новые
классы представления записей к приложению, вам необходимо изменить доступ к их
конструкторам по умолчанию на public, поскольку вы создадите представления са-
мостоятельно.
В первом блоке public этого класса перечисление добавляет идентификатор
IDD_DBSAMPLE_FORM в качестве члена класса. Это идентификатор пустого диалога,
который мастер Application Wizard должен включить в программу. Вы добавите эле-
менты управления к этому диалогу для отображения полей базы данных из таблицы
Products, которые хотите отобразить. Идентификатор диалога передается базовому
классу CRecordView в списке инициализации конструктора класса представления:
Подключение к источникам данных 941
CProductView: :CProductView() : CRecordView(CProductView: :IDD)
m_pSet = NULL;
// TODO: добавить сюда код конструирования
Это действие связывает класс представления с диалоговым окном, что необходи-
мо для включения работы механизма передачи данных между объектом набора запи-
сей и объектом представления.
В определении класса также имеется указатель на объект CProductset — m_pSet,
инициализированный в конструкторе значением NULL. Более удобно устанавливать
значение этого указателя в функции-члене OnlnitialUpdate (), которая должна быть
реализована следующим образом:
void CProductView::OnlnitialUpdate()
m_pSet = &GetDocument()->m_DBSampleSet;
CRecordView::OnlnitialUpdate();
Эта функция вызывается при создании объекта представления записей и устанав-
ливает значение m pSet равным адресу члена m_DBSampleSet документа, таким об-
разом, устанавливая представление на объект набора записей.
На рис. 19.14 показано, как данные из базы в конечном итоге отображаются в
представлении.
Таблица базы данных
4
DoFieldExchange() — член объекта
CRecordset, передающий данные
между базой и набором записей
Объект представления
DoDataExchange() — член объекта
представления, передающий
данные между набором
записей и представлением
Объект набора записей
Рис. 19.14. Отображение данных в представлении записей
942 Глава 19
Передача данных между данными-членами в объекте CProductset, которые соот-
ветствуют полям таблицы Products, и элементами управления диалогового окна, ас-
социированного с объектом CProductView, управляется членом DoDataExchange ()
класса CProductView. Код этой функции пока отсутствует, поскольку сначала потребу-
ется добавить элементы управления к диалогу, которые должны отображать данные,
и затем связать элементы управления с членами данных набора записей.
Создание диалога представления
Первый шаг состоит в размещении элементов управления в диалоговом окне, по-
этому обратитесь в панель Resource View, разверните список диалоговых ресурсов
и дважды щелкните на Idd_Dbsample_Form. Вы можете удалить из диалога объект
статического текста с сообщением TODO. Если выполнить щелчок правой кнопкой
мыши на диалоговом окне, вы увидите его свойства, как показано на рис. 19.15.
Если вы пройдетесь вниз по списку свойств, то увидите, что свойство Style
(Стиль) установлено в Child (Дочерний), поскольку диалог должен быть дочерним
окном и заполнять клиентскую область. Свойство Border (Рамка) установлено в None
(Нет), поскольку если диалог заполняет клиентскую область, он не нуждается в рамке.
Вы должны добавить статический текстовый элемент управления для идентифика-
ции каждого поля из набора записей, которое вы хотите отобразить, а также редакти-
рующий элемент управления для его отображения.
Можете при необходимости увеличить диалог, перетаскивая его границы. Затем
поместите элементы управления на его поверхность, как показано на рис. 19.16.
Properties
IDD_DBSAMPLE_FORM (Dialog) IDIgEditor
(Name)
(Name) IDDJ
3D Look False
Absolute Align False
Accept Files False
Application Window False
Border None
Capti '
Center False
Center Mouse False
Client Edge
Clip Children
Clip Siblings
Context Help
Control
Control Parent
Disabled
Font(Size)
Horizontal Scrollbar
ID
Layout RTL
Left Scroll bar
Local Edit
False
False
False
False
False
False
False
MS Shell Dlg(B)
False
IDD_DBSAMPLE_FORM
False
False
False
(Name)
Puc. 19.15. Окно свойств диалога
Idd_Dbsample Form
Подключение к источникам данных 943
Product ID Sample ed Category ID | Sample ed
Product Name | sample edit box
Unit Price Sample ed
Units In Stock I sample ed Units On Order I sample ed
Puc. 19.16. Размещение элементов управления в диалоге
Вы можете добавить текст к каждому статическому элементу управления, просто
набирая его на клавиатуре сразу после помещения этого элемента в диалог. Я ввел
текст для каждого статического элемента, чтобы он соответствовал имени каждого
поля базы данных. Хорошей идеей будет присвоить каждому элементу осмысленные
и различные идентификаторы, поэтому щелкните правой кнопкой мыши на каждом
из них и модифицируйте их свойства. На рис. 19.17 показаны свойства элемента, со-
ответствующего ролю Product ID.
Рис. 19.17. Окно свойств элемента,
соответствующего полю Product ID
Полезно использовать имя поля в качестве части идентификатора (ID) элемента,
поскольку это указывает, что должен отображать данный элемент. На рис. 19.17 по-
казан идентификатор первого редактирующего элемента в заголовке окна свойств по-
944 Глава 19
еле модификации. Вы можете изменить идентификаторы остальных редактирующих
элементов аналогичным образом. Поскольку обновление базы данных в настоящем
примере не планируется, необходимо обеспечить, чтобы данные, отображаемые в
каждом поле редактирования, не могли изменяться с клавиатуры. Вы можете обеспе-
чить это, установив свойство Read Only (Только для чтения) для каждого их элемен-
тов редактирования в значение True. Фон таких полей редактирования будет иметь
другой цвет, указывая на то, что текст в них не может быть изменен (рис. 19.18).
При желании можно добавить в диалоговое окно и другие поля. Наиболее важное
из них для последующего развития нашего примера — это Product ID, поэтому вклю-
чите его. Сохраните диалоговое окно и затем перейдите к последнему ;
нию элементов управления с переменными в классе набора записей.
Рис. 19,18, Редактирующие элементы управ-
ления, предназначенные только для чтения
: связыва-
>1
Связывание элементов управления с набором записей
Как вы видели ранее на рис. 19.14, получение данных из набора записей для
отображения соответствующим элементом управления — это работа для функции
DoDataExchange () из класса CProductView. Член m_pSet обеспечивает средства до-
ступа к членам объекта CProductset, содержащего поля, извлеченные из базы дан-
ных, поэтому связать элементы управления с CProductset достаточно просто. В MFC
определена группа функций DDX_Field в глобальном контексте, специально предна-
значенных для передачи данных между представлением и набором записей. В частно-
сти, функция DDX_FieldText () имеет перегруженные версии, передающие широкое
разнообразие типов данных между полем набора записей и полем редактирования в
объекте CRecordView. Ниже перечислены типы, которые могут быть переданы функ-
цией DDX FieldText().
short
int
UINT
long
DWORD
float
double
CString
COleDateTime
COleCurrency
Подключение к источникам данных
945
При вызове функции DDX FieldText () вы должны передать четыре аргумента.
□ Объект CDataExchange, определяющий направление передачи данных — в на-
бор записей или из него. Вы просто применяете указатель, передаваемый в
виде аргумента функции DoDataExchange ().
□ Идентификатор элемента управления, являющегося источником или местом
назначения данных.
□ Ссылку на поле — член данных объекта CRecordset, являющегося источником
или местом назначения данных.
□ Указатель на объект CRecordset, с которым должен произойти обмен данными.
Таким образом, для реализации передачи данных между набором записей и
элементом управления для поля Product ID вставьте следующий вызов функции
DDX_FieldText () в тело функции DoDataExchange ():
DDX_FieldText(pDX, IDC_PRODUCTID, m_pSet->m_ProductID, m_pSet);
Первый аргумент функции DDX_FieldText () — pDX. Второй — идентификатор пер-
вого редактирующего элемента управления диалога этого представления, третий аргу-
мент использует член m_pSet класса CProductView для доступа к члену m_ProductID
объекта набора записей, а последний аргумент — указатель на объект набора записей.
Итак, вы можете оформить код функции DoDataExchange () следующим образом:
void CProductView::DoDataExchange(CDataExchange* pDX)
CRecordView::DoDataExchange(pDX);
DDX_FieldText (pDX, ID COPRODUCT ID , mj?Set->m_ProductID, mjpSet);
DDX_FieldText (pDX, IDC_PRODUCTNAME, mjpSet-3ra_ProductNaine , m_p$et);
DDX_FieldText(pDX, IDCJJNITPRICE, m_pSet->m__UnitPrice, mjpSet) ;
DDX_FieldText (pDX, IDCJJNITS INSTOCK, mjoSet->m_UnitsInStock, mj>Set) ;
DDX_FieldText(pDX, IDCjCATEGORYID, mjpSet->m_CategoryID, mjpSet) ;
DDX_FieldText (pDX, IDC_UNITSONORDER, mjpSet->m__UnitsOnOrder, mjpSet) ;
Программный механизм для передачи данных между базой и диалоговым окном,
которым владеет объект CProductView, проиллюстрирован на рис. 19.19.
Класс набора записей и класс представления записей кооперируются для обеспече-
ния передачи данных между базой и элементами управления диалогового окна. Класс
CProductset обрабатывает передачу данных между базой и своими данными-члена-
ми, a CProductView имеет дело с передачей между данными-членами CProductset и
элементами управления диалога.
Тестирование примера
Верите или нет, но вы уже можете запустить этот пример. Для этого просто со-
берите его обычным образом и затем выполните. Приложение должно отобразить
окно, подобное тому, что показано на рис. 19.20.
Базовый класс CRecordView автоматически реализует кнопки панели инструмен-
тов, которые позволят передвигаться от одной записи в наборе к следующей или
предыдущей. Есть также кнопки панели для перемещения непосредственно к первой
или последней записи в наборе. Конечно, продукты отображаются в последователь-
ности по умолчанию. Было бы хорошо получать их отсортированными по категориям
и по идентификаторам в рамках каждой категории. Ниже вы увидите, как это можно
сделать.
946 Глава 19
Вызовы RFX
DoFieldExchangef)
CProductSet
m ProductID
► m ProductName
► m UnitsInStock
Таблица Products
в примере данных
Puc. 19.19. Передача данных между базой и диалоговым окном
Рис. 19.20. Работа представления записей
Подключение к источникам данных
947
Сортировка набора записей
Как было показано ранее, данные, извлекаются из базы набором записей посред-
ством SQL-оператора SELECT, сгенерированного каркасом с использованием члена
GetDefaultSQL (). Вы можете добавить конструкцию ORDER BY к сгенерированному
оператору, установив значение члена m__strSort класса CProductset, унаследованно-
го от CRecordSet. Это отсортирует выходную таблицу запроса на основе строки, хра-
нящейся в strSort. Вам достаточно только установить правильное значение члена
str Sort, указав в нем имя поля либо имена полей, по которым требуется выполнить
сортировку, каркас добавит необходимые ключевые слова ORDER BY. В случае указания
нескольких имен полей разделяйте их запятыми. Но куда должен быть добавлен код?
Передача данных между базой и набором записей происходит при вызове члена
Open () класса набора записей. В вашей программе функция-член Open () объекта на-
бора записей вызывается в члене OnlnitialUpdate () базового класса представле-
ния — CRecordView (). Таким образом, вы можете поместить код для установки специ-
фикации сортировки в член OnlnitialUpdate () класса CProductView, как показано
ниже:
void CProductView::OnlnitialUpdate()
{
m_pSet = SGetDocument()->m_productSet;
m_pSet->m_strSort = " [CategoryID] , [ProductID]"; //Установка полей сортировки
CRecordView:: OnlnitialUpdate ();
}
Вы просто устанавливаете m_strSort в наборе записей в строку, содержащую имя
поля Category ID, за которым следует имя поля Product ID. Квадратные скобки по-
лезны даже тогда, когда в имени нет пробелов, поскольку они позволяют отличить
строки, содержащие эти имена, от других строк, так что вы можете немедленно уви-
деть имена полей. Конечно, они не обязательны, если в имени поля пробелов нет.
Модификация заголовка окна
Есть еще одна вещь, которую вы можете добавить к функции в этот момент.
Заголовок окна будет информативнее, если будет содержать имя отображаемой та-
блицы. Вы можете добиться этого, добавив код установки заголовка в объекте доку-
мента:
void CProductView::OnlnitialUpdate()
m_pSet = &GetDocument()~>m_productSet;
m_pSet->m_strSort = ” [CategoryID],[ProductID]//Установить поля сортировки
CRecordView::OnlnitialUpdate();
// Установить заголовок окна в имя таблицы
if (mjpSet->IsOpen()) // Убедиться, что набор записей открыт
{
CString strTitle = __Т( "Table Name") ; // Установить базовую строку заголовка
CString strTable = m_pSet-X3etTableName ();
if(!strTable.IsEmpty()) // Убедиться, что мы имеем гася таблицы
strTitle += _Т(": ") + strTable; // и добавить к базовой таблице
GetDocument()->SetTitie (strTitle) ; // Установить заголовок документа
}
}
948 Глава 19
После проверки того, что набор записей действительно открыт, вы инициали-
зируете локальный объект CString базовой строкой заголовка. Затем вы получаете
имя таблицы от объекта набора записей вызовом функции-члена GetTableName ().
При этом могут возникнуть различные ситуации, которые помешают установить имя
таблицы. Например, в наборе записей может присутствовать более одной таблицы.
После добавления к базовому заголовку в strTitle двоеточия, за которым следует
имя извлекаемой таблицы, вы устанавливаете результат как заголовок документа, вы-
зывая функцию-член документа SetTitle ().
Если вы заново соберете приложение и вновь запустите его, оно будет работать,
как и раньше, но с новым заголовком окна (рис. 19.21). Значение Product ID следуют
в убывающем порядке в пределах каждого значения Category ID, причем Category
ID также упорядочены.
Рис. 19.21. Добавление заголовка, отображающего имя таблицы
Использование второго
объекта набора записей
Теперь, когда вы можете видеть все продукты в базе данных, разумным усовершен-
ствованием программы будет добавление возможности видеть заказы по каждому кон-
кретному продукту. Для этого потребуется добавить еще один класс набора записей,
чтобы обработать информацию об адресах, и дополнительный класс представления
для отображения некоторых полей из этого набора. Кроме того, вы добавите кноп-
ку к диалогу Products, позволяющую переключаться на диалог Orders, когда нужно
просмотреть заказы текущего продукта. Это позволит работать в окружении, показан-
ном на рис. 19.22.
Диалоговое окно Products — начальная точка, от которой вы можете двигать-
ся вперед и назад по всем доступным продуктам. Щелчок на кнопке Show Orders
(Показать заказы) переключает на диалог, в котором вы можете видеть заказы теку-
щего продукта. Вы можете вернуться к диалоговому окну Products, щелкнув на кноп-
ке Show Products (Показать продукты).
Подключение к источникам данных
949
Product ID
Edit
Category ID
Edit
Этот диалог позволяет
перемещаться по
доступным продуктам
Product Name
Edit
I Unit Price
Edit
Units On Order
Sample ed
Ha
Units In Stock
Edit
Show Orders
Щелчок на кнопке Show Orders
открывает диалог Orders
для текущего продукта
Щелчок на кнопке
Show Products возвращает
к диалогу Products
Order ID
Edit
Customer ID
Edit
Product ID
Edit
Этот диалог позволяет
перемещаться по заказам
данного продукта
Quantity
Edit
Show Products
*
Puc. 19.22. Усовершенствование приложения просмотра базы данных
Добавление класса набора записей
Начать следует с добавления класса набора записей для заказов; щелкните правой
кнопкой мыши на DBSample в Class View и выберите Add1^Class (Добавить1^Класс)
из контекстного меню. Выберите MFC из набора категорий Visual C++ и MFC ODBC
Consumer (Потребитель MFC ODBC) в качестве шаблона. После щелчка на кнопке
Add (Добавить) в диалоговом окне Add Class (Добавление класса) отображается диа-
логовое окно MFC ODBC Consumer Wizard (Мастер потребителей MFC ODBC), по-
казанное на рис. 19.23.
Выберите в качестве типа потребителя Snapshot (Снимок), отметив соответству-
ющий переключатель, и затем щелкните на кнопке Data Source (Источник данных),
чтобы перейти к диалоговому окну Select Data Source (Выбор источника данных),
где можно будет идентифицировать источник данных; это должно осуществляться на
вкладке Machine Data Source (Источник данных машины). После выбора Northwind
в качестве источника данных, как это делали ранее, отобразится диалоговое окно
Select Database Object (Выбор объекта базы данных), показанное на рис. 19.24.
950 Глава 19
Рис. 19.23. Мастер потребителей MFC ODBC
Select Database Object
Г ables
Cancel
Categories
Customers
E mployees
i Order Details
%..... ....
Orders
Products
Shippers
S uppliers
-I Views
Puc. 19.24. Выбор таблиц в диалоговом окне Select Database Object
Подключение к источникам данных 951
Теперь нужно выбрать две таблицы для ассоциирования с новым классом набора
записей, который вы собираетесь создать, поэтому удерживайте нажатой клавишу
<Ctrl> и щелкайте на именах таблиц Orders и Order Details. Затем щелкните на
кнопке ОК для завершения процесса выбора. Это вернет вас в диалоговое окно ма-
стера MFC ODBC Consumer Wizard, где вы увидите имя класса и введенные имена
файлов. Можете изменить имя класса на COrderset и, соответственно, имена фай-
лов, как показано на рис. 19.25. Щелчок на кнопке Finish завершит процесс и вызовет
генерацию класса COrderset.
Как вы видели в классе CProductset, который создавали как часть начального
проекта, реализация функции GetDefaultConnect () класса COrderset предварена
директивой #еггог, предотвращающей компиляцию, так что закомментируйте ее.
Член данных в классе СО г de г se
создается для каждого поля каждой табли-
цы. Когда для определенного набора записей вы выбираете две или более таблиц,
то всегда возможно, и даже вероятно, что имена полей будут дублироваться; поле
Order ID, например, присутствует в обеих таблицах. Чтобы гарантировать отличие
имен членов данных, соответствующих полям, они снабжаются префиксами — име-
нами таблиц. Если вам не нужны все эти поля, можете удалить или закомментировать
любые из них, но как уже говорилось ранее, вы не должны удалять переменные, со-
ставляющие первичные ключи. Удалив член данных, отвечающий за поле таблицы,
вы также должны удалить его инициализацию в конструкторе класса и его вызов
RFX_ () в функции-члене DoFieldExchange (). Вы также должны изменить начальное
значение члена m_nFields в конструкторе COrderset, чтобы он отражал количе-
ство полей, оставшихся в классе. Данные-члены, которые вам понадобятся в данном
примере: m_OrdersOrderID, m_OrderDetailsOrderID, m_OrderDetailsProductID,
m__Order Detail sQuantity и m_OrdersCustomerID. Если вы оставите только их, то
должны изменить значение m_nFields на 5. Измените типы всех членов с CstringW
на CString.
Рис. 19.25. Установка имен классов
952 Глава 19
Чтобы связать новый набор записей с документом, вам понадобится добавить
член данных к определению класса С DBS ample Doc, поэтому щелкните правой кноп-
кой мыши на имени класса в панели Class View и выберите Add Member Variable
(Добавить переменную-член) из контекстного меню. Укажите в качестве типа
COrderset, а в качестве имени переменной — m_Orderset. Вы можете оставить ее
как public-член класса. Щелкните на кнопке ОК для завершения добавления данных-
членов к документу. Компилятор должен знать, что COrderset — это класс, прежде
чем он начнет компилировать класс CBSampleDoc. Если вы заглянете в содержимое
заголовочного файла DBSampleDoc .h, то увидите, что в верхней части уже добавлен
оператор #include:
♦pragma once
♦include "ProductSet.h”
♦include ’'orderset .h"
class CDBSampleDoc : public CDocument
11 Остальная часть определения класса
обавление класса представления для набора записей
На этом этапе вы можете ожидать добавления класса, унаследованного от
CRecordView, с использованием пункта Add1^Class (Добавить1^Класс) из контекстно-
го меню для отображения данных из объекта COrderset. Это было верно для ранних
версий Visual C++, но, к сожалению, Visual C++ 2005 не предоставляет такой возмож-
ности. Диалоговое окно для добавления нового класса вообще не позволяет выбирать
CRecordView в качестве базового класса, так что вам всегда придется вручную созда-
вать классы, унаследованные от CRecordView.
Прежде чем создавать класс представления, вы должны создать еще один ресурс
диалога, чтобы иметь идентификатор ресурса, который можно использовать в опре-
делении класса представления.
Создание ресурса диалога
Переключив:
II
ись на вкладку Resource View, щелкните правой кнопкой на пап-
ке Dialog и выберите пункт Insert Dialog (Вставить диалог) из контекстного меню.
Можете удалить из диалога обе кнопки по умолчанию. Затем измените идентификатор
и стиль диалога в окне его свойств, щелкнув правой кнопкой мыши на нем и выбрав
пункт Properties из всплывающего меню. Измените свойство ID на IDD_ORDERS_FORM.
Также потребуется изменить свойство Style на Child и свойство Border на None.
После этого вы готовы к наполнению диалогового окна элементами управления
для полей, которые вы желаете отобразить из таблиц Orders и Order Details. Если
вы переключитесь на Class View и выберите имя класса COrderset, то сможете уви-
деть имена переменных, появившихся в процессе работы с диалогом. Добавьте эле-
менты управления к диалоговому окну, как показано на рис. 19.26.
Здесь присутствуют четыре элемента управления для полей OrderlD, CustomerlD,
ProductID и Quantity из таблиц, ассоциированных с классом COrderset, вместе со
статическими элементами управления, идентифицирующими их. Вы можете добавить
элементы управления для отображения еще нескольких полей по своему желанию, до
тех пор, пока не удаляете члены класса. Не забудьте модифицировать идентификато-
ры (ID) редактирующих элементов управления, чтобы они отражали назначение этих
элементов. Вы можете использовать имена полей таблицы с префиксом — именем
Подключение к источникам данных
953
таблицы для установки соответствия имен данных-членов. И, наконец, вы должны
сделать редактирующие элементы управления доступными только для чтения, устано-
вив свойство Read Only в True. Альтернативно вы можете установить их доступными
только для чтения в один прием, выбрав их все при нажатой клавише <Ctrl> и затем
установив свойство Read Only в True.
Элемент управления — кнопка, помеченная как Show Products (Показать продук-
ты), используется для возврата к представлению таблицы Products, поэтому изме-
ните ID этой кнопки на IDC_PRODUCTS. Когда вы упорядочите все по своему усмотре-
нию, сохраните ресурс диалога.
Рис. 19.26. Элементы управления для класса COrderset
Создание класса представления записей
Создайте файл Orderview.h, который будет содержать определение COrderView.
Для этого щелкните правой кнопкой мыши на DBS ample в Solution Explorer и выбе-
рите Add^New Item (Добавить1^Новый элемент) из контекстного меню. Выберите
шаблон для создания файла .h и введите в качестве его имени COrderView. После
создания файла добавьте в него код определения класса:
#pragma once
class COrderSet; // Объявление имени класса
class CDBSampleDoc; // Объявление имени класса
/ / Представление COrderView
class COrderView : public CRecordView
DECLARE_DYNCREATE(COrderView)
protected:
virtual ~COrderView(){}
virtual void DoDataExchange(CDataExchange* pDX) ; // Поддержка DDX/DDV
virtual void OnlnitialUpdate();
public:
enum { IDD = IDD_ORDERS_FORM } ;
COrderSet* m_pSet;
// Определение встроенных функций
CDBSampleDoc* GetDocument () const
return reinterpret—cast<CDBSampleDoc*> (m__pDocument);
COrderSet* GetRecordset ();
virtual CRecordset* OnGetRecordset();
COrderView(); // теперь конструктор public
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContexts de) const;
#endif
954 Глава 19
Этот код основан на сгенерированном CProductView. Макрос DECIARE DYNCREATE
позволяет каркасу MFC создавать объекты класса во время выполнения. В обычном
документе MFC, в представлении и в классе обрамляющего окна должен присутство-
вать этот макрос. Дополняющий его макрос IMPLEMENT_DYNCREATE вы добавите в
файл . срр позднее. Я пропустил отладочную версию GetDocument (), поскольку класс
CProductView содержит версию этой функции, верифицирующую объект документа.
Встроенная версия определения класса COrderView просто предполагает, что приве-
дение к CDBSampleDoc* будет нормальным. Я включил объявления AssertValidO и
Dump (), которые компилируются только в случае отладочного кода, так что определе-
ния должны быть включены в файл . срр класса. Перечисление определяет иденти-
фикатор для диалога, и вы используете его в определении конструктора. Член m_pSet
будет содержать адрес объекта набора записей, который поставляет данные для ото-
бражения в этом представлении.
Реализация класса COrderView попадет в файл COrderView. срр, поэтому создайте
этот файл внутри проекта, используя процедуру, которую вы выполнили для файла
.h. Добавьте начальную директиву #include для классов, к которым понадобится об-
ращаться:
#include "stdafx.h"
#include "DBSample.h"
#include "Orderview.h"
#include "OrderSet.h"
#include "DBSampleDoc.h"
Это еще не полный набор — вы добавите еще несколько директив #include по
мере разработки реализации класса.
Далее можете добавить макрос для динамического создания объектов COrderView:
IMPLEMENT—DYNCREATE(COrderView, CRecordView)
Конструктор должен только инициализировать член m_pSet значением NULL:
COrderView::COrderView()
: CRecordView(COrderView::IDD), m_pSet(NULL)
}
Здесь вы вызываете конструктор базового класса с идентификатором диалога,
определенным в перечислении класса, в качестве аргумента. Это идентифицирует
диалог, ассоциированный с представлением.
Теперь добавьте определения двух функций, которые могут использоваться при
выполнении программы в отладочном режиме:
// Диагностика COrderView
#ifdef _DEBUG
void COrderView::AssertValid() const
CRecordView::AssertValid();
void COrderView::Dump(CDumpContext& de) const
CRecordView::Dump(de);
#endif //_DEBUG
Функция DoDataExchange () связывает элементы управления в диалоге с полями
набора записей. Определение этой функции выглядит следующим образом:
Подключение к источникам данных
955
void COrderView::DoDataExchange(CDataExchange* pDX)
CRecordView::DoDataExchange(pDX);
DDX_FieldText(pDX, IDC_ORDERDETAILS_ORDERID,
m_pSet->m_OrderDetailsOrderID, m_pSet);
DDX_FieldText(pDX, IDC_ORDERS_CUSTOMERID,
m_pSet->m_OrdersCustomerID, m_pSet);
DDX_FieldText(pDX, IDC_ORDERDETAILS_PRODUCTID,
m_pSet->m_OrderDetailsProductID, m_pSet);
DDX_FieldText(pDX, IDC_ORDERDETAILS_QUANTITY,
m_pSet->m_OrderDetailsQuantity, m_pSet);
Вы используете член m pSet для доступа к полям отображаемого объекта
COrderset. Второй аргумент каждого вызова метода DDX FieldText () идентифици-
рует элемент управления для поля, указанного в третьем аргументе. Как вы уже виде-
ли в определении класса CProductView, первый аргумент определяет, передаются ли
данные в элемент управления либо из него. Последний аргумент просто идентифици-
рует набор записей, участвующий в процессе.
Две функции должны быть определены для участия в процессе извлечения набора
записей. Вы вызовете функцию GetRecordSet () для получения указателя на объект
COrderset, инкапсулирующий набор записей. Ее можно реализовать следующим об-
разом:
COrderSet* COrderView::GetRecordset ()
ASSERT(m_pSet !=NULL);
return m_pSet;
Член m_pSet содержит указатель на набор записей. Макрос MFC по имени ASSERT
здесь прерывает программу с сообщением, если выражение между скобками дает в
результате вычисления 0. То есть, он просто проверяет, что указатель на COrderset
не равен NULL. Макрос ASSERT обладает тем преимуществом, что работает только в
отладочной версии приложения. В рабочей версии он ничего не делает.
Функция OnGetRecordset () — чистая виртуальная функция базового класса, по-
этому вы обязаны определить ее здесь. Реализовать ее можно так:
CRecordset* COrderView::OnGetRecordset()
return m_pSet;
В данном случае она просто возвратит адрес, содержащийся в m_pSet. Очевидно,
что в ситуациях, когда вам необходимо пересоздавать набор записей, код будет более
сложным.
Однако на этом работа над классом представления не закончена. Следующий шаг —
необходимо более точно определить, что должен содержать набор записей о заказах.
Настройка набора записей
SQL-оператор SELECT для объекта COrderset порождает таблицу, которая будет со-
держать все комбинации записей из двух участвующих таблиц. Этих записей получит-
ся великое множество, поэтому вы должны добавить к запросу эквивалент конструк-
ции WHERE, чтобы ограничить выбираемые записи только теми, что имеют смысл. Но
существует и еще одна проблема: когда вы переключаетесь с отображения таблицы
956 Глава 19
Products, то не хотите видеть никаких старых заказов. Вы хотите видеть только те
заказы, что относятся к идентификатору просматриваемого продукта, то есть имею-
щие тот же Product ID, который содержится в текущей записи CProductset. Это
также повлияет на конструкцию WHERE. В контексте MFC конструкция WHERE SQL-
оператора SELECT для набора записей называется фильтром.
Добавление фильтра к набору записей
Фильтр к запросу добавляется путем присваивания строки члену m_strFilter объ-
екта набора записей. Этот член наследуется от базового класса CRecordSet. Как и с
конструкцией ORDER BY, которую вы добавляете, присваивая значение члену набора
записей m strSort, место для его реализации находится в члене OnlnitialUpdate ()
класса представления записей, непосредственно перед вызовом функции базового
класса.
Вам нужно установить в фильтре два условия. Одно из них служит для ограничения
сгенерированных в наборе записей посредством равенства значения поля OrderlD в
таблице Orders полю с тем же именем таблицы Order Details. Это условие может
быть записано так:
[Orders].[OrderlD] = [Order Details].[OrderlD]
Второе условие, которое необходимо применить к записям, удовлетворяющим
первому условию, заключается в том, что вам нужны только те записи, у которых зна-
чение поля Product ID равно значению поля Product ID текущей записи из объекта
набора, отображенного в таблице Products. Это означает, что вам нужно сравнивать
значение поля Product ID объекта CRecordSet с переменным значением. Переменная
в этой операции называется параметром, и условие фильтра записывается особым об-
разом:
ProductID = ?
Вопросительный знак представляет значение параметра фильтра, а выбранные
записи — это те, у которых поле ProductID равно значению параметра. Значение,
которое подставляется вместо вопросительного знака, устанавливается в функции
DoFieldExchange () — члене объекта набора записей. Очень скоро мы реализуем ее,
но сначала завершим спецификацию фильтра.
Вы можете определить строку переменной фильтра, включающую оба условия,
следующим образом:
//Установить фильтр по равенству поля Product ID соответствующим ID таблицы заказов
m_pSet->m_strFilter =
"[ProductID] = ? AND [Orders].[OrderlD] = [Order Details].[OrderlD]
Этот фрагмент вы включите в член OnlnitialUpdate () класса COrderView, но
перед этим завершите установку параметра фильтра.
Определение параметра фильтра
Добавьте член данных в класс COrderset, предназначенный для хранения те-
кущего значения поля ProductID из объекта CProductset. Этот член также дол-
жен служить параметром для подстановки вместо знака вопроса в фильтре объекта
CProductset. Поэтому щелкните правой кнопкой на имени класса COrderset в Class
View и выберите пункт Add^Add Variable (Добавить1^Добавить переменную) из кон-
текстного меню. Тип переменной должен совпадать с типом члена m_ProductID клас-
са CProductset, который у нас относится к типу long, и вы можете специфициро-
вать его имя как m ProductIDparam.
Подключение к источникам данных
957
Также вы можете оставить его public-членом. Инициализировать этот член дан-
ных нужно в конструкторе, и там же установить счетчик параметров. Каркас прило-
жения требует обязательной установки количества параметров в вашем наборе запи-
сей для отображения числа используемых вами параметров; в противном случае он
не будет правильно работать. Добавьте выделенный полужирным код в определение
конструктора COrderset:
COrderSet::COrderSet(CDatabase* pdb)
: CRecordset(pdb)
m_OrderDetailsOrderID = 0;
m_OrderDetailsProductID = 0;
m_OrderDetailsQuantity ® 0;
m_OrdersOrderID = 0;
m_OrdersCustomerID = L’’”;
m_nFields =5;
m__ProductIDparam = 0L;
m__nParams = 1;
m nDefaultType = snapshot;
Установить начальное значение параметра
Установить количество параметров
Весь невыделенный код генерируется мастером Class Wizard для инициализации
членов данных, соответствующих полям набора записей и специфицирующий тип
как snapshot. Вы должны удалить инициализацию всех прочих полей набора запи-
сей. Новый код инициализирует параметр нулем и устанавливает счетчик параметров
в 1. Переменная m nParams унаследована от базового класса CRecordset. Поскольку
здесь присутствует счетчик параметров, вы можете догадаться, что параметров филь-
тра набора записей может быть и более одного.
В этой точке вы также можете удалить или закомментировать члены класса
COrderset, предназначенные для хранения полей из набора записей, которые вам не
нужны. Удалите или закомментируйте ненужные поля из определения класса, оставив
только следующие:
long m__OrderDetailsOrderID; // То же, что Order ID в таблице Orders.
long mjDrderDetailsProductID; //То же, что Product ID в таблице Products,
int m_OrderDetailsQuantity;
long m_OrdersOrderID; // Уникальный номер заказа.
CString m_OrdersCustomerID; // To же вхождение, что и в таблице Customers,
long m__ProductIDparam;
Чтобы идентифицировать переменную m_ProductIDparam в классе как параметр
для подстановки в фильтр объекта COrderset, вы должны также добавить некоторый
код в функцию-член класса DoFieldExchange ():
void COrderSet::DoFieldExchange(CFieldExchange* pFX)
pFX->SetFieldType(CFieldExchange::outputcolumn);
RFX__Long (pFX, _T(”[Order Details] . [OrderlD]’’), m_OrderDetailsOrderID) ;
RFX_Long(pFX, _T(”[Order Details].[ProductID] "),
m_OrderDetailsProductID);
RFX_Int(pFX, _T("[Order Details].[Quantity]"), m_OrderDetailsQuantity);
RFX_Long(pFX, _T(”[Orders].[OrderlD]”), m_OrdersOrderID);
RFX_Text (pFX, _T (” [Orders] . [CustomerlD] ’’) , m_OrdersCustomerID) ;
// Установить тип поля как параметр
pFX->SetFieldType(CFieldExchange:: param);
RFX__Long(pFX,_T(”ProductIDParam”) , m_ProductIDparam) ;
958 Глава 19
Мастер Class Wizard предоставляет код для передачи данных между базой и пере-
менными полей, которые он добавил к классу. Для каждого члена данных набора за-
писей создается собственный вызов функции RFX_ (). Вы можете удалить те их них,
которые не требуются вашему приложению, оставив лишь те, что показаны в пред-
ыдущем коде.
Первая из новых строк кода содержит вызов члена SetFieldType () объекта pFX
для установки режима следующего вызова RFX_ () в param. Эффект от этого состоит в
том, что третий аргумент во всех последующих вызовах RFX_ () будет интерпретиро-
ван как параметр, заменяющий вопросительный знак в фильтре для набора записей.
Если у вас более одного параметра, то параметры подставляются вместо вопроситель-
ных знаков в строке m_stгFilter в последовательности слева направо, поэтому важ-
но убедиться, что вызовы RFX_ () также расположены в правильном порядке, когда
их более одного. При установке режима в param второй аргумент вызова RFX_ () иг-
норируется, так что вы можете указать вместо него NULL либо любую строку по свое-
му усмотрению.
Инициализация представления записей
Теперь вы можете реализовать переопределение функции OnlnitialUpdate () в
классе COrderView. Эта функция вызывается каркасом MFC перед начальным отобра-
жением представления, так что вы можете поместить в эту функцию код, выполняю-
щий всю необходимую однократную инициализацию. В этом случае вы специфициру-
ете фильтр для набора записей. Вот определение функции, которая это делает.
void COrderView::OnlnitialUpdate()
BeginWaitCursor();
CDBSampleDoc* pDoc = static_cast<CDBSampleDoc*>(GetDocument());
m_pSet = &pDoc->m_OrderSet; // Получить указатель на набор записей
// Использовать открытую БД для набора записей о продуктах
m_pSet->in_pDat abase = pDoc->in_DBSampleSet .m_p Database;
// Установить текущий Product ID в качестве параметра
m_pSet->m_ProductIDparam = pDoc->m_DBSampleSet.m_ProductID;
// Установить фильтр как поле Product ID
m_pSet->m_strFilter «
"[ProductID] = ? AND [Orders].[OrderlD] = [Order Details].[OrderlD]";
CRecordView::OnlnitialUpdate();
EndWaitCursor();
Добавьте это определение функции в Orderview. срр. Версия класса COrderset,
реализованная мастером Class Wizard, не переопределяет член GetDocument (), по-
скольку она изначально не ассоциирована с классом документа. В результате вы долж-
ны привести указатель от члена базового класса GetDocument () к указателю на объект
CDBSampleDoc. Альтернативно вы можете добавить в класс переопределяющую версию
GetDocument (), чтобы она выполняла приведение. Ясно, что вам понадобится указа-
тель на объект документа, поскольку вам необходим доступ к членам этого объекта.
Вызов BeginWaitCursor () в начале OnlnitialUpdate () приводит к отображе-
нию курсора в виде песочных часов во время выполнения этой функции. Причина
этого в том, что на выполнение этой функции может потребоваться ощутимое время,
особенно когда в запросе участвуют несколько таблиц. Обработка запроса и передача
данных в набор записей происходит именно здесь. Курсор возвращается к нормаль-
ному виду вызовом EndWaitCursor () в конце функции.
Подключение к источникам данных
959
Первое, что делает код — это установка члена m_pDatabase объекта COrderset
в то же значение, что и у объекта CProductset. Если вам не нужно это делать, то
каркас заново открывает базу данных при открытии набора записей. Поскольку база
данных уже открыта для набора записей продуктов, это приводит к напрасной трате
времени.
Далее вы устанавливаете значение переменной параметра m_ProductIDparam в
текущее значение, хранящееся в члене m_ProductID набора записей продуктов. Это
значение заменяет вопросительный знак в фильтре при открытии набора записей,
выбирая тем самым те записи, которые вам нужны; затем устанавливается фильтр для
набора записей заказов в строку, которую вы видели ранее.
Доступ к многотабличным представлениям
Поскольку вы реализовали программу в однодокументном интерфейсе, ваше при-
ложение имеет только один документ и одно представление. Доступность единствен-
ного представления может показаться проблемой, но на практике это не так. Вы
можете заставить объект обрамляющего окна программы создать экземпляр класса
COrderView и переключать текущее окно для отображения набора записей заказов.
Вам придется отслеживать текущее окно; это можно делать, присваивая уни-
кальный идентификатор каждому окну представления записи в приложении. Пока
у нас есть только два представления: представление продуктов и представление за-
казов. Чтобы определить идентификаторы для них, создайте новый файл по имени
OurConstants .h и добавьте в него следующий код:
// Определения констант
#pragma once
// Произвольные константы для идентификации представлений записей
const unsigned int PRODUCT_VIEW =1;
const unsigned int ORDER_VIEW =2;
После этого вы можете использовать одну из этих констант для идентификации
каждого представления и записывать идентификатор текущего представления в объ-
екте обрамляющего окна. Для записи идентификатора текущего представления до-
бавьте public-член данных типа unsigned int в класс CMainFrame и назовите его
m_CurrentView!D. Сделав это, вы сможете инициализировать его в конструкторе
класса CMainFrame, добавив для этого следующий код:
CMainFrame::CMainFrame()
: ni—Cur ген tViewID (PRODUCT—VIEW)
{
Изначально приложение стартует с представления продуктов, поэтому вы ини-
циализируете m_CurrentViewID соответствующим образом. Добавьте директи-
ву #include для OurConstants .h в начало MainFrm. срр, чтобы это определение
PRODUCT—VIEW было доступно в исходном файле.
Переключение представлений
Чтобы активизировать механизм переключения представлений, вы добавляете
общедоступную функцию-член в класс CMainFrame, именуемую SELECTView (), с па-
раметром, указывающим новый идентификатор. Эта функция выполняет переключе-
ние текущего представления на любое, специфицированное переданным в аргументе
идентификатором.
960 Глава 19
Щелкните правой кнопкой мыши на СМаinFrame и выберите Add^Add Function
(Добавить^Добавить функцию) из контекстного меню, чтобы добавить новый член
к классу. Укажите в качестве типа возврата void, а в качестве имени функции —
Selectview. Именем параметра может быть ViewID, а его типом — unsigned int.
Реализация этой функции выглядит следующим образом.
void CMainFrame::SelectView(unsigned int ViewID)
CView* pOldActiveView = GetActiveView(); //Получить текущее представление
// Получить указатель на новое представление, если оно существует.
// Если оно не существует, указатель будет равен null.
CView* pNewActiveView = static_cast<CView*>(GetDlgltem(ViewID));
// Если это первое обращение к новому представлению,
// оно еще не существует, поэтому его нужно создать
if (pNewActiveView == NULL)
switch(ViewID)
case ORDER_VIEW: // Создать представление Order
pNewActiveView = new COrderView;
break;
default:
AfxMessageBox(L"Invalid View ID") ;
return;
// Переключение представлении
// Получить контекст текущего представления для применения нового значения
CCreateContext context;
context.m_pCurrentDoc = p01dActiveView->GetDocument();
pNewActiveView->Create(NULL, NULL, OL, CFrameWnd::rectDefault,
this, ViewID, &context);
pNewActiveView->OnInitialUpdate();
SetActiveView(pNewActiveView); // Активизировать новое представление
p01dActiveView->ShowWindow(SW_HIDE); // Скрыть старое представление
pNewActiveView->ShowWindow(SW_SHOW); // Показать новое представление
pOldActiveView->SetDlgCtrlID(m_CurrentViewID); // Установить ID
// старого представления
pNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
m_CurrentViewID = ViewID; // Сохранить ID нового представления
RecalcLayout();
Операции этой функции делятся на три части.
1. Получение указателей на текущее представление и новое представление.
2. Создание нового представления, если оно еще не существует.
3. Помещение нового представления на место текущего.
Адрес текущего активного представления предоставляется членом GetActiveView ()
объекта СМаinFrame. Чтобы получить указатель на новое представление, вы вызыва-
ете член GetDlgltem () объекта обрамляющего окна. Если представление с иденти-
фикатором, указанным в аргументе функции, существует, возвращается адрес пред-
ставления, в противном случае возвращается NULL, и вам нужно будет создать новое
представление.
Подключе:
сточникам данных
961
После создания объекта представления вы определяете объект context типа
CCreateContext. Объект CCreateContext необходим только тогда, когда вы созда-
ете окно для представления, которое должно быть подключено к документу. Объект
CCreateContext содержит данные-члены, которые могут быть связаны с документом,
обрамляющим окном и представлением, а для приложений MDI — также и с шаблоном
документа. Когда вы переключаетесь между представлениями, вы создаете новое окно
для нового представления, подлежащего отображению. Всякий раз, когда вы создаете
окно нового представления, вы используете объект CCreateContext для установки
соединения между представлением и объектом документа. Вы должны сохранить ука-
затель на объект документа только в члене m pCurrentDoc на context. Вообще вам
может понадобиться сохранить дополнительные данные в объекте CCreateContext,
прежде чем вы создадите представление; это зависит от конкретных условий и вида
создаваемого окна.
В вызове члена Create () объекта представления, который создает окно для но-
вого представления, в качестве аргумента передается объект context. Это устанав-
ливает правильное отношение между вашим документом и верифицирует указатель
документа. Аргумент this в вызове Create () специфицирует текущее обрамляющее
окно в качестве родительского окна, а аргумент ViewID — идентификатор окна. Этот
идентификатор позволяет получить адрес окна при последующих вызовах члена
GetDlgltem () родительского окна.
Чтобы сделать новое представление активным, вы вызываете член SetActiveView ()
класса CMainFrame. Затем новое представление заменяет текущее активное представ-
ление. Для удаления окна старого представления вызывается член ShowWindow ()
представления с аргументом SWHIDE, используя указатель на старое представление.
Чтобы отобразить окно нового представления, вызывается та же функция с аргумен-
том SW_SHOW, используя указатель на новое представление.
SetActiveView(pNewActiveView); // Активизировать новое представление
p01dActiveView->ShowWindow(SW_HIDE); // Скрыть старое представление
pNewActiveView->ShowWindow(SW_SHOW); // Показать новое представление
p01dActiveView->SetDlgCtrlID(m_CurrentViewID);// Установить ID старого
// представления
pNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
m_CurrentViewID = ViewID; // Сохранить ID нового представления
Вы восстанавливаете идентификатор старого активного представления в значение
идентификатора, которое вы определили для него в члене m_CurrentViewID класса
CMainFrame, добавленного ранее. Вы также устанавливаете идентификатор нового
представления в AFX IDW_PANE FIRST для идентификации его в качестве первого
окна приложения. Это необходимо, потому что приложение имеет только одно пред-
ставление, поэтому первое представление — оно же и единственное. Позднее вы со-
храняете идентификатор нового окна в члене m_CurrentViewID, так чтобы можно
было следующий раз заменить текущее представление. Вызов RecalculateLayout ()
приводит к перерисовке при выборе нового представления.
Вам следует добавить директиву #include для Orderview.h в начало файла
MainFrm.срр, чтобы в нем было доступно определение класса COrderView. После со-
хранения MainFrm.срр можно перейти к добавлению кнопки к диалогу Products для
вызова диалога Orders. Затем вы сможете добавить обработчики для этой кнопки и
ее аналога в диалоге Orders для вызова члена SELECTView () класса CMainFrame.
962 Глава 19
Обеспечение операции переключения
Чтобы реализовать механизм переключения представлений, вернитесь на вкладку
Resource View и откройте IDD_DBSAMPLE_FORM. Вы должны добавить кнопочный эле-
мент управления в диалоговое окно, как показано на рис. 19.27.
ProductID Sample ed Category ID Sample ed
Product Name sample edit box
Unit Price I sample ed Units In Stock I sample ed
Units On Order | Sample ed Show Orders I
Puc. 19.27. Добавление кнопки Show Orders
(Показать заказы)
Установите ID этой кнопки в IDC_ORDERS, в соответствии с другими элементами
управления в диалоговом окне.
После сохранения ресурса вы можете создать обработчик для кнопки, щелкнув
на ней правой кнопкой мыши и выбрав Add Event Handler (Добавить обработчик со-
бытий) из контекстного меню. Воспользуйтесь мастером добавления обработчиков
событий (Event Handler Wizard), чтобы добавить в класс CProductView функцию
OnOrders () для сообщения BN_CLICKED; этот обработчик вызывается при щелчке на
кнопке. Для завершения этого обработчика потребуется добавить всего одну строку
кода:
void CProductView::OnOrders()
static_cast<CMainFrame*> (GetParentFrame ()) ->SelectView(ORDER_VIEW) ;
}
Член Get Pa rent Frame () объекта представления наследован от CWnd — непрямо-
го базового класса CMainFrame. Функция возвращает указатель на родительское об-
рамляющее окно, и вы используете его для вызова функции SELECTView (), которую
только что добавили в класс CMainFrame. Значение аргумента ORDER_VIEW заставляет
обрамляющее окно переключиться на диалоговое окно Orders. Если это происходит
первый раз, то при этом создается объект представления и соответствующее окно.
При втором и последующих вызова выполняется переключение на выбранное окно
к выбранному представлению заказов, используя повторно уже существующее пред-
ставление.
Вы должны добавить следующие директивы #include в ProductView.cpp:
#include "OurConstants.h"
#include "MainFrm.h"
Следующая задача — добавить обработчик для кнопки, которую вы ранее поме-
стили в диалог IDD_ORDERS_FORM. В окне редактора, показывающем этот диалог, вос-
пользуйтесь тем же процессом для добавления обработчика OnProductsO в класс
COrderView и добавьте в его реализацию единственную строку кода:
Подключение к источникам данных 963
void COrderView::OnProducts()
static_cast<CMainFrame*>(GetParentFrame ()) ->SelectView(PRODUCT_VIEW);
Это работает так же, как и обработчик щелчка на предшествующей кнопке.
Опять-таки вы должны добавить директивы #include для файлов OurConstants ,h и
MainFrm.h в начало файла Orderview.срр и затем сохранить его.
Обработка активизации представления
Когда вы переключаетесь на представление, которое уже существует, то должны
убедиться, что набор записей обновлен, что диалог повторно инициализирован для
отображения корректной информации. Когда активизируется или деактивизирует-
ся существующее представление, каркас вызывает функцию-член OnActivateView ()
этого класса, так что это подходящее место для обновления набора записей и диа-
логового окна. Вы можете переопределить эту функцию в каждом из классов пред-
ставлений. Вы можете сделать это, щелкнув на кнопке Overrides (Переопределения)
в окне Properties для класса представления и выбрав из списка OnActivateView. Не
забудьте добавить переопределение этой функции к обоим классам представлений.
Для завершения реализации переопределенной функции OnActivateView () клас-
са COrderView добавьте следующий код:
void COrderView::OnActivateView(BOOL bActivate,
CView* pActivateView, CView* pDeactiveView)
if (bActivate)
// Получить указатель на документ
CDBSampleDoc* pDoc = GetDocument ();
// Получить указатель на родительское окно
CMainFrame* pMFrame = static cast<CMainFrame*> (GetParentFrame ());
// Если последним представлением было представление продуктов, нужно
И повторно запросить набор записей с Product ID из набора записей продуктов
if (pMFrame->m CurrentViewID=PRODUCT VIEW)
// Проверить, что набор записей открыт
if (!m_pSet->IsOpen())
return;
// Установить текущий Product ID в качестве параметра
m_j?Set->m_ProductIDparam = pDoc->m__DBSampleSet. m_ProductID;
m_pSet->Requery () ; // Получить данные из БД
// Если мы достигли EOF, записей больше нет
if (mj?Set->IsEOF ())
AfxMessageBox(L”No orders for the current product ID”) ;
// Установить заголовок окна
CString strTitle = _Т("Table Name: ”);
CString strTable = m_jpSet->GetTableName ();
if(!strTable.IsEmpty())
strTitle += strTable;
else
strTitle += _T("Orders - Multiple Tables") ;
pDoc->SetTitie(strTitle);
CRecordView::OnlnitialUpdate() ; // Обновить значения в диалоге
CRecordView: .‘OnActivateView(bActivate, pActivateView, pDeactiveView);
964 Глава 19
Вы выполняете этот код, только если представление активизировано, и в этом
случае аргумент bActivate равен TRUE. После получения указателей на документ и
родительское обрамляющее окно, вы проверяете, было ли предыдущим представле-
нием представление продуктов, а затем повторно запрашиваете набор записей. Эта
проверка пока не обязательна, поскольку предыдущим представлением всегда будет
представление продуктов, но когда вы добавите к приложению другое представление,
это будет не всегда верно, так что лучше поместить этот код уже сейчас.
Чтобы повторно запросить базу данных, вы устанавливаете параметр-член
COrderset по имени m_ProductIDparam в текущее значение члена m_ProductID на-
бора записей продуктов. Это заставит выбрать заказы для текущего продукта. Вы не
должны устанавливать член m_strFilter набора записей, поскольку он был установ-
лен в функции OnlnitialUpdate (), когда впервые создавался при первом создании
объекта CRecordView. Функция-член IsEOF () объекта COrderset унаследована от
CRecordset и возвращает TRUE, если набор записей пуст при его пересоздании.
Добавьте в функцию OnActivateView () класса CProductView следующий код:
void CProductView:‘.OnActivateView(BOOL bActivate,
CView* pActivateView, CView* pDeactiveView)
if (bActivate)
{
// Обновить заголовок окна
CString strTitle = _T("Table Name") ;
CString str Table
m_pSet->GetTableNaine ();
strTitle +=_T(": ") + strTable; '
GetDocument()->SetTitle(strTitle);
}
CRecordView::OnActivateView(bActivate, pActivateView, pDeactiveView);
В данном случае все, что нужно делать для активизации представления — это об-
новить заголовок окна. Поскольку представление продуктов — ведущее представление
для остальной части приложения, вы всегда захотите вернуть представление в его со-
стояние перед тем, как деактивизировать. Если вы не делаете ничего помимо обнов-
ления заголовка окна, представление отображается в своем предыдущем состоянии.
Просмотр заказов для продукта
Теперь вы готовы к тому, чтобы собрать исполняемый модуль новой версии при-
мера. Когда вы запустите пример, то должны будете иметь возможность просматри-
вать заказы для любого продукта простым щелчком на кнопке Show Orders (Показать
заказы) в диалоговом окне продуктов. Типичное представление заказа можно видеть
на рис. 19.28.
Щелчок на кнопке Show Products (Показать продукты) возвращает в диалоговое
окно продуктов, так что вы сможете продолжить просмотр списка продуктов. В этом
диалоге вы можете использовать кнопки панели инструментов для просмотра всех за-
но. Давайте добавим еще одно представление для отображения подробностей имени
и адреса заказчика. Это будет не особенно сложно, поскольку механизм для переклю-
чения между представлениями уже создан.
Подключение к источникам данных
965
Рис. 19.28. Типичное представление заказа
Просмотр подробностей о заказчике
Базовый механизм, который вы добавите, будет работать через другой кнопочный
элемент управления в диалоге заказа, который позволит переключиться к новому диа-
логу с данными заказчика. Наряду с элементами управления, предназначенными для
отображения данных заказчика, вы добавите сюда еще две кнопки: одну для возврата
в представление заказов, и еще одну — для возврата в представление продуктов. Вам
понадобится другой идентификатор, соответствующий представлению заказчиков, и
его можно добавить с помощью следующей строки кода в файле OurConstants. h:
const unsigned int CUSTOMER__VIEW = 3;
Теперь добавив набор записей для подробностей о заказчике.
Добавление набора записей заказчиков
Процесс точно такой же, как и тот, что вы проходили в классе COrderset. Исполь-
зуйте пункт контекстного меню Add1^ Class (Добавить1^Класс) в Class View и выбери-
те шаблон MFC ODBC Consumer (Потребитель MFC ODBC) для определения клас-
са CCus tome г Set, с классом CRecordset в качестве базового. Выберите базу данных
North wind, как и ранее, и таблицу Customer для набора записей. Выберите Snapshot
(Снимок) в качестве типа доступа к таблице. Затем класс должен быть создан с данны-
ми-членами, перечисленными ниже:
CStringW m_CustomerID;
CStringW m_CompanyName;
CStringW m_ContactName;
CStringW m_ContactTitle;
CStringW m_Address;
CStringW m_City;
CStringW m_Region;
CStringW m_PostalCode;
CStringW m_Country;
CStringW m_Phone;
CStringW m_Fax;
966 Глава 19
Не забудьте закомментировать директиву #еггог в файле Customer Set. срр.
Замените тип CStringW на CString и затем сохраните файл класса. В этот момент
вы можете добавить член CCustomerView к документу, чтобы он создавался вместе
с созданием объекта документа. Щелкните правой кнопкой мыши на имени класса
CDBSampleDoc в Class View и добавьте переменную типа CCustonerSet по имени
m_CustomerSet. Спецификатор доступа можно оставить как public.
Вы обнаружите, что директива #include для CustomerSet.h уже добавлена к
DBSampleDoc. h. После сохранения модифицированных вами файлов создайте ресурс
диалога заказчика.
Создание ресурса для диалога заказчика
Этот процесс в точности повторяет тот, что применялся для создания диалога за-
казов. Перейдите в Resource View и создайте новый диалоговый ресурс с идентифи-
катором, равным IDD_CUSTOMER_FORM, не забыв установить стиль Child и рамку None
в окне свойств диалога. После удаления кнопок по умолчанию добавьте в диалоговое
окно элементы управления, соответствующие именам полей таблицы Customers, как
показано на рис. 19.29.
Л......................... ......................................................................................................................
Customer ID
Company Name
Address
City
Phone
I
Sample edit box
Sample edit box
I
Sample edit box
Sample edit box
Sample edit box
Show Orders
S
Show Products
Рис. 19.29. Диалог отображения деталей о заказчике
Две кнопки позволяют переключаться либо на диалог заказов, из которого вы по-
падаете в данный диалог, либо непосредственно на диалог продуктов. Размер окна
приложения определяется размером первого отображенного диалогового окна, поэ-
тому поскольку диалог заказчика немного больше, увеличьте размер диалога Products
по меньшей мере до его размера.
Специфицируйте идентификаторы элементов управления, используя имена полей
в качестве основы. В этом может помочь отображение списка членов CCustomerSet
в Class View. Присвойте идентификаторам кнопок значения IDC ORDERS и
IDC—PRODUCTS. После сохранения диалогового ресурса вы готовы к созданию класса
представления для набора записей.
Создание класса представления заказчиков
Класс представления для набора записей заказчиков создается вручную — так же,
как это делалось для класса COrderView. Добавьте в проект файлы Customerview.h
и Customerview, срр и вставьте следующий код определения класса в файл
Customerview.h:
Подключение к источникам данных
967
// Представление записей CCustomerView
#pragma once
class CCustomerSet;
class CDBSampleDoc;
class CCustomerView : public CRecordView
DECLARE—DYNCREATE(CCustomerView)
public:
enum { IDD = IDD_CUSTOMER—FORM };
CCustomerSet* m_pSet;
public:
CCustomerView();
CCustomerSet* GetRecordset();
virtual CRecordset* OnGetRecordset();
protected:
virtual void DoDataExchange(CDataExchange* pDX) ; // Поддержка DDX/DDV
virtual void OnlnitialUpdate();
virtual void OnActivateView(BOOL bActivate, CView* pActivateView,
CView* pDeactiveView);
// Реализация
protected:
virtual ^CCustomerView(){}
#ifdef _DEBUG
virtual void AssertValidO const;
virtual void Dump(CDumpContext& de) const;
#endif
Этот класс содержит, по сути, те же члены, что и Customerview.
Добавьте приведенный ниже начальный код в CustomerView. срр.
// Реализация CCustomerView
#include ’’stdafx.h"
#include ”resource.h”
IMPLEMENT_DYNCREATE(CCustomerView, CRecordView)
CCustomerView::CCustomerView(): CRecordView(CCustomerView::IDD), m_j?Set(NULL)
CCustomerSet* CCustomerView::GetRecordset()
ASSERT(m_pSet !=NULL);
return m__pSet;
CRecordset* CCustomerView::OnGetRecordset()
return m_pSet;
// Диагностика COrderView
#ifdef _DEBUG
void CCustomerView::AssertValid() const
CRecordView::AssertValid();
void CCustomerView::Dump(CDumpContext& de) const
CRecordView::Dump(de);
#endif //_DEBUG
968 Глава 19
Как видите, это такой же стандартный код, что и в классе COrderView, и в нем
определены такие же функции. Первая директива # include предназначена для пред-
варительно скомпилированных заголовков, а вторая представляет определения иден-
тификаторы ресурсов.
Обработка щелчков на кнопочных элементах управления осуществляется в диало-
говом окне IDD_CUSTOMER_FORM таким же способом, как это делалось ранее при до-
бавлении функций OnOrders () и OnProducts() в класс CCustomerView.
void CCustomerView::OnOrders()
static cast<CMainFrame*> (Ge tParen tFrame ())->SelectView (ORDER VIEW);
Аналогичную строку кода потребуется добавить и в функцию OnProducts ().
void CCustomerView::OnProducts()
static__cast<CMainFrame*>(GetParentFrame ()) ->SelectView (PRODUCT VIEW) ;
После этого нужно добавить код для спецификации фильтра для набора записей
заказчиков, чтобы получать подробности только о том заказчике, который соответ-
ствует значению поля Customer ID из текущего заказа в объекте COrderset.
Добавление фильтра
Вы можете определить фильтр в члене OnlnitialUpdate () класса CCustomerView.
Поскольку подразумевается возврат только одной записи, соответствующей каждому
Customer ID, о сортировке беспокоиться не придется. Ниже показан код этой функ-
ции.
void CCustomerView::OnlnitialUpdate()
BeginWaitCursor();
CDBSampleDoc* pDoc = static_cast<CDBSampleDoc*>(GetDocument());
m_pSet = &pDoc->m_CustomerSet; //Инициализация указателя на набор записей
// Установка БД для набора записей заказчиков
m_pSet->m_pDatabase = pDoc->m_DBSampleSet.m_pDatabase;
// Установка текущего Customer ID в качестве значения параметра фильтра
m_pSet->m_CustomerIDparam = pDoc->m_OrderSet.m_OrdersCustomerID;
m_pSet->m_strFilter =”CustomerID = // Фильтр по полю CustomerlD
CRecordView::OnlnitialUpdate();
if (m_pSet->IsOpen())
CString strTitle = m_pSet->m_pDatabase->GetDatabaseName();
CString strTable = m_pSet->GetTableName();
if(!strTable.IsEmpty())
strTitle += _T(":”) + strTable;
GetDocument()->SetTitle(strTitle);
EndWaitCursor();
После получения указателя на документ вы сохраняете адрес члена-объекта
CCustomerSet документа в члене m_pSet представления. Вы знаете, что база данных
уже открыта, поэтому можете установить указатель базы в наборе записей заказчиков
равным тому, что уже есть в объекте CProductset.
Подключение к источникам данных 969
Параметр фильтра будет определен в члене m_CustomerIDparam класса CCustomerSet.
Очень скоро вы добавите этот член к классу. Он устанавливается в текущее значение
по члену m CustomerlD объекта COrderset, принадлежащего документу. Вы опреде-
лите фильтр таким способом, чтобы набор записей содержал только запись с тем же
Customer ID, что и в текущем заказе.
Функция OnActivateView () обрабатывает активизацию представления заказчи-
ков, и вы можете реализовать ее в Customerview, срр следующим образом:
void CCustomerView::OnActivateView(BOOL bActivate,
CView* pActivateView, CView* pDeactiveView)
if(bActivate)
if(’m_pSet->IsOpen())
return;
CDBSampleDoc* pDoc = static_cast<CDBSampleDoc*>(GetDocument());
// Установить текущий Customer ID в качестве параметра
m_pSet->m_CustomerIDparam = pDoc->m_OrderSet .m_OrdersCustomerID;
m_pSet->Requery(); // Получить данные из <L
CRecordView::OnlnitialUpdate(); // Перерисовать диалог
// Проверить, не пустой ли набор записей
if(m_pSet->IsEOF())
AfxMessageBox(Ь”Нет подробностей о заказчике для текущего customer ID");
CString strTitle = _T(’’Table Name:’’);
CString strTable = m pSet->GetTableName();
if(!strTable.IsEmpty())
strTitle += strTable;
else
strTitle += _T (’’Multiple Tables’’);
pDoc->SetTitle(strTitle);
CRecordView::OnActivateView(bActivate, pActivateView, pDeactiveView);
Если эта функция вызывается по причине активизации (а не деактивизации) пред-
ставления, то bActivate имеет значение TRUE. В этом случае вы устанавливаете па-
раметр фильтра по набору записей заказов и повторно отправляете запрос к базе дан-
ных.
Член m CustomerlDparam набора записей объекта CCustomerSet, ассоциирован-
ного с этим объектом представления, устанавливается по Customer ID из набора за-
писей заказов, хранящегося в документе. Это будет Customer ID для текущего заказа.
Вызов функции Requery () объекта CCustomerSet извлекает записи из базы с исполь-
зованием установленного вами фильтра. В результате вы получаете подробности о за-
казчике, разместившем текущий заказ, в объекте CCustomerSet, которые затем пере-
даются объекту CCustomerView для отображения диалога.
Добавьте следующие операторы tfinclude в начало файла Customerview, срр:
#include ’’Productset .h”
#include "OrderSet.h"
#include ’’CustomerSet .h"
#include ’’DBSampleDoc .h”
#include ’’OurConstants. h”
#include ’’MainFrm.h’’
970 Глава 19
Необходимость в первых трех операторах объясняется тем, что в них определены
классы, использованные в определении класса документа. DBSampleDoc.h нужен по-
тому, что ссылка на класс CDBSampleDoc встречается в OnlnitialUpdate (), а осталь-
ные два файла . h содержат определения, на которые есть ссылки в обработчиках
кнопок в классе CCustomerView.
Реализация параметра фильтра
Добавьте public-переменную типа CString в класс CCustomerSet для соответ-
ствия члену m_CustomerID набора записей и дайте ей имя m_CustomerIDparam. Если
вы используете для этого механизм Add*=>Add Variable (Добавить1^Добавить перемен-
ную) в Class View, то новый член уже будет инициализирован в конструкторе; в про-
тивном случае добавьте инициализацию в последующий код. Установите счетчик па-
раметров в конструкторе CCustomerSet следующим образом:
CCustomerSet::CCustomerSet(CDatabase* pdb)
: CRecordset(pdb)
, m_CustomerIDparam(_T""))
m_CustomerID = _T(””);
m_CompanyName = _T("");
m_ContactName = _T(””);
m_ContactTitle = _T ('"');
m_Address = _T(””);
m_City = _T("");
m_Region = _T("”);
m^PostalCode = _T(””);
m_Country = _T (’”’);
m_Phone = _T (;
m__Fax - _T (” ") ;
m_nFields = 11;
m_nParams = 1; // Количество параметров
m__nDefaultType = snapshot;
Член m_CustomerIDparam инициализируется пустой строкой, а счетчик параме-
тров в m_nParams устанавливается в 1.
Чтобы установить параметр m_CustomerIDparam, вы добавляете операторы в член
DoFieldExchange (),как и ранее:
void CCustomerSet::DoFieldExchange(CFieldExchange* pFX)
pFX->SetFieldType(CFieldExchange::outputcolumn);
RFX_Text(pFX, _T(”[CustomerlD]"), m_CustomerID);
RFX_Text(pFX, _T("[CompanyName]"), m_CompanyName);
RFX_Text (pFX, _T (’’ [ContactName] ”) , m_ContactName) ;
RFX_Text (pFX, _T (’’ [ContactTitle] ”) , m_ContactTitle) ;
RFX_Text (pFX, _T (’’ [Address] ’’), m_Address);
RFX_Text(pFX, _T(”[City]”), m_City);
RFX_Text (pFX, _T (" [Region] ’’), m_Region);
RFX_Text (pFX, _T(” [PostalCode] ’’), m_PostalCode) ;
RFX_Text (pFX, _T (’’ [Country] ’’), m_Country) ;
RFX_Text (pFX, _T (” [Phone] ’’) , m_Phone) ;
RFX_Text(pFX, _T(”[Fax]”), m_Fax);
pFX->SetFieldType(CFieldExchange::param); // Установить режим param
RFX_Text(pFX, _T("CustomerlDParam"), m_CustomerIDparam);
Подключение к источникам данных
971
Я пропустил строки комментариев в начале функции, чтобы сэкономить место.
После установки режима param вызовом члена SetFieldType () объекта pFX вы вы-
зываете функцию RFX_Text () для передачи значения параметра для подстановки в
фильтре. Вы используете здесь RFX_Text () потому, что переменная параметра имеет
тип CString. Существуют различные функции RFX_ (), поддерживающие диапазон
типов параметров.
После завершения этой модификации сохраните файл Customer Set .срр.
Связывание диалога заказов с диалогом
информации о заказчике
Чтобы дать возможность переключаться на диалог информации о заказчике, по-
требуется кнопочный элемент управления в диалоге IDD_ORDERS_FORM, поэтому
откройте его в Resource View и добавьте дополнительную кнопку Show Customer
(Показать заказчика), как показано на рис. 19.30.
Order ID Sample ed
Customer ID
Sample ed
Quantity
Product ID Sample ed
Sample ed
Puc. 1930. Добавление кнопки Show Customer
Я слегка изменил исходное расположение элементов управления. Вы можете рас-
положить их по-своему. Определите идентификатор для нового кнопочного элемен-
та управления как IDC CUSTOMER. После сохранения диалога добавьте обработчик
кнопки щелчком правой кнопкой мыши на ней и выбором пункта Add Event Handler
(Добавить обработчик событий) из контекстного меню. Обработчик требует только
одной строки кода:
void COrderView::OnCustomer()
static__cast<CMainFrame*> (GetParen tFrame ()) ->SelectView (CUSTOMER_VIEW);
}
Это получает адрес обрамляющего окна и использует его для вызова члена
SELECTView О класса CMainFrame для переключения на представление заказчиков.
Предпоследний шаг, необходимый для завершения программы — добавление кода к
функции SELECTView (), который имеет дело с представлением CUSTOMER_VIEW, пе-
реданным ему. Это потребует только трех строк кода, как показано ниже.
void CMainFrame::Selectview(UINT ViewID)
CView* pOldActiveView = GetActiveView(); // Получить текущее представление
// Получить указатель на новое представление, если оно существует.
// Если оно не существует, указатель будет равен null.
CView* pNewActiveView = static cast<CView*>(GetDlgltem(ViewID));
972 Глава 19
// Если это первое обращение к новому представлению,
// оно еще не существует, поэтому его нужно создать
if (pNewActiveView == NULL)
switch(ViewID)
case ORDER_VIEW: // Создать представление заказов
pNewActiveView = new COrderView;
break;
case CUSTOMER_VIEW:
pNewActiveView =
break;
// Создать представление заказчика
new CCustomerView
default:
AfxMessageBox("Invalid View ID");
return;
// Переключение представлении
11 Получить контекст текущего представления для применения
//в новом представлении
CCreateContext context;
context. m_pCurrentDoc = pOldAc tiveView->Ge tDocumen t ();
pNewActiveView-XZreate (NULL, NULL, OL, CFrameWnd::rectDefault,
this, ViewID, & con text);
pNewActiveView->OnInitialUpdate();
SetActiveView (pNewActiveView); // Активизировать новое представление
pOldActiveView->ShowWindow(SW_HIDE); // Скрыть старое представление
pNewActiveView->ShowWindow (SW—SHOW); // Показать новое представление
pOldActiveView->SetDlgCtrlID (m—CurrentViewID);// Установить ID старого
// представления
pNewActiveView->SetDlgCtrlID (AFX_IDW_PANE_FIRST) ;
CurrentViewID = ViewID; 11 Сохранить ID нового представления
RecalcLayout();
Единственное изменение, необходимое вдобавок к оператору case в switch — соз-
дать объект CCustomerView, когда он не существует. Каждый объект представления
будет использован повторно при следующем обращении, поэтому они создаются
только один раз. Код переключения между представлениями работает с любым их ко-
личеством, поэтому если вы хотите, чтобы эта функция справлялась с большим чис-
лом представлений, вы просто должны добавить дополнительные case в оператор
switch для каждого нового представления, которое вам понадобится. Хотя объекты
представлений создаются здесь динамически, вам не нужно заботиться об их удале-
нии. Поскольку они ассоциированы с объектом документа, каркас их удалит при за-
крытии приложения.
Поскольку вы обращаетесь к классу CCustomerView в функции SELECTView (),
нужно добавить оператор #include для файла Customerview.h в начальный блок
MainFrm.срр.
Чтобы завершить приложение, добавьте реализацию функции DoDataExchange ()
в класс CCustomerView в файле Customerview, срр:
включение к источникам данных
973
void CCustomerView::DoDataExchange(CDataExchange* pDX)
CRecordView::DoDataExchange(pDX);
DDX_FieldText(pDX, IDC_ADDRESS,
m_pSet->m_Address, m_pSet);
DDX_FieldText(pDX, IDC_CITY,
m_pSet->m_Cityz m_pSet);
DDX_FieldText(pDX, IDC_COMPANYNAME,
m_pSet->m_CompanyName, m_pSet);
DDX_FieldText(pDX, IDC_PHONE,
m_pSet->m_Phone, m_pSet);
DDX_FieldText(pDX, IDC_CUSTOMERID,
m_pSet->m__CustomerID, m_pSet);
Это использует функции DDX_, как и раньше, для передачи данных от редактиру-
ющих элементов управления к членам класса CCustomerView, Чтобы все это коррек-
тно с компилировалось, потребуется добавить директиву # include для заголовочного
файла Customerview. h.
Испытание программы просмотра базы данных
К этому моменту программа готова. Вы можете собрать приложение и запустить
его. Как и ранее, главным представлением базы данных является представление про-
дуктов. Как и ранее, щелчок на кнопке Show Orders (Показать заказы) открывает
представление заказов. Вторая кнопка формы должна быть активной, и щелчок на
ней позволит просмотреть подробности о заказчике, разместившем текущий заказ,
что проиллюстрировано на рис. 19.31.
Table Name: [Customers] - DBSample
File Edit Record View Help
p e a i н
Customer ID
Company Marne
Address
City
Phone
Show Orders
Ready
QUICK
QUICK-Stop
TaucherstraBe 10
Cunewalde
0372-03 5188
iShow Products
Puc. 1931. Работа программы просмотра базы данных
Эти две кнопки позволяют переключаться к представлениям заказов и продуктов
соответственно.
974 Глава 19
Резюме
Теперь вы должны чувствовать себя уверенно в отношении того, как MFC связыва-
ется с базами данных через ODBC.
Ниже перечислены ключевые моменты, с которыми вы познакомились в настоя-
щей главе.
□ Библиотека MFC обеспечивает поддержку OLE DB и ODBC для доступа к базам
данных.
□ Чтобы использовать базу данных через ODBC, она должна быть зарегистриро-
вана.
□ Подключение к базе представлено объектами CDatabase и CDaoDatabase.
□ Объект набора записей представляет SQL-оператор SELECT, примененный к
определенному набору таблиц. Где необходимо, каркас автоматически созда-
ет объект базы данных, представляющий подключение к базе, когда создается
объект набора записей.
□ Конструкция WHERE может быть добавлена к объекту — набору записей через
член данных m_st г Filter.
□ Конструкция ORDER BY может быть добавлена к объекту — набору записей через
член данных m_strSort.
□ Объект представления записей служит для отображения содержимого объекта
набора записей.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Вновь используя таблицу Products, добавьте к приложению диалоговое окно
управления запасами. Оно должно быть доступно через кнопку в диалоговом
окне продуктов и должно само содержать кнопку возврата к диалогу продуктов.
Поля, которые оно должно отображать, идентификатор продукта, наимено-
вание продукта, нижний уровень запасов, при котором их следует пополнять,
цену за единицу и количество единиц на складе. Пока не беспокойтесь о филь-
трации или сортировке — просто добейтесь работоспособности базового меха-
низма.
2. Усовершенствуйте предыдущий проект так, чтобы диалог управления запасами
автоматически отображал информацию о продуктах, которые были показаны в
диалоговом окне продуктов на момент щелчка на кнопке.
3. Реализуйте систему, которая предупреждала бы пользователя базы данных в
окне управления заказами о том, что текущий запас ниже или близок к границе,
когда его следует пополнить. Не извещайте пользователя об этом, если установ-
лена нулевая граница минимальных запасов для некоторых продуктов.
20
Обновление
источников данных
В этой главе, на основе знаний о доступе к базам данных через интерфейс ODBC,
полученных в предыдущей главе, вы получите возможность испытать свои силы в об-
новлении базы данных Northwind Traders.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
□ Транзакции базы данных.
□ Способы обновления базы данных с помощью объектов набора записей.
□ Передача данных из набора записей в базу данных в ходе выполнения опера-
ции обновления.
□ Методика обновления существующей строки в таблице.
□ Методика добавления новой строки в таблицу.
Операции обновления
При написании кода, обеспечивающего только просмотр информации базы дан-
ных, единственный возникающий при этом вопрос — наличие полномочий для по-
лучения доступа к данным. До тех пор, пока база данных обладает соответствующей
защитой доступа, данные в ней находятся в безопасности. Однако, как только дело до-
ходит до написания кода обновления базы данных, ситуация существенно меняется.
Поскольку код изменяет содержимое базы данных, это может привести к нарушению
ее целостности и превратить содержимое таблицы в бессмыслицу или даже сделать
ее непригодной к использованию. Прежде чем применять код в реальных приложе-
ниях, всегда необходимо тщательно его проверять на тестовой базе данных.
Как правило, обновление базы данных предполагает изменение одного или не-
скольких полей в строке существующей таблицы (например, изменение объема зака-
за) или добавление новой строки — например, строки нового заказа в базе данных
976 Глава 20
Northwind. Мы разработаем примеры кода для выполнения обеих этих задач, но вна-
чале рассмотрим связанные с этим проблемы.
Большинство осложнений, которые могут возникать при выполнении операций
обновления базы данных, становятся очевидными при рассмотрении многопользо-
вательских баз данных. При отсутствии соответствующего контроля за процессом
обновления параллельный доступ со стороны нескольких пользователей может соз-
давать проблемы двух типов. Первый вид проблем возникает в тех случаях, когда
пользователю разрешено извлекать запись во время выполнения обновления этой же
самой записи. Пользователь, который выполняет только чтение данных, теоретиче-
ски может получить устаревшие данные, предшествовавшие обновлению, или даже
смешанные данные, когда ряд полей будет содержать старые данные, а ряд — новые.
Второй тип проблемы возникает при одновременном обновлении данных нескольки-
ми пользователями, когда один пользователь начинает обновлять запись во время ее
обновления другим пользователем. Если обновление затрагивает только одну запись
таблицы, существует вероятность того, что обновление будет утеряно. Если обновле-
ние затрагивает записи из нескольких таблиц, данные в базе могут оказаться несо-
гласованными. Прежде чем приступить к ознакомлению со способами решения этих
проблем, рассмотрим работу основных операций обновления набора записей.
Операции обновления CRecordset
В предыдущей главе рассматривалось извлечение данных из выбранных полей
таблицы или таблиц базы и их передача членам объекта набора записей с помощью
функций RFX— () члена — объекта набора записей DoFieldExchange (). Эти же функ-
ции используются для обновления полей в таблице базы данных или для добавления
новой строки.
Класс CRecordset содержит пять функций-членов, которые поддерживают опера-
ции обновления, как показано в табл. 20.1.
Таблица 20.1. Функции-члены класса CRecordset
функция Описание
Edit () Эту функцию вызывают, чтобы начать обновление существующей записи. Она возбуж-
дает исключение CDBException, если обновление таблицы невозможно, и исключе-
ние CMemoryException — при возникновении ситуации переполнения памяти.
AddNew () Эту функцию вызывают, чтобы начать добавление совершенно новой записи. Она
возбуждает исключение CDBException, если запись новой записи в конец суще-
ствующей таблицы невозможна.
Update () Эта функция служит для завершения обновления существующей записи или добав-
ления новой. Она возбуждает исключение CDBException, если ни одна запись не
была обновлена, или в случае возникновения ошибки.
Delete () Удаляет текущую запись, создавая и выполняя SQL-запрос delete. При возник-
новении ошибки — например, если база данных доступна только для чтения —
функция возбуждает исключение CDBException. После выполнения операции
Delete () всем данным-членам набора записей будут присвоены нулевые значе-
ния — что эквивалентно набору нулевых значений. Прежде чем выполнять любую
другую операцию по отношению к объекту набора записей, необходимо перейти к
другой записи.
Cancelupdate () Эта функция отменяет любую отложенную операцию обновления существующей за-
писи или добавления новой.
Обновление источников данных 977
Ни одна из перечисленных в табл. 20.1 функций не имеет параметров. Первые че-
тыре функции могут вызывать исключения, поэтому во избежание внезапного завер-
шения программы при возникновении ошибок следует поместить вызов этих функ-
ций в блок try и добавить в программу блок catch.
Для удаления текущей записи из объекта набора записей достаточно вызывать его
член Delete (). Затем, прежде чем предпринимать попытку использования любой
из вышеприведенных функций, в наборе записей потребуется выполнить переход к
новой позиции, поскольку после вызова функции Delete () значения данных-членов
этого объекта будут неправильными.
Общая последовательность событий обновления существующей записи или добав-
ления новой показана на рис. 20.1.
Обновление существующей записи
1. Вызов функции EDIT0 применительно
к объекту набора записей:
- Сохранение текущих значений данных-членов
полей набора записей в буфере.
2. Установка новых значений данных-членов полей.
3. Вызов функции Update() применительно
к объекту набора записей:
- Проверка наличия измененных полей
посредством сравнения с сохраненными
значениями.
- Создание и выполнение SQL-запроса INSERT
для обновления измененных полей базы данных.
- Освобождение буфера, содержащего старые
сохраненные значения полей.
Добавление новой записи
1. Вызов функции AddNew() применительно
к объекту набора записей:
- Сохранение текущих значений данных-членов
полей набора записей в буфере.
- Установка текущих значений данных-членов
полей набора записей равным PSEUDO J4ULL.
2. Установка новых значений данных-членов полей.
3. Вызов функции Update() применительно
к объекту набора записей:
- Проверка на наличие полей, отличных от NULL.
- Создание и выполнение SQL-запроса INSERT
для обновления полей базы данных,
отличных от NULL.
- Восстановление старых сохраненных
значений из буфера.
_
. 20,1, Общая последовательность событий обновления существующей записи или добав-
ления новой
При вызове функции AddNew () по отношению к набору записей, чтобы начать до-
бавление новой записи в таблицу, функция сохраняет текущие значения всех данных-
членов объекта набора записей, которые соответствуют значениям полей в буфере,
а затем устанавливает значения данных-членов равными PSEUDO_NULL. Это значение
не является нулем или значением null, как имеет место в случае указателя. Оно пред-
ставляет собой значение, которое указывает на то, что значение члена данных не уста-
новлено. При вызове функции Update () для завершения добавления записи восста-
навливаются исходные значения данных-членов набора записей, предшествовавшие
вызову функции AddNew (). Если требуется, чтобы набор записей содержал значения
новой записи, необходимо вызвать функцию-член Requery () объекта набора записей.
В случае успешного выполнения операции эта функция возвращает значение TRUE
(значение MFC-типа BOOL). Функцию Requery () вызывают также в тех случаях, когда
необходимо получить другое представление данных, извлечение данных из которого
будет выполняться с помощью другой SQL-команды или другого фильтра записей.
Передача данных между данными-членами набора записей и базой данных всег-
да выполняется с помощью члена DoFieldExchange () объекта набора записей.
Следовательно, функции RFX_ () обеспечивают две возможности — как запись в базу
данных, так и чтение из нее.
978 Глава 20
Проверка допустимости операций
Всегда целесообразно убедиться, что операция, которую нужно выполнить, до-
пустима для данного объекта набора записей. Слишком легко может оказаться, что
набор записей доступен только для чтения — для этого достаточно забыть сбросить
атрибут “только для чтения” файла базы данных (например, Northwind.mdb)! При по-
пытке обновления таблицы, доступной только для чтения, возбуждается исключение,
которого можно полностью избежать, всего лишь проверяя допустимость операции.
Использование исключений для выявления непредвиденных ошибок не эффективно,
и в общем случае этого следует избегать. Лучше по возможности заранее проверять
допустимость операции, как в данном случае. В результате код обработки исключе-
ний будет использоваться в действительно исключительных ситуациях.
Функция-член CanUpdate () класса CRecordset возвращает значение TRUE, если
изменение записей в таблице, представленной объектом набора записей, возможно.
Если требуется добавить новую запись, вначале для проверки можно вызвать функ-
цию-член CanAppend () класса CRecordset. Эта функция возвращает значение TRUE,
если добавление новых записей в таблицу разрешено.
Блокировка записей
Блокировка записей препятствует доступу других пользователей к заблокирован-
ной записи во время обновления строки таблицы. Степень блокировки записи во вре-
мя обновления определяется режимом блокировки, установленным в объекте набо-
ра записей. В объекте CRecordset определены два режима блокировки, называемые
оптимистическим и пессимистическим (табл. 20.2).
Таблица 20.2. Режимы блокировки записей CRecordset
Режим блокировки
Описание
CRecordset::optimistic
CRecorset::pessimistic
В оптимистическом режиме блокировки запись блокируется только
при выполнении функции-члена Update (). Это минимизирует время, в
течение которого запись недоступна другим пользователям базы дан-
ных. Если время редактирования может занимать длительное время,
часто пессимистическая блокировка не подходит, поскольку другим
пользователям может требоваться получение доступа к базе данных.
Стандартным подходом является использование оптимистической бло-
кировки и применение какого-то механизма разрешения конфликтов.
В пессимистическом режиме блокировки запись блокируется немед-
ленно после вызова функции Edit () и остается заблокированной и,
следовательно, недоступной для других пользователей, до завершения
выполнения функции Update О или до отмены операции обновления.
Понятно, что такой подход может существенно сказаться на производи-
тельности в тех случаях, когда обновления подготавливаются интерак-
тивно. Однако во многих ситуациях этот режим просто незаменим для
поддержания целостности данных.
По умолчанию для объекта набора записей применяется оптимистический ре-
жим блокировки, поэтому его установка требуется только при необходимости уста-
новки пессимистического режима. Для этого необходимо вызывать функцию-член
SetLockingMode () объекта набора записей с аргументом CRecordset: :pessimistic.
Естественно этот режим можно переустановить, вызывая функцию с аргументом
CRecordset::optimistic.
Обновление источников данных 979
Транзакции
Основная идея применения транзакций в контексте базы данных — обеспечение
возможность безопасной отмены операций в случае необходимости. Транзакция
объединяет строго определенные последовательности одного или более изменений
базы данных в одну операцию, что позволяет в случае возникновения ошибки вер-
нуть все элементы данных в исходное состояние (выполнить откат) в любой момент
до завершения транзакции. Понятно, что сбой обновления в тот момент, когда оно
выполнено частично, например, из-за возникновения аппаратного сбоя, мог бы ока-
зать крайне негативное влияние на целостность базы данных. Транзакции не сводят-
ся к обновлению только одной таблицы. Они могут включать в себя очень сложные
операции с базой данных, требующие выполнения ряда изменений множества та-
блиц, и их выполнение может занимать значительное время. В подобных ситуациях
поддержка транзакций совершенно необходима для обеспечения целостности базы
данных.
При выполнении операций, ориентированных на транзакции, система базы дан-
ных управляет обработкой транзакции и записывает информацию, необходимую
для восстановления данных, чтобы в случае возникновения какой-то проблемы в
ходе этого процесса можно было отменить любые действия, выполняемые тран-
закций по отношению к данным. Выполнение операций с базой данных на основе
транзакций позволяет защитить базу данных от ошибок, которые могут возникать
во время обработки. Как правило, обработка с применением транзакций по мере
необходимости блокирует записи, а также обеспечивает, чтобы другие пользовате-
ли базы данных, обращающиеся к измененным транзакцией данным, немедленно
видели все изменения.
Транзакции поддерживаются большинством коммерческих систем баз данных,
установленных на мэйнфреймах, но в системах баз данных, работающих на ПК, это
не всегда так. Тем не менее, класс CDatabase в библиотеке MFC поддерживает тран-
закции. Это же относится и к поддержке Microsoft ODBC баз данных Access. Поэтому
при желании можно поэкспериментировать с обработкой транзакций применитель-
но к базе данных Sample Data.
Операции транзакций класса CDatabase
Управление транзакциями осуществляется с помощью членов объекта класса
CDatabase, который обеспечивает подключение к базе данных. Для выяснения того,
поддерживаются ли транзакции для данного подключения, необходимо вызвать
функцию-член CanTransact () объекта CDatabase. Эта функция возвращает значе-
ние TRUE, если транзакции поддерживаются. Кроме того, функция-член CanUpdate ()
класса CDatabase возвращает значение FALSE, если источник данных доступен толь-
ко для чтения.
При обработке транзакций используются три функции-члена класса CDatabase,
описанные в табл. 20.3.
Последовательность событий в ходе выполнения транзакции очень проста.
□ Вызов функции BeginTrans () для запуска транзакции.
□ Вызов соответствующих функций Edit (), Update (), AddNew (), необходимых
для обработки набора записей.
□ Вызов функции CommitTrans () для завершения транзакции.
980 Глава 20
Таблица 20.3. Функции-члены класса CDatabase для обработки транзакций
Функция
Описание
BeginTrans()
CommitTrans()
Rollback()
Запускает транзакцию в базе данных. Все последующие операции с набором за*
писей являются частью транзакции до тех пор, пока не будет вызвана функция
CommitTrans () либо Rollback (). Функция возвращает значение TRUE, если
запуск транзакции был успешным.
Фиксирует транзакцию, что приводит к быстрому завершению всех операций с
набором записей, являющихся частью транзакции. При ошибке функция возвра*
щает значение false, и в этом случае состояние источника данных оказывается
неопределенным.
Выполняет откат всех операций с набором записей, выполненных с момента вы-
зова функции BeginTrans (), и восстанавливает состояние источника данных,
соответствующее моменту вызова этой функции.
Вне транзакции операции Edit () или AddNew () по отношению к набору записей
выполняются при вызове функции Update (). Внутри транзакции они не выполняют-
ся до тех пор, пока функция CommitTrans () не будет вызвана применительно к объ-
екту CDatabase. Чтобы отменить транзакцию в любой момент после вызова функции
BeginTrans (), достаточно обратиться к функции Rollback ().
Выполнение операций CommitTrans () и Rollback () может создавать определен-
ные проблемы (например, может происходить утеря позиции в обрабатываемом на-
боре записей, вследствие чего в программе может потребоваться выполнение опреде-
ленных действий для восстановления указателя записи после завершения или отмены
транзакции). Два члена класса CDatabase облегчают решение этой задачи. После вы-
зова функции CommitTrans () необходимо вызвать функцию-член GetCursorCommit
Behavior () класса CDatabase, а после вызова функции Rollback () — функцию-член
GetCursorRollbackBehavior (). Обе эти функции возвращают одно из трех значе-
ний типа int, указывающих действия, которые необходимо выполнить (табл. 20.4).
Таблица 20.4. Значения, возвращаемые функциями-членами
GetCursorCommitBehavior () и GetCursorRollbackBehavior () класса CDatabase
Значение Описание
sql cb preserve Операция фиксации или отката никак не повлияла на подключение набора записей
к источнику данных, поэтому выполнение дополнительных действий не требуется.
SQL CB CLOSE Необходимо вызвать функцию Recovery () для объекта набора записей, чтобы
восстановить текущую позицию в наборе записей.
sql cb delete Необходимо закрыть набор записей путем вызова функции-члена close ()
объекта, а затем снова его открыть при необходимости.
На практике использование транзакций может создавать дополнительные про-
блемы, поскольку конкретные применяемые драйверы могут оказывать влияние на
момент открытия набора записей. При использовании некоторых драйверов набор
записей необходимо открывать до вызова функции BeginTrans (). В случае приме-
нения других, в том числе драйверов Microsoft Access ODBC, функция Rollback ()
не будет работать, если только набор записей не будет открыт после вызова функ-
ции-члена BeginTrans (). Поэтому, прежде чем предпринимать попытку применения
транзакций в приложении, необходимо уяснить для себя, как предполагается исполь-
зовать конкретные драйверы.
Обновление источников данных 981
Простой пример обновления
Пора рассмотреть выполнение операций обновления на практике, начиная с
общего примера. В этом примере вначале мы опустим большинство уже описанных
нюансов, но впоследствии он будет усложнен за счет добавления некоторых из опи-
санных возможностей. Приложение обновления таблицы базы данных можно создать
ценой минимальных усилий, используя мастер MFC Application Wizard, который мы
уже применяли в предыдущей главе. Мы создадим программу, которая позволяет об-
новлять определенные поля в таблице Order Details (Сведения о заказе).
Используя шаблон MFC Application (Приложение MFC), создайте проект
DBSimpleUpdate. Выберите переключатель Database view without file support
(Представление баз данных без файловой поддержки) и укажите ODBC в качестве па-
раметра Client type (Тип клиента), как это делалось в предыдущей главе. Как и ранее,
мы будем использовать базу данных Northwind через драйвер ODBC, но на этот раз в
качестве типа набора записей необходимо выбрать Dynaset (Динамический набор).
В многопользовательской среде динамический набор автоматически обновляется лю-
быми изменениями, выполняемыми в записи при обращении к ней программы. Это
обеспечивает постоянную актуальность всех данных в приложении. Для выполнения
действий по изменению существующей записи или добавлению новых записей в каче-
стве типа набора записей должен быть выбран Dynaset.
Поскольку мы планируем обновлять базу данных, набор записей необходимо пре-
образовать в одну таблицу базы данных. Классы базы данных MFC не поддерживают
обновление наборов записей, предполагающие соединение двух или более таблиц.
В качестве набора записей, используемого по умолчанию, выберите таблицу Order
Details (Сведения о заказе), как показано на рис. 20.2.
Рис. 20.2. Выбор таблицы Order Details
982 Глава 20
В случае выбора нескольких таблиц обновление набора записей подавляется, по-
скольку набор записей автоматически определяется в качестве доступного только для
чтения. Для объединений нескольких таблиц классы базы данных поддерживают до-
ступ только для чтения, но не для обновления.
Представление и класс набора записей и связанные с ними имена файлов можно
изменить в соответствии с обрабатываемой таблицей, как показано на рис. 20.3.
Рис. 20.3. Сгенерированные мастером классы
Настройка приложения
Таблица Order Details содержит пять столбцов — Order ID (Идентификатор за-
каза), Product ID (Идентификатор продукта), Unit Price (Цена единицы продукта),
Quantity (Количество) и Discount (Скидка). Если отобразить вкладку Class View
(Представление класса) и взглянуть на члены объекта COrderDetailsSet, можно
увидеть соответствующие им данные-члены. Диалоговое окно представления набора
записей должно содержать статическое текстовое поле и элемент редактирования,
которые вместе соответствуют каждому из этих данных-членов. Они размещены так,
как показано на рис. 20.4, но их можно разместить любым удобным образом.
Присвойте элементам редактирования идентификаторы, соответствующие име-
нам полей, как это было сделано в предыдущей главе
менту присвойте идентификатор IDC_DISCOUNT. Стиль, установленный для элемен-
тов редактирования по умолчанию, допускает ввод данных с клавиатуры, но, исходя
из предположения, что необходимо ограничить количество полей набора записей,
которые можно изменять, первые три элемента редактирования следует определить
как доступные только для чтения. Для этого необходимо использовать вкладку стилей
диалогового окна Properties (Свойства).
например, последнему эле-
Обновление источников данных
983
Рис. 20.4. Диалог для таблицы Order Details
Значение, отображаемое в элементе управления, доступном только для чтения,
можно определять в программе, но его нельзя вводить в элемент управления с кла-
виатуры. Атрибут “только для чтения” всех этих элементов управления можно уста-
новить одновременно, выбирая каждый из них при нажатой клавише <Ctrl>, а затем
щелкая правой кнопкой мыши и выбирая пункт Properties (Свойства) из контекстно-
го меню. Любые параметры, установленные затем в диалоговом окне Properties, будут
применены ко всем выбранным элементам управления. При организации диалогово-
го окна, показанной на рис. 20.4, ввод данных возможен только в текстовых полях
Quantity (Количество) и Discount (Скидка).
Теперь осталось только связать элементы редактирования с соответствующими
данными-членами набора записей, а как было показано в предыдущей главе, для чего
достаточно для каждого поля данных в наборе записей добавить вызов функции DDX_
в функцию DoDataExchange () класса COtderDetailsView. Этот код выглядит следу-
ющим образом.
void COrderDetailsView::DoDataExchange(CDataExchange* pDX)
CRecordView::DoDataExchange(pDX);
DDX_FieldText(pDX
DDX_FieldText(pDX
DDX_FieldText(pDX
DDX_FieldText(pDX
DDX_FieldText(pDX
IDC_ORDERID
IDCJPRODUCTID
IDC_UNITPRICE
IDC_QUANTITY, m_pSet->m_Quantity
IDC_DISCOUNT, m_pSet->m_Discount
m__pSet->m_OrderID, m_pSet);
m_pSet->m__ProductID, m__pSet);
mpSet->m UnitPrice, m_pSet);
m_pSet);
m pSet);
Верите или нет, но создание программы для обновления таблицы Order Details
завершено.
Практическое занятие
Обновление базы данных
Если вы правильно определили все элементы управления и не забыли заком-
ментировать директиву #error,
расположенную перед определением функции
GetDefaultConnect () в классе COrderDetailsSet, эта программа должна быть
полностью готовой к сборке. Указанная директива служит для устранения проблем,
которые могут возникать при подключении к базе данных. Во время выполнения про-
граммы можно будет перемещаться по строкам таблицы, используя кнопки панели
984 Глава 20
инструментов. При вводе данных в поля редактирования Quantity или Discount заказа
они обновляются при перемещении назад или вперед в наборе записей. Окно при-
ложения показано на рис. 20.5.
Как видите, для продукта с идентификатором Product ID, равным 72 в заказе с
идентификатором Order ID, равным 10248, введены некоторые произвольные зна-
чения.
Описание полученных результатов
ществует два уровня об-
В результате щелчка на одной из кнопок панели инструментов для перехода к дру-
гой записи вызывается обработчик OnMove (), предоставленный используемым по
умолчанию базовым классом CRecordView. Перед выполнением перехода к новой за-
писи в наборе записей посредством вызова функции Move () класса CRecordset, на-
следуемой классом COrderDetailsSet, эта функция записывает любые изменения,
введенные в набор записей. Помните, что в данном случае
мена данными. Функции RFX_ (), вызываемые в функции-члене DoFieldExchange ()
класса COrderDetailsSet, выполняют передачу данных между строкой в наборе
записей базы данных и данными-членами класса. Функции DDX_ (), вызываемые
в функции-члене DoDataExchange () класса COrderDetailsView, осуществляют
передачу данных между элементами редактирования и данными-членами класса
COrderDetailsSet. При изменении значения в элементе редактирования новые дан-
ные передаются соответствующим данным-членам объекта набора записей. При пере-
ходе к следующему набору записей с помощью щелчка на кнопке панели инструмен-
тов функция DoFieldExchange () записывает новые данные в базу.
Рис. 20.5. Окно программы обновления таблицы
Order Details
Обновление источ
ков данных 985
Управление процессом обновления
Рассмотренный пример программы достаточно хорош, но запись данных в базу
без выполнения каких-либо очевидных действий со стороны пользователя несколько
сбивает с толку. Можно было бы вначале определить все элементы редактирования до-
ступными только для чтения, чтобы по умолчанию ввод данных с клавиатуры подавлял-
ся для всех элементов управления. Затем можно было бы добавить в диалоговое окно
кнопку Edit Order (Изменить заказ), которая делала бы соответствующие элементы до-
ступными для ввода данных с клавиатуры. Это диалоговое окно показано на рис. 20.6.
В этой программе мы реализуем два примечательных режима: режим “только для
чтения”, при котором обновление невозможно, поскольку элементы управления будут
доступны только для чтения, и режим правки, в котором ввод данных с клавиатуры
возможен для некоторых из элементов управления, что позволяет обновлять набор
записей. Идея этого подхода состоит в том, чтобы при щелчке на кнопке Edit Order
пользователь активизировал ввод данных с клавиатуры для тех полей редактирова-
ния, которые должны быть доступными для обновления, тем самым включая режим
редактирования. Добавьте кнопку в диалоговое окно приложения DBSimpleUpdate.
Кнопке можно присвоить идентификатор IDC_EDITORDER. Обработчик для кноп-
ки добавляется в класс COrder De tai Is View. Для этого потребуется щелкнуть на
кнопке правой кнопкой мыши и выбрать пункт Add Event Handler (Добавить обра-
ботчик событий) из контекстного меню. Сократите имя функции обработчика до
OnEditorder().
В идеале в режиме обновления следует подавить использование кнопок панели
инструментов или команд меню Record (Запись), предназначенных для перехода к
другой строке в таблице, поскольку щелчок на кнопке должен завершать операцию
обновления, а не перемещать текущую позицию в наборе записей.
При щелчке на кнопке Edit Order атрибут “только для чтения” полей количества
и скидки должен удаляться. Кроме того, диалоговое окно должно содержать кнопку,
активизирующую выполнение обновлений. После щелчка на кнопке Edit Order диало-
говое окно приложения должно принимать вид, показанный на рис. 20.7.
Теперь поля редактирования Quantity и Discount допускают ввод данных, надпись
кнопки Edit Order изменилась на Update (Обновить), а в диалоговом окне появилась
новая кнопка Cancel (Отмена), которая позволяет пользователю при необходимо-
986 Глава 20
сти отменить операцию. Щелчок на кнопке Edit Order должен отключать не только
кнопки панели инструментов, но и текущую запись. Это же относится к меню Record.
Теперь программа находится в режиме “редактирования”.
Order ID:
Sample edi
Product ID: Sample edi
Unit Price:
Sample edi
Quantity:
Sample edi
Cancel
Discount:
Sample edi
Update
U__________
Puc. 20.7. Редактирование некоторых полей заказа
Кнопку Cancel можно добавить в диалоговое окно, но не желательно, чтобы она
отображалась сразу после запуска приложения. Поэтому в качестве значения ее свой-
ства Visible следует установить False.
Для кнопки Cancel также требуется обработчик события, поэтому добавьте обра-
ботчик OnCancel () в класс COrderDetailsView, так же, как это делалось для кнопки
Edit Order — код этого обработчика будет представлен несколько позже.
Процесс выполнения операции обновления следующий: пользователь вводит дан-
ные в доступные для этого поля диалогового окна и щелкает на кнопке Update, чтобы
завершить обновление. Затем диалоговое окно возвращается в исходное состояние
режима “только для чтения”
для чтения. Если пользователь решает отказаться от выполнения операции обновле-
ния, он щелкает на кнопке Cancel.
Чтобы приложение вело себя описанным образом, и чтобы процессом обновле-
ния можно было эффективно управлять, после щелчка на кнопке Edit Order необхо-
димо выполнить ряд перечисленных ниже действий.
□ Изменить текст на кнопке Edit Order на Update, чтобы кнопка превратилась в
кнопку завершения операции обновления.
□ Обеспечить отображение кнопки Cancel в диалоговом окне
сделать ее видимой.
в котором все поля редактирования доступны только
иначе говоря,
Отразить в классе переход в режим “редактирования”. Это необходимо потому,
что одну и
Order и Update.
же кнопку мы будем использовать для выполнения двух задач: Edit
Активизировать ввод данных с клавиатуры для элементов управления, отобра-
жающих поля, обновление которых нужно разрешить.
Теперь соберем воедино код, который будет обеспечивать выполнение всех этих
действий.
Обновление источников данных 987
Реализация режима обновления
Начнем с фиксации того, находится ли приложение в режиме обновления. Это
можно сделать, добавляя в класс COrder De tai Is View объявление enum и переменную
типа enum, отражающую текущий режим. Добавьте следующие две строки в раздел
public класса:
enum Mode {READ-ONLY, UPDATE};
Mode m Mode;
// Режимы приложения
// Запись текущего режима
Первоначально приложение находится в режиме READ-ONLY, поэтому Ш—Mode мож-
но инициализировать в конструкторе соответствующим образом:
COrderDetailsView::COrderDetailsView()
: CRecordView(COrderDetailsView::IDD)
,Ш-Mode(READ-ONLY)
m_pSet = NULL;
// TODO: сюда необходимо вставить код конструктора
Переключения надписи кнопки и режима программы можно выполнять в обра-
ботчике OnEditorder (), добавленном в класс представления. В принципе, начальная
версия функции должна реализовать функциональные возможности, подобные следу-
ющим:
void COrderDetailsView::OnEditorder()
if(m_Mode =« UPDATE)
{ //Если во время щелчка на кнопке программа находилась в режиме обновления
/ / Отключение ввода для полей редактирования
// Изменение текста кнопки Update на Edit Order
// Сокрытие кнопки Cancel
// Активизация команд меню Record и кнопок панели инструментов
/ / Завершение обновления
Ш—Mode = READ-ONLY; // Переход в режим "только чтение"
else
{ //Если во время щелчка на кнопке программа находилась в режиме "только чтение"
// Активизация ввода для полей редактирования
// Изменение текста кнопки Edit Order на Update
// Отображение кнопки Cancel
// Отключение команд меню Record и кнопок панели инструментов
// Начало обновления
Ш—Mode = UPDATE; // Переключение в режим обновления
Код переключения режима уже готов. На данный момент функция лишь изменяет
значение члена Ш—Mode с READ-ONLY на UPDATE и обратно, фиксируя текущий режим.
Остальные необходимые действия описаны в виде комментариев. Теперь последова-
тельно рассмотрим, как можно реализовать каждую из этих строк.
Активизация и отключение полей редактирования
Чтобы изменить свойства элемента управления, необходимо вызвать определен-
ную связанную с ним функцию. Следовательно, мы должны располагать доступом к
988 Глава 20
объекту, который представляет элемент управления. Добавление переменной элемен-
та управления в класс представления не составляет сложности. Достаточно щелкнуть
на элементе управления правой кнопкой мыши в панели Resource View (Представ-
ление ресурсов) и в контекстном меню выбрать пункт Add Variable (Добавить пере-
менную). Диалоговое окно, отображаемое для элемента редактирования значения
скидки, показано на рис. 20.8.
Add Member Variable Wizard - BBSimpleUpdate
Welcome to the Add Member Variable Wizard
Access:
public
/I Control variable
Variable t
Control ID:
CEdit
Variable name:
IDC DISCOUNT
Category:
Control
Control
ars
J
m DiscountCtn
Comment £ / notation not required):
Control variable for discount edit control
Cancel
1
r
Puc. 20.8. Добавление переменной для элемента редактирования значения скидки
В качестве имени переменной введите m_DiscountCtrl. Поскольку переменная
связана с элементом редактирования, она имеет тип CEdit. Чтобы добавить эту пере-
менную в класс представления, щелкните на кнопке Finish (Завершить). Повторите
описанный процесс для элемента редактирования, отображающего объем заказа, и
присвойте переменной имя m_QuantityCtrl.
После того, как две переменные элементов управления добавлены в класс пред-
ставления, мы получили доступ к элементам управления для обновления их стилей.
Поэтому измените функцию OnEditorder () следующим образом:
void COrderDetailsView::OnEditorder()
if(m_Mode == UPDATE)
{ // Если во время щелчка на кнопке программа находилась в режиме обновления
// Отключение ввода для полей редактирования
m_QuantityCtrl.SetReadOnly();
m_DiscountCtrl.SetReadOnly();
// Изменение текста кнопки Update на Edit Order
// Сокрытие кнопки Cancel
/ / Активизация команд меню Record и кнопок панели инструментов
// Завершение обновления
m Mode = READ ONLY; // Переход в режим ’’только чтение"
Обновление источников данных 989
else
{ //Если во время щелчка на кнопке программа находилась в режиме "только чтение
// Активизация ввода для полей редактирования
m_QuantityCtrl.SetReadOnly(FALSE);
m_DiscountCtrl.SetReadOnly(FALSE);
// Изменение текста кнопки Edit Order на Update
// Отображение кнопки Cancel
// Отключение команд меню Record и кнопок панели инструментов
// Начало обновления
m_Mode = UPDATE; // Переключение в режим обновления
Функция-член SetReadOnly () класса CEdit имеет параметр типа BOOL, который
по умолчанию получает значение TRUE. Поэтому вызов этой функции без аргументов
означает использование значения аргумента, заданного по умолчанию, и установку
свойства "только для чтения” элемента управления равным TRUE. Передача функции
при ее вызове значения FALSE ведет к установке свойства "только для чтения” в зна-
чение FALSE. В данном случае можно было бы уменьшить объем кода функции, уда-
лив из нее обращения к функции SetReadOnly () в операторе if-else и добавив два
оператора после оператора i f:
m_QuantityCtrl. SetReadOnly (m__Mode == UPDATE);
m_DiscountCtrl.SetReadOnly(m_Mode == UPDATE);
Значение выражения аргумента m_Mode == UPDATE равно TRUE, когда значение
m_Mode равно UPDATE. В противном случае это значение равно FALSE, потому эти два
обращения к функции SetReadOnly () выполняют ту же задачу, которую выполняли
исходные четыре обращения. Недостаток этой реализации состоит в том, что дей-
ствия, выполняемые кодом, становятся менее понятными.
Вспомните, что многие функции библиотеки MFC содержат параметры типа BOOL,
которые могут иметь значения TRUE и FALSE. Это связано с тем, что они были созда-
ны до того, как тип bool стал доступным языке C++. При желании для параметров
типа BOOL всегда можно использовать значения типа bool в качестве аргументов, но
я предпочитаю применять значения аргументов TRUE и FALSE для параметров типа
BOOL, а значения true и false — для переменных типа bool.
Изменение надписи кнопки
Получить доступ к объекту, соответствующему кнопке Edit Order, можно, добавляя
в класс представления член данных элемента управления m_EditOrderCtrl, как это
было сделано для элементов редактирования. Эта переменная имеет тип CButton, яв-
ляющийся классом MFC, который определяет кнопку. Эту переменную можно исполь-
зовать для определения надписи в функции-члене OnEdit order () посредством вызова
функции-члена SetWindowText (), унаследованной классом CButton от класса CWnd:
void COrderDetailsView::OnEditorder()
if(m_Mode == UPDATE)
{ / / Если во время щелчка на кнопке программа находилась в режиме обновления
/ / Отключение ввода для полей редактирования
m_QuantityCtrl.SetReadOnly();
m_DiscountCtrl. SetReadOnly ();
990 Глава 20
// Изменение текста кнопки Update на Edit Order
m_EditOrderCtrl.SetWindowText( T("Edit Order”));
// Сокрытие кнопки Cancel
// Активизация команд меню Record и кнопок панели инструментов
// Завершение обновления
m_Mode = READ_ONLY; // Переход в режим "только чтение"
else
{ //Если во время щелчка на кнопке программа находилась в режиме "только чтение"
// Активизация ввода для полей редактирования
m_QuantityCtrl.SetReadOnly(FALSE);
m_DiscountCtrl.SetReadOnly(FALSE);
// Изменение текста кнопки Edit Order на Update
m_EditOrderCtrl.SetWindowText(_T("Update"));
// Отображение кнопки Cancel
// Отключение команд меню Record и кнопок панели инструментов
// Начало обновления
m_Mode = UPDATE;
// Переключение в режим обновления
Каждый вызов функции SetWindowText () устанавливает в качестве текста, ото-
бражаемого на кнопке, строку, которая передается в качестве аргумента функции. Тип
этого параметра — LPCTSTR, и в качестве него можно использовать аргумент CString
или стоковую константу типа т.
Управление видимостью кнопки Cancel
Для управления видимостью кнопки Cancel требуется соответствующая этой
кнопке управляющая переменная, поэтому добавьте управляющую переменную
m_CancelEditCtrl, как это было сделано для кнопки Edit Order. Поскольку класс
CButton наследует члены от класса CWnd, для управления видимостью кнопки можно
вызывать унаследованную функцию-член ShowWindow () объекта CButton, как показа-
но в следующем примере кода:
void COrderDetailsView::OnEditorder()
if(m_Mode == UPDATE)
{ // Если во время щелчка на кнопке программа находилась в режиме обновления
// Отключение ввода для полей редактирования
m_QuantityCtrl.SetReadOnly() ;
m_DiscountCtrl.SetReadOnly() ;
// Изменение текста кнопки Update на Edit Order
m_EditOrderCtrl.SetWindowText(_T("Edit Order"));
// Сокрытие кнопки Cancel
m_CancelEditCtrl.ShowWindow(SW_HIDE);
// Активизация команд меню Record и кнопок панели инструментов
// Завершение обновления
m_Mode = READ_ONLY; // Переход в режим "только чтение"
else
{ //Если во время щелчка на кнопке программа находилась в режиме "только чтение"
Обновление источников данных 991
// Активизация ввода для полей редактирования
m_QuantityCtrl.SetReadOnly(FALSE);
m_DiscountCtrl.SetReadOnly(FALSE);
// Изменение текста кнопки Edit Order на Update
m_EditOrderCtrl.SetWindowText(_T("Update"));
// Отображение кнопки Cancel
m__CancelEditCtrl. ShowWindow (SW_SHOW) ;
// Отключение команд меню Record и кнопок панели инструментов
// Начало обновления
m_Mode = UPDATE; // Переключение в режим обновления
Функция ShowWindow (), которую класс CButton наследует от класса CWnd, тре-
бует аргумента типа int, имеющий одно из фиксированных значений (полный на-
бор аргументов описан в документации). Значение SW_HIDE этого аргумента служит
для сокрытия кнопки, когда переменная m_Mode имеет значение UPDATE, а значение
SW_SHOW — для отображения и активизации кнопки при переводе приложения в ре-
жим редактирования.
Отключение меню Record
Когда член m_Mode объекта представления имеет значение UPDATE, пункты меню
должны быть отключены. Однако это не стоит выполнять в обработчике события
OnEditorder (), поскольку, как вы сейчас убедитесь, существует более простой и эф-
фективный способ. Поэтому удалите две соответствующие строки комментариев в
операторе if обработчика события OnEditorder ().
Состоянием пунктов меню и кнопок панели инструментов можно управлять, до-
бавив в класс представления специальные обработчики обновления. Отобразите ре-
сурс меню приложения в панели Resource View. В файле DBMSimpleUpdte. г с этот
ресурс меню хранится с идентификатором IDRMA IN FRAME. Добавьте в меню Record
обработчик сообщения UPDATE_COMMAND_UI для каждого пункта меню — начиная с
пункта First Record (Первая запись). Раскройте меню Record, щелкнув на нем левой
кнопкой мыши, а затем щелкните правой кнопкой мыши на пункте меню First Record
в панели Resource View и в контекстном меню выберите пункт Add Event Handler.
Результирующее диалоговое окно мастера создания обработчиков событий (Event
Handler Wizard) показано на рис. 20.9.
Как видите, в качестве типа сообщения выбрано UPDATE_COMMAND_UI, а в каче-
стве класса, в который будет добавлен обработчик — COrderDetailsView. Описание
в нижней части диалогового окна указывает основное назначение обработчика
UPDATE COMMAND UI
— оно полностью соответствует тому, что требуется в данном слу-
чае. Щелкните на кнопке Add and Edit (Добавить и редактировать), чтобы добавить
обработчик и повторить процесс для остальных трех пунктов меню Record.
Типом аргумента, передаваемого функции обработчика UPDATE_COMMAND_UI, явля-
ется CcmdUI, а этот класс содержит функцию-член Enable (), которую можно вызы-
вать для активизации или отключения элемента. Значение TRUE этого аргумента акти-
визирует элемент, а значение FALSE отключает его. Значение параметра, используемое
по умолчанию — TRUE, поэтому для активизации элемента можно вызывать функцию
без аргумента. Отключение пунктов меню и кнопок панели инструментов требуется,
когда переменная m_Mode имеет значение UPDATE, но ситуации, в которых может ока-
заться необходимым их отключение, несколько осложняются поведением пунктов
меню и кнопок панели инструментов непосредственно перед обращением к ним.
992 Глава 20
. 20.9. Мастер создания обработчиков событий
Когда текущая запись является первой в наборе записей, по умолчанию программа
отключает пункты меню и кнопки панели инструментов, соответствующие идентифи-
каторам ID_RECORD_FIRST и ID_RECORD_PREV. Аналогично, когда текущая запись яв-
ляется последней в наборе записей, программа отключает элементы I D_RECORD_NEXT
и ID_RECORD__LAST. Это поведение программы необходимо сохранить в ситуации,
когда переменная m_Mode имеет значение READ_ONLY. Для этого применяются унасле-
дованные функции класса представления, которые проверяют, является ли текущая
запись первой или последней записью в наборе. Чтобы добиться нужного результата,
достаточно добавить по одной строке кода в каждый из этих обработчиков. В функ-
ции OnUpdateRecordFirst () и OnUpdateRecordPrev () потребуется добавить одну и
ту же строку кода. Например:
void
COrderDetailsView::OnUpdateRecordFirst(CCmdUI* pCmdUI)
// Отключение элемента, если значение m_Mode - UPDATE
// Активизация элемента, если значение m_Mode - READ_ONLY,
//и запись не является первой
pCmdUI->Enable((m Mode READ ONLY) && ’IsOnFirstRecord());
Функция IsOnFirstRecord() возвращает значение TRUE, если представление
отображает первую запись набора записей, и значение FALSE — в противном случае.
В результате элементы (пункт меню и соответствующая кнопка панели инструментов)
отключаются, если либо переменная m_Mode имеет значение UPDATE, либо функция-
член IsOnFirstRecord () объекта COrderDetailsView возвращает значение TRUE.
Элементы активизируются, если переменная m_Mode имеет значение READ_ONLY либо
функция IsOnFirstRecord () возвращает значение FALSE. Этот обработчик оказыва-
ет влияние как на пункт меню, так и на кнопку панели инструментов, поскольку оба
эти элемента имеют один и тот же идентификатор — ID_RECORD_FIRST.
Обновление источников данных 993
Обработчики, соответствующие идентификаторам ID_RECORD_NEXT и ID_RECORD_
LAST, также требуют одной и той же строки кода:
void COrderDetailsView::OnUpdateRecordLast(CCmdUI* pCmdUI)
// Отключение элемента, если значение m_Mode - UPDATE
// Активизация элемента, если значение m_Mode - READ_ONLY,
//и запись не является последней
pCmdUI->Enable((m_Mode == READ-ONLY) && ’IsOnLastRecord());
Этот обработчик работает аналогично предыдущему.
Фактическое выполнение обновления
Теперь осталось только выполнить действительное обновление по щелчку на
кнопке Update. Для обновления записи пользователь вначале щелкает на кнопке Edit
Order, потому на этом этапе необходимо вызвать функцию-член Edit () объекта набо-
ра записей, чтобы начать процесс изменения набора записей. После щелчка на кноп-
ке Update необходимо вызвать функцию-член Update () объекта набора записей, что-
бы сохранить новые данные в записи базы. Описанный алгоритм можно реализовать
с помощью функции-члена mjpSet класса представления следующим образом:
void COrderDetailsView::OnEditorder()
if(m_pSet->CanUpdate())
if(m_Mode «« UPDATE)
{//Если во время щелчка на кнопке программа находилась в режиме обновления
/ / Отключение ввода для полей редактирования
m_QuantityCtrl.SetReadOnly();
m_DiscountCtrl.SetReadOnly();
11 Изменение текста кнопки Update на Edit Order
m_EditOrderCtrl.SetWindowText(_T("Edit Order”));
// Сокрытие кнопки Cancel
m_CancelEditCtrl.ShowWindow(SW_HIDE);
// Завершение обновления
m_joSet->Update ();
m_Mode = READ_ONLY; /1 Переход в режим "только чтение"
}
else
{//Если во время щелчка на кнопке программа находилась
//в режиме "только чтение”
// Активизация ввода для полей редактирования
m_QuantityCtrl.SetReadOnly(FALSE);
m_DiscountCtrl.SetReadOnly(FALSE);
// Изменение текста кнопки Edit Order на Update
m_EditOrderCtrl.SetWindowText(_T("Update"));
/ / Отображение кнопки Cancel
m__CancelEditCtrl. ShowWindow(SW_SHOW) ;
// Начало обновления
m pSet->Edit();
994 Глава 20
m_Mode = UPDATE; // Переключение в режим обновления
}
catch(CException* рЕх)
pEx->ReportError(); // Отображение сообщения об ошибке
else
AfxMessageBox( Т("Набор записей недоступен для обновления."));
Как было сказано в начале этой главы, функции Edit () и Update () могут гене-
рировать исключения в случае возникновения ошибки, поэтому кроме описанного
кода в блок try были включены дополнительные вызовы функций. Понятно, что
если обновление набора записей невозможно, выполнение какой-либо обработки в
функции OnEditorder () бессмысленно. При генерировании исключения мы вызы-
ваем функцию ReportError () для отображения сообщения об ошибке. Параметр
исключения блока catch — указатель на объект CException, поэтому блок catch вы-
полняется для объектов исключений типа CException или любого класса, произво-
дного от CException. Этот обработчик требуется для согласования с исключением
CMemoryExceprion, которое может генерироваться функцией Edit (), а также ис-
ключения CDBException, которое может генерироваться обеими функциями Edit ()
и Update (). Обратите внимание на использование указателя в качестве параметра
блока catch. Вспомните, что это связано с тем, что эти исключения — исключения
MFC, генерируемые макросом THROW, а не исключения C++, возбуждаемые с помощью
ключевого слова throw. Если бы они были исключениями последнего типа, в каче-
стве типа параметра блока catch нужно было бы использовать ссылку.
Кроме того, мы предусмотрели вызов функции-члена CanUpdate () для проверки
того, что набор записей доступен для обновления. Если эта функция возвращает зна-
чение FALSE, мы отображаем сообщение об ошибке в окне сообщения.
Реализация операции отмены
Кнопка Cancel (Отмена) должна отменять операцию обновления. Для этого доста-
точно вызвать функцию-член CancelUpdate () объекта COrderDetailsSet. Конечно,
придется выполнить небольшую “уборку”, но все действия полностью совпадают с вы-
полняемыми при щелчке на кнопке Update, за исключением того, что в данном слу-
чае не нужно вызывать функцию Edit (). Код обработчика OnCancel () имеет следу-
ющий вид:
void COrderDetailsView::OnCancel()
m_pSet->CancelUpdate(); // Отмена операции обновления
m_EditOrderCtrl.SetWindowText(_Т("Edit"));// Переключение текста кнопки
m_CancelEditCtrl.ShowWindow(SW_HIDE); // Сокрытие кнопки Cancel
m_QuantityCtrl.SetReadOnly(TRUE); // Установка состояния поля
// редактирования объема заказа
m_DiscountCtrl.SetReadOnly(TRUE); // Установка состояния поля
// редактирования скидки
m_Mode = READ_ONLY; // Перетслючение режима
Функция CancelUpdate () завершает операцию обновления и восстанавливает
значения полей объекта набора записей, которые предшествовали вызову функции
Обновление источников данных 995
Edit (). Поскольку щелчок на кнопке Cancel возможен только в режиме редактиро-
вания, обновление кнопок и других элементов управления можно выполнить так же,
как в обработчике OnEditorder (). Вот и все. Теперь можно осуществить пробный
запуск приложения.
Практическое занятие
Интерактивное обновление
Если код введен без ошибок, после компиляции и запуска программы она должна
работать так, как планировалось. После щелчка на кнопке Edit Order (Изменить за-
каз) ввод данных возможен только в полях редактирования Quantity (Количество) и
Discount (Скидка). Пример выполнения операции обновления показан на рис. 20.10.
Теперь кнопки перехода к новой записи отключены, равно как и пункты меню
Record (Запись). Завершение обновления после ввода новых данных выполняется
щелчком на кнопке Update (Обновить). Это приводит к записи новых данных в базу
и возврату приложения в нормальное состояние, в котором все поля редактирования
отключены, а кнопки и меню находятся в своих исходных состояниях.
Рис. 20.10. Обновление информации по заказу
Добавление строк в таблицу
Расширим рассмотренный пример, чтобы реализовать возможность добавления
нового заказа в базу данных Northwind. Это позволит подробнее рассмотреть некото-
рые из практических проблем и сложностей, которые могут встретиться при выпол-
нении подобной операции.
Сам по себе заказ не является простой записью в таблице базы данных Northwind.
Его определяют две таблицы. Основные данные заказа хранятся в таблице Orders
(Заказы), которая содержит информацию о клиентах. Каждая запись заказа в таблице
Orders связана с одной или более записями в таблице Order Details (Сведения о
996 Глава 20
заказах) (по одной для каждого продукта, включенного в заказ) по столбцу Order ID
(Идентификатор заказа). Отношение между этими таблицами отражено на рис. 20.11.
Однако процесс добавления нового заказа затрагивает не только эти две табли-
цы. При создании нового заказа необходимо предоставить пользователю средства
выбора клиента из таблицы Customers (Клиенты). Таблица Orders содержит поле,
определяющее сотрудника, которым должен быть один из сотрудников, записанных
в таблице Employees (Сотрудники). Очевидно, что после того, как информация,
требующаяся для включения в новую запись таблицы Orders, определена, придет-
ся выбрать один или более продуктов из числа определенных в таблице Products
(Продукты). Необходимость внесения изменений во все эти таблицы делает задачу
достаточно сложной. Ее можно несколько упростить, по умолчанию заполняя поле
столбца Employee ID (Идентификатор сотрудника) значением равным, например, 1.
Это позволит избежать необходимости внесения изменений в таблицу Employees в
ходе выполнения данного примера. Но в любом случае вначале потребуется опреде-
лить общую логику действий.
Таблица Orders
Всего 5 столбцов
Продукты, соответствующие
заказу в таблице Orders,
отражены в одной или более
записей таблицы Order Details.
Таблица Order Details
Рис. 20.11. Отношение между таблицами Orders и Order Details
Обновле:
н
е источников данных 997
чествующих заказов, мы будем использовать еще две формы. Одна из них обе-
и
с таблицей Products. Кнопки диалоговых окон
Процесс ввода заказа
Кроме уже созданной диалоговой формы просмотра и редактирования информа-
ции
спечивает выбор клиента, делающего заказ, и ввод требуемой даты доставки заказа, а
вторая позволяет вводить информацию о продуктах и их количествах, включаемых в
заказ. Диалоговое окно выбора клиента связано с таблицей Customers базы данных, а
диалоговое окно выбора продуктов
обеспечивают переход из одного окна в другое. Общая логика работы с диалоговыми
окнами показана на рис. 20.12.
От создания новой записи в таблице Or de г s следует воздержаться до ввода первой
записи в таблицу Order Details. Это позволит избежать создания заказа, который не
содержит ни одного продукта. Теперь соберем воедино все необходимые диалоговые
ресурсы, а затем реализуем код, обеспечивающий выполнение операций.
. 20.12. Работа с диалоговыми окнами
Создание ресурсов
В уже существующее окно потребуется добавить дополнительную кнопку, иници-
ализирующую процесс создания нового заказа, поэтому добавьте кнопку с надписью
New Order (Новый заказ) и идентификатором IDC_NEWORDER. После этого новую
кнопку можно разместить в той же позиции, что и кнопку Cancel, поскольку в каж-
дый конкретный момент времени будет видна только одна из этих кнопок. Кнопка
New Order должна быть видима по умолчанию. В то же время, если вы предпочитаете
избегать маскирования одного ресурса другим, эти кнопки можно разместить в раз-
личных позициях.
998 Глава 20
Чтобы при размещении кнопки New Order в той же позиции, что и кнопка
Cancel, она отображалась поверх нее, необходимо чтобы в таблице перемеще-
ния по элементам управления она следовала за ней. Измененная диалоговая форма
IDD—SIMPLEUPDATE, в которой кнопка New Order расположена поверх кнопки Cancel
и поэтому маскирует ее, показана на рис. 20.13.
Обработчик для кнопки New Order можно добавить в объект COrderDetailsView,
щелкая на кнопке правой кнопкой мыши и выбирая пункт Add Event Handler
Добавить обработчик событий) из контекстного меню. При желании определенное
по умолчанию имя обработчика событий можете сократить до OnNewOrder (). Чтобы
обработчик события был создан именно для новой кнопки, перед щелчком на ней
может потребоваться ее перемещение. Код этого обработчика мы добавим несколько
позже. Конечно, в данном случае можно было бы использовать кнопку Cancel, изме-
няя ее надпись и действие обработчика в зависимости от состояния члена m_Mode
объекта COrderDetailsView, однако предлагаемый сейчас подход позволит получить
представление о возможном способе работы с двумя кнопками.
Order ID:
Product ID:
Unit Price:
Quantity:
Discount:
u Sample ed
Sample ed
| Sample ed
| Sample ed
I Sample ed
New Order
Edit Order
Puc. 20.13. Измененная диалоговая форма IDD^SIMPLEUPDATE
Необходимые новые диалоговые формы можно создать, щелкая правой кнопкой
мыши на папке Dialog (Диалог) в панели Resource View и из контекстного меню вы-
бирая пункт Insert Dialog (Вставить диалог). Присвойте новым диалоговым формам
идентификаторы I DD_CUS TOMER_FORM и IDD__PRODUCT_FORM соответственно. Для обо-
их диалогов необходимо выбрать значения Child (Дочерний) для свойства Style
(Стиль) и None (Нет) для свойства Border (Рамка). Кроме того, размеры всех трех
'налоговых форм можно определить приблизительно равными и немного превыша-
ющими размеры первоначальной формы.
Создание наборов записей
Нам требуются два класса набора записей, соответствующие таблицам Customers
и Products базы данных. Добавление каждого из них выполняется одинаково: не-
обходимо щелкнуть правой кнопкой мыши на объекте DBMSimpleUpdate в панели
Class View (Представление классов) и в контекстном меню выбрать пункт Add*^Class
(Добавить1^ Класс), а затем в качестве шаблона выбрать MFC ODBC Consumer
(Потребитель MFC ODBC). В качестве имен классов можно указать CCustomerSet
и CProductset соответственно для таблиц Customers и Products, а также в обоих
случаях выбрать Snapshot (Снимок) для типа набора записей. Измените тип полей
Обновление источников данных 999
CStringW на CString и не забудьте в каждом классе удалить или закомментировать
директиву #еггог, предшествующую определению функции GetDefaultConnect ().
Создание представлений наборов записей
Теперь можно создать классы представлений записей, которые подключаются к но-
вым диалоговым окнам, поэтому добавьте в проект файлы . h и . срр, содержащие код
новых классов CCustomerView и CProductView, которые нам предстоит определить.
Класс CCustomerView инкапсулирует представление набора записей CCustomerSet и
использует ресурс диалоговой формы IDD_CUSTOMER. Поэтому исходное определение
класса имеет следующий вид:
// Customerview.h : заголовочный файл
#pragma once
class CCustomerSet;
class CCustomerView : public CRecordView
public:
CCustomerView();
virtual ^CCustomerView();
public:
enum { IDD = IDD_CUSTOMER__FORM };// Данные формы
CCustomerSet* m_pSet;
/ / Операции
public:
CCustomerSet* GetRecordset();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& de) const;
#endif
Член m_pSet, хранящий указатель на объект набора записей, можно инициализи-
ровать в конструкторе в файле СиstomerView. срр:
#include "stdafx.h"
#include "DBSimpleUpdate.h" // Главней заголовочный файл приложения
#include "Customerview.h"
// Конструктор
CCustomerView::CCustomerView()
: CRecordView(CCustomerView::IDD),
m_pSet(NULL)
Конструктор определяет также объект IDD_CUSTOMER__FORM в качестве диалогово-
го окна этого представления, передавая его идентификатор конструктору базового
класса. Директива #include файла DBSimpleUpdate.h, предшествующая директиве
#include для Customerview. h, необходима, поскольку без нее идентификатор
IDD_CUSTOMER_FORM не будет распознан во время компиляции. Файл DBSimpleUpdate.h
1000 Глава 20
содержит директиву #include файла Resource.h, содержащего определения иденти-
фикаторов созданных ресурсов.
Теперь добавим в файл Customerview, срр определения функций, которые долж-
ны генерироваться в режиме отладки:
// Диагностика CCustomerView
#ifdef _DEBUG
void CCustomerView::AssertValid() const
CRecordView::AssertValid();
void CCustomerView:: Dump (CDumpContextS de) const
CRecordView::Dump(de);
#endif //_DEBUG
Возможно, эти функции и не потребуются, но их целесообразно определить
(просто на тот случай, если что-то пойдет не так). Теперь в программу можно до-
бавить функции, которые будут переопределять функции DoDataExchange () ,
OnGetRecordset () и OnlnitialUpdate (), унаследованные от базового класса. Ниже
приведен пример настройки этих функций, чтобы они работали с набором записей
должным образом. Щелкните правой кнопкой мыши на имени класса CCustomerView
в панели Class View и в контекстном меню выберите пункт Properties (Свойства).
В панели инструментов щелкните на кнопке Overrides (Переопределения) (если не
помните, какая именно кнопка выполняет эту функцию, дождитесь отображения под-
сказки) и для каждого переопределения выберите имя функции в левой колонке и
опцию <Add> в колонке справа, чтобы добавить функцию.
Реализацию переопределения функции OnGetRecordset () можно выполнить сразу:
CRecordset* CCustomerView::OnGetRecordset()
if (m_pSet == NULL) // При отсутствии адреса набора записей
m_pSet = new CCustomerSet(NULL); // необходимо его создать
m_pSet->Open(); // и открыть
return m_pSet; // Возврат адреса набора записей
Если значением m_pSet является NULL, мы создаем набор записей и открываем
его, прежде чем возвращать его адрес. Если значение m_pSet не равно NULL, набор
записей уже создан, и потому мы возвращаем адрес, который он содержит. В файл
. срр потребуется добавить директиву #include для CCustomerSet. h, поскольку этот
код содержит ссылку на имя класса CCustomerSet.
Теперь можно реализовать функцию GetRecordset (), которая использует эту
функцию:
CCustomerSet* CCustomerView::GetRecordset()
return static cast<CCustomerSet*>(OnGetRecordset());
В данном случае необходимо выполнить явное преобразование типа, поскольку в
иерархии классов преобразование типа выполняется от базового класса к производно-
му. Остальные два переопределения функций будут реализованы несколько позже.
Обновление источников данных 1001
Поскольку объект CCustomerSet создается в куче (в области динамического рас-
пределения памяти), следует позаботиться о его удалении в деструкторе класса
CCustomerView:
CCustomerView::-CCustomerView()
if (m_pSet)
delete m_pSet;
Переопределение функции OnlnitialUpdate () в классе CCustomerView должно
быть реализовано так, как было показано ранее:
void CCustomerView::OnlnitialUpdate()
BeginWaitCursor();
GetRecordset();
CRecordView::OnlnitialUpdate();
if (m_pSet->IsOpen())
CString strTitle = m_pSet->m_pDatabase->GetDatabaseName();
CString strTable = m_pSet->GetTableName();
if(’strTable.IsEmpty())
strTitle += _T(":”) + strTable;
GetDocument()->SetTitle(strTitle);
EndWaitCursor();
Определение класса CProductView во многом подобно определению класса
CCustomerView. Основное различие заключается в связанном с ним диалоге:
// Productview.h : заголовочный файл
#pragma once
class CProductset;
class CProductView : public CRecordView
public:
CProductView();
virtual -CProductView();
public:
enum { IDD = IDD_PRODUCT_FORM }; // Данные формы
CProductset* m_pSet;
/I Операции
public:
CProductset* GetRecordset();
// Реализация
protected:
#ifdef _DEBUG
virtual void AssertValidO const;
virtual void Dump(CDumpContextS de) const;
#endif
Теперь, применяя метод, использованный для предыдущего класса, в класс
CProductView можно добавить переопределения функций OnGetRecordset (),
1002 Глава 20
DoDataExchange () и OnlnitialUpdate (), поместив в начало файла Productview,
срр следующие директивы:
#include "stdafx.h"
#include "DBSimpleUpdate.h"
#include "Productview.h"
#include "ProductSet.h"
Конструктор должен инициализировать член m_pSet, а деструктор — удалять объ-
ект, указанный этим объектом, поскольку он будет создаваться в куче. Определения
конструктора и деструктора имеют следующий вид:
CProductView::CProductView()
: CRecordView(CProductView::IDD),
m_pSet(NULL)
}
CProductView::^CProductView()
if (m_pSet)
delete m_pSet;
Код реализации функции OnGetRecordset () показан ниже.
CRecordset* CProductView: '.OnGetRecordset ()
if (m_jpSet == NULL) // Если набор записей отсутствует
m_pSet = new CProductset(NULL); // его необходимо создать,
m_pSet->Open(); // а затем открыть
return m_pSet; // Возвращение адреса набора записей
}
Реализация функции GetRecordset () в классе CProductView также в основном
идентична ее реализации в классе CCustomerView:
CProductset* CProductView::GetRecordset ()
return static__cast<CProductSet*> (OnGetRecordset ());
}
Можно также добавить определения диагностических функций, которые будут ис-
пользоваться в режиме отладки:
#ifdef _DEBUG
void CProductView::AssertValid() const
CRecordView::AssertValid();
void CProductView::Dump(CDumpContexts de) const
CRecordView::Dump(de);
#endif //_DEBUG
К функциям, реализация которых в классе CProductView еще не определена, мы
вернемся несколько позже в этой главе, а пока можно приступить к заполнению диа-
логов необходимыми элементами управления.
Обновление источников данных 1003
обавление элементов управления в ресурсы диалогов
Хотя мы связали диалоговое окно IDD_CUSTOMER_FORM только с таблицей
Customers, в процессе добавления новой записи придется предоставлять всю инфор-
мацию, которая необходима для создания новой записи в таблице Orders. Источники
данных для каждого из полей новой записи таблицы Orders показаны на рис. 20.14.
Часть полей заполняется данными, извлеченными из выбранной пользователем
записи таблицы Customers. Поскольку мы создаем новый заказ, необходимо синтези-
ровать новый уникальный идентификатор заказа. Для этого можно найти максималь-
ный идентификатор, используемый в данный момент в таблице Orders, и просто уве-
личить это значение на 1.
Чтобы выбрать клиента, пользователь выполняет прокрутку в наборе записей до
отображения требуемого клиента. Затем из набора записей можно извлечь данные,
необходимые для создания новой записи в таблице Orders. В диалоговом окне можно
отобразить текущую дату в качестве даты оформления заказа и предоставить элемент
управления для выбора необходимой даты доставки. Остальным полям можно просто
присвоить произвольные значения, дабы излишне не усложнять пример.
Генерируется как
max(OrderlD)+1
Произвольное
значение,
Ввод из
аналога
Произвольное
значение,
Текущая
дата
Устанавливается
равным
Произвольное
значение,
равное 9.95
Рис. 20,14, Источники данных для полей новой записи таблицы Orders
Естественно, в диалоговом окне не обязательно отображать всю информацию та-
блицы Customers — для обеспечения выбора достаточно отобразить имя, идентифи-
цирующее клиента. Однако набор записей должен содержать все остальные данные.
Элементы управления в диалоге IDD_CUSTOMER_FORM можно разместить, как показано
на рис. 20.15.
На рис. 20.15 видно, как будут использоваться элементы управления, и как выпол-
няется присваивание необходимых идентификаторов. Элементы выбора даты позво-
ляют вводить или выбирать значения даты или времени. Выбор даты или времени
зависит от значения, установленного в свойстве Format (Формат). Изображенные на
этом рисунке элементы управления используют формат Short Date (Короткая дата).
1004 Глава 20
Выбор даты осуществляется щелчком на стрелке вниз и выбором даты из открыва-
ющегося окна календаря. Может выполнить эти действия для ознакомления с рабо-
той этого элемента управления. Обратите внимание, что элемент управления, ото-
бражающий дату создания заказа, не доступен пользователю, поскольку его значение
представляет собой всего лишь текущую дату. Такое состояние элемента управления
обеспечивается установкой значения свойства Disabled (Отключен) равным True.
Обратите также внимание, что свойства Read Only (Только для чтения) полей редак-
тирования идентификатора заказа и имени клиента установлены равными True, дабы
предотвратить изменение отображаемых значений.
IIDC.COMPANYNAME -»
Подключается к таблице Customers
IDC_NEWORDERID
Уникальный идентификатор, созданный
как max(OrderlD)+1 на основе
данных из таблицы Orders
IDC_SELECTPRODUCTS
Выполняет переход к диалогу
для выбора продуктов,
включаемых в заказ
г- IDCCANCELORDER
Отменяет создание нового заказа
IDC_REQUIREDDATE
Дата выполнения заказа
IDCORDERDATE
Текущая дата
Это элементы
выбора даты/времени
Self ctCustomer and Required Ship Date
OrderlD: I Sample ed Order Date:
Required Date: 02 05/2005
Customer Name: Sample edit box
Select Products
Cancel
Puc. 20.15. Элементы управления в диалоге TDD CUSTOMER FORM
В класс CCustomerView можно добавить переменные для хранения значений, по-
лученных из элементов управления выбора даты/времени. Щелкните правой кноп-
кой мыши на поле даты заказа и в контекстном меню выберите пункт Add Variable
(Добавить переменную). Открывающееся при этом диалоговое окно показано на
рис. 20.16.
Поскольку нам необходима переменная для хранения значения, полученного из
элемента управления, а не для обеспечения доступа к самому элементу, в качестве
значения Category (Категория) необходимо выбрать Value (Значение), а в качестве
Variable type (Тип переменной) — Ctime. Создаваемому члену можно присвоить имя
m OrderDate.
Обновление источников данных 1005
Рис. 20.16. Диалоговое окно мастера добавления переменноичлена
Мастер добавления переменной-члена (Add Member Variable Wizard) автоматиче-
ски добавляет код инициализации переменной к конструктору класса и вызов функции
DDX_— в функцию DoDateExchange (), обеспечивающую эффективный обмен данны-
ми между переменной и элементом управления. Аналогично можно добавить перемен-
ную m_RequiredDate для второго элемента управления выбора даты/времени.
Хотя это представление связано с набором записей, соответствующим таблице
Customers, с набором записей CCustomerSet потребуется связать только элемент ре-
дактирования, отображающий имя клиента, поскольку только это поле отображается
в диалоге. Для этого в функцию DoDataExchange () класса CCustomer можно доба-
вить вызов функции DDX_j
void CCustomerView::DoDataExchange(CDataExchange* pDX)
CRecordView::DoDataExchange(pDX);
DDX_DateTimeCtrl(pDX, IDC_ORDERDATE, m_OrderDate);
DDX_DateTimeCtrl(pDX, IDC_REQUIREDDATE, m_RequiredDate);
DDX__FieldText (pDX, IDC__COMPANYNAME, m_pSet~>m_CompanyName, m_pSet) ;
}
На этом этапе можно также щелкнуть правой кнопкой мыши на каждой из кнопок
и в контекстном меню выбрать пункт Add Event Handler, чтобы добавить для них об-
работчики событий. При желании используемые по умолчанию имена функций мож-
но сократить до OnSelectproducts () и OnCancelorder(). Код этих обработчиков и
код, связанный с остальными элементами управления, будет создан позже.
После того как клиент выбран, диалоговое окно IDD_PRODUCT_FORM обеспечива-
ет выбор продуктов, которые должны быть включены в заказ. Приложение выполня-
ет переход к этому диалоговому окну в результате щелчка на кнопке Select Products
(Выбрать продукты) в диалоговом окне выбора клиента. Диалоговое окно должно
отображать достаточный объем информации для выбора продукта, а также должно
1006 Глава 20
содержать элементы управления для ввода количества единиц продукта и размера
скидки. Это диалоговое окно показано на рис. 20.17.
Обратите внимание, что большинство элементов управления этого диалогового
окна, за исключением полей ввода значений объема заказа и скидки, доступны только
для чтения. Это связано с тем, что пользователь должен вводить только два упомя-
нутых значения. Поле имени продукта можно связать с набором записей, добавляя
оператор функции DoDataExchange () в класс CProductView:
void CProductView::DoDataExchange(CDataExchange* pDX)
CRecordView::DoDataExchange(pDX);
DDX_FieldText(pDX, IDC_PRODUCTNAMEr m_pSet->m_ProductName, m_pSet);
Функция DDX_FieldText () выполняет обмен данными между членом m_ProductSet
объекта CProductset и элементом управления с идентификатором IDD_PRODUCTNAME.
Добавьте обработчики событий BN_CLICKED для кнопок Select Product (Выбрать
продукт) и Done (Готово) в объект CProductView, применяя ту же методику, которая
была использована для ресурса предыдущего диалогового окна. Им можно присвоить
имена OnSelectproduct () и OnDone (), чтобы они соответствовали надписям кнопок.
Поля редактирования, отображающие идентификатор заказа и имя клиента, долж-
ны быть инициализированы значениями, которые будут определяться в предыдущем
диалоговом окне. Поэтому нам потребуются переменные класса для хранения значе-
ний этих элементов управления.
IDC_SELECTPRODUCT Завершает выбор текущего продукта
IDC_ORDERQUANTITY Значение, вводимое пользователем;
значение, выбираемое по умолчанию, равно 1
IDC.NEWORDERJD Значение, получаемое из диалогового окна выбора клиента
IDC_CUSTOMERNAME Значение, получаемое из диалогового окна выбора клиента
IDC_PRODUCTNAME Подключается к таблице Products
IDC_ORDERDISCOUNT Значение, вводимое пользователем;
значение, выбираемое по умолчанию, равно 0
IDC.DONE Завершает создание заказа
Order ID:
Customer:
Product Name:
dect a product
Sample ed
Sample edit box
Sample edit box
?nd en ег the quantity
and discount
I
Quantity:
Sample ed
Discount:
Sample ed
Select Product
Done
Pwc. 20.17. Диалоговое окно для ввода количества единиц продукта и размера скидки
Обновление источников данных 1007
4
Щелкните правой кнопкой мыши на каждом из этих элементов управления в диало-
говом окне IDD PRODUCT—FORM и в контекстном меню выберите команду Add Variable.
Для каждой из этих переменных должна быть выбрана категория Value, а в каче-
стве значения Acess (Доступ) установлено Public (Общедоступная). Для переменной
m_0rderID установите тип long, а для переменной m_CustomerName — тип CString.
Пользователь вводит значения в полях редактирования объема заказа и скидки,
поэтому класс CProductView должен содержать переменные и для хранения этих зна-
чений. Категорией обеих этих переменных также должна быть Value. Для хранения
значения объема заказа добавьте переменную m_Quantity типа int, а для хранения
значения скидки — переменную m_Discount типа float. Теперь эти переменные
должны инициализироваться в конструкторе, а функция DoDataExchange () будет вы-
глядеть следующим образом:
void CProductView::DoDataExchange(CDataExchange* pDX)
CRecordView::DoDataExchange(pDX);
DDX_FieldText(pDX, IDC_PRODUCTNAME, m_pSet->m_ProductName, m_pSet);
DDX_Text(pDX, IDC_NEWORDER, m_0rderID);
DDX_Text(pDX, IDC_COMPANYNAME, m_CustomerName);
DDX_Text(pDX, IDC_ORDERQUANTITY, m_Quantity);
DDX_Text(pDX, IDC_ORDERDISCOUNT, m_Discount);
Сейчас, когда диалоги определены, можно реализовать механизм перехода от
одного диалогового окна к другому.
Реализация переключения диалоговых окон
Общая логика переключения диалоговых окон показана на рис. 20.12. Механизмом
перехода от одного диалогового окна к другому служит щелчок на кнопке, поэтому
обработчики кнопок будут содержать код, вызывающий это переключение. Вначале
можно определить идентификаторы представлений каждого из трех диалоговых
окон, так что добавьте заголовочный файл ViewConstants .h. На этот раз для иденти-
фикации представлений мы попробуем воспользоваться объявлением перечисления,
соответственно, файл будет содержать следующий код:
// Определение констант, идентифицирующих представления записей
#pragma once
enum ViewID{ ORDER_DETAILS, NEW_ORDER, SELECT_PRODUCT };
Для записи идентификатора текущего представления класс CMainFrame должен со-
держать переменную типа ViewID, поэтому добавьте переменную m_CurrentViewID,
щелкнув правой кнопкой мыши на объекте Ста inFrame в панели Class View и выбрав
пункт Add Member Variable (Добавить переменную-член) из контекстного меню. Эта
переменная должна быть инициализирована, потому измените конструктор класса
CMainFrame следующим образом:
CMainFrame::CMainFrame () : m_CurrentViewID (ORDER_DETAILS)
// TODO: здесь следует вставить код инициализации члена
Приведенный код определяет представление, с открытия которого всегда бу-
дет запускаться приложение. Добавьте в файл . срр директиву #include для файла
1008 Глава 20
ViewConstants.h, поскольку он содержит определения идентификаторов представ-
лений.
Теперь в класс CMainFrame можно добавить функцию-член Selectview(), которая
выполняет переключение диалоговых окон. Возвращаемый тип этой функции — void,
а единственный параметр имеет тип ViewID, поскольку аргументом будет один из
идентификаторов представлений, определенных в объекте enum. Ниже показан код
реализации функции Selectview ().
//Обеспечивает переключение представлений. Аргумент указывает новое представление
void CMainFrame:: SelectView (ViewID viewID)
CView* pOldActiveView - GetActiveView (); // Получение текущего представления
// Получения указателя на новое представление, если оно существует.
// Если оно не существует, указателем будет null
CView* pNewActiveView = static cast<CView*>(GetDlgltem(viewID));
// Если переход к новому представлению выполняется впервые,
// оно не будет существовать, поэтому его нужно создать
// Представление Order Details всегда создается первым, поэтому
//о его создании можно не беспокоиться.
if (pNewActiveView == NULL)
switch(viewID)
case NEW_ORDER: // Создание представления для добавления нового заказа
pNewActiveView = new CCustomerView;
break;
case SELECT_PRODUCT: // Создание представления для
11 добавления продукта в заказ
pNewActiveView - new CProductView;
break;
default:
AfxMessageBox(_T (’’Неверный идентификатор представления"));
return;
// Переключение представлений
// Получение контекста текущего представления
// для его применения к новому представлению
CCreateContext context;
context.m_pCurrentDoc = p01dActiveView->GetDocument();
pNewActiveView->Create(NULL, NULL, OL, CFrameWnd::rectDefault,
this, viewID, &context);
pNewActiveView->OnInitialUpdate();
SetActiveView(pNewActiveView); // Активизация нового представляения
p01dActiveView->ShowWindow(SW_HIDE); // Сокрытие старого представления
pNewActiveView->ShowWindow(SW_SHOW); // Отображение нового представления
pOldActiveView->SetDlgCtrlID(m_CurrentViewID); //Определение идентификатора
//старого представления
pNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
m_CurrentViewID - viewID; // Сохранение идентификатора нового представления
RecalcLayout();
Обновление источников данных 1009
Приведенный код ссылается на классы CCustomerView и CProductView, поэтому
исходный файл должен содержать директивы #include для включения заголовочных
файлов CustomerView.h и Productview.h.
Переход от диалогового окна сведений о заказе к диалоговому окну, начи-
нающему создание заказа, осуществляется в обработчике OnNeworder () класса
COrderDetailsView:
void CCustomerView::OnSelectproducts()
static_cast<CMainFrame*>(GetParentFrame())->SelectView(SELECT_PRODUCT);
Эта функция получает указатель на родительское обрамляющее окно представле-
ния — объект CMainFrame приложения, — а затем использует его для вызова функции
Selectview (), выбирающей диалоговое окно обработки нового заказа. Файл исхо-
дного кода должен содержать директиву #include для ViewConstants .h, поскольку
код ссылается на представление NEW_ORDER, и директиву #include для MainFrm.h,
чтобы можно было получить определение CMainFrame.
Обработчик кнопки Select Products в классе CCustomerView выполняет переход к
диалоговому окну объекта CProductsView:
void CCustomerView::OnSelectproducts()
static cast<CMainFrame*>(GetParentFrame())->SelectView(SELECT PRODUCT);
Обработчик кнопки Cancel этого же класса просто выполняет переход к предыду-
щему представлению:
void CCustomerView::OnCancelorder()
static_cast<CMainFrame*>(GetParentFrame())->SelectView(ORDER_DETAILS);
He забудьте добавить в файл Customerview.срр директивы #include для заголо-
вочных файлов ViewConstants .h и Customerview. срр.
Последняя операция переключения представлений должна быть реализована в об-
работчике OnDone () в классе CProductView ():
void CProductView::OnDone()
static_cast<CMainFrame*>(GetParentFrame())->SelectView(ORDER_DETAILS);
Эта функция выполняет переход обратно к исходному представлению приложе-
ния, которое обеспечивает просмотр и редактирование сведений о заказе. Конечно,
при желании вместо этого можно было бы выполнять переход к диалоговому окну
CCustomerView для обеспечения ввода последующих записей заказа. При этом необ-
ходимо не забыть о добавлении директив #include для файлов ViewConstants .h и
MainFrm.h.
Переход от исходного диалогового окна просмотра сведений о заказе к диалого-
вому окну редактирования должен также управлять видимостью кнопки New Order.
В противном случае в диалоговом окне редактирования эта кнопка будет скрывать
кнопку Cancel. Добавьте в класс COrderDetailsView управляющую переменную
m NewOrderCtrl, соответствующую идентификатору IDC_NEWORDER. Затем можно
внести изменения в обработчик OnEditorder ():
1010 Глава 20
void COrderDetailsView::OnEditorder()
if(m_pSet->CanUpdate())
try
if(m_Mode == UPDATE)
{ // Если во время щелчка на кнопке программа находилась
//в режиме обновления
// Отключение ввода для полей редактирования
m_QuantityCtrl.SetReadOnly();
m_DiscountCtrl.SetReadOnly();
// Изменение текста кнопки Update на Edit Order
m_EditOrderCtrl.SetWindowText(_T("Edit Order"));
// Сокрытие кнопки Cancel
m_CancelEditCtrl.ShowWindow(SW_HIDE);
// Отображение кнопки создания нового заказа
m_NewOrderCtrl.ShowWindow(SW_SHOW);
// Завершение обновления
m_pSet->Update();
Mode = READ_ONLY; // Переход в режим "только чтение"
else
{ // Если во время щелчка на кнопке программа находилась
//в режиме "только чтение"
// Активизация ввода для полей редактирования
m_QuantityCtrl.SetReadOnly(FALSE);
m_DiscountCtrl.SetReadOnly(FALSE);
// Изменение текста кнопки Edit Order на Update
m_EditOrderCtrl.SetWindowText(_T("Update"));
// Сокрытие кнопки создания нового заказа
m_NewOrderCtrl.ShowWindow(SW_HIDE);
// Отображение кнопки Cancel
m_CancelEditCtrl.ShowWindow(SW_SHOW);
/ / Начало обновления
m_pSet->Edit();
m_Mode = UPDATE; // Переключение в режим обновления
catch(CException* рЕх)
pEx->ReportError(); // Отображение сообщения об ошибке
else
AfxMessageBox(_Т("Набор записей недоступен для обновления."));
Теперь, в зависимости от того, хранится ли в переменной m_UpdateMode зна-
чение READ_ONLY или UPDATE, можно скрывать или отображать кнопку New Order.
Потребуется также обеспечить видимость кнопки в обработчике OnCancel ():
Обновление источников данных 1011
void COrderDetailsView::OnCancel()
m_pSet->CancelUpdate(); // Отмена операции обновления
m_EditOrderCtrl.SetWindowText(_Т("Edit")); // Переключение текста кнопки
m_CancelEditCtrl.ShowWindow(SW_HIDE); // Сокрытие кнопки Cancel
m_NewOrderCtrl. ShowWindow (SW—SHOW) ; // Отображение кнопки New Order
m_QuantityCtrl.SetReadOnly(TRUE); // Установка состояния поля
// редактирования объема заказа
m_DiscountCtrl.SetReadOnly(TRUE); // Установка поля редактирования скидки
m_UpdateMode = !m_UpdateMode; // Переключение режима
Итак, основной механизм переключения представлений реализован. Еще предсто-
ит вернуться назад и добавить код, выполняющий обновление базы данных. Однако
на этом этапе целесообразно выполнить пробную компиляцию и запуск приложения
для устранения любых возможных опечаток и других ошибок. Когда программа зара-
ботает, вы должны иметь возможность просматривать записи клиентов и продуктов.
Обязательно проверьте все ветви программы.
Создание идентификатора заказа
Для создания идентификатора нового заказа необходим набор записей табли-
цы Orders. Щелкните правой кнопкой мыши на объекте DBSimpleUpdate в панели
Class View и в контекстном меню выберите пункт Add1^ Class. В качестве шаблона
выберите MFC ODBC Consumer и щелкните на кнопке Add (Добавить). Затем вы-
берите Northwind в качестве базы данных, a Orders — в качестве таблицы, для ко-
торой нужно создать набор записей. Для типа укажите Dynaset, поскольку этот на-
бор записей будет повторно использоваться при добавлении новых заказов. Введите
COrderSet в качестве имени класса и задайте соответствующие имена для фай-
лов OrderSet. h и OrderSet. срр. Чтобы создать класс, щелкните на кнопке Finish
(Готово). Тип членов CStringW можно изменить на CString, а директиву #еггог в
файле класса OrderSet. срр можно закомментировать.
Сохранение идентификатора нового заказа
В этом разделе подробно рассматриваются операции с набором записей. При каж-
дом создании нового заказа в классе CCustomerView будет требоваться уникальный
идентификатор заказа, поэтому следует подумать, где и как это лучше всего делать.
Несмотря на то что идентификатор отображается одним из полей редактирования
в представлении CCustomerView, в действительности новый идентификатор должен
создаваться объектом COrderSet, поскольку он зависит от данных, хранящихся в этом
наборе записей. Рациональным подходом было бы добавление в класс CCustomerView
переменной, устанавливающей значение идентификатора в элементе редактирова-
ния, которая определялась бы функцией объекта COrderSet.
Обратитесь к диалогу IDD_CUSTOMER_FORM в панели Resource View и щелкните
правой кнопкой мыши на элементе редактирования идентификатора заказа (этот эле-
мент редактирования имеет идентификатор IDC__NEWORDERID). Выберите пункт Add
Variable из контекстного меню, а затем введите имя переменной. Установите тип и
категорию переменной, как показано на рис. 20.18.
1012 Глава 20
Рис. 20.18. Создание переменной для хранения идентификатора нового заказа
По умолчанию типом переменной является CString, поэтому не забудьте его из-
менить на long. Функции DDX_Text (), выполняющие передачу данных в элемент ре-
дактирования и из него, имеют множество разновидностей, которые соответствуют
различным типам данных, представленным в раскрывающемся списке этого диалого-
вого окна.
Создание нового идентификатора заказа
Объект COrderSet относится к объекту документа, поэтому наряду с членом
m_DBSimpleUpdateSet, который был создан мастером Application Wizard, добавь-
те в класс CDBSimpleUPdateDoc член данных по имени m_OrderSet. Для этого, как
обычно, щелкните правой кнопкой мыши на имени класса в панели Class View и в
контекстном меню выберите пункт Ad d^ Add Variable. Документ COrderset создается
автоматически при создании объекта документа. Когда объект заказа определен в до-
кументе, он доступен в любом из классов представлений, нуждающемся в нем.
Для обеспечения генерации уникального идентификатора нового заказа в класс
COrderSet следует добавить новую функцию-член. Перейдите в панель Class View и
добавьте функцию CroateNewOrderID () с возвращаемым типом long и без парамет-
ров.
Первым делом функция CreateNewOrderlD () должна проверять, открыт ли набор
записей:
long COrderSet::CreateNewOrderlD()
if (! IsOpen ())
Open(CRecordset::dynaset);
// Остальной код реализации функции...
Обновление источников данных 1013
Функция IsOpen (), вызываемая в операторе if, возвращает значение TRUE, если
набор записей открыт, и значение FALSE в противном случае. Для открытия набора
записей потребуется вызвать член Open (), унаследованный от класса CRecordSet. Эта
функция посылает базе данных SQL-запрос с типом набора записей, который указан
в первом аргументе. В данном случае первый аргумент указан в виде CRecordset: :
dynaset, что, как легко догадаться, открывает набор записей в качестве динамиче-
ского набора. В данном случае явное указание типа набора записей не обязательно,
поскольку при отсутствии этого аргумента был бы применен используемый по умол-
чанию тип, заданный при создании класса — dynaset. Однако его явное указание
служит дополнительным поводом для описания других его возможных значений
(табл. 20.5).
Таблица 20.5. Типы набора записей
Тип
Описание
CRecordset::snapshot
CRecordset::forwardonly
CRecordset::dynamic
AFX_DB_U SE_DE FAULT_TY PE
Набор записей открывается как снимок; снимки и динамические
наборы рассматривались в предыдущей главе.
Набор записей открывается как доступный только для чтения и в
нем можно выполнять прокрутку записей только вперед.
(При открытии набора записей он автоматически позиционируется
на первую запись.)
Набор записей открывается с возможностью прокрутки в обоих
направлениях, а изменения, выполненные другими пользователями,
отражаются в его полях.
Набор записей открывается с типом, указанным по умолчанию, ко-
торый хранится в унаследованном члене m nDefaultType,
инициализируемом в конструкторе.
Функция Open () получает еще два параметра, для которых мы принимаем значе-
ния аргументов, используемые по умолчанию. Второй параметр — указатель на строку,
которая может быть именем таблицы, SQL-оператором SELECT, вызовом предвари-
тельно определенной процедуры запроса или нулевой строкой — последнее значение
используется по умолчанию. Если это значение — null, функция использует строку,
возвращенную функцией GetDefaultSQL (). Третий параметр — битовая маска, кото-
рая может служить для указания множества параметров подключения, в том числе
для указания доступа только для чтения, что делает запись в набор записей вообще
невозможной, или для разрешения добавления записей только в конец набора, что
запрещает редактирование или удаление записей. Подробнее эти параметры описаны
в документации по функции.
Когда набор записей открыт, необходимо просмотреть все записи для отыскания
наибольшего значения в поле OrderlD. Это выполняется с помощью следующего
кода:
long COrderSet::CreateNewOrderID()
if (! IsOpen ())
Open(CRecordset::dynaset);
// Проверка на предмет отсутствия записей в наборе записей
long newOrderlD = 0;
if (! (IsBOFO && IsEOFO ) )
// Записи существуют,
1014 Глава 20
MoveFirstO; // поэтому перейти к первой записи
while(!IsEOF()) // Сравнение со всеми остальными
// Сохранение идентификатора заказа, если он - наибольший
if(newOrderlD < m_OrderID)
newOrderlD = m_OrderID;
MoveNext(); // Переход к следующей записи
}
return ++newOrderID;
Члены IsBOF () и IsEOF () класса набора записей возвращают значение true, если
указатель оказывается за пределами, соответственно, начала или конца записей. Это
свидетельствует об отсутствии активной записи в текущий момент времени и о необ-
ходимости использования полей. Когда набор записей пуст, обе функции возвращают
значение TRUE. Если же какие-то записи существуют, программа выполняет переход
к первой записи вызовом функции-члена MoveFirst (). Существует также функция-
член MoveLast (), которая выполняет переход к последней записи набора.
Мы создаем локальную переменную newOrderlD с начальным значением, рав-
ным 0, в которой впоследствии будет храниться максимальный идентификатор зака-
за, записанный в таблице. Цикл while с помощью функции-члена MoveNext () выпол-
няет перемещение по всем записям набора, отыскивая наибольшее значение члена
m_0rderID. Прежде чем вызывать любую из функций-членов перемещения по набору
записей, в зависимости от направления перемещения необходимо обратиться либо
к IsBOF (), либо к IsEOF (). В случае вызова функции перемещения вне пределов,
определяемых концом или началом набора записей, функция возбуждает исключение
типа CDBException.
Кроме использованных в данном случае функций перемещения объект набора за-
писей предоставляет еще три функции, описанные в табл. 20.6.
Таблица 20.6. Дополнительные функции перемещения набора записей
Функция Описание
MoveLast () Выполняет переход к последней записи набора записей. Эту функцию (или функцию
MoveFirst О) нельзя использовать с набором записей, в котором разрешено пере-
мещение только вперед. В противном случае функция возбудит исключение типа
CDBException.
MovePrev () Выполняет переход к записи, которая предшествует текущей. Если такой записи
нет, функция выполняет переход на одну позицию за пределами первой записи.
Расположенные перед этой позицией поля набора записей недопустимы и функция
isbof () возвращает значение true.
Move()
Эта функция используется для перемещения на одну или более записей
наборе
записей. Первый аргумент типа long указывает количество строк, на которые необ-
ходимо выполнить перемещение. Второй аргумент типа word определяет сущность
операции перемещения. Четыре возможных значения второго аргумента делают
функцию эквивалентной другим рассмотренным функциям перемещения.
Подробнее эта функция описана в документации по Visual C++.
По завершении цикла переменная newOrderlD будет содержать максимальный
идентификатор заказа, поэтому прежде чем возвратить значение нового идентифика-
тора, достаточно увеличить его на единицу.
Обновление источников данных 1015
Последнее действие, которое потребуется выполнить — передача значения эле-
менту управления, чтобы оно отображалось в диалоговом окне IDD CUSTOMER FORM.
Этой цели служит вызов функции UpdateData () с аргументом FALSE для объекта
представления набора записей. Эта функция наследуется классом представления за-
писей из класса CWnd. Значение аргумента, равное FALSE, вызывает передачу данных
из члена класса представления элементам управления диалогового окна. Значение
TRUE вызывает извлечение данных из элементов управления и их сохранение в чле-
нах. В обоих случаях необходимые операции выполняются посредством обращения к
функции-члену DoDataExchange () представления, которое вызывается каркасом при-
ложений.
Инициализация создания идентификатора
Представление клиентов нуждается в доступном идентификаторе нового зака-
за при первом отображении. Добавьте в класс CCustomerView функцию-член типа
public и реализуйте ее следующим образом:
void CCustomerView::SetNewOrderID(void)
// Получение идентификатора нового заказа из объекта COrderSet в документе
m_NewOrderID = static_cast<CDBSimpleUpdateDoc*>
(GetDocument())->m_OrderSet.CreateNewOrderlD();
UpdateData(FALSE); // Передача данных элементам управления
Типом указателя, возвращаемого унаследованной функцией GetDocument (), явля-
ется Cdocument. Этот указатель требуется для получения доступа к члену m_OrderSet
производного класса, поэтому его тип нужно привести к CDBSimpleUpdateDoc*.
Затем применительно к члену m_OrderSet класса документа можно вызвать функ-
цию-член, которая возвращает идентификатор нового заказа, и сохранить результат
в члене M_NewOrderID класса CCustomerView. Вызов унаследованной функции-чле-
на UpdateData () представления передает данные-члены представления элементам
управления. Теперь в файл исходного кода потребуется добавить директиву #include
для включения файла DBSimpleUpdateDoc.h, поскольку код ссылается на имя класса
DBSimpleUpdateDoc.
Поскольку мы создаем единственный объект CCustomerView и используем его при
необходимости, новый идентификатор должен быть доступным при каждом пере-
ходе к этому представлению. Член Selectview () объекта CMainFrame выполняет
переключение представлений, и в нем же впервые создается объект CCustomerView.
Поэтому здесь целесообразно инициировать процесс создания нового иденти-
фикатора заказа. Для этого достаточно добавить несколько строк кода для вызо-
ва члена SetNewOrderlD () в том представлении, которое соответствует объекту
CCustomerView.
void CMainFrame::Selectview(ViewID viewID)
CView* pOldActiveView = GetActiveView (); // Получение текущего представления
// Получения указателя на новое представление, если оно существует.
// Если оно не существует, указателем будет null
CView* pNewActiveView = static_cast<CView*>(GetDlgltem(viewID));
// Если переход к новому представлению выполняется впервые,
// оно не будет существовать, поэтому его нужно создать
// Представление Order Details всегда создается первым, поэтому
//о его создании можно не беспокоиться.
1016 Глава 20
if (pNewActiveView == NULL)
switch(viewID)
case NEW_ORDER: //Создание представления для добавления нового заказа
pNewActiveView = new CCustomerView;
break;
case SELECT__PRODUCT: //Создание представления для добавления продукта в заказ
pNewActiveView = new CProductView;
break;
default:
AfxMessageBox(_T("Неверный идентификатор представления"));
return;
// Переключение представлений
// Получение контекста текущего представления
// для его применения к новому представлению
CCreateContext context;
context.m_pCurrentDoc = p01dActiveView->GetDocument();
pNewActiveView->Create(NULL, NULL, OL, CFrameWnd::rectDefault,
this, viewID, &context);
pNewActiveView->OnInitialUpdate();
SetActiveView(pNewActiveView); // Активизация нового представления
if(viewID==NEW_ORDER)
static_cast<CCustomerView*>(pNewActiveView)->SetNewOrderID();
p01dActiveView->ShowWindow(SW_HIDE); // Сокрытие старого представления
pNewActiveView->ShowWindow(SW_SHOW); // Отображение нового представления
pOldActiveView->SetDlgCtrlID (m_CurrentViewID); //Определение идентификатора
//старого представления
pNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
m_CurrentViewID = viewID; //Сохранение идентификатора нового представления
RecalcLayout();
Мы всего лишь проверяем значение viewID. Если им является NEW_ORDER, вызыва-
ется член SetNewOrderlD () объекта нового представления. Поскольку типом объек-
та pNewActiveView является CView, для вызова функции-члена его следует привести
к типу действительно используемого представления.
Хранение данных заказа
Новая запись в таблице Orders не должна создаваться до тех пор, пока не бу-
дет создана первая запись представления Order Details для заказа. Поэтому необ-
ходим способ передачи данных, накопленных в объекте CCustomerView, в объект
CProductView. Проще всего это сделать, определив новый класс, представляющий
заказ. Он должен содержать только данные-члены для каждого значения данных, ко-
торое требуется сохранить. За исключением поля даты поставки, которое не содер-
жит значения при создании нового заказа, данные-члены совпадают соответствующи-
ми полям в классе COrderSet. Создайте в проекте новый файл заголовка Order .h и
добавьте в него следующий код:
// Сохраняет данные нового заказа
#pragma once
class COrder
Обновление источников данных 1017
public:
// Данные-члены совпадают с полями в классе COrderSet
long m_OrderID;
CString m_CustomerID;
long m_EmployeeID;
CTime m_OrderDate;
CTime m_RequiredDate;
long m_ShipVia;
double m_Freight;
CString m_ShipName;
CString m_ShipAddress;
CString m_ShipCity;
CString m_ShipRegion;
CString m_ShipPostalCode;
CString m_ShipCountry;
11 Конструктор по умолчанию
COrder():
m_0rderID(0)r 11 Будет устанавливаться объектом CCustomerView
m_EmployeeID(1), // Идентификатору сотрудника присвоено
// произвольное значение
m_ShipVia(3), // Произвольная компания доставки
m_CustomerID(__Т(””)), m_Freight(0.0), m_ShipName(_Т("”))г
m_ShipAddress(_Т("")), m_ShipCity(_Т ("")), mjShipRegion(_Т("")),
m_ShipPostalCode(_Т ("")), m_ShipCountry(_Т(” ”))
SYSTEMTIME Now;
GetLocalTime(&Now); // Получение текущего времени
m_OrderDate = m_RequiredDate = CTime(Now); // Установка текущего времени
//в качестве значений полей
Вообще говоря, не рекомендуется делать все данные-члены класса общедоступны-
ми (public), как в данном случае, но поскольку все классы наборов записей, генери-
руемые мастером классов, содержат только члены типа public, определение членов
в нашем классе как private практически ничего не изменило бы.
Если в класс документа добавить член данных m_Order типа COrder, его можно
будет использовать для передачи данных заказа объекту CProductView. Для этого
достаточно вынудить объект CCustomerView при щелчке на кнопке Select Products
загружать данные-члены, подготовленные для выбора объектом CProductView.
Обработчик кнопки в объекте CCustomerView можно реализовать следующим обра-
зом:
void CCustomerView::OnSelectproducts()
// Получение указателя на документ
CDBSimpleUpdateDoc* pDoc = static_cast<CDBSimpleUpdateDoc*>(GetDocument());
// Установка значений полей на основании данных,
// полученных из объекта CCustomerSet
pDoc->m_Order.m_CustomerID = m_pSet->m_CustomerID;
pDoc->m_Order.m_ShipAddress = m_pSet->m_Address;
pDoc-> m_0rder .m_ShipCity = m_pSet->m__City;
pDoc-> m_0rder.m_ShipCountry = m_pSet->m_Country;
pDoc-> m_Order .m_ShipName = m_pSet->m_CompanyName;
pDoc-> m_Order.m_ShipPostalCode = m_pSet->m_PostalCode;
1018 Глава 20
pDoc-> m_Order.m_ShipRegion = m_pSet->m_Region;
// Установка значений полей на основании значений,
/ / введенных в диалоговом окне CCustomerView
pDoc-> m_0rder.m_OrderID = m_NewOrderID; // Сгенерированный новый
// идентификатор
pDoc-> m_0rder.m_OrderDate = m_OrderDate; // Из поля даты заказа
pDoc-> m_0rder.m_RequiredDate = m_RequiredDate; //Из поля даты
/ / выполнения заказа
static_cast<CMainFrame*>(GetParentFrame())->SelectView(SELECT_PRODUCT);
Все эти действия не требуют особых пояснений. Мы всего лишь копируем значе-
ния из набора записей и объектов представления записей в объект Order, являющий-
ся членом объекта документа.
Установка дат
С элементами выбора дат в диалоговом окне CCustomerView связана неболь-
шая проблема: в данный момент соответствующие им члены m_OrderDate и
m__RequiredDate еще не инициализированы, поэтому вначале элементы управления
не отображают никаких осмысленных значений. Для начала потребуется отобразить
текущую дату, поэтому в конец функции-члена OnlnitialUpdate (), вызываемой при
первом создании объекта представления, необходимо добавить код инициализации
этих переменных:
void CCustomerView::OnlnitialUpdate()
BeginWaitCursor ();
GetRecordset();
CRecordView::OnlnitialUpdate();
if (m_pSet->IsOpen())
CString strTitle = m_pSet->m_pDatabase->GetDatabaseName();
CString strTable = m_pSet->GetTableName();
if(’strTable.IsEmpty())
strTitle += _T(":") + strTable;
GetDocument()->SetTitle(strTitle);
EndWaitCursor();
// Инициализация значений времени
SYSTEMTIME Now;
GetLocalTime(&Now); // Получение текущего времени
m_OrderDate = m_RequiredDate = CTime(Now); // Установка текущего времени
//в качестве значений полей
Этот код устанавливает значения обеих переменных CTime равными текущему, по-
добно тому, как это было сделано в конструкторе класса COrder.
Теперь объект CCustomerView определен достаточно хорошо. Он отображает кор-
ректную дату и накапливает все значения полей строки таблицы Orders. Поэтому
можно приступить к процессу выбора продукта.
Выбор продуктов для включения в заказ
Для представления выбора продукта требуются переменные элементов управле-
ния, которые обеспечивали бы отображение уже установленных значений идентифи-
Обновление источников данных 1019
катора заказа и имени клиента. Эти значения будут извлекаться из члена Order объ-
екта документа. Для этого в класс CProductView можно добавить соответствующую
функцию. Ей можно присвоить имя InitializeView(), а в качестве возвращаемого
типа указать void. Эту функцию можно вызывать из члена Select View () объекта
СМаinFrame приложения. Тем самым мы обеспечим гарантированную инициализа-
цию элементов управления перед отображением диалогового окна.
Но прежде чем реализовать функцию InitializeView (), необходимо учесть не-
сколько дополнительных обстоятельств. Добавление новой записи в таблицу Orders
осуществляется только при первом щелчке на кнопке Select Product добавления
продукта в заказ. Последующие щелчки на кнопке должны просто добавлять в заказ
другой продукт, поэтому нам требуется способ определения того, выполняется ли до-
бавление записей в конец таблицы Orders после щелчка на кнопке. Эту задачу мож-
но решить, добавив в объект CProductView переменную m_OrderAdded типа bool,
значение которой вначале равно false, а затем обработчиком кнопки Select Product
устанавливается равным true. Поэтому добавим ее в класс. Ее можно инициализиро-
вать в функции-члене InitializeView (), реализованной следующим образом:
void CProductView::InitializeView()
// Получение указателя на документ
CDBSimpleUpdateDoc* pDoc = static_cast<CDBSimpleUpdateDoc*>(GetDocument());
m__0rderID = pDoc->m_Order .m_0rderID;
m__Cus tome rName = pDoc->m__Order .m_ShipName;
m_Quantity =1; / / Обязателен заказ не менее одной единицы продукта
m__Discount =0; // Значение скидки, используемое по умолчанию, отсутствует
m_OrderAdded = false; // Вначале заказ не добавлен
UpdateData(FALSE); // Передача данных элементам управления
Эта функция инициализирует члены класса представления для полей идентификато-
ра заказа и имени клиента, копируя значения из соответствующего члена объекта Order
документа. Она позволяет также гарантировать заполнение полей объема заказа и скид-
ки подходящими начальными значениями. Объем заказа любого продукта должен быть
не менее 1, а используемое по умолчанию значение скидки равно 0. Как уже было ска-
зано ранее, вызов унаследованной функции-члена UpdateData () с аргументом FALSE
ведет к передаче данных из переменных класса элементам управления. Чтобы опреде-
ление класса документа было доступным, в начало файла исходного кода придется доба-
вить директиву #include для включения заголовочного файла DBSimpleUpdateDoc. h.
Для обеспечения правильной работы функцию InitializeView() достаточно вы-
зывать при каждом переходе к диалоговому окну выбора продукта. Очевидно, что наи-
более рационально это делать в функции-члене SelectView () класса CMainFrame:
void CMainFrame::Selectview(ViewID viewID)
CView* pOldActiveView = GetActiveView (); // Получение текущего представления
// Получения указателя на новое представление, если оно существует.
// Если оно не существует, указателем будет null
CView* pNewActiveView = static_cast<CView*>(GetDlgltem(viewID));
// Если переход к новому представлению выполняется впервые,
// оно не будет существовать, поэтому его нужно создать.
// Представление Order Details всегда создается первым, поэтому
//о его создании можно не беспокоиться.
if (pNewActiveView == NULL)
1020 Глава 20
switch(viewID)
case NEW_ORDER: // Создание представления для добавления нового заказа
pNewActiveView = new CCustomerView;
break;
case SELECT_PRODUCT: // Создание представления для добавления
// продукта в заказ
pNewActiveView = new CProductView;
break;
default:
AfxMessageBox (_T ("Неверный идентификатор представления"));
return;
/ / Переключение представлений
// Получение контекста текущего представления
// для его применения к новому представлению
CCreateContext context;
context.m_pCurrentDoc = p01dActiveView->GetDocument();
pNewActiveView->Create(NULL, NULL, 0L, CFrameWnd::rectDefault,
this, viewID, &context);
pNewActiveView->OnInitialUpdate();
SetActiveView(pNewActiveView); // Активизация нового представления
if(viewID==NEW_ORDER)
static_cast<CCustomerView*>(pNewActiveView)->SetNewOrderID();
else if(viewID == SELECT_PRODUCT)
static_cast<CProductView*>(pNewActiveView)->InitializeView();
p01dActiveView->ShowWindow(SW_HIDE); // Сокрытие старого представления
pNewActiveView->ShowWindow(SW_SHOW); // Отображение нового представления
pOldActiveView->SetDlgCtrlID(m_CurrentViewID); //Определение идентификатора
//старого представления
pNewActiveView->SetDlgCtrlID (AFX_IDW_PANE_FIRST) ;
m_CurrentViewID = viewID; // Сохранение идентификатора нового представления
RecalcLayout();
Значение параметра ViewID, равное SELECT_PRODUCT, будет приводить к инициа-
лизации переменных класса CProductView, соответствующих полям идентификатора
заказа и имени клиента, а также переменной типа bool, управляющей созданием но-
вой записи в таблице Orders.
Добавление нового заказа
Последняя часть создаваемой программы — код добавления нового заказа. До-
бавление нового заказа всегда выполняется функцией-членом OnSelectproducts ()
класса CProductView. Эффект от щелчка на кнопке Select Products зависит от значе-
ния члена данных m_0 г de г Added. Если оно равно false, функция должна добавить
новые записи в таблицы Orders и Order Details. Если значение этой переменной
равно true, добавление новой записи должно выполняться только в таблицу Order
Details, поскольку речь идет о еще одном продукте в рамках одного и того же зака-
за. Все значения, требуемые для создания новой записи объекта Orders, хранятся в
члене m Order документа. Нужно лишь скопировать их в члены объекта COrderSet,
который также является членом документа. Объект документа обладает всем необхо-
димым для выполнения этой задачи, поэтому добавьте в класс CDBS imp 1 eUpdate Do с
функцию-член AddOrder () с возвращаемым типом bool и реализуйте ее так, как по-
казано ниже.
Обновление источников данных 1021
bool CDBSimpleUpdateDoc::AddOrder()
if(!m_OrderSet.IsOpen())
m_OrderSet.Open();
// Если набор записей не открыт,
// его нужно открыть
if(m_OrderSet.CanAppend()) // Если добавление записи возможно,
{ //ее необходимо добавить
m_OrderSet.AddNew(); // Начало добавления новой записи
m_OrderSet.m_CustomerID = m_0rder.m_CustomerID;
m_OrderSet.m_EmployeeID = m_0rder.m_EmployeeID;
m_OrderSet.m_Freight = m_Order.m_Freight;
m_OrderSet.m_OrderDate = m_0rder.m_OrderDate;
m_OrderSet .m_OrderID = m_0rder .m_0rderID;
m_OrderSet.m_RequiredDate = m_0rder.m_RequiredDate;
m_0rderSet.m_ShipAddress = m_0rder.m_ShipAddress;
m_OrderSet.m_ShipName = m_Order.m_ShipName;
m_OrderSet.m_ShipPostalCode = m_Order.m_ShipPostalCode;
m_OrderSet.m_ShipRegion = m_0rder.m_ShipRegion;
m_OrderSet.m_ShipVia = m_Order.m_ShipVia;
// Для поля Shipped Date значение не передается
m_OrderSet.SetFieldNull(&m_OrderSet.m_ShippedDate);
m_0rderSet.Update(); // Завершение добавления новой записи
return true; // Возврат признака успешного добавления
else
AfxMessageBox(_Т("Дополнение таблицы Orders невозможно"));
catch (CException* рЕх) // Перехват любых исключений
pEx->ReportError(); // Отображение сообщения об ошибке
return false; // Признак неудачи
Ранее в этой главе было показано, что функции добавления и редактирования за-
писей в наборе могут возбуждать исключения. Поэтому во избежание прерывания
приложения в этих случаях код помещен в блок try и любые исключения будут пере-
хвачены.
После получения подтверждения открытия набора записей COrderSet с помощью
вызова функции С ап Арре nd () мы проверяем возможность добавления в него запи-
сей. Добавление новой записи состоит из трех шагов, которые описаны ниже.
1. Вначале вызывается функция-член AddNew () набора записей. Она начинает про-
цесс и сохраняет текущие значения данных-членов в наборе записей, поскольку
впоследствии они будут изменены на значения null. Это значение не имеет ни-
чего общего с нулевым значением указателей и не является нулем — в данном
случае нулевое значение означает отсутствие значения переменной.
2. Значения всех данных-членов полей набора записей устанавливаются равными
требуемым в записи. Это достаточно просто. Мы просто копируем значения, со-
храненные в членах объекта m_0rder, в члены объекта набора записей. Член
m_ShippedDate является нулевым, поскольку его значение еще не установлено.
3. Далее вызывается функция Update () для выполнения действительной записи
и одновременного восстановления первоначальных значений объекта набора
1022 Глава 20
записей. В данном случае это не применяется, но при отображении набора за-
писей, куда выполняется добавление, для вывода значений новой записи при-
шлось бы вызвать член Requery () объекта набора записей.
Теперь можно приступить к определению общей логики работы обработчика
OnSelectProduct () класса CProductView. Для выполнения передачи данных, кото-
рые были введены в поля редактирования, данным-членам объекта представления
необходимо вызвать функцию UpdateData () представления. Основной код функции
обработчика выглядит следующим образом:
void CProductView::OnSelectproduct()
UpdateData(TRUE); // Передача данных из элементов управления
// Получение указателя на документ
CDBSimpleUpdateDoc* pDoc = static_cast<CDBSimpleUpdateDoc*>(GetDocument());
if(’m_0rderAdded) // Если заказ не был добавлен,
m_OrderAdded = pDoc->AddOrder(); // попытаться его добавить
if(m_0гdeгAdded)
// Код добавления новой записи в таблицу Order Details...
После вызова функции UpdateData () по отношению к объекту CProductView
мы получаем указатель на объект документа. Это необходимо для вызова функции-
члена AddOrder () документа, которая будет выполнять необходимые действия.
Затем мы проверяем член m_OrderAdded. Добавление записи в таблицу Orders тре-
буется только в том случае, если значением этой переменной является false. Член
AddOrder () объекта документа возвращает значение типа bool, равное true, если за-
каз был успешно добавлен, и значение false — в случае возникновения любой ошиб-
ки. Это значение используется для установки значения члена m_OrderAdded объекта
CProductView и в качестве индикатора возможности продолжения добавления запи-
сей в таблицу сведений о заказе. В случае неудачи отображение какого-либо сообще-
ния не требуется. Функция AddOrder () уже сделала это.
Вероятно, для обработки кода добавления записи в таблицу Order Details наи-
более подходит объект документа, но требуемые для этого функции-члены класса
документа нуждаются в доступе к четырем значениям членов классов CProductView
и CProductset — значениям полей идентификатора продукта, объема заказа, цены
за единицу продукта и размера предоставляемой скидки. В классе документа значе-
ние идентификатора заказа можно получить из его члена m_0rder, поэтому о нем
можно не беспокоиться. Для добавления записи в таблицу Order Details в класс
CDBSimpleUpdateDoc можно добавить функцию AddOrderDetails. Ее возвращаемым
типом должен быть void, и она должна иметь четыре параметра: ID типа long, price
типа double, quantity типа int и discount типа float.
Эта функция может быть реализована следующим образом:
void CDBSimpleUpdateDoc::AddOrderDetails(long ID, double price,
int quantity, float discount)
if(!m_DBSimpleUpdateSet.IsOpen())
m_DBSimpleUpdateSet.Open();
m_DBSimpleUpdateSet.AddNew();
// Если набор записей не открыт,
// его нужно открыть
// Начало добавления новой записи
// Установка значений данных-членов для продукта
m_DBSimpleUpdateSet.m_0rderID = m_0rder.m_0rderID;
Обновление источников данных 1023
m_DBSimpleUpdateSet.m_Quantity = quantity;
m_DBSimpleUpdateSet.m_Discount = discount;
m_DBSimpleUpdateSet.m_ProductID = ID;
m_DBSimpleUpdateSet.m_UnitPrice = price;
m_DBSimpleUpdateSet.Update(); // Завершение добавления новой записи
}
catch(CException* рЕх) // Перехват любых исключений
{
pEx~>ReportError(); // Отображение сообщения об ошибке
}
}
Эта функция устанавливает значения членов m_DBSimpleUpdateSet, а затем об-
новляет таблицу, по сути, так же, как это выполнялось для таблицы Orders. Как и
ранее, код должен быть помещен в блок try, чтобы обеспечить перехват любых ис-
ключений, которые могут генерироваться функцией AddNew () или Update ().
Эта функция должна вызываться при каждом вызове обработчика кнопки Select
Product в классе CProductView, поэтому обработчик можно изменить:
void CProductView::OnSelectproduct()
{
UpdateData(TRUE); 11 Передача данных из элементов управления
// Получение указателя на документ
CDBSimpleUpdateDoc* pDoc = static_cast<CDBSimpleUpdateDoc*>(GetDocument());
if(’m_0rderAdded) // Если заказ не был добавлен,
m_OrderAdded = pDoc->AddOrder(); // попытаться его добавить
if(m_OrderAdded)
{
pDoc->AddOrderDetails(m_pSet->m_ProductID,
m_pSet->m_UnitPrice,
m_Quantity,
m_Discount);
// Теперь необходимо переустановить значения полей объема заказа и скидки
m_Quantity = 1;
m_Discount =0;
UpdateData(FALSE); // Передача данных элементам управления
}
}
Для обновления таблицы Order Details применяется объект m_DBSimpleUpdateSet.
Это значение было использовано исходным представлением приложения, и оно хра-
нится в объекте документа. Значения объема заказа и скидки извлекаются из дан-
ных-членов объекта представления, соответствующих элементам редактирования,
которые обеспечивают ввод этих значений. Значение идентификатора заказа было
установлено при отображении диалогового окна, поэтому оно должно отображаться
только для информационных целей. Значения идентификатора продукта и цены еди-
ницы продукта извлекаются из объекта CProductset, связанного с представлением.
После вызова функции Update () для записи значения объема заказа и скидки необхо-
димо вернуть к установленным по умолчанию.
[Практическое занятие | Добавление новых заКЭЗОВ
Как можно судить по идентификатору заказа, после добавления ряда заказов был
добавлен заказ, показанный на рис. 20.19.
1024 Глава 20
DABeginri g Visual C++ 2 305 'Mode I toes DBXNor thw n:j.[Custome s
File Edit E&cord view Help
Order ID;
11081
Order Date;
Retired Date:
i-WVzooe
Company Name:
Heutr See ЕИ katessen
Sated Рга±к±ь
Ready
Puc. 20.19. Добавление очередного заказа
Далее был выполнен щелчок на кнопке Select Product, после чего выбран продукт,
количество и скидка, как показано на рис. 20.20.
Рис. 20.20. Выбор продукта и установка для него количества единиц и размера скидки
Обновление источников данных 1025
Щелчок на кнопке Select Product добавляет выбранный продукт к заказу данного
клиента, после чего можно выбрать другой продукт. Каждый щелчок на кнопке Select
Product добавляет новую запись в таблицу Order Details заказа с текущим иденти-
фикатором заказа. Когда заказ сформирован, достаточно щелкнуть на кнопке Done
(Готово), чтобы завершить процесс.
В том, что заказ добавлен, легко убедиться, перейдя к последнему заказу в форме
просмотра сведений о заказах, как показано на рис. 20.21.
D:\BeginningVisual С+* 2005\Model Access DB\Northwki<l:|[Customers... -
file Edit E&c&rd view Help
£ Щ в й M 1 > H T ,
OrderlD:
Product ID:
Unit Prior;
Qu-mty:
Di scant
11DB1
67
14
ISO
C.05
Edt Or de
Puc. 20.21. Очередной заказ добавлен
Вы могли обратить внимание, что представления не сбрасываются к началу набо-
ра записей по завершении операции ввода заказа. Попытайтесь выполнить первое
упражнение, предложенное в конце этой главы, чтобы устранить этот недостаток для
набора записей клиентов. Эта задача должна быть не особенно сложной.
Резюме
В этой главе рассматривалась реализация элементарных обновлений с использова-
нием поддержки ODBC из библиотеки MFC.
Ниже перечислены ключевые моменты, с которыми вы познакомились в настоя-
щей главе.
Обновление возможно только в том случае, если набор записей соответствует
одной единственной таблице. Наборы записей, соответствующие соединению
таблиц, не могут быть обновлены.
□ Чтобы начать редактирование записи в наборе записей, необходимо вызвать
функцию-член Edit () объекта набора записей.
1026 Глава 20
□ Чтобы начать добавление новой записи в набор, потребуется вызвать функ-
цию-член AddNew () объекта набора записей.
Чтобы завершить изменение существующей записи или добавление новой, не-
обходимо вызвать функцию-член Update () объекта набора записей.
□ Прежде чем инициализировать обновление набора записей, всегда следует убе-
диться, что набор записей открыт, и что операция обновления, которую требу-
ется выполнить, является допустимой.
□ Транзакция объединяет в единое целое последовательности операций обнов-
ления базы данных, что позволяет восстановить исходное состояние базы дан-
ных в случае ошибки.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Измените разработанное в этой главе приложение обновления так, чтобы диа-
логовое окно добавления нового заказа всегда отображало клиентов в алфавит-
ном порядке, и чтобы при каждом его открытии оно отображало первого кли-
ента.
2. Измените пример так, чтобы представление выбора продуктов для включения в
заказ (CProductView) отображало общую сумму нового заказа.
3. Расширьте приведенный в этой главе пример, чтобы сотрудников можно было
выбирать из записей таблицы Employees (Сотрудники).
4. Дополнительно расширьте пример, чтобы поле грузоотправителя можно было
выбирать из числа записей таблицы Shippers (Грузоотправители).
21
Приложения,
использующие средства
Windows Forms
В этой главе вы научитесь создавать графический интерфейс пользователя для
приложений Windows Forms. Вы узнаете, как обеспечить поддержку взаимодействия
пользователя с элементами управления интерфейса. Для достижения этой цели в
ходе ознакомления с материалом этой главы будет постепенно создаваться единствен-
ное приложение, которое к концу главы превратится полностью функциональную
программу Windows Forms умеренного объема.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
□ Используя возможности конструктора форм Form Design, интерактивно стро-
ить графический интерфейс пользователя приложения.
□ Добавлять в форму элементы управления.
□ Добавлять к элементам управления обработчики событий.
Общее представление о Windows Forms
Windows Forms (Формы Windows) — это набор средств для создания Windows-при-
ложений, выполняющихся в среде CLR (Common Language Runtime — общеязыковая
исполняющая среда). Форма — это окно, служащее основой окна приложения или
диалогового окна, в которое можно добавлять другие элементы управления, предна-
значенные для взаимодействия с пользователем. Пакет Visual C++ 2005 поставляется
со стандартным набором более 60 элементов управления, которые можно применять
в формах. Поскольку количество доступных элементов управления очень велико, в
этой книге рассматриваются только наиболее показательные из них, но при этом чи-
татели должны получить достаточно полное представление об их применении, чтобы
1028 Глава 21
самостоятельно продолжить ознакомления с остальными элементами. Многие стан-
дартные элементы управления, такие как Button, представляющие кнопки, предна-
значенные для обработки щелчков мыши на них, или TextBox, которые позволяют
вводить текст, реализуют простые функции взаимодействия с пользователем. Другие
являются контейнерами — то есть могут содержать другие элементы управления.
Например, элемент управления GroupBox может содержать другие элементы управле-
ния вроде Button или TextBox, и его функция состоит просто в группировании эле-
ментов управления с какой-либо целью и, возможно, в снабжении группы надписью,
которая будет отображаться внутри графического интерфейса пользователя.
Форма и используемые с ней элементы управления представлены классом C++/
CLI. Каждый класс обладает набором свойств, которые определяют поведение и
внешний вид элемента управления или формы. Например, видимость элемента управ-
ления в приложении и возможность взаимодействия с ним пользователя определя-
ются установкой соответствующих значений свойств. Определение свойств элемента
управления может выполняться интерактивно при построении графического интер-
фейса пользователя с помощью средств IDE (Integrated Development Environment —
интегрированная среда разработки). Установка значений свойств может произво-
диться также во время выполнения с помощью добавляемых в программу функций,
либо с помощью кода, добавляемого в существующие функции через панель реактора.
Классы определяют также функции, вызываемые для выполнения операций с элемен-
тами управления.
При создании проекта приложения создается как окно приложения Windows
Forms, построенное на основе класса Form, так и весь код, обеспечивающий отобра-
жение этого окна приложения. После создания проекта Windows Forms разработка
приложения сводится к выполнению четырех отдельных операций.
□ Интерактивное создание графического интерфейса пользователя на вкладке
Form Design (Конструктор формы), отображаемой в панели Editor (Редактор),
путем выбора элементов управления в окне Toolbox (Панель инструментов) и
их помещения в форму. На этом этапе можно также создавать дополнительные
окна форм.
□ Изменение свойств элементов управления и форм в окне Properties (Свойства)
в соответствии с потребностями приложения.
Обработчики событий щелчков для элементов управления можно созда-
вать, дважды щелкая на элементе управления на вкладке Form Design. В окне
Properties элемента управления в качестве его обработчика события можно
также определять существующую функцию.
□ Для удовлетворения потребностей приложения можно изменять и расширять
классы, автоматически создаваемые в результате взаимодействия с вкладкой
Form Design.
В ходе ознакомления с материалом этой главы вы получите возможность ознако-
миться с выполнением этих операций на практике.
Общее представление
о приложениях Windows Forms
Прежде всего, необходимо получить представление о работе исходного кода при-
ложения Windows Forms. Используя шаблон Windows Forms Application (Приложение
Приложения, использующие средства Windows Forms 1029
Windows Forms), создайте новый проект CLR и присвойте ему имя Ех21_01. Панель
Editor отобразит графическое представление окна приложения, поскольку построе-
ние графического интерфейса пользователя с помощью приложения Windows Forms
выполняется графически. Даже двойной щелчок на имени файла Forml. h в панели
Solution Explorer (Проводник решений) не приводит к отображению кода. Но его
можно открыть, щелкая правой кнопкой мыши в окне Editor (Редактор) и выбирая
пункт View Code (Показать код) из контекстного меню.
Код определяет класс Forml, представляющий окно приложения. Прежде всего,
следует отметить, что он определен в собственном пространстве имен:
namespace Ех21_01
using namespace
using namespace
using namespace
using namespace
using namespace
using namespace
System;
System::ComponentModel
System:Collections;
System::Windows::Forms
System::Data;
System::Drawing;
// остальная часть кода
Компиляция проекта приводит к созданию новой сборки, код которой относится
к пространству имен Ех21_01, совпадающему с именем проекта. Пространство имен
позволяет различать типы с одинаковыми именами в различных сборках, поскольку
каждое имя типа уточняется именем конкретного пространства имен.
Существуют шесть рекомендованных к использованию пространств имен библио-
теки .NET, охватывающих функциональные возможности, которые, скорее всего, по-
требуются в приложениях (табл. 21.1).
Таблица 21.1. Пространства имен библиотеки .NET
Пространство имен Содержимое
System
System::ComponentModel
System: Collections
System::Windows::Forms
System::Data
System::Drawing
Это пространство имен содержит классы, которые определяют типы дан-
ных, используемые во всех приложениях CLR. Оно содержит также классы
событий и обработчиков событий, исключения и классы, поддерживающие
функции общего применения.
Это пространство имен содержит классы, которые поддерживают работу
компонентов графического интерфейса пользователя в приложениях CLR.
Это пространство имен содержит классы коллекций, предназначенные для
всевозможной организации данных, в том числе классы определения спи-
сков, очередей, словарей (карт) и стеков.
Это пространство имен содержит классы, которые поддерживают исполь-
зование в приложении средств Windows Forms.
Это пространство имен содержит классы, которые поддерживают набор
компонентов ADO.NET, используемый для доступа и обновления источ-
ников данных. Подробнее осуществление доступа к источникам данных в
приложении CLR описано в следующей главе.
Это пространство имен определяет классы, которые поддерживают основ-
ные графические операции, подобные рисованию на форме или компоненте.
Класс Forml — производный от класса Form, который определен в пространстве
имен System: : Windows : : Forms. Класс Form представляет окно приложения или
1030 Глава 21
диалогового окна, а класс Forml, который определяет окно для пространства имен
Ех21_01, наследует все члены класса Form.
Раздел в конце класса Forml содержит определение функции InitializeComponent ().
Эта функция вызывается конструктором для определения окна приложения и любых
компонентов, добавляемых в форму. Согласно приведенным комментариям, этот раз-
дел кода не следует модифицировать с помощью редактора кода, поскольку он обнов-
ляется автоматически при интерактивном изменении окна приложения. При исполь-
зовании средств Form Design важно соблюдать рекомендацию этого комментария и
не изменять вручную автоматически генерируемый код. В противном случае рано
или поздно это неизбежно приведет к возникновению ошибок. Конечно, весь код
для приложения Windows Forms можно создать с нуля, но применение возможностей
Form Design для интерактивного создания графического интерфейса пользователя
приложения значительно ускоряет работу и снижает вероятность ошибок. Тем не ме-
нее, это не означает, что не нужно знать, как работает этот код.
Вначале код функции InitializeComponent О выглядит следующим образом:
void InitializeComponent(void)
this->components = gcnew System::ComponentModel::Container() ;
this->Size = System::Drawing::Size(300,300) ;
this->Text - L’’Forml”;
this->Padding = System::Windows::Forms::Padding(0);
this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::Font;
Член components класса Forml унаследован от базового класса, и его задача — от-
слеживание компонентов, постепенно добавляемых в форму. Первый оператор сохра-
няет в члене components дескриптор объекта Container, представляющего коллек-
цию, которая хранит компоненты графического интерфейса пользователя в списке.
Каждый новый компонент, добавляемый в форму с помощью средств Form Design,
добавляется в этот объект Container.
Изменение свойств формы
Остальные операторы функции InitializeComponent () определяют свойства
объекта Forml. Ни одно из этих свойств не следует изменять непосредственно в коде,
но их значения можно выбирать посредством окна Properties (Свойства) формы.
Поэтому снова обратитесь к вкладке Forml.h[Design] формы в окне редактора и щел-
кните на ней правой кнопкой мыши, чтобы открыть окно Properties, показанное на
рис. 21.1.
Чтобы получить представление о доступных возможностях, имеет смысл просмо-
треть список свойств формы. Щелчок на любом из них приводит к отображению опи-
сания в нижней части окна. На рис. 21.1 приведено окно Properties, ширина которо-
го увеличена для отображения описаний. Чтобы изменить значение свойства, можно
выбрать ячейку в правой колонке. Свойства, слева от которых отображен символ +,
обладают несколькими значениями, и щелчок на этом символе отображает значения,
чтобы их можно было изменять по отдельности. Щелкая на кнопке, можно также упо-
рядочить свойства по алфавиту. Это облегчает поиск свойства с известным именем.
Попробуйте изменить значение Width (Ширина) свойства Size (Размер) на
600, а значение Height (Высота) — на 400. Можно также изменить свойство Text
(Текст) (размещенное в категории Appearance (Внешний вид) на ”А Winning
Application”. Это приведет к изменению текста в строке заголовка окна приложе-
Приложения, использующие средства Windows Forms 1031
ния. Если вы вернетесь к вкладке Forml. h в окне Editor, то увидите, что код функции
InitializeComponent О изменился, отражая внесенные изменения свойств.
Рис. 21.1. Окно Properties объекта Fox:
Обратите внимание, что, устанавливая значение свойства Windowstate, окно при-
ложения можно определить так, чтобы при запуске программы оно открывалось с
максимальным размером. При используемом по умолчанию значении Normal окно
открывается с указанными размерами, но в списке значений этого свойства можно
выбирать Maximized (Развернутое) или Minimized (Свернутое).
Запуск приложения
Как всегда, выполнение приложения начинается в функции main (), которая в
файле Ех21_01. срр определена следующим образом:
int main(array<System::String A> Aargs)
// Активизация визуальных эффектов Windows XP до создания каких-либо
// элементов управления
Application::EnableVisualStyles();
// Создание главного окна и его запуск
Application::Run(gcnew Forml());
return 0;
Функция main () вызывает две статические функции, которые определены в клас-
се Application, определенном в пространстве имен System: :Windows: :Forms.
Статические функции класса Application образуют основу любого приложения
1032 Глава 21
Windows Forms. Функция EnableVisualStyles (), вызываемая в функции main () пер-
вой, активизирует для приложения визуальные стили. Функция Run () запускает для
приложения цикл сообщений Windows и делает видимым объект Form, передаваемый
в качестве аргумента. Приложение, выполняемое в среде CLR, обязательно остается
приложением Windows, поэтому с циклом сообщений оно работает точно так же, как
все остальные приложения Windows.
лирующих некоторые функции, но зато
Toolbox ▼ 4 X
|- All Windows Forms л
Pointer
Backgroundworker
_ BmdingNavigator
Bindingsource
@ Button
0 CheckBox
Checked ListBox
l ] ColorDialog
Й Combo Box
ContextMenuStrip
DataGridView
Индивидуальная настройка графического
интерфейса пользователя
Процесс индивидуальной настройки графического интерфейса пользователя в со-
ответствии с потребностями конкретного приложения предполагает добавление стан-
дартных элементов управления и/или дополнительных форм, а также настройку их
свойств. Нам предстоит преобразовать проект Ех21__01 в программу генерирования
выигрышных номеров лотереи. Это позволит приобрести хорошие практические на-
выки работы со значительным набором элементов управления из числа доступных.
Дизайн этой программы будет не идеальным, поскольку она включает ряд элементов
управления, без особой необходимости
она позволит на практике исследовать различные возможности действующего при-
ложения. В качестве примера были выбраны две лотереи. Если вам больше по нраву
какие-то другие лотереи, соответствующее изменение примера не должно вызывать
особые сложности.
По поводу ввода лотерейных номеров необходимо сделать одно замечание — вероятно, не сто-
ит выбирать номера от 1 до 6, поскольку их выигрыш весьма маловероятен. Конечно, в дей-
ствительности выпадение выигрыша на эти номера столь же вероятно, как и на любые
другие, но ни в одной известной мне лотерее номера с 1 по 6 практически никогда не оказы-
вались среди выигрышных. Поэтому можно смело полагать, что выбор любого из них ведет
к проигрышу.
Чтобы отобразить доступные элементы управления, вначале
выберите вкладку Forml .h[Design] в окне Editor, а затем нажми-
те комбинацию клавиш <Ctrl+Alt+X> или выберите пункт меню
View=>ToolbOX (Вид=>Панель инструментов). Откроется окно спи-
ска доступных элементов управления, показанное на рис. 21.2.
Первый блок в окне Toolbox озаглавлен как All Windows Forms
(Все формы Windows) и он содержит список всех доступных эле-
ментов управления, которые можно использовать в форме. Если
блок All Windows Forms еще не развернут, щелкните на его сим-
воле +. Свернуть этот блок обратно можно, щелкнув на знаке -,
расположенном слева от заголовка блока. Как вы убедитесь при
этом, элементы управления сгруппированы также по типу, на-
чиная с блока Common Controls (Общие элементы управления).
Вероятно, поначалу удобнее использовать расположенную в верх-
ней части списка группу, содержащую все элементы управления.
Но после ознакомления с доступными элементами управления,
возможно, вы предпочтете свернуть все группы и разворачивать
их по одной, по мере необходимости.
та DateTimePicker
1 *_ i
] DirectoryEntry
DirectorySearcher
*' DomainUpDown
J ErrorProvider
EventLog
Toolbox со спис-
ком доступных
элементов управ-
ления
Приложения, использующие средства Windows Forms 1033
Добавление элементов управления в форму
Добавьте элемент управления MenuStrip в верхнюю часть формы, щелкнув на эле-
менте MenuStrip в окне Toolbox, а затем там, куда его нужно поместить
части клиентской области окна приложения. Слева над элементом управления ото-
бразится небольшая стрелка. Щелчок на ней приведет к открытию всплывающего
окна, показанного на рис. 21.3.
в верхней
MenuStrip Tasks
i Em bed in ToolStripContainer
Insert Standard Berns
RenderMode
Dock
GripStyle
Edit Eems...
Рис. 21.3. Окно MenuStrip Tasks
Первый элемент во всплывающем окне MenuStrip Tasks (Задачи строки меню)
внедряет строку меню внутрь элемента управления Tools tripcontainer, предостав-
ляющего панели, расположенные вдоль всех четырех сторон формы, и центральную
область, в которой можно помещать другой элемент управления. Второй элемент ге-
нерирует четыре стандартных элемента меню: File (Файл), Edit (Правка), Tools
(Сервис) и Help (Справка), содержащие списки соответствующих пунктов меню.
В данном примере эти элементы не требуются, но возможность мгновенного генери-
рования меню весьма пригодится во многих приложения. В то же время, добавление
этого меню предоставляет то преимущество, что меню File будет содержать пункт Exit
(Выход), позволяющий закрывать приложение. Элемент RenderMode позволяет вы-
бирать стиль изображения строки меню, и для него можно оставить значение, вы-
бранное по умолчанию. Опция Dock (Стыковка) позволяет выбирать сторону формы,
к которой будет пристыкована строка меню, или же оставлять строку меню в отсты-
кованном состоянии. Опция GripStyle определяет видимость рамки для перетаски-
вания меню. В Visual C++ 2005 строки меню снабжены видимыми рамками для пере-
мещения. Выбор последней опции Edit Items (Редактировать элементы) приводит
к отображению диалогового окна, в котором можно изменять свойства строки меню
или любых его элементов.
Добавление меню
Чтобы добавить меню, введите текст меню в поле строки меню. Добавьте в строку
меню три элемента, присвоив им имена &Р1ау, &Limits и &Не1р. Символ & предше-
ствует символу, выполняющему роль клавиши быстрого доступа для данного элемента
меню — так, например, клавиатурная комбинация <Alt+P> служит комбинацией бы-
строго доступа к меню Play (Игра). Теперь окно Editor, содержащее форму со строкой
меню
Как видите, имя строки меню, menus tripl, отображается под окном формы.
Эта информация служит лишь для облегчения выбора — например, для изменения
свойств. Свойство (Name) элемента управления — это имя переменной в коде, ко-
торый ссылается на объект этого элемента управления. Поэтому значение свойства
(Name) элемента управления MenuStrip выглядит как menuStripl. Для таких элемен-
олжно выглядеть, как показано на рис. 21.4.
1034 Глава 21
тов управления, как MenuStrip, с которыми не предполагается работать программно,
можно оставлять любые имена, присвоенные им по умолчанию. Но имя, присвоен-
ное по умолчанию, может оказаться достаточно громоздким для работы с ним в коде.
Поэтому его лучше изменить.
Forml.h [Design] ▼ X
menuStnpl
Aw. 21 А. Добавление нового пункта меню
Меню Play не будет содержать подменю, однако щелчок на нем будет генериро-
вать событие. Поскольку нам придется выполнять обработку события, элементу
меню удобно присвоить имя, которое короче присвоенного по умолчанию. Доступ к
свойствам можно получить, щелкая правой кнопкой мыши на элементе управления
в форме и выбирая из контекстного меню пункт Properties, но на этот раз попро-
буйте щелкнуть на имени menuStripl под формой и выбрать команду Edit Items из
контекстного меню. В результате откроется диалоговое окно Items Collection Editor
(Редактор коллекции элементов), показанное на рис. 21.5.
Выбор в левой панели диалогового окна имени, соответствующего меню Play, при-
ведет к отображению его свойств на правой панели. После этого можно изменить
значение свойства (Name). На рис. 21.5 представлено диалоговое окно после вне-
сения изменений. Преимущество использования этого диалогового окна состоит в
том, что оно позволяет работать со всеми элементами строки меню, настраивая их
свойства должным образом. Кроме того, окно позволяет добавлять новые элементы в
строку меню или изменять порядок отображения пунктов.
Добавление подменю
Можно добавить три элемента (Upper (Верхний), Lower (Нижний) и Reset
(Сброс)) в меню Limits (Пределы) и элемент About (О программе) — в меню Help
(Справка). Новые элементы меню можно добавлять, используя диалоговое окно Items
Collection Editor, показанное на рис. 21.5. Для этого достаточно выбрать меню (или
строку меню), которое требуется добавить, и щелкнуть на кнопке Add (Добавить).
Кроме того, с помощью панели Editor (Редактор) можно работать непосредственно
со строкой меню.
Приложения, использующие средства Windows Forms 1035
Items Collection Editor
Select item and f dd to list belo<<
ts Menulem
JpjolStn pMenuItem
plnyMe nultem
У embers.
5- merirjStripl
i pla .nuttem
I irriiteT DolStripMenultem
ЁЬ helpToolstripMenune m
ShortcutKeyDisp layString
Text
T extAlign
T extDirectiori
T extlmnge Relation
El Behavior
AutoS ire
AutoToolTip
CheckOnCI ick
DoiibleCIrckEnzjbled
Enabled
T oofTipT ext
Visible
El Data
E Design
(Name)
Gene rnteMember
&Pfey
MiddleCente г
Horizontal
ImageBeforeT ext
True
False
False
False
True
True
pfayMenuReni
True
OK
Cancel
Puc. 21.5. Диалоговое окно Items Collection Editor
например, upperMenuItem. Для этого
(ля этого потребуется
Чтобы добавить элементы в меню, щелкните на нем в строке меню. Затем щел-
кните на расположенном под ним элементе меню и введите нужный текст. Нам не-
обходимо изменить свойства новых элементов меню, поэтому, прежде всего, щелкая
правой кнопкой мыши на элементе меню Upper и выбирая в контекстном меню пункт
Properties, откройте его окно свойств. Нам придется обеспечивать обработку собы-
тий для этого элемента меню, поэтому заданное по умолчанию имя свойства имеет
смысл заменить каким-либо более кратким
элемента можно также добавить клавишу быстрого доступа
щелкнуть на стрелке вниз в колонке значения свойства Shor tout Keys, в результате
чего откроется список, показанный на рис. 21.6.
Выберите клавишу или клавиши-модификаторы, установив соответствующие
флажки, и выберите клавишу быстрого доступа из раскрывающегося списка. Как ви-
дите, для этого элемента меню в качестве клавиш быстрого доступа была выбрана
комбинация <Ctrl+Alt+U>. На рис. 21.6 видно также, что в качестве значения свой-
ства ToolTipText был определен текст ’’Set upper limit for values” (“Установить
верхний предел значений”). Помещение курсора мыши над этим элементом меню по-
сле короткой задержки будет приводить к отображению подсказки. Установка значе-
ния свойства AutoToolTip, расположенного в начальной части списка, равным True
приведет к отображению подсказки, текст которой будет совпадать с текстом элемен-
та меню. Если же оставить это значение равным False, в качестве текста подсказки
будет использовано значение свойства ToolTipText. Устанавливая значение свойства
ShowShortcutKeys, можно также управлять отображением комбинации клавиш бы-
строго доступа вместе с текстом элемента меню.
Щелчок на элементе меню Lower приведет к отображению его свойств в окне
Properties. После этого значение свойства (Name) можно изменить на 1 owerMenuItem,
значение свойства ShortcutKeys
на
же можно изменить свойство (Name) элемента меню Reset на resetMenuItem, значе-
ние свойства shortCutKeys — на Ctrl+Alt+R, а текст подсказки
to original values” (“Сбросить пределы в первоначальные значения”).
на Ctrl+Alt+L, а значение свойства ToolTipText —
Set lower limit for values” (“Установить нижний предел значений”). Точно так
на ’’Reset limits
1036 Глава 21
Properties ▼ ¥ -
u pper Men ultem Syste m.Wi n do ws. Fo rms.Too I Stri p Me n ulte
TextAlign
TextDirection
TextlmageRelatiori
Behavior
AutoSize
AutoToolTip
CheckOn Click
DoubleClickEnabled
Enabled
ToolTipText
Visible
Data
(Applicationsettings)
DropDown
DropDownltems
Tag -
Design
(Name)
GenerateMember
Modifiers
Layout
Misc _
ShortcutKeys
S howS ho rtcutKeys
Middlecenter
Horizontal
ImageBeforeText
True
False
False
False
True
Set upper limit for values
True
(none)
(Collection)
Modifiers:
3 Alt
Key:
Ctrl+Alt+U
True
I
U
ShortcutKeys
The shortcut key associated with the menu item.
Puc. 21.6. Установка значения свойства
ShortcutKeys
Можно также изменить значение свойство (Name) элемента меню About на
aboutMenuItem и добавить для него текст подсказки. Обычно элемент меню About не
имеет комбинации клавиш быстрого доступа. Элемент меню Play создает совершенно
новый набор значений для лотереи, поэтому при желании можно добавить текст под-
сказки, указывающий на это обстоятельство.
обавление элемента управления с вкладками
Элемент управления TabControl предоставляет несколько вкладок, каждая из
которых может содержать собственный набор элементов управления. Этот элемент
управления можно использовать для создания в клиентской области окна приложения
записей более чем одной лотереи. Откройте окно Toolbox (<Ctrl+Alt+X>) и выберите
в списке элемент TabControl — он расположен в группе Containers (Контейнеры).
Щелкните в клиентской области формы и добавьте элемент управления с вкладками,
после чего откройте его окно свойств — для этого нажмите клавишу < F4>.
Все элементы управления, перечисленные в группе Containers, могут содержать
другие элементы управления, и, следовательно, все они предлагают средства объеди-
нения элементов управления в группу. Понятно, что каждая вкладка может содержать
собственный набор элементов управления, причем элемент управления вкладки мо-
жет содержать любое количество вкладок.
Нам необходимо, чтобы элемент управления вкладки заполнял всю клиентскую
область окна. Это определяется значением свойства Dock, расположенного в группе
свойств Layout элемента управления TabControl. Окно Properties элемента управле-
ния с вкладками с раскрытой ячейкой значения свойства Dock приведено на рис. 21.7.
Приложения, использующие средства Windows Forms 1037
Properties
ta bControll System.Wi ndows.Rorms.TabControI
,r~-
Item Size
Multiline
Padding
ShowToolTips
SizeMode
Tabindex
TabPages
TabStop
Visible
Data
(ApplicationSettingi
(DataBindings)
58, 18
False
False
No rma I
(Collection)
True
True
Design
Focus
CausesValidation
Layout
Anchor
Dock
Location
Margin
MaximumSize
MinimumSize
Size
True
Top, Left
None
Dock
Defines which borders of the control are bound to
the container.
Puc. 21.7. Свойство Dock в окне Properties
элемента управления с вкладками
Элемент управления с вкладками содержит две вкладки, но при необходимости в
него можно добавлять новые вкладки, щелкая на кнопке со стрелкой в правом верх-
нем углу элемента управления и выбирая команду Add Tab (Добавить вкладку) из
всплывающего меню (рис. 21.8).
Рис. 21.8. Добавление новой вкладки
1038 Глава 21
нажатие клавиши <F4> при выбранном элементе
Необходимо изменить текст каждой вкладки на что-либо более вразумительное.
Это же относится к значениям свойства (Name). Перейдите в окно Properties эле-
мента управления TabControl
управления приведет к отображению его окна свойств, если оно еще скрыто. Затем
выберите поле значения свойства TabPage и щелкните на появившейся кнопке с сим-
волом многоточия. Откроется диалоговое окно Tab Page Collection Editor (Редактор
коллекции вкладок), показанное на рис. 21.9.
Рис. 21.9. Диалоговое окно Tab Page Collection Editor
Текст вкладки должен отражать название лотереи, с которой связаны элементы
управления этой вкладки, поэтому значение свойства (Name) изменено на lottoTab,
а значение свойства Text
ства (Name) — имя, используемое в классе Forml для переменной, которая ссылается
на эту вкладку, а значение свойства Text — текст, отображаемый на вкладке. При же-
лании можете выбрать другие значения.
Вкладка, озаглавленная как tabPage2, предназначена для работы с записями
второй лотереи, которая в данном случае носит название Euromillions. Как и ранее,
можете выбрать название любой предпочитаемой лотереи. Щелкните на элементе
tabPage2 в левой панели, чтобы отобразить его свойства на правой панели. После
выбора новой вкладки имя элемента tabPagel в левой панели должно измениться на
lottoTab, а текст первой вкладки в форме должен обновиться. В качестве значения
свойства Text элемента управления tabPage2 определен текст Euromillions, а в ка-
честве свойства (Name) — euroTab. Третью вкладку мы будем использовать для ото-
бражения вкладки Web Page (Web-страница) для ввода номеров лотереи, поэтому из-
мените значение свойства Text вкладки tabPage3 на Web Раде, а значение свойства
имени — на WebTab. После внесения этих изменений можно щелкнуть на кнопке ОК,
чтобы закрыть диалоговое окно.
— на Lotto, что можно видеть на рис. 21.9. Значение свой-
Приложения, использующие средства Windows Forms 1039
Использование элементов управления GroupBox
Элемент управления GroupBox (Групповая рамка) можно использовать для объеди-
нения других элементов управления в группу. Кроме того, этот элемент управления
выравнивает группу относительно ограничивающей рамки и позволяет присваивать
ей отличительное название. Запись лотереи Lotto состоит из шести различных номе-
ров от 1 до 49. Добавьте на вкладку Lotto элемент управления GroupBox, щелкнув на
элементе управления в окне Toolbox, а затем внутри вкладки Lotto. После этого мож-
но изменить значения свойств Text, (Name) и Dock элемента управления GroupBox
на значения, показанные на рис. 21.10.
Properties
lottoVa lues S у ste m. W i neo ws. Fa r ms. G го u p В ox
Backgroundimage
BackgroundlmageLi Tile
Cursor
(none)
Flatstyle
E Font
ForeColor
RightT oLeft
Text
UseWaitCursor
E Behavior
E Data
E Design
(Name)
GenerateMember
Locked
Modifiers
Fl Focus
CausesValidation
El Layout
Anchor
Auto Size
Auto Size Mode
Dock
E Location
SjyiaraiiL
Dock
Default
Standard
Microsoft Sans Serif, 8.25pt
ControlText
No
Values 1 to 49
False
lottoVa lues
True
False
Private
True
Top, Left
False
GrowOnly
3, 3
Defines which borders of the control are bound to
the container.
Puc, 21,10, Установка свойств
элемента управления GroupBox
Значение свойства Text отображает диапазон возможных значений номеров ло-
тереи, отображаемых на этой вкладке. Значение свойства Dock вынуждает элемент
управления GroupBox заполнять свой контейнер, которым является вкладка Lotto
элемента управления Tab. Значение свойства (Name) определяет имя переменной в
классе Forml, идентифицирующей этот элемент управления.
Запись лотереи на вкладке Euromillions предполагает наличие двух групп значений:
иапазона чисел от 1 до 9. Номера в первой
первой, состоящей из пяти различных значений от 1 до 50, и второй, состоящей из
двух различных “звездных” значений из д
группе могут совпадать с двумя “звездными” значениями. Для реализации такой струк-
туры нам придется использовать еще один контейнер на вкладке Euromillions, поэто-
му щелкните на элементе Splitcontainer в группе Containers диалогового окна
Toolbox, а затем для помещения этого элемента управления на вкладку Euromillions
щелкните внутри нее. Чтобы выбрать вкладку, вначале щелкните на надписи вклад-
1040 Глава 21
ки в верхней части элемента управления с вкладками — это приведет к выбору эле-
мента управления с вкладками с выделенной надписью. Затем потребуется щелкнуть
внутри области отображения вкладки, чтобы выбрать выделенную вкладку внутри
элемента управления. По умолчанию значением свойства Dock элемента управле-
ния Splitcontainer является Fill, поэтому элемент управления должен заполнять
вкладку. Этот элемент управления содержит две панели, каждая из которых может со-
держать другие элементы управления. Для изменения относительных размеров пане-
лей можно перетаскивать разделяющую их границу.
Откройте окно Properties элемента управления и определите свойства
Orientation и (Name), как показано на рис. 21.11.
Properties ▼ ’ X
euroEntry System.Windows.Forms.SplitContainer ’
UseWaitCursor ---i=aise---
В Behavior Property Pages
AllowDrop
ContextMenuStnp
Enabled
ImeMode
Orientation
Tabindex
TabStop
Visible
E Data
E Design
(Name)
GenerateMember
Locked
Modifiers
E Focus
CausesValidation
E Layout
Ancho г
Dock
FixedPanel
IsSplitterFixed
E Location
E .Ma rain
False
(none)
True
No Control
Horizontal
0
euroEntry
True
False
Private
True
Top, Left
Fill
None
True
3,3
IsSplitterFixed
Determines if the splitter can move.
Puc. 21.11. Установка свойств элемента
управления Splitcontainer
Теперь панели должны быть разделены горизонтальной границей. Перетащите ее
так, чтобы высота нижней панели была приблизительно вдвое меньше высоты верх-
ней. Затем измените значение свойства IsSplitterFixed на True (см. рис. 21.11).
Тем самым вы зафиксируете разделитель в выбранной позиции. Если оставить значе-
ние свойства IsSplitterFixed равным False, это даст возмлжность пользователю
перетаскивать разделитель в любую позицию во время работы приложения.
Элемент управления GroupBox позволяет сгруппировать содержимое каждой из пане-
лей элемента управления Splitcontainer, поэтому добавьте этот элемент управления в
каждую из них. В качестве значений свойств Text, (Name) и Dock элемента управления
GroupBox в верхней панели установите соответственно "Values 1 to 50” (“Значения
от 1 до 50”), euroValues и Fill, а в качестве значений свойств элемента управления
GroupBox нижней панели
euroStars и Fill. Окно редактора должно выглядеть, как показано на рис. 21.12.
соответственно "Values 1 to 9" (“Значения от 1 до 9”)
Приложения, использующие средства Windows Forms 1041
Рис. 21.12. Настройка элементов управления GroupBox
Теперь мы располагаем достаточно обширной иерархией элементов управле-
ния. Клиентская область формы содержит элемент управления вкладок, вкладка
Euromillions имеет элемент управления Splitcontainer, а в каждой из панелей этого
элемента управления имеется элемент управления GroupBox. Теперь необходимо до-
бавить кнопки в каждый из элементов управлений GroupBox на каждой из панелей.
Использование элементов управления Button
Элементы управления Button (Кнопка) из группы Common Controls представля-
ют собой обычные кнопки, которые, как правило, отображаются в диалоговом окне.
Элемент управления кнопки мы будем использовать для ввода каждого из значений в
запись лотереи. Для лотереи Euromillions нам потребуется семь кнопок: пять в группо-
вой рамке на верхней панели для ввода значений от 1 до 50 и две в групповой рамке
на нижней панели для ввода значения от 1 до 9.
Поместите пять кнопок в групповую рамку на верхней панели и две кнопки в
групповую рамку на нижней панели, расположив их симметрично. Позицию любой
кнопки можно изменять, перетаскивая ее с помощью мыши. В процессе перетаски-
вания должны отображаться вертикальная и/или горизонтальная направляющие
линии, облегчающие выравнивание элемента управления относительно других эле-
ментов. Для равномерного размещения элементов управления по горизонтали мож-
но воспользоваться также пунктом меню Formats Horisontal Spacing^ Make Equal
(Формат1^Расстояние по горизонтали =>Сделать равным). Советуем ознакомиться
также с другими элементами меню Format. Они позволяют распределять элементы
по вертикали, выравнивать элементы управления, центрировать в форме выбранную
группу элементов управления и настраивать высоту и ширину элементов управления.
Щелкая на элементах управления при нажатой клавише <Ctrl>, можно выбирать лю-
бое число элементов управления.
Выбранное расположение кнопок на вкладке Euromillion и направляющие линии,
отображаемые при позиционировании кнопки button?, показаны на рис. 21.13.
1042 Глава 21
DLJ A Winning Application
Play Limits Help
Lotto Euromillions । Web Page_______________________________________________________________
Values 1 to 50 - - — ---- ---------- -------------------
г
button 1
button2
button4
Values 1 to 9 -
button^
Puc. 21.13. Расположение кнопок на вкладке Euromillion
1 и 2. Свойство (Name) каждой кнопки также
Значения для отображения на этих кнопках в качестве текста будут генерировать-
ся, поэтому необходимо изменить свойства каждой из них. Я предлагаю в качестве
значений свойства Text пяти верхних кнопок установить 1, 2, 3, 4 и 5, ав качестве
значений свойства Text двух нижних
можно изменить. Кнопкам на верхней панели можно присвоить имена euroValuel,
euroValue2 и так до euroValue5, а двум кнопкам на нижней панели — euroStarl и
euroStar2. При желании можно изменить свойство BackColor каждой кнопки, кото-
рое определяет цвет фона. Для пяти верхних кнопок выбран цвет Silver (Серебро),
а для двух нижних — Gold (Золото). Оба эти цвета были выбраны из палитры цве-
тов Web. После выполнения этих действий окно приложения с выбранной вкладкой
Euromillions должно выглядеть так, как показано на рис. 21.14.
Рис. 21.14, Вкладка Euromillions приложения
Приложения, использующие средства Windows Forms 1043
в диапазоне от 1 до 49. Поэтому добавьте
выбрать для вво,
Теперь можно вернуться на вкладку Lotto, щелкнув на ней в окне Editor. Затем до-
бавьте туда необходимые кнопки. Лотерея Lotto очень проста: потребуется всего лишь
;а шесть различных значений
шесть элементов управления Button в элемент управления GroupBox, уже присутствую-
щий на вкладке Lotto. После размещения кнопок должным образом измените отобража-
емый на них текст, чтобы по умолчанию они отображали значения от 1 до 6, и измени-
те значения их свойства (Name) соответственно на lottoValuel, lottoValue2 и так
до lottoValue6. Можете также изменить значение свойства BackColor этих кнопок
по своему усмотрению — лично я выбрал цвет SkyBlue (Небесно-голубой) из панели
Web. По завершении этих действий и после его запуска окно приложения должно вы-
глядеть так, как показано на рис. 21.15.
Рис. 21.15. Вкладка Lotto приложения
Теперь можно добавить элемент управления на вкладку Web Раде.
Использование элемента управления WebBrowser
Отображение Web-страницы в приложении — значительно более простая задача,
чем можно было подумать, поскольку элемент управления WebBrowser (Web-брау-
зер) из группы Common Controls выполняет все необходимые действия. Щелкните
на вкладке Web Раде в окне Editor формы. Затем щелкните на элементе WebBrowser
в окне Toolbox, а потом внутри вкладки, чтобы поместить туда элемент управле-
ния. Свойства элемента управления можно отобразить, нажав клавишу <F4>. Окно
Properties для этого элемента управления показано на рис. 21.16.
На рис. 21.16 окно Properties показано после изменения значения свойства
(Name) на WebBrowser, а значения свойства Url — на URL-адрес Web-страницы, ко-
торую должен отображать элемент управления. Здесь можете ввести URL-адрес мест-
ной организации, проводящей лотерею. Убедитесь, что значение свойства Dock рав-
но Fill. Теперь вкладка содержит все, что требуется для отображения Web-страницы.
Конечно, страница будет отображаться только в том случае, если во время выполне-
ния программы компьютер будет иметь активное подключение к Internet. После по-
1044 Глава 21
вторной компиляции и запуска программы вкладка Web Раде должна выглядеть по
добно показанной на рис. 21.17.
Рис. 21.16. Окно свойств элемента управления WebBrowser
Рис. 21.17. Вкладка Web
во
выполнения
Приложения, использующие средства Windows Forms 1045
Если Web-страница не открывается, это может быть обусловлено отсутствием ак-
тивного подключения к Internet. Обратите внимание, что по умолчанию при необхо-
димости элемент управления WebBrowser отображает полосы прокрутки. Кроме того,
он обеспечивает навигацию по Internet путем выбора активных ссылок на Web-стра-
нице.
Итак, мы почти (пока не полностью) завершили создание графического интер-
фейса пользователя для приложения. Теперь пора подробно рассмотреть функциони-
рование приложения.
Работа приложения Winning Application
Щелчок на элементе меню Play открывает все поля ввода записи той вкладки, ко-
торая отображается в окне приложения в данный момент времени. Таким образом,
элемент Play можно применять к вкладке Lotto или Euromillions. Номера лотереи
отображаются в порядке возрастания. Переход на вкладку Web Раде приводит к ото-
бражению Web-страницы, на которой можно принять участие в розыгрыше лотереи.
После того как запись создана, может потребоваться изменение того или иного
значения — например, поскольку вы испытываете беспричинную неприязнь к опреде-
ленным числам или считаете, что числа больше 30 не принесут вам удачу. Щелчок
на кнопке мог бы приводить к генерации нового числа для замены того, на кнопке
которого выполняется щелчок. Естественно, новый номер должен отличаться от уже
присутствующих в текущей записи.
Еще одна возможность — выбор конкретного числа, которое, как вы надеетесь,
будет счастливым— например, соответствующего дате или месяцу дня рождения или
количеству орешков, оставленных на тарелке во время сегодняшнего обеда. Эту воз-
можность можно реализовать путем добавления контекстного меню, открывающего-
ся по щелчку правой кнопкой мыши на конкретной кнопке. Для этого можно было
бы использовать пункт Choose (Выбрать) контекстного меню. Обработка события,
генерируемого в результате щелчка на элементе контекстного меню, требует выпол-
нения определенных действий, поскольку нам придется обеспечить возможность вво-
да данных. Кроме того, потребуется также проверить допустимость вводимого числа.
Оно должно принадлежать разрешенному диапазону и не должно дублировать уже су-
ществующие значения в текущей группе кнопок.
Элементы меню Limits1^ Upper (Пределы^Верхний) и Limits1^Lower (Пределы1^
Нижний) позволяют использовать более ограниченный диапазон значений для ге-
нерации записи. При этом также требуется выполнение соответствующих проверок.
Диапазон должен быть частью диапазона, допустимого для данной лотереи, и при
этом быть достаточно широким, чтобы обеспечивать генерацию требуемого количе-
ства различных значений.
И, наконец, элемент меню Help^About (Справка^О программе) должен отобра-
жать окно сообщения с информацией о приложении.
Первое, что необходимо сделать для реализации описанного функционирования
приложения, это добавить контекстное меню для кнопок, отображающих значения
номеров лотереи. Это удивительно просто.
Добавление контекстного меню
Контекстное меню в приложении CLR Forms — всего лишь еще один элемент
управления ContextMenuStrip, расположенный в группе элементов управления
Menus & Toolbars (Меню и панели инструментов) в окне Toolbox. Чтобы добавить его
1046 Глава 21
в приложение, щелкните на элементе управления ContextMenuStrip, а затем в серой
области в нижней части окна Editor. Контекстное меню отобразится в форме под уже
существующим меню. Щелчок в первом меню позволит вставить имя элемента. В каче-
стве текста элемента меню можно ввести &Choose, в результате чего комбинация кла-
виш <Alt+C> станет комбинацией быстрого доступа к этому элементу. Контекстному
меню желательно присвоить более понятное имя, поэтому откройте окно Properties
элемента управления ContextMenuStrip и измените значение его свойства (Name)
на buttonContextMenu. Можно также изменить значение свойства (Name) элемента
меню Choose на chooseValue.
Теперь можно активизировать элемент управления buttonContextMenu в каче-
стве контекстного меню для каждой из кнопок вкладок ввода номеров обеих лотерей.
Для этого измените значение свойства ContextMenuStrip каждой кнопки на имя эле-
мента управления ContextMenuStrip — buttonContextMenu. Это имя отображается в
раскрывающемся списке ячейки значения свойства, поэтому для установки значения
достаточно щелкнуть на нем.
Создание обработчиков событий
Процесс визуального построения графического интерфейса пользователя с помо-
щью средств Form Design автоматически генерирует код для добавляемых элементов
управления. Теперь существует член класса Forml для каждого добавленного в форму
элемента управления, а в функцию InitializeComponent () помещен код инициализа-
ции каждого из этих членов. Если взглянуть на эту функцию, легко заметить, что теперь
она содержит значительный объем кода, устанавливающего значения свойств отдель-
ных элементов управления и группирующего элементы в соответствующие контейне-
ры. Графическое создание графического интерфейса пользователя — очень простая за-
дача. Чтобы получить готовое приложение, достаточно лишь немного подумать.
Чтобы программа работала должным образом, потребуется создать функции об-
работчиков событий, которые приложение должно распознавать и обрабатывать, и
реализовать эти функции, чтобы взаимодействие пользователя с графическим ин-
терфейсом приводило к нужному результату. Хотя автоматическое генерирование
кода обработки событий невозможно, среда IDE все же может помочь в этом, гене-
рируя скелет функций обработчиков событий и регистрируя их с помощью делегатов
событий.
События элемента управления можно просматривать, щелкая на кнопке событий
в окне Properties конкретного элемента управления. В результате отобразятся все
возможные события данного элемента управления, и имя функции, которая должна
обрабатывать конкретное событие, можно будет установить в ячейке, расположенной
справа от имени события. Естественно, идентификация функций обработчиков собы-
тий требуется только для тех событий, которые необходимо распознавать, и в боль-
шинстве случаев они составляют лишь небольшую часть всех событий, доступных
для данного элемента управления. Нам потребуется создать функции обработчиков
события Click (Щелчок) для всех элементов меню и для каждой из кнопок на вклад-
ках. Существует способ быстрого создания функции обработчика события Click для
элемента управления. Для этого достаточно дважды щелкнуть на элементе управле-
ния в окне Editor, в результате чего отобразится созданный код, а функция будет за-
регистрирована в качестве обработчика объекта события. В результате для элемента
управления будет создан уникальный обработчик события, но иногда требуется, что-
бы одна и та же функция обрабатывала события данного типа более чем для одного
элемента управления. В этом случае следует обратиться в окно Properties.
Приложения, использующие средства Windows Forms 1047
Обработчики событий для элементов меню
Начнем с создания функции обработчика события для элемента меню Play. Двой-
ной щелчок на этом элементе меню создает следующий код обработчика события:
private: System::Void playMenu!tem_Click(System::ObjectA sender,
System::EventArgsA e)
В этом скелете обработчика тело функции не содержит никакого кода. Первый
параметр — дескриптор, ссылающийся на элемент управления, выдавший событие,
а второй аргумент предоставляет информацию о самом событии. Тип первого ар-
гумента соответствует типу элемента управления, инициировавшего событие — в
данном случае это ToolStripMenuItemA, поскольку функция обработчика вызыва-
ется при щелчке на элементе меню Play. Дескриптор элемента меню хранится в чле-
не playMenuItem класса Forml, и его тип можно проверить в определении класса.
Аналогично, фактический тип второго аргумента обработчика события Click также
зависит от типа элемента управления.
Имя функции обработчика, генерируемое по умолчанию — playMenu!tem_Click.
Не следует изменять имя функции в коде. Собственно, я рекомендую вообще не из-
менять какие-либо автоматически сгенерированные имена в коде. Это всегда следует
делать в окне Properties. Если имя, созданное для функции обработчика события, вас
не устраивает, его можно изменить через свойство события Click в окне Properties
данного элемента управления.
Регистрация имени функции обработчика объектом события выполняется с помо-
щью следующего оператора в функции InitializeComponent ():
this->playMenuItem->Click +»
gcnew System::EventHandler(this, &Forml: :playMenuItem__Click);
Этот оператор добавляет имя функции к делегату Click в объекте playMenuItem,
который является членом класса Forml. Делегаты и процесс регистрации функций
обработчиков событий рассматривались в главе 9.
Обработчики для элементов меню Limits1^ Upper, Limits=> Lower и Help=>About мож-
но добавить, дважды щелкая на каждом из них. Это же можно выполнить для элемен-
та Choose контекстного меню.
Добавление членов в класс For.
Прежде чем приступить к реализации обработчиков для элементов меню, необхо-
димо создать несколько дополнительных полей, которые потребуются для хранения
данных, связанных с ограничениями значений лотерейных номеров. Вы уже умеете
добавлять новое поле в класс Forml. Этот метод мы часто использовали для добав-
ления членов в класс. Перейдите на вкладку Class View (Представление классов),
щелкните правой кнопкой мыши на классе Forml и в контекстном меню выберите
команду Add^Add Variable (Добавить1^Добавить переменную). Можно также про-
сто добавить код вручную, но в этом случае убедитесь, что вы не затрагиваете раз-
дел определения класса, зарезервированный для использования операциями Form
Design. В качестве приватных членов потребуется добавить следующие переменные:
private:
int lottoValuesCount; 11 Количество значений в записи лотереи Lotto
int euroValuesCount; // Количество значений в записи лотереи Euromillions
1048 Глава 21
int euroStarsCount;
int lottoLowerLimit;
int lottoUpperLimit;
int lottoUserMinimum;
int lottoUserMaximum;
// Количество "звездных” значений в записи
// лотереи Euromillions
// Минимально допустимое значение в лотерее Lotto
// Максимально допустимое значение в лотерее Lotto
// Нижнее граничное значение лотереи Lotto,
// введенное пользователем
// Верхнее граничное значение лотереи Lotto,
// введенное пользователем
int euroLowerLimit; // Минимально допустимое значение
//в лотерее Euromillions
int euroUpperLimit; // Максимально допустимое значение
//в лотерее Euromillions
int euroStarsLowerLimit;// Минимально допустимое "звездное” значение
// в лотерее Euromillions
int euroStarsUpperLimit;// Максимально допустимое "звездное” значение
// в лотерее Euromillions
int euroUserMinimum; // Нижнее граничное значение лотереи
// Euromillions, введенное польователем
int euroUserMaximum; // Верхнее граничное значение лотереи
// Euromillions, введенное польователем
int euroStarsUserMinimum; // Нижнее граничное "звездное" значение лотереи
// Euromillions, введенное пользователем
int euroStarsUserMaximum;// Верхнее граничное "звездное" значение лотереи
// Euromillions, введенное пользователем
В класс Forml необходимо также добавить приватное поле типа Random:
Random* random; // Генерирует псевдослучайные числа
Объект типа Random может генерировать псевдослучайные значения различных
типов. Мы используем его для генерации значений номеров лотереи.
Все эти поля должны быть инициализированы в конструкторе класса. Убедитесь,
что определение конструктора Forml выглядит подобно приведенному ниже:
public ref class Forml : public System::Windows::Forms::Form
public:
Forml(void)
: lottoValuesCount(6) ,
euroValuesCount(5), euroStarsCount(2),
lottoLowerLimit(1),lottoUpperLimit(49),
lottoUserMinimum(lottoLowerLimit),lottoUserMaximum(lottoUpperLimit),
euroLowerLimit(1), euroUpperLimit(50),
euroStarsLowerLimit(1),euroStarsUpperLimit(9),
euroUserMinimum(euroLowerLimit),euroUserMaximum(euroUpperLimit),
euroStarsUserMinimum(euroStarsLowerLimit),
euroStarsUserMaximum(euroStarsUpperLimit)
InitializeComponent();
/ /
random = gcnew Random;
//
Мы определили все новые члены класса, которые требуются на данном этапе, по-
этому вернемся к обработке событий.
Приложения, использующие средства Windows Forms 1049
Обработка события меню Play
Обработчик р 1 а уМе nu 11 em_C 1 i с k () должен создавать новый набор значений,
отображаемых на кнопках видимой в данный момент вкладки. Вспомните, что ранее
в этой главе мы определили значения свойства (Name) для вкладок элемента управ-
ления TabControl соответственно как lottoTab и euroTab. Если теперь взглянуть
на достаточно обширное определение класса Forml, в нем будут присутствовать две
переменные TabPageA с этими именами. Объекты типа ТаЬРаде обладают свойством
Visible типа bool, значение которого равно true, если страница видна, и false,
если она скрыта. Вот и все, что требуется для реализации обработчика элемента
меню Play.
Общая логика использования обработчиком значений свойства Visible страниц
вкладок такова:
private: System::Void playMenuItem_Click(System::ObjectA sender,
System::EventArgsA e)
if(lottoTab->Visible)
11 Генерация и установка значений записи лотереи Lotto
else if(euroTab->Visible)
// Генерация и установка значений записи лотереи Euromillions
Если значение свойства Visible страницы lottoTab истинно, пользователь мо-
жет создать новую запись лотереи Lotto, а если истинно значение свойства Visible
страницы euroTab, создавать можно запись лотереи Euromillions. Хотя обе вкладки
не могут быть видимы одновременно, имеет смысл выполнять проверку условия для
обеих страниц вкладок, поскольку пользователь может щелкнуть на элементе меню
Play тогда, когда открыта вкладка Web Раде.
Процесс генерации набора значений для обеих лотерей кое в чем совпадает. Для
лотереи Lotto необходимо сгенерировать шесть различных случайных целых чисел
из определенного диапазона. Для лотереи Euromillions потребуется сгенерировать
пять различных целых чисел из определенного диапазона, а затем получить два раз-
личных целых числа из другого диапазона. Для генерирования произвольного числа
целочисленных значений из заданного диапазона имеет смысл воспользоваться вспо-
могательной функцией. Ее можно определить следующим образом:
void GetValues (array<int>A values, int min, int max)
// Заполнение массива различными случайными целыми числами
//от минимального до максимального...
Добавьте функцию GetValues () в качестве приватного члена класса Forml и до-
полните описание, как показано в следующем фрагменте кода:
void GetValues (array<int>A values, int min, int max)
values[0] = random->Next(min, max); // Генерация первого случайного значения
// Генерация остальных случайных значений
for (int i = 1 ; i<values->Length ; i++)
1050 Глава 21
for(;;) // Выполнение цикла до обнаружения первого допустимого значения
// Генерация случайных целых чисел в интервале от min до max
values[i] = random->Next(min, max);
// Проверка его отличия от предшествующих значений
if(IsValid(values[i], values, i) ) // Сравнение с предшествующими
// значениями...
break; // ...если они различны — завершение цикла
В качестве первого элемента подходит любое значение из допустимого диапазона.
Последующие значения необходимо сравнивать со значениями, сгенерированными
ранее, и это делает функция IsValid (). Ее можно реализовать следующим образом:
// Проверка того, что данное число отличается от значений элементов массива,
// расположенных в позициях, индекс которых меньше значения indexLimit
bool IsValid(int number, array<int>A values, int indexLimit)
for (int i = 0 ; i< indexLimit ; i++)
if(number == values[i])
return false;
return true;
Добавьте эту функцию как приватный член в класс Forml. Ее работа проста: она
сравнивает первый аргумент с элементами массива, указанного вторым аргумен-
том, значения индекса которого меньше третьего аргумента, и возвращает значение
false, если первый аргумент равен любому из элементов массива; в противном слу-
чае функция возвращает true, что указывает на допустимость первого аргумента.
Выполнение неопределенного по длительности цикла for в функции GetValues ()
и генерация новых случайных значений продолжается до тех пор, пока функция
IsValid () не возвратит значение true, что приведет к завершению внутреннего
цикла и выполнению внешнего цикла for для отыскания следующего уникального
значения.
Теперь функцию GetValues () можно применять в реализации обработчика собы-
тия Click меню Play:
private: System: :Void playMenuItem^Click(System: :ObjectA sender,
System::EventArgsA e)
array<int>A values; // Переменная для хранения дескриптора массива целых чисел
if(lottoTab->Visible)
// Генерация и установка значений записи лотереи Lotto
values = gcnew array<int>(lottoValuesCount); // Создание массива
GetValues(values, lottoUserMinimum, lottoUserMaximum); // Генерация
// значений
SetValues(values, lottoValues);
else if(euroTab->Visible)
// Генерация и установка значений записи лотереи Euromillions
Приложения, использующие средства Windows Forms 1051
values = gcnew array<int>(euroValuesCount);
GetValues(values, euroUserMinimum, euroUserMaximum);
Setvalues(values, euroValues);
values = gcnew array<int>(euroStarsCount);
GetValues(values, euroStarsUserMinimum, euroStarsUserMaximum);
Setvalues(values, euroStars);
Создание запаси лотереи Lotto состоит из трех описанных ниже шагов.
1. Создание массива для хранения значений.
2. Генерация значений с помощью функции GetValues ().
3. Установка значений в качестве надписей на кнопках с помощью функции
SetValues ().
Для записи лотереи Euromillions эти действия выполняются дважды: один раз для
определения пяти значений и второй раз — для определения двух “звездных” значе-
ний.
При реализации функции SetValues () можно воспользоваться тем, что кноп-
ки размешены внутри элемента управления GroupBox. Свойство Controls объекта
GroupBox возвращает коллекцию всех элементов, добавленных в объект. Коллекция,
возвращаемая свойством Controls для самого элемента управления GroupBox, обла-
дает определенным по умолчанию индексированным свойством, обеспечивающим до-
ступ к элементам управления в коллекции. Коллекция представляет собой стек типа
“первым вошел, последним вышел”, поэтому индексные значения свойства обеспечи-
вают доступ к элементам управления в обратном порядке по отношению к порядку
их добавления в групповую рамку. Функцию SetValues () можно реализовать в виде
приватного члена класса Forml следующим образом:
//Установка значений в качестве надписей на кнопках в элементе управления GroupBox
void SetValues(array<int>A values, GroupBoxA groupBox)
Array::Sort(values); // Сортировка значений по возрастанию
int count = values->Length - 1;
for (int i = 0 ; i<groupBox->Controls->Count ; i++)
safe__cast<ButtonA> (groupBox->Controls [i]) ->Text =
values[count-i].ToStringO;
После сортировки значений массива мы устанавливаем переменную count как зна-
чение индекса последнего элемента массива. Затем используем цикл для сохранения
в обратном порядке строковых представлений значений в свойстве Text каждого эле-
мента управления Button. Выражение groupBox->Controls [i] создает дескриптор
типа ControlА, который ссылается на элемент управления, соответствующий индексу
i в коллекции, и перед обращением к свойству Text для установки его значения мы
преобразуем этот тип в Button74.
Нам не нужно реализовать возможности, которые функция SetValues () предо-
ставляет посредством объекта GroupBox. Все кнопки являются явными членами
класса Forml, поэтому для установки их свойства Text можно использовать простей-
ший подход и обращаться к каждой из них непосредственно. Для этого требуется,
по меньшей мере, две функции — одна для записи лотереи Lotto, и одна для лотереи
Euromillions, хотя последнюю удобнее разбить на три функции, в результате чего об-
щее количество необходимых функций становится равным трем. Функция установки
значений записи лотереи Lotto может иметь следующий вид:
1052 Глава 21
void SetNewValues(array<int>A values)
Array::Sort(values);
lottoValuel->Text = values[0].ToString();
lottoValue2->Text = values[1].ToString();
lottoValue3->Text = values[2].ToString();
lottoValue4->Text = values[3].ToString();
lottoValue5->Text = values[4].ToString();
lottoValue6->Text = values[5].ToString();
Эта функция использует дескриптор каждой кнопки для установки ее значения
Тех^Функции установки свойства Text записи лотереи Euromillions можно реализо-
вать практически так же. Затем придется изменить функцию обработчика playMenu-
Item_Click (), чтобы она вызывала эти функции для установки значений.
Теперь можно заново скомпилировать Ех21_01, чтобы проверить, все ли работает
так, как должно. Если вам удалось ввести весь код без ошибок, программа должна ге-
нерировать значения для лотереи Lotto, как показано на рис. 21.18.
A Winning Application
Play Limits Help
Lotto . Euromillions Web Page
Values 1 to 49 ------------------
IB
Puc, 21,18, Генерация значений для лотереи Lotto
Числа на кнопках отображаются в порядке возрастания, и поэтому форма выгля-
дит четкой и аккуратной. Неупорядоченное отображение чисел на кнопках может
свидетельствовать о неупорядоченном добавлении кнопок в элемент управления
GroupBox.
Программа создает также значения для лотереи Euromillions, что можно видеть на
рис. 21.19.
Теперь, когда основные функциональные возможности реализованы, поря продол-
жить разработку приложения.
Обработка событий меню Limits
Меню Limits содержит три элемента, и для каждого из них необходим обработчик
события Click. Последовательно дважды щелкните на каждом элементе меню, чтобы
Приложения, использующие средства Windows Forms 1053
сгенерировать функции обработчиков и зарегистрировать каждую из них в качестве
обработчика события Click.
Обработчик события Click элемента меню Reset наиболее прост в реализации,
поскольку единственная его задача — возврат установленных пользователем предель-
ных значений к предельным значениям, определенным по умолчанию, для отобра-
жаемой в текущий момент лотереи. Обработчики событий Click остальных двух
элементов этого меню будут выполнять существенно больше действий. Нам придется
обеспечить средства для ввода предельных значений, и очевидный способ выполне-
ния этой задачи — отображение диалогового окна после щелчка на элементе меню.
Понятно, что следующий шаг по разработке приложения требует уяснения того, как
можно создать это диалоговое окно.
Рис. 21.19. Генерация значений для лотереи Euromillions
Создание диалогового окна
Диалоговое окно Toolbox предлагает несколько стандартных диалогов. Все они
предоставляют достаточно большие возможности, но ни одно из них не подходит в
настоящем случае. Поскольку данная программа требует выполнения очень специфич-
ных действий, нам придется самостоятельно создать диалоговое окно. Диалоговое
окно — это всего лишь форма, значение свойства FormBorderStyle которой установ-
лено равным FixedDialog, поэтому инструмент Form Designer (Конструктор форм)
значительно облегчит создание диалогового окна.
Выберите команду главного меню Projects Add New Item (Проект’Ф Добавить но-
вый элемент) или нажмите комбинацию клавиш <Ctrl+Shift+A>, чтобы открыть диа-
логовое окно Add New Item (Добавление нового элемента), показанное на рис. 21.20.
В списке Categories: (Категории:) в левой панели установите категорию UI
(Интерфейс пользователя), а в левой панели Templates: (Шаблоны:) выберите ша-
блон Windows Form (Форма Windows) и введите имя, как показано на рис. 21.20. Это
диалоговое окно, которое будет отображаться при вводе верхнего или нижнего преде-
ла лотереи Lotto. Позднее мы создадим диалоговую форму для лотереи Euromillions.
Щелчок на кнопке Add (Добавить) приводит к добавлению в проект новой формы и
1054 Глава 21
ее отображению в окне Editor. Тип класса нового диалогового окна — это введенное
имя LottoLimitsDialog.
Рис. 21.20. Диалоговое окно Add New Item
Нажмите клавишу <F4>, чтобы открыть окно Properties для новой формы. Зна-
чение свойства Text можно изменить на ’’Set Limits for Lotto Values” ("Установка
предельных значений лотереи Lotto") — этот текст отображается в строке заголовка
диалогового окна. Ширину окна можно настроить, перетаскивая его правый край до
тех пор, пока строка заголовка не будет видна полностью. Можно также установить
значение свойства Startposition (Начальная позиция) в группе свойств Layout
(Макет) равным Center Parent (По центру родительской формы), чтобы диалого-
вое окно отображалось в центре отображающей его родительской формы, в данном
примере — окна приложения. Поскольку создаваемое окно будет диалоговым окном,
а не окном приложения, установите значение свойства FormBorder Style равным
FixedDialog. Во время его отображения диалоговое окно не должно быть доступным
для сворачивания или разворачивания на весь экран пользователем, поэтому для уда-
ления из него упомянутых возможностей установите значения свойств MinimizeBox
и MaximizeBox в группе Window Style равными False. Диалоговое окно должно за-
крываться кнопками, которые мы создадим для него, поэтому установите значение
свойства ControlBox равным False, чтобы удалить из строки заголовка рамки управ-
ляющих и системных элементов.
Следующий шаг — добавление двух кнопок в нижнюю часть формы. Они будут
кнопками ОК и Cancel (Отмена) диалогового окна. Для левой кнопки в качестве
значения свойства Text установите ”ОК”, а в качестве значения свойства (Name) —
lottoOK. Можно также установить равным ОК значение свойства DialogResult в
группе Behavior. Значениями для этих же свойств правой кнопки должны быть соот-
ветственно ’’Cancel”, lottoCancel и Cancel. Эффект установки значения свойства
DialogResult кнопок заключается в установке значения DialogResult диалогового
окна соответствующим этому свойству той кнопки, на которой был выполнен щелчок
Приложения, использующие средства Windows Forms 1055
для закрытия диалогового окна. Это позволяет программно проверять, какая кнопка
использовалась для закрытия диалогового окна, и выполнять различный код, в зави-
симости от того, была ли это кнопка ОК или Cancel.
Теперь, когда мы добавили кнопки в диалоговое окно, можно вернуться к свой-
ствам диалога и установить значения свойств AcceptButton и CancelButton в груп-
пе свойств Misc (Разные) равными, соответственно, lottoOK и lottoCancel. В ре-
зультате при открытом диалоговом окне нажатие клавиши <Enter> клавиатуры будет
равносильно щелчку на кнопке ОК, а нажатие клавиши <Esc> — щелчку на кнопке
Cancel.
Диалоговое окно содержит несколько элементов управления, которые можно при-
менять для обеспечения ввода предельных значений. Элемент управления ListBox
(Список) предоставляет пользователю возможность выбирать нужное значение из
списка доступных значений, так что можете воспользоваться им в данном случае.
В форму диалогового окна рядом с элементами управления ListBox потребуется до-
бавить два элемента управления Label (Надпись), как показано на рис. 21.21.
Свойствами (Name) верхнего и нижнего списков должны быть соответственно
lottoLowerList и lottoUpperList. Как видите, размеры элементов ListBox из-
менены так, чтобы их высота была равной высоте элементов управления Label, а
ширина достаточной для отображения одиночного предельного значения. Кроме
того, я изменил значение свойства Size (Размер) шрифта на 10, а значение свой-
ства AcrollAlwaysVisible — на True. Убедитесь, что в качестве значения свойства
SelectionMode (Режим выбора) обоих списков выбрано значение One (Один), по-
скольку необходимо обеспечить так, чтобы одновременно из списка можно было вы-
бирать только один элемент.
Графический интерфейс пользователя диалогового окна готов, но чтобы он вы-
полнял необходимые
чать с кода, который заполняет элементы управления ListBox предельными значе-
ниями.
;ействия, придется снова заняться созданием кода. Можно на-
Добавление списка в элемент управления ListBox
Список элементов управления ListBox представляет собой набор объектов, хра-
нящихся в виде дескрипторов типа Object
вид объектов. В данном случае в каждом списке потребуется хранить целочисленные
поэтому в списке можно хранить любой
1056 Глава 21
предельные значения, и, в основном, при необходимости преобразования значений
типа int в и из объектов типа Int32 можно полагаться на функции автоматической
упаковки и распаковки. Свойство Items объекта ListBox возвращает ссылку на кол-
лекцию объектов списка. Эта коллекция имеет метод Add (), который добавляет в
список объект, передаваемый в качестве аргумента. Объект ListBox обладает множе-
ством свойств, в том числе свойством Enabled, значение которого равно true, когда
пользователь может взаимодействовать со списком, и false, когда взаимодействие
должно быть запрещено.
В основном процесс загрузки списка для обоих элементов управления ListBox
одинаков, поэтому можно создать код обобщенной приватной функции-члена класса
LottoLimitsDialog, добавляющей в список диапазон целых чисел:
void SetList (ListBoxA listBox, int min, int max, int selected)
listBox->BeginUpdate(); // Подавление рисования списка
for (int n = min ; n <= max ; n++)
listBox->Items->Add(n);
listBox->EndUpdate(); // Возобновление рисования списка
г
listBox->SelectedItem = Int32(selected);
Аргументами функции SetList () являются элемент управления списка, в который
должен быть добавлен список, минимальное и максимальное целочисленные значе-
ния добавляемого диапазона и целое число, которое должно быть выбрано в списке.
Функция добавляет список целые числа из диапазона от min до max включительно,
применяя при этом функцию Add () к объекту коллекции, возращенному свойством
Items объекта ListBox. Она устанавливает также выбранное значение в качестве
первоначально выбранного элемента списка при его отображении, устанавливая это
значение в качестве значения свойства Selecteditern элемента управления списка.
Когда пользователь выбирает в списке предельное значение, его необходимо
куда-нибудь поместить, чтобы к нему можно было обращаться из функции, принад-
лежащей объекту Forml. За извлечение предельного значения и его сохранение в
объекте Forml отвечает обработчик событий элементов меню. Один из способов
выполнения этой задачи — добавление в класс LottoLimitsDialog пары приватных
членов, выполняющих сохранение верхнего и нижнего предельных значений, и по-
следующее добавление в класс public-свойств, обеспечивающих внешний доступ к
этим значениям. Это достигается добавлением следующего кода в описание класса
LottoLimitsDialog:
private:
int lowerLimit; // Нижнее предельное значение из элемента управления
int upperLimit; // Верхнее предельное значение из элемента управления
public:
property int LowerLimit // Свойство доступа к нижнему предельному значению
int get(){ return lowerLimit; }
void set(int limit)
lowerLimit = limit;
lottoLowerList->Selected!tem = Int32(limit);
Приложения, использующие средства Windows Forms 1057
property int UpperLimit // Свойство доступа к верхнему предельному значению
int get(){ return upperLimit; }
UpperLimit = limit;
lottoUpperList->Selected!tem = Int32(limit);
Нам требуется возможность обновления свойств, поскольку обработчик события
Click элемента меню Limits1^Reset (Пределы1^ Сброс) изменяет предельные значе-
ния, и объекты ListBox должны содержать выбранное в текущий момент верхнее
или нижнее предельное значение. Кроме сохранения значения в объекте класса по-
требуется также обновлять объекты ListBox, чтобы они отражали новые предельные
значения.
Теперь в классе LottoLomitsDialog можно создать две общедоступные функции-
члены, устанавливающие два элемента управления ListBox:
public:
void SetLowerLimitsList(int min, int max, int selected)
SetList(lottoLowerList, min, max, selected);
lowerLimit = selected;
void SetUpperLimitsList(int min, int max, int selected)
SetList(lottoUpperList, min, max, selected);
upperLimit = selected;
Каждая функция использует функцию SetList () для определения диапазона
значений в соответствующем объекте ListBox и последующей установки значения
selected в члене, используемом для хранения предельного значения.
Обработка событий кнопок диалогового окна
Добавьте функцию обработчика события Click для кнопки ОК, для чего снова об-
ратитесь к вкладке Design формы LottoLimitsDialog и дважды щелкните на кнопке
ОК, чтобы добавить каркасный код.
Для кнопки Cancel обработчик события Click не нужен. Щелчок на этой кнопке
влечет за собой всего лишь закрытие диалогового окна без необходимости выполне-
ния каких-то дальнейших действий.
Обработчик события Click кнопки ОК можно реализовать, как показано ниже.
System::Void lottoOK_Click(System::ObjectA sender, System::EventArgsA e)
11 Если в текущий момент существует выбранное верхнее предельное
// значение, его нужно сохранить
if(lottoUpperList->SelectedItem != nullptr)
upperLimit = safe_cast<Int32>(lottoUpperList->Selected!tem);
11 Если в текущий момент существует выбранное нижнее предельное значение,
// его нужно сохранить
if(lottoLowerList->Selected!tem ’ = nullptr)
lowerLimit = safe cast<Int32>(lottoLowerList->Selected!tem);
1058 Глава 21
Сначала функция сохраняет верхнее предельное значение из объекта lotto-
UpperList ListBox в переменной-члене, добавленной для этой цели. Свойство
Selectedltem объекта ListBox обеспечивает доступность в виде дескриптора типа
Object* выбранного в текущий момент элемента, и в качестве меры предосторожно-
сти код проверяет, чтобы возвращенный дескриптор не был нулевым. Перед сохране-
нием тип выбранного элемента потребуется привести к действительно используемому
типу — I n 13 2. Затем функция автоматической упаковки преобразует объект в целое
число. После этого обработчик аналогичным образом сохраняет нижнее предельное
значение, извлеченное из другого объекта ListBox. По завершении работы обработ-
чика диалоговое окно автоматически закрывается.
Управление состоянием объектов ListBox
В ответ на события Click обоих элементов меню Limits1^ Up ре г и Limits^Lower ис-
пользуется один и тот же объект диалогового окна, но при этом нежелательно, чтобы
в каждой из ситуаций было возможно изменение обоих списков. Для события элемен-
та меню Upper требуется подавление выбора нижнего предельного значения, а для
элемента меню Lower — подавление списка выбора верхнего предельного значения.
Для решения этой задачи в класс LottoLimitsDialog можно добавить пару общедо-
ступных функций-членов. Функция установки состояния объектов ListBox для эле-
мента меню Upper имеет следующий вид:
void SetUpperEnabled ()
lottoUpperList->Enabled = true;
lottoLowerList->Enabled = false;
// Активизирует верхний элемент
// управления списка
// Отключает нижний элемент
// управления списка
Значение свойства Enabled (Активизировано) объекта lottoUpperList было
установлено равным true, чтобы разрешить пользователю взаимодействовать с ним.
Установка значения этого свойства в false делает объект доступным только для чте-
ния. Для элемента меню Lower потребуется выполнить противоположные действия:
void SetLowerEnabled()
lottoUpperList->Enabled = false; // Отключает верхний элемент
// управления списка
lottoLowerList->Enabled = true; // Активизирует нижний элемент
// управления списка
Мы выполнили большой объем работы, чтобы обеспечить должное поведение
объекта диалогового окна в приложении, но пока не располагаем самим этим объек-
том. Объект окна приложения берет на себя заботу о его создании.
Создание объекта диалогового окна
Конструктор класса Forml может создавать объект диалогового окна. Он может
также в диалоговом окне инициализировать объекты ListBox. Добавьте в класс
Forml приватный член для хранения дескриптора диалогового окна:
private: LottoLimitsDialog* lottoLimitsDialog;
В тело конструктора Forml поместите следующий код:
Приложения, использующие средства Windows Forms 1059
lottoLimitsDialog = gcnew LottoLimitsDialog;
XottoLimitsDialog->SetLowerLimitsList(1, lottoUpperLimit-lottoValuesCount+1,
lottoUserMinimum);
lottoLimitsDialog->SetUpperLimitsList(lottoValuesCount, lottoUpperLimit,
lottoUserMaximum);
Этот код очень прост. Первый оператор создает объект диалогового окна.
Следующие два оператора вызывают функции, которые инициализируют списки в
объектах ListBox. Максимальное значение в объекте ListBox, устанавливающее
минимальный верхний предел, вычисляется так, чтобы можно было создать нужное
количество значений в записи. Например, если максимальное значение равно 49, а
количество значений в записи — 6, максимальные значение нижнего предела должно
быть равно 44 — в случае использования любого более высокого значения создание
шести различных значений было бы невозможно. Аналогичные рассуждения справед-
ливы по отношению к минимальному значению верхнего предела; оно не может быть
меньше количества значений в записи. Выбранные элементы в списках — lottoUser-
Minimum и lottoUserMaximum.
Поскольку конструктор класса Forml ссылается на имя класса LottoLimitsDialog,
в файл заголовка Forml . h потребуется добавить директиву #include определения
класса:
#include ’’LottoLimitsDialog.h”
Использование диалогового окна
Обращение к диалоговому окну будет выполняться в коде обработчиков событий
Click элементов Upper и Lower меню Limits. Для отображения диалогового окна как
модального необходимо вызвать функцию ShowDialog () для объекта диалогового
окна. При желании функции ShowDialog () в качестве аргумента можно передать де-
скриптор родительской формы. Функции обработчика события Click можно реали-
зовать следующим образом:
System:: Void lowerMenu!tem_Click (System:: ObjectА sender, System::EventArgsЛ e)
if(lottoTab->Visible)
lottoLimitsDialog->SetLowerEnabled();
::DialogResult result = lottoLimitsDialog->ShowDialog(this);
if(result == ::DialogResult::OK)
// Обновление вводимых пользователем предельных значений
//из свойств диалогового окна
lottoUserMaximum s lottoLimitsDialog->UpperLimit;
lottoUserMinimum ~ lottoLimitsDialog->LowerLimit;
System::Void upperMenu!tem__Click (System::ObjectЛ sender, System::EventArgsл e)
if(lottoTab->Visible)
lottoLimitsDialog->SetUpperEnabled();
::DialogResult result => lottoLimitsDialog->ShowDialog(this);
if(result == ::DialogResult::OK)
1060 Глава 21
// Обновление вводимых пользователем предельных значений
//из свойств диалогового окна
lottoUserMaximum = lottoLimitsDialog->UpperLimit;
lottoUserMinimum = lottoLimitsDialog->LowerLimit;
Обе эти функции работают одинаково. Они вызывают функцию установки со-
стояния элементов управления списков, а затем отображают диалоговые окна как
модальные, вызывая функцию ShowDialog () для объекта диалогового окна. Если бы
требовалось отобразить диалоговое окно как немодальное, вместо этой функции для
объекта диалогового окна нужно было бы вызывать функцию Show ().
При вызове функции ShowDialog () она не выполняет возврат до тех пор, пока ди-
алоговое окно не будет закрыто. Это означает, что код обновления предельных значе-
ний не будет выполняться до тех пор, пока новые предельные значения не будут запи-
саны в объект диалогового окна обработчиком события Click кнопки lottoOK. При
отображении диалогового окна в качестве немодального с помощью функции Show (),
она немедленно выполняет возврат. Следовательно, в этом случае требуется доступ к
данным, которые могут быть изменены в диалоговом окне, и для этого необходимо
иное средство. Добавление функции обработчика события Closing (Закрытие) диа-
логовой формы — одна из возможностей; еще одна возможность была бы связана с
передачей данных в обработчик кнопки, которая закрывает диалоговое окно.
Функция ShowDialog () возвращает значение перечисления DialogResult, ко-
торое затем сохраняется в локальной переменной result. Возвращаемое значение
функции ShowDialog () указывает кнопку диалогового окна, на которой был выпол-
нен щелчок, и если значение является перечислимой константой :: DialogResult::
ОК, это свидетельствует о щелчке на кнопке ОК. Таким образом, когда для закрытия
диалогового окна используется кнопка ОК, код в каждой функции обработчика обнов-
ляет только поля lottoUserMaximum и LottoUserMinimum.
Обратите внимание на присутствие операции :: в спецификации типа :: Dialog-
Result и в выражении :: DialogResult: :0К. Операция разрешения контекста обя-
зательно должна предшествовать имени DialogResult для различения имени пере-
числимой переменной в глобальной области определения от свойства с такими же
именем, являющегося членом класса Forml.
Конечно, к свойству DialogResult объекта lottoLimitsDialog можно было бы
обращаться непосредственно, поэтому оператор i f можно было бы записать следую-
щим образом:
if(lottoLimitsDialog->DialogResult == ::DialogResult::OK)
11 Обновление вводимых пользователем предельных значений
/ / из свойств диалогового окна
lottoUserMaximum = lottoLimitsDialog->UpperLimit;
lottoUserMinimum = lottoLimitsDialog->LowerLimit;
Первая версия более удобна, поскольку в коде видно, что он выполняет проверку
значения, возвращенного функцией ShowDialog ().
Полный набор констант, определяемый перечислением DialogResult, приведен
ниже.
Yes (Да) No (Нет) OK Cancel (Отмена)
Retry (Повторить) Ignore (Игнорировать) None (Нет)
Приложения, использующие средства Windows Forms 1061
Значение свойства DialogResult, установленное для диалогового окна, опреде-
ляет, было ли закрыто диалоговое окно. Если в качестве его значения установлено
None, диалоговое окно не закрыто. Это значение свойства можно устанавливать, если
по какой-то причине требуется предотвратить закрытие диалогового окна — напри-
мер, в случае недопустимости введенного значения.
В настоящий момент обработчик события Click кнопки ОК не проверяет допу-
стимость введенных значений. При этом вполне возможна установка такого верхнего
и нижнего предельного значений, которые делают невозможным присвоение записи
лотереи шести уникальных значений. Для устранения этой проблемы можно восполь-
зоваться свойством DialogResult формы.
Проверка допустимости вводимых значений
Если запись для Lotto должна содержать 6 уникальных значений, разность между
выбираемыми пользователем верхним и нижним предельными значениями должна
быть больше или равна 5. Для проверки этого условия можно изменить обработчик
события Click кнопки ОК:
System::Void lottoOK_Click(System::ObjectА sender, System::EventArgsA e)
int upper == 0;
int lower « 0;
// Если в текущий момент существует выбранный элемент верхнего
предельного значения, его нужно сохранить
if(lottoUpperList->SelectedItem != nullptr)
upper = safe_cast<Int32>(lottoUpperList->SelectedItem);
11 Если в текущий момент существует выбранный элемент нижнего предельного
// значения, его нужно сохранить
if(lottoLowerList->SelectedItem != nullptr)
lower = safe_cast<Int32>(lottoLowerList->SelectedItem);
if (upper - lower < 5)
MessageBox::Show(L’’Upper limit: ’’ + upper + L" Lower limit: " + lower +
L"\nUpper limit must be at least 5 greater that the lower limit.” +
L"\nTry again.",
L"Limits Invalid",
MessageBoxButtons::0K,
MessageBoxIcon::Error);
MessageBox::Show(Ь"Верхний предел: ’’ + upper + L" Нижний предел: ’’ + lower +
Ь’’\пЗначение верхнего предела должно быть, по меньшей мере, на 5
больше значения нижнего предела. ’’ +
Ь’’\пПовторите ввод.’’,
Ь’’Предельные значения недопустимы",
MessageBoxButtons::0К,
MessageBoxIcon::Error);
DialogResult = ::DialogResult::None;
else
upperLimit = upper;
lowerLimit = lower;
1062 Глава 21
Теперь функция сохраняет значения, выбранные в объектах ListBox, в локальных
переменных lower и upper. Если разность между значениями меньше 5, программа
отображает окно сообщения и блокирует закрытие диалогового окна путем установки
значения свойства DialogResult равным None. Статическая функция Show () в клас-
се MessageBox отображает окно сообщения, определяемого аргументами функции.
Использованная в этой программе версия функции Show () принимает четыре аргу-
мента, которые описаны в табл. 21.2.
Таблица 21.2. Аргументы функции Show()
Тип параметра Описание
String^
Текст, предназначенный для отображения в окне сообщения.
string^ Текст, предназначенный для отображения в строке заголовка окна сообщения.
MessageBoxButtons Перечислимая константа, указывающая кнопки, предназначенные для ото-
бражения в окне сообщения. Перечислимая константа MessageBoxButtons
определяет следующие значения: OK, OKCancel, YesNo, YesNoCancel,
RetryCancel, AbortRetrylgnore.
Перечислимая константа, указывающая пиктограмму, предназначенную для
отображения в окне сообщения. Перечислимая константа MessageBoxicon
определяет следующие значения: Asterisk, Exclamation, Error, Hand,
Infomation, None, Question, Stop, Warning.
MessageBoxicon
Существует большое количество перегруженных версий статической функции
Show () — от очень простои с единственным параметром типа String* до значитель-
но более сложных, принимающих свыше десятка параметров.
В случае компиляции и выполнения программы с последующей установкой недо-
пустимых предельных значений откроется диалоговое окно, подобное показанному
на рис. 21.22.
=* vin nng > pplicatior.
Play Limits Help
Lotto Euromillions Ci——-
-values ito - Set Limits for Lotto Values
Select lower limit value
Limits Invalid
Upper limit: 40 Lower limit: 37
Upper limit must be at least 5 greater that the lower limit.
Try Again.
Puc. 21.22, Диалоговое окно сообщения о недопустимом вводе (одна кнопка)
Приложения, использующие средства Windows Forms 1063
Круглая пиктограмма красного цвета с белым крестом определена четвертым ар-
гументом функции Show (), а отображение единственной кнопки ОК — результат дей-
ствия третьего аргумента.
Функция Show (), вызываемая для отображения окна сообщения, возвращает зна-
чение типа DialogResult, которое указывает, какая кнопка была использована для
закрытия окна сообщения. Это возвращаемое значение можно применять для опреде-
ления действий, которые нужно выполнять после закрытия окна сообщения. В об-
работчике lottoOK__Click () кнопки ОК в диалоговом окне определения предельных
значений значение, возвращенное функцией Show () для окна сообщения, можно ис-
пользовать для определения того, нужно ли закрывать диалоговое окно предельных
значений.
System::Void lottoOK_Click(System::ObjectЛ sender, System::EventArgsA e)
int upper = 0;
int lower =0;
// Если в текущий момент существует выбранный элемент верхнего
// предельного значения, его нужно сохранить
if (lottoUpperList->SelectedItem != nullptr)
upper = safe_cast<Int32>(lottoUpperList->Selected!tem);
// Если в текущий момент существует выбранный элемент нижнего
// предельного значения, его нужно сохранить
if(lottoLowerList->SelectedItem ’= nullptr)
lower = safe_cast<Int32>(lottoLowerList->SelectedItem);
if (upper - lower < 5)
:: DialogResult result =
MessageBox: :Show(L’’Upper limit: ” 4- upper + L" Lower limit: ’’ 4- lower +
L”\nUpper limit must be at least 5 greater that the lower limit.” +
L"\nTry again.”,
L”Limits Invalid",
MessageBoxButtons::0K,
MessageBoxIcon::Error);
::DialogResult result =
MessageBox::Show(L"Верхний предел: " 4- upper 4- L" Нижний предел: ” 4- lower 4-
Ь”\пЗначение верхнего предела должно быть, по меньшей мере, на 5
больше значения нижнего предела. ” 4-
Ь”\пПовторите ввод.”,
L"Предельные значения недопустимы",
MessageBoxButtons::OKCancel,
MessageBoxIcon::Error);
if(result == ::DialogResult::0K)
DialogResult = ::DialogResult::None;
else
DialogResult = ::DialogResult::Cancel;
else
upperLimit = upper;
lowerLimit = lower;
1064 Глава 21
Поскольку третьим аргументом функции Show () является MessageBoxButtons::
OKCancel, теперь окно сообщения содержит две кнопки, как показано на рис. 21.23.
Рис. 21.23. Диалоговое окно сообщения о недопустимом вводе (две кнопки)
В обработчике события Click кнопки ОК диалогового окна предельных значений
возвращаемое значение функции Show () сохраняется в переменной result. Тип этой
переменной должен быть указан с помощью операции разрешения контекста. В про-
тивном случае компилятор будет ее интерпретировать как свойство DialogResult
объекта lottoLimitsDialog, и компиляция кода потерпит неудачу. Если переменная
result содержит значение : : DialogResult: :0К, мы устанавливаем значение свой-
ства DialogResult объекта lottoLimitsDialog равным :: DialogResult: :None,
что предотвращает закрытие диалогового окна и разрешает изменение предельного
значения. В противном случае мы устанавливаем значение свойства DialogResult
равным :: DialogResult:: Cancel, что для диалогового окна равносильно щелчку на
кнопке Cancel, поэтому оно закрывается.
Обработчик события элемента меню Reset
Обработчик события элемента меню Reset можно реализовать так, как показано
ниже.
System:: Void resetMenuItem_Click(System::ObjectЛ sender, System:: Event ArgsЛ e)
if(lottoTab->Visible)
// Сброс устанавливаемых пользователем пределов лотереи Lotto
lottoUserMaximum = lottoUpperLimit;
lottoUserMinimum = lottoLowerLimit;
lottoLimitsDialog->UpperLimit = lottoUpperLimit;
lottoLimitsDialog->LowerLimit = lottoLowerLimit;
else if(euroTab->Visible)
Приложения, использующие средства Windows Forms 1065
// Сброс устанавливаемых пользователем пределов лотереи Euromillions
euroUserMaximum = euroUpperLimit;
euroUserMinimum = euroLowerLimit;
euroStarsUserMaximum = euroStarsUpperLimit;
euroStarsUserMinimum = euroStarsLowerLimit;
// Код обновления диалогового окна пределов лотереи Euromillions...
Этот код всего лишь сбрасывает предельные значения в полях объекта Forml, а за-
тем соответствующим образом обновляет свойства объекта диалогового окна. В при-
веденную функцию придется добавить код, выполняющий сброс диалогового окна
в состояние, которое позволит обрабатывать ввод предельных значений лотереи
Euromillions.
Теперь можно повторно скомпилировать программу и попробовать изменить пре-
дельные значения лотереи Lotto. Типичное окно приложения показано на рис. 21.24.
I
Play
Lotto
Values 1
и
Limits Help
Euromillions - 1 -
Puc. 21.24, Изменение предельных значений для лотереи Lotto
Как видите, приложение автоматически генерирует линейку прокрутки списка в
элементе управления списка. Обратите внимание, что прокрутка к какому-либо эле-
менту не приводит к его выбору. Чтобы выбрать элемент
;о щелчка на кнопке ОК не-
обходимо щелкнуть также и на нем. Выбор элемента меню Limits^ Reset сбрасывает
оба предела в их исходные значения.
Добавление второго диалогового окна
Создание второго диалогового окна для установки предельных значений лотереи
Euromillions — достаточно простая задача. Процесс ничем не отличается от создания
первого диалогового окна. Создайте в проекте новую форму, нажав комбинацию кла-
виш <Ctrl+Shift+A>, чтобы открыть диалоговое окно Add New Item, в котором выбери-
те категорию UI и шаблон Windows Form. Присвойте форме имя EuroLimitsDialog.
1066 Глава 21
Значения свойств этого диалогового окна можно установить аналогично свойствам
предыдущего диалогового окна (табл. 21.3).
Таблица 21.3. Значения свойств диалогового окна EuroLimitsDialog
Свойство формы
FormBorderStyle
ControlBox
MinimizeBox
MaximizeBox
Text
Значение, которое нужно установить
FixedDialog
False
False
False
Set Euromillions Limits (Установка предельных значений лотереи Euromillions)
ОК” и "Cancel”, а в качестве значений
euroOK и euroCancel. Необходимо также установить значения
Добавьте в диалоговое окно кнопки ОК и Cancel. В качестве значений свойства
Text кнопок установите соответственно
свойства (Name)
свойств DialogResult равными ОК и Cancel. После того, как кнопки будут определе-
ны, можно вернуться к свойствам диалоговой формы и установить в качестве значе-
ний свойства CancelButton соответственно euroOK и euroCancel.
Для приобретения опыта работы с более широким множеством элементов управ-
ления и во избежание однообразия в приложении, для обработки вводимых значе-
ний мы не будем использовать элементы управления ListBox, как это имело место в
первом диалоговом окне. В этом диалоговом окне потребуется предусмотреть сред-
ства для ввода верхнего и нижнего пределов набора пяти значений, а также для ввода
набора двух “звездных” значений. Это будет мало способствовать изяществу реализа-
ции, но в целях приобретения опыта работы с как можно большим числом различ-
ных элементов управления мы используем элементы управления NumericUpDown в
первом случае и элементы управления ComboBox
эти элементы управления можно добавить вместе с соответствующими элементами
управления Label, поместив каждую группу элементов управления внутри GroupBox,
как показано на рис. 21.25. Понятно, что вначале следует добавить элементы управле-
ния GroupBox, а затем поместить остальные элементы управления внутрь них.
во втором. В диалоговую форму
EuroL mitsDialog.h [Design]*
Set Euromillions Limits
Set Values Limits
Lower Umit: 1
Set Stars Limits
Lower Umit:
Upper Limit: *9 C
Upper Umit:
OK
Cancel
Puc. 21.25. Диалоговое окно установки
пределов для лотереи Euromillions
Приложения, использующие средства Windows Forms 1067
("Установите предельные звездные значения"). Код
Для указания функции элементов управления внутри каждой групповой рамки зна-
чение свойства Text верхней групповой рамки определено как ’’Set Values Limits’’
("Установка предельных значений"), а значение этого свойства нижней групповой
рамки—как ’’Set Stars Limits’’
не будет обращаться к объектам GroupBox, поэтому значения их свойства (Name)
роли не играют. Значения свойства Text каждого элемента управления Label можно
установить, как показано на рис. 21.25.
Для значений свойства (Name) элементов управления NumericUpDown в верхней
групповой рамке следует выбрать соответственно lowerValuesLimits и uppervalues-
Limits. Значения, отображаемые этими элементами управления, можно установить,
задавая значения свойств Maximum и Minimum. Эти значения для левого элемента
управления lowerValuesLimits должны быть установлены равными 44 и 1, а для
правого — 49 и 6. Значение свойства Value элемента управления upperValuesLimit
можно определить как 4 9; это значение отображается в элементе управления внача-
ле. Если установить также значения свойств Readonly каждого из элементов управ-
ления NumericUpDown равными True, это предотвратит ввод значения с клавиатуры.
В этом приложении использование элемента управления NumericUpDown очень про-
стое. Шаг увеличения значения можно изменить, устанавливая значение свойства
Increment (Шаг). Типом свойства Increment является Decimal, поэтому для него
можно использовать и нецелые значения.
В качестве значений свойства (Name) элементов управления ComboBox в нижней
групповой рамке можно задать lowerStarsLimits и upperStarsLimits. Ввод значе-
ний, которые будут отображаться в элементе управления ComboBox, достаточно прост.
Щелкните на небольшой стрелке в верхнем правом углу левого элемента управления
ComboBox, чтобы открыть меню ComboBox Tasks (Задачи ComboBox), показанное на
рис. 21.26.
EuroLimitsDialog.h [Design]*
Е и го Li mitsD ia Ion. h *
Set Euromillions Limits
Lower Limit 1
Upper Emit: 49
Lower Limit: 0
ComboBox Tasks
Hi Use data bound items:
Unbound Mode
Edit Herns
lL J
Opens the Items collection editor
Puc. 21.26. Меню ComboBox Tasks
Выберите пункт меню Edit Items в нижней части меню, чтобы открыть диалого-
вое окно String Collection Editor (Редактор коллекции строк), представленное на
рис. 21.27.
На рис. 21.27 показаны значения, введенные для левого элемента управления
ComboBox. Для правого элемента управления можно ввести значения от 2 до 9 вклю-
чительно.
1068 Глава 21
String Collection Editor
Enter the strings in the collection (one per line):
1
3
4
5
6
Cancel
Puc. 21.27. Диалоговое окно String Collection Editor
Элемент управления ComboBox — не лучший выбор для этого приложения, посколь-
ку он допускает как ввод текста, так и выбор из списка, а нам требуется, чтобы пре-
дельные значения можно было выбирать только из списка. Своим названием элемент
ComboBox (Поле со списком) обязан тому, что он объединяет в себе возможности эле-
мента управления ListBox (Список), который позволяет выбирать значение из спи-
ска, и элемента управления TextBox (Текстовое поле), обеспечивающего ввод текста.
Получение данных из элементов управления диалогового окна
Получение предельных значений из элементов управления, по сути, не будет отли-
чаться от их получения из диалогового окна предельных значений для лотереи Lotto.
Вначале добавьте в класс EuroLimitsDialog несколько новых членов данных для хра-
нения предельных значений вводимых пользователем:
private:
int lowerValuesLimit;
int upperValuesLimit;
int lowerStarsLimit;
int upperStarsLimit;
В целях предосторожности эти члены лучше инициализировать в конструкторе
класса:
EuroLimitsDialog(void)
:lowerValuesLimit(1)
,upperValuesLimit(50)
,lowerStarsLimit(1)
,upperStarsLimit(9)
InitializeComponent();
11
// TODO: здесь необходимо вставить код конструктора
И
Приложения, использующие средства Windows Forms 1069
В классе диалогового окна необходимо определить набор public-свойств, обеспе-
чивающих доступ к предельным значениям из объекта окна приложения:
public:
property int LowerValuesLimit
int get() { return lowerValuesLimit; }
void set(int limit)
lowerValuesLimit = limit;
lowerValuesLimits->Value = limit; // Устанавливает как выбранное
// значение в NumericUpDown
property int UpperValuesLimit
int get() { return UpperValuesLimit; }
void set(int limit)
UpperValuesLimit = limit;
upperValuesLimits->Value = limit; // Устанавливает как выбранное
// значение в NumericUpDown
property int LowerStarsLimit
int get() { return lowerStarsLimit; }
void set(int limit)
lowerStarsLimit = limit;
lowerStarsLimits->Selected!tem = limit; // Устанавливает как
// выбранное значение в ComboBox
lowerStarsLimits->SelectedIndex = // Устанавливает индекс для
/ / выбранного элемента
lowerStarsLimits->FindString(limit.ToString());
property int UpperStarsLimit
int get() { return UpperStarsLimit; }
void set(int limit)
UpperStarsLimit = limit;
upperStarsLimits->Selected!tem = limit; // Устанавливает как
// выбранное значение в ComboBox
upperstarsLimits->SelectedIndex = // Устанавливает индекс для
// выбранного элемента
upperStarsLimits->FindString(limit.ToString ());
Для каждого свойства функция ge t () возвращает значение соответствующего при-
ватного члена класса диалогового окна. Функция s е t () устанавливает значение чле-
на данных и обновляет элемент управления в диалоговом окне, чтобы установленное
значение стало выбранным. Значение свойства Selectedlndex — индекс выбранного
1070 Глава 21
элемента. Для элемента управления ComboBox эта установка выполняется функцией
Findstring (), которая возвращает значение индекса первого найденного в коллек-
ции элементов совпадения с аргументом. Расположенное в этой позиции значение из-
начально отображается в элементе управления.
Добавьте обработчик события Click кнопки ОК в класс EuroLimitsDialog, дваж-
ды щелкнув на кнопке в окне Design. Реализация обработчика для кнопки Cancel не
требуется. Обработчик кнопки ОК можно реализовать, как показано ниже.
System::Void euroOK_Click(System::ObjectЛ sender, System::EventArgsA e)
::DialogResult result;
// Извлечение предельных значений
int valuesLower = Decimal::ToInt32(lowerValuesLimits->Value);
int valuesUpper - Decimal::ToInt32(upperValuesLimits->Value);
if(valuesUpper - valuesLower < 4) // Проверка достаточности
// введенного диапазона
result = MessageBox::Show(this, // Диапазон не достаточен, поэтому
’’Upper values limit: ”+valuesUpper + // отображается окно сообщения
’’ Lower values limit: ”+ valuesLowert
”\nUpper values limit must be at least 4 greater that the lower limit.’’+
"\nTry Again.’’,
"Limits Invalid",
MessageBoxButtons::OKCancel,
MessageBoxicon::Error);
result = MessageBox::Show(this,
"Верхний предел: "tvaluesUpper +
’’ Нижний предел: ’’+ valuesLower+
"ХпЗначение верхнего предела должно быть, по меньшей мере, на 4
больше значения нижнего предела.’’+
’’ \пПовторите ввод.",
"Предельные значения недопустимы",
MessageBoxButtons::OKCancel,
MessageBoxicon::Error);
if (result =- :: DialogResult::OK) // При щелчке на кнопке ОК окна сообщения
DialogResult = ::DialogResult::None; // блокировка закрытия
// диалогового окна
else // Щелчок был выполнен на кнопке Cancel окна сообщения
DialogResult = ::DialogResult::Cancel; // поэтому диалоговое окно
// нужно закрыть
return;
// Извлечение предельных "звездных" значений
int starsLower = lowerStarsLimits->Selected!tem == nullptr ?
lowerStarsLimit :
Int32::Parse(lowerStarsLimits->SelectedItem->ToString());
int starsUpper = upperStarsLimits->SelectedItem == nullptr ?
upperStarsLimit :
Int32: : Parse (upperStarsLimits-'>SelectedItem->ToString () ) ;
Приложения, использующие средства Windows Forms 1071
if(starsUpper - starsLower < 1) // Проверка достаточности введенного диапазона
result = MessageBox: : Show (this, // Диапазон не достаточен, поэтому
"Upper stars limit: "+starsUpper + // отображается окно сообщения
" Lower stars limit: "+ starsLower*
"\nUpper stars limit must be at least 1 greater that the lower limit."*
"\nTry Again.",
"Limits Invalid",
MessageBoxButtons::0KCancel,
MessageBoxIcon::Error);
result = MessageBox::Show(this,
"Верхний предел: "*starsUpper +
" Нижний предел: " + starsLower*
"\пЗначение верхнего предела должно быть, по меньшей мере, на 1
больше значения нижнего предела."*
"\пПовторите ввод.",
"Предельные значения недопустимы",
MessageBoxButtons::OKCancel,
MessageBoxIcon::Error);
if (result == ::DialogResult: :0K) // При щелчке на кнопке ОК окна сообщения
DialogResult = ::DialogResult::None; // блокировка закрытия
11 диалогового окна
else // Щелчок был выполнен на кнопке Cancel окна сообщения
DialogResult = ::DialogResult::Cancel; // поэтому диалоговое окно
/ / нужно закрыть
// Сохранение новых предельных значений
lowerValuesLimit = valuesLower;
upperValuesLimit = valuesupper;
lowerStarsLimit = starsLower;
upperStarsLimit = starsUpper;
Свойство Value элемента управления NumericUpDown возвращает значение типа
Decimal. Чтобы его преобразовать к типу Int32, мы передаем его в качестве аргу-
мента статической функции Tolnt32 () класса Decimal. Возвращаемое этой функци-
ей значение автоматически распаковывается так, чтобы его можно было хранить в
переменной типа int.
Типом значения, возвращенного свойством Selecteditem элемента управления
ComboBox, является Object*, поэтому в качестве меры предосторожности мы про-
веряем, не является ли оно нулевым. Если оно нулевое, мы устанавливаем значение
локальной переменной равным текущему значению, записанному в объекте диалого-
вого окна; если оно не нулевое, мы сохраняем значение, представленное свойством
Selectedltem. Значение нельзя сохранить непосредственно, но вызов функции
ToString () применительно к объекту создает строковое представление объекта,
которое затем можно преобразовать к типу int с помощью статической функции
Parse () класса Int32.
Нам потребуется приватный член класса Forml, хранящий дескриптор нового диа-
логового окна:
private:
EuroLimitsDialog* euroLimitsDialog; // Диалоговое окно установки предельных
// значений лотереи Euromillions
1072 Глава 21
Чтобы создать новый объект диалогового окна и обновить свойства предельных
звездных значений, в конец кода конструктора класса Forml необходимо добавить
следующие операторы:
euroLimitsDialog = gcnew EuroLimitsDialog;
euroLimitsDialog->LowerStarsLimit = euroStarsLowerLimit;
euroLimitsDialog->UpperStarsLimit = euroStarsUpperLimit;
Установка свойств LowerStarsLimit и UpperStarsLimit объекта диалогового
окна обеспечивает отображение этих значений в элементе управления ComboBox при
первоначальном открытии диалогового окна. В случае отсутствия выбранного эле-
мента в элементе управления ComboBox при открытии диалогового окна он не ото-
бражает никаких значений.
Не забудьте добавить в файл заголовка Forml .h директиву #include для включе-
ния определения класса EuroLimitsDialog:
#include "EuroLimitsDialog.h"
Отключение элементов управления ввода данных
При выборе пункта меню Limits1^ Upper необходимо предотвратить ввод нижне-
го предельного значения, а при выборе пункта меню Limits1^ Lower — ввод верхнего
предельного значения. Для решения этой задачи добавьте в класс EuroLimitsDialog
две функции-члена.
public:
// Отключение элементов управления для ввода верхних предельных значений
void SetLowerEnabled(void)
upperValuesLimits->Enabled = false;
upperStarsLimits->Enabled = false;
lowerValuesLimits->Enabled = true;
lowerStarsLimits->Enabled = true;
// Отключение элементов управления для ввода нижних предельных значений
void SetUpperEnabled(void)
upperValuesLimits->Enabled = true;
upperStarsLimits->Enabled = true;
lowerValuesLimits->Enabled = false;
lowerStarsLimits->Enabled = false;
Значение свойства Enabled элемента управления определяет его состояние активи-
зации. Значение true активизирует элемент управления, а значение false — отключа-
ет, чтобы пользователь не мог с ним взаимодействовать. Функция SetLowerEnabled ()
отключает элементы управления, используемые для ввода верхних предельных зна-
чений, и включает элементы управления для ввода нижних предельных значений.
Функция SetUpperEnabled () выполняет противоположные действия.
Обновление обработчиков элементов меню Limits
Последний шаг по поддержке ввода предельных значений лотереи Euromillions
связан с обновлением обработчиков события Click элементов меню Limits в классе
Forml. Обработчик элемента меню Upper нужно привести к виду, показанному ниже.
Приложения, использующие средства Windows Forms 1073
System::Void upperMenu!tem_Click(System: :ObjectA sender, System::EventArgsA e)
::DialogResult result;
if(lottoTab->Visible)
lottoLimitsDialog->SetUpperEnabled();
result = lottoLimitsDialog->ShowDialog(this);
if (result == ::DialogResult::OK)
lottoUserMaximum = lottoLimitsDialog->UpperLimit;
lottoUserMinimum = lottoLimitsDialog->LowerLimit;
else if(euroTab->Visible)
euroLimitsDialog->SetUpperEnabled();
result = euroLimitsDialog->ShowDialog(this);
if(result == ::DialogResult::0K)
euroUserMaximum = euroLimitsDialog->UpperValuesLimit;
euroUserMinimum = euroLimitsDialog->LowerValuesLimit;
euroStarsUserMaximum = euroLimitsDialog->UpperStarsLimit;
euroStarsUserMinimum = euroLimitsDialog~>LowerStarsLimit;
Локальная переменная result использована в обоих операторах if, поэтому те-
перь она объявлена в начале функции. После соответствующей активизации элемен-
тов управления в диалоговом окне с помощью вызова функции SetUpperEnabled ()
мы отображаем диалоговое окно как модальное. Если пользователь закрывает его, щел-
кая на кнопке ОК, доступные результаты сохраняются в свойствах объекта диалогового
окна. Изменения в обработчике события Click элемента меню Lower аналогичны:
System::Void lowerMenuItem_Click (System::ObjectA sender, System::EventArgsA e)
::DialogResult result;
if(lottoTab->Visible)
lottoLimitsDialog->SetLowerEnabled();
result = lottoLimitsDialog->ShowDialog(this);
if(result == ::DialogResult::OK)
lottoUserMaximum = lottoLimitsDialog->UpperLimit;
lottoUserMinimum = lottoLimitsDialog->LowerLimit;
else if(euroTab->Visible)
euroLimitsDialog->SetLowerEnabled();
result = euroLimitsDialog->ShowDialog(this);
if(result == ::DialogResult::OK)
euroUserMaximum = euroLimitsDialog->UpperValuesLimit;
euroUserMinimum = euroLimitsDialog->LowerValuesLimit;
euroStarsUserMaximum = euroLimitsDialog->UpperStarsLimit;
euroStarsUserMinimum = euroLimitsDialog->LowerStarsLimit;
1074 Глава 21
Логика этого кода полностью аналогична логике предыдущей функции обработ-
чика.
Реализация элемента меню Не1р>=>About
Теперь, когда вы знакомы с классом MessageBox, реализация этого элемента меню
не представляет сложности. При щелчке на элементе меню Help1^About (Справка1^
О программе) достаточно отобразить окно сообщения:
System::Void aboutToolStripMenuItem_Click(System::Object74 sender,
System::EventArgsл e)
MessageBox::Show(L” (c) Copyright Ivor Horton", L"About A Winning Application",
MessageBoxButtons::0K, MessageBoxIcon::Exclamation);
Щелчок на этом элементе меню приводит к тому, что функция обработчика ото-
бражает окно сообщения, показанное на рис. 21.28.
Winning Application
Play Limits Help
Lotto Euromillions Web Page
Values 1 to -Э- ------------- -
About A Winning Application L \
© Copyright Ivor Horton
Puc. 21.28. Окно сообщения, отображаемое в результате выбора элемент
та меню Help^About
Обработка щелчка на кнопке
Щелчок на кнопке должен изменять отображаемое на кнопке значение на новое слу-
чайное значение. Естественно, оно должно также отличаться от значений, выводимых
на других кнопках, и от значения, отображавшегося на кнопке, на которой выполнялся
щелчок. Целесообразно представлять весь набор значений в упорядоченном виде; это
может привести к отображению нового значения на другой кнопке, но, скорее всего,
это было бы предпочтительнее вывода значений в неупорядоченном виде.
Процесс обработки щелчка на кнопке будет одинаковым для всех кнопок, поэтому
объем кода можно сократить, создав для выполнения этой задачи обобщенную функ-
цию. Определите приватную функцию-член класса Forml, которая будет генерировать
новое значение для данного объекта Button из массива кнопок.
//Генерирует для кнопки новое значение, отличающееся от текущих значении кнопок
void SetNewValue(ButtonА button, array<ButtonA>A buttons,
int lowerLimit, int upperLimit)
int index =0; // Индекс кнопки в массиве кнопок
// Массив для хранения значении кнопок
array<int>A values = gcnew array<int>(buttons->Length);
// Извлечение значении из массива кнопок и выяснение индекса кнопки
for (int i = 0 ; i < values->Length ; i++)
(
values[i] = Int32::Parse(buttons[i]->Text); // Извлечение текущего
// значения кнопки
// Если текущий дескриптор совпадает с дескриптором кнопки,
// сохранение значения индекса
if(button == buttons [i])
index = i;
int newValue =0; // Сохранение нового значения кнопки
11 Проверка его отличия от значений других кнопок
for(;;) // Выполнение цикла до получения подходящего значения
newValue = random->Next(lowerLimit, upperLimit); // Генерация значения
if(IsValid(newValue, values, values->Length)) // Если оно OK...
break; // ... завершение цикла
values[index] = newValue; // Сохранение нового значения в элементе
// с соответсвующим индексом
Array::Sort(values);
for (int i = 0 ; i < values->Length ; i++)
buttons [i]->Text = values [i] .ToStringO;
// Сортировка значений
// и установка значений
// как текста,
// отображаемого на кнопках
Два первых параметра функции — кнопка, которой должно быть присвоено но-
вое значение, и массив кнопок в группе, к которой принадлежит первая кнопка. Два
следующих параметра указывают верхнее и нижнее предельные значения. В первом
цикле текущие значения кнопок сохраняются в массиве значений values. В этом же
цикле выясняется значение индекса в массиве buttons дескриптора Button74, пере-
данного в первом аргументе. Оно требуется для выяснения того, какой элемент мас-
сива values должен быть заменен новым значением.
Новое значение создается в неопределенном цикле for. Этот же механизм при-
менялся для создания первоначальных значений кнопок. После получения нового
допустимого значения оно сохраняется в массиве values. Затем, прежде чем их со-
хранять в качестве значений свойств Text кнопок массива buttons, осуществляется
сортировка элементов в массиве values. Эту функцию можно будет использовать для
обработки событий Click всех кнопок.
Если вы еще не сделали этого, дважды щелкните на первой кнопке вкладки Lotto,
чтобы сгенерировать для нее функцию обработчика. Имя функции обработчика можно
изменить, открывая окно Properties кнопки, щелкая на кнопке Events (События) и из-
меняя значение для события Click. После нажатия клавиши <Enter> код будет обнов-
лен новым значением. В данном случае это значение изменено на lottoValue_Click.
1076 Глава 21
Обработчик события Click потребуется изменить, чтобы он вызывал ранее добав-
ленную в класс Forml функцию SetNewValue ():
System::Void lottoValue_Click(System::ObjectA sender,
ButtonA button = safe_cast<ButtonA>(sender);
System::EventArgsA e)
// Создание массива дескрипторов кнопок
array<ButtonA>A buttons = {lottoValuel, lottoValue2, lottoValue3,
lottoValue4, lottoValue5, lottoValue6};
// Изменение значения, отображаемого на кнопке
SetNewValue(button, buttons, lottoUserMinimum, lottoUserMaximum);
Доступность функции SetNewValue () существенно упрощает написание функ-
ции обработчика. Первый оператор сохраняет дескриптор кнопки, на которой был
выполнен щелчок. Первый параметр обработчика события — дескриптор объекта,
генерирующего события, поэтому необходимо лишь привести его к соответствующе-
му типу. Затем нужно просто собрать дескрипторы кнопок в массив и вызвать новую
функцию — дело сделано!
Нужно еще внести изменения в обработчики событий Click остальных кнопок
вкладки Lotto, но для этого не потребуется вносить изменения непосредственно в
какой-то код. Откройте окно Properties для второй кнопки и
Events. Щелчок на значении события Click открывает список существующих обра-
[елкните на кнопке
ботчиков событий, как показано на рис. 21.29. Если выбрать из списка обработчик
lottoValue__Click, обработчик события первой кнопки будет зарегистрирован так-
же в качестве обработчика события второй кнопки.
Properties Д X
lotto Value 2 System.Wmdows.Forms.Button
El Action
Click
M о u se Ca ptu re Cha nge d
lottoValueClick
playMenu!tem_Click
upperMenu!tem_Click
Mo use Click
Б Appearance
Paint
El Behavior
ChangeUICues
ControlAdded
Contra I Re moved
HelpRequested
Que ryAccessi b i I ity H e I p
Style Changed
SystemColorsChanged
El Data
El (DataBindings)
El Drag Drop
DragDrop
DragEnter
DragLeave
DragOver
GiveFeedback
QueryContinueDrag
E Focus
lowerMenuItem_Click
a bo utMe n uItem_Cl ick
chooseVa I ue_Cl ick
reset Me n ulte m_Cl ick
otto Value Click
Enter __________________________
Click
Occurs when the component is clicked
Рис» 21.29. Список существующих обработчиков событий для Click
Приложения, использующие средства Windows Forms 1077
Этот процесс можно повторить для остальных четырех кнопок вкладки Lotto, что-
бы один и тот же обработчик события вызывался в ответ на событие Click для лю-
бой из упомянутых кнопок.
Обработчики события Click кнопок вкладки Euromillions будут очень простыми.
Дважды щелкните на первой из пяти кнопок в группе Values (Значения), чтобы соз-
дать обработчик события. Откройте окно Properties для кнопки и измените значение
события Click на euroValue Click. Затем измените код обработчика, как показано
ниже.
System::Void euroValue__Click(System::0bjectA sender, System::EventArgsA e)
ButtonA button = safe_cast<ButtonA>(sender);
array<ButtonA>A buttons = {euroValuel, euroValue2, euroValue3,
euroValue4, euroValue5 };
SetNewValue(button, buttons, euroUserMinimum, euroUserMaximum);
Этот обработчик работает практически аналогично обработчику кнопок вклад-
ки Lotto. Массив содержит дескрипторы пяти кнопок группы значений, а функция
SetNewValue () выполняет все остальные необходимые действия. Если открыть окно
Properties для каждой из остальных четырех кнопок группы, эту функцию можно вы-
брать в качестве их функции обработчика события Click. Убедитесь, что выбираете
функцию euroValue_Click, а не lottoValue_Click!
Повторите эту же процедуру для кнопок Stars (Звездные) на вкладке Euromillions.
Их обработчик реализуется следующим образом:
System::Void euroStar__Click(System::ObjectА sender, System::EventArgsA e)
ButtonA button = safe_cast<ButtonA>(sender);
array<ButtonA>A buttons = { euroStarl, euroStar2 };
SetNewValue(button, buttons, euroStarsUserMinimum, euroStarsUserMaximum);
Присвойте обработчику события Click второй кнопки имя euro_StarClick, и за-
дача будет решена. После повторной компиляции программы вы должны получить
возможность генерировать новое значение для любой кнопки на любой из вкладок,
просто щелкая на кнопке. Для завершения примера осталось только предоставить
пользователю возможность вводить значения для кнопок.
Реакция на щелчок в контекстном меню
Щелчок на кнопке правой кнопкой мыши вызывает контекстное меню с един-
ственным пунктом Choose (Выбрать). Когда пользователь выбирает этот элемент,
программа должна отобразить диалоговое окно, позволяющее вводить соответству-
ющее значение. Щелкните на имени контекстного меню на вкладке Design класса
Forml, а затем дважды щелкните на элементе меню Choose, чтобы создать обработ-
чик события Click.
Первая проблема, которую потребуется решить — определить группу кнопок, щел-
чок на которых вызвал событие. Каждая группа кнопок находится в собственном
элементе управления GroupBox, а класс GroupBox имеет свойство Controls, кото-
рое возвращает ссылку на объект типа Control::Controlcollection, представляю-
щий собой коллекцию элементов управления в групповой рамке. В классе Control::
Controlcollection определена функция Contains (), которая возвращает значение
t rue, если переданный в качестве аргумента элемент управления входит в состав кол-
1078 Глава 21
лекции, и значение false — в противном случае. Таким образом, мы располагаем воз-
можностью определения того, к какой группе кнопок принадлежит кнопка, сгенери-
ровавшая событие Click. В общих чертах реализация обработчика события выглядит
так, как показано ниже.
System::Void chooseValue_Click(System::ObjectА sender, System::EventArgsA e)
// Извлечение кнопки, щелчок на которой вызвал контекстное меню; затем...
if(lottoValues->Controls->Contains(theButton))
11 кнопка принадлежит группе lotto...
else if(euroValues->Controls->Contains(theButton))
// кнопка принадлежит группе Values...
else if(euroStars->Controls->Contains(theButton))
// кнопка принадлежит группе Stars...
Этот код позволяет определить, какая группа кнопок была задействована. По край-
ней мере, в принципе. Однако остается нерешенной еще одна небольшая проблема:
как выяснить, на какой именно кнопке был выполнен щелчок правой кнопкой мыши
для открытия контекстного меню?
Обработчик chooseValue Click () вызывается при щелчке на элементе меню
Choose, поэтому параметр sender обработчика определяет элемент меню, а не кноп-
ку. Нам требуется обработчик, отвечающий на первоначальный щелчок на кнопке, и
его можно создать, дважды щелкнув на buttonContextMenu в панели Design для клас-
са Forml. Созданную функцию обработчика можно завершить следующим образом:
System: :Void buttonContextMenu_Opening(System::ObjectА sender,
System::ComponentModel::CancelEventArgsA e)
contextButton = safe_cast<ButtonA>(buttonContextMenu->SourceControl);
Это приводит sender к типу ButtonA и сохраняет его в члене contextButton
класса Forml. Поскольку в данном случае обработчик предназначен для контекстного
меню, параметр sender указывает компонент, на котором был выполнен щелчок, для
его отображения. Конечно, переменную contextButton нужно также добавить в ка-
честве приватного члена класса Forml:
private:
ButtonА contextButton; // Кнопка, на которой был выполнен щелчок
// правой кнопкой для вызова контекстного меню
Теперь осталось только определить необходимые последующие действия.
Логика обработки элемента меню Choose
Процесс обработки щелчка на элементе меню Choose может оставаться неизмен-
ным, независимо от задействованной группы кнопок, и сводится к выполнению опи-
санных ниже действий.
Приложения, использующие средства Windows Forms 1079
1. Отображение диалогового окна для ввода значения.
Проверка допустимости введенного значения — то есть того, что оно находится
в допустимом диапазоне и отличается от значений остальных кнопок.
можности повторного ввода значения или закрытие диалогового окна.
4. Если значение допустимо, обновление соответствующей кнопки новым значе-
нием.
Первый шаг по реализации этого процесса — создание новой диалоговой формы.
Создание диалоговой формы
Нажмите комбинацию клавиш <Ctrl+Shift+A>, чтобы отрыть диалоговое окно
Add New Item. Затем выберите категорию UI и шаблон Windows Form. Введите имя
UserValueDialog и щелкните на кнопке Add. Теперь, нажимая клавишу <F4>, можно
открыть окно Properties для формы и установить ее свойства так, чтобы она стала
диалоговым окном. Выберите False для значений свойств ControlBox, MinimizeBox
и MaximizeBox, и значение "User Value Input" (“Ввод значения пользователем”) в
качестве значения свойства Text.
Добавьте в диалоговую форму кнопки ОК и Cancel, а также элементы управления
Label и TextBox, как показано на рис. 21.30.
Рис. 21.30. Построение диалогового
окна UserValueDialog
В качестве значений свойств Text, (Name) и DialogResult кнопки ОК укажите
ОК, а в качестве значений Text, (Name) и DialogResult кнопки Cancel — Cancel. Для
значения свойства (Name) элемента управления TextBox выберите textbox, а свой-
ства Text Align (Выравнивание текста) — Center (По центру). Для свойства (Name)
элемента управления Label определите label, а значением свойства Text может
быть любой текст, поскольку мы будем его изменять в коде в соответствии с конкрет-
ной ситуацией.
Теперь можно еще раз открыть свойства диалоговой формы и установить значе-
ния свойств AceptButton и CancelButton равными, соответственно, ОК и Cancel.
1080 Глава 21
Разработка класса диалогового окна
Значение, введенное в элементе управления TextBox, должно быть доступным для
объекта Forml, поэтому в класс UserValueDialog потребуется добавить свойство для
его хранения:
public:
property int Value;
Это свойство — пример простейшего скалярного свойства, поэтому для него под-
ходят функции get () и set (), предлагаемые по умолчанию.
Объекту диалогового окна необходима информация о предельных значениях, по-
скольку обработчик кнопки ОК в классе диалогового окна проверяет допустимость
значения. По этой же причине объекту диалогового окна требуется информация о
текущих значениях, отображаемых на кнопках для предотвращения их дублирования.
Для хранения данных в класс UserValueDialog можно добавить следующие три чле-
на свойств:
public:
property int LowerLimit;
property int UpperLimit;
property array<int>A Values; // Текущие значения кнопок
Объект Forml должен иметь возможность изменять значение свойства Text эле-
мента управления label в зависимости от действующих предельных значений кноп-
ки при открытии диалогового окна. Для этого в класс UserValueDialog следует до-
бавить общедоступную функцию-член:
public:
void SetLabelText(int lower, int upper)
label->Text = L"Enter your value between " + lower +L" and ” + upper;
Предельные значения можно было бы выбирать из свойств объекта диалогового
окна, но для этого требовалось бы всегда вначале устанавливать значения свойств.
Использование параметров для определения предельных значений устраняет эту за-
висимость.
Объект диалогового окна можно создать в конструкторе класса Forml, но в этот
класс придется добавить приватный член для хранения дескриптора:
private: UserValueDialogA UserValueDialog;
Кроме того, в файл заголовка Forml. h нужно добавить директиву #include для
включения файла UserDialog.h.
Добавление к конструктору приведенной ниже строки создает объект диалогового
окна:
UserValueDialog = gcnew UserValueDialog;
Двойной щелчок на кнопке ОК в форме UserValueDialog создаст для кнопки об-
работчик события Click. Эта функция извлекает значение, введенное в элементе
управления TextBox, и проверяет его нахождение между предельными значениями
и отличие от текущего набора значений. Если по любой причине значение не допу-
стимо, функция отображает окно сообщения. Ее можно реализовать следующим об-
разом:
Приложения, использующие средства Windows Forms 1081
System::Void OK_Click(System::ObjectA sender, System::EventArgsA e)
::DialogResult result; // Сохраняет значение, возвращенное функцией Show()
if(String::IsNullOrEmpty(textBox->Text)) // Проверяет ввод нулевой или
11 пустой строки
result = MessageBox::Show(this,
L”No input - enter a value.’’,
L"Input Error’’,
MessageBoxButtons::RetryCancel,
MessageBoxIcon::Error);
result = MessageBox::Show(this,
Ь’’Ввод отсутствует — введите значение.’’,
L"Ошибка ввода”,
MessageBoxButtons::RetryCancel,
MessageBoxIcon::Error);
if (result == ::DialogResult::Retry) // Если нажата кнопка Retry (Повтор),
DialogResult = ::DialogResult::None;// ...предотвращение закрытия
/ / диалогового окна...
else // .. .в противном случае...
DialogResult = ::DialogResult::Cancel;//...закрытие диалогового окна,
return;
int value = Int32::Parse(textBox->Text); // Извлечение значения,
// введенного в текстовом поле
bool valid = true; // Индикатор допустимости ввода
for each(int n in Values) // Сравнение введенного значения
//с текущими значениями
if (value == n) // Если оно совпадает с ними.. .
valid = false; // ...оно не допустимо.
break; // Выход из цикла
// Проверка предельных значений и результата предыдущей проверки
// допустимости значения
if(!valid ) ) value < LowerLimit | ) value > UpperLimit)
result = MessageBox::Show(this,
L’’Input not valid." +
L"Value must be from ’’ + LowerLimit +
L" to ’’ + UpperLimit +
L’’\nand must be different from existing values.’’,
L”Input Error",
MessageBoxButtons::RetryCancel,
MessageBoxIcon::Error);
result = MessageBox::Show(this,
L"Недопустимый ввод." +
L"Значение должно быть от ’’ + LowerLimit +
L" до ’’ + UpperLimit +
L"\nn должно отличаться от существующего.’’,
Е’’0шибка ввода",
MessageBoxButtons::RetryCancel,
MessageBoxIcon::Error);
1082 Глава 21
if(result == ::DialogResult::Retry)
DialogResult = ::DialogResult::None;
else
DialogResult = ::DialogResult::Cancel;
else
Value = value; // Сохранение введенного значения в свойстве
Окно сообщения отображается, если значение свойства Text текстового поля
является нулевым или пустой строкой. Оно отображает сообщение об ошибке и со-
держит кнопки Retry (Повтор) и Cancel (Отмена) вместо кнопок ОК й Cancel, обе-
спечивая возможность изменения введенного значения. Щелчок на кнопке Retry
означает, что пользователь желает повторить ввод, поэтому мы предотвращаем
закрытие диалогового окна, устанавливая его свойство DialogResult равным : :
DialogResult: :None. Единственная другая возможность — щелчок на кнопке Cancel
окна сообщения, при котором значение свойства DialogResult устанавливается рав-
ным : : DialogResult:: Cancel, что равносильно щелчку на кнопке Cancel диалого-
вого окна.
Свойство Text элемента управления TextBox возвращает дескриптор типа
String*. Его можно преобразовать в целочисленный, передавая дескриптор статиче-
ской функции Parse () класса Int32. Значение, полученное из текстового поля, срав-
нивается с элементами массива values, который представляет текущий набор значе-
ний кнопок. Новое значение должно отличаться от всех существующих, поэтому при
обнаружении любого совпадения значение переменной valid устанавливается рав-
ным false и выполняется выход из цикла.
В условии оператора if, следующего за каждым циклом for, значение сравни-
вается с предельными значениями и проверяется текущее значение переменной
valid. Если значение любого из этих выражений равно false, значение всего усло-
вия— false, и выводится окно сообщения. Оно работает аналогично предыдущему,
отображает сообщение об ошибке и содержит кнопки Retry и Cancel. Если значение
действительно допустимо, мы сохраняем его в свойстве Value объекта диалогового
окна, готовым для извлечения обработчиком события объекта Forml, который запу-
стил весь процесс.
Обработка события Click элемента меню Choose
Теперь можно завершить скелет функции обработчика chooseValue_Click (),
используя возможности, добавленные в класс UserValueDialog. Дескриптор кноп-
ки, на которой был выполнен щелчок правой кнопкой мыши, уже хранится в чле-
не contextButton, поскольку вначале выполняется добавленный ранее обработчик
buttonContextMenu_Opening().
System::Void chooseValue_Click(System::Object* sender, System: :EventArgs* e)
array<int>* values; // Массив для хранения текущих значений кнопок
array<Button*>* theButtons; // Дескриптор массива кнопок
// Проверка, принадлежит ли кнопка групповой рамке lottoValues
if(lottoValues->Controls->Contains(contextButton))
11 Кнопка относится к группе lotto...
array<Button*>* buttons = {lottoValuel, lottoValue2, lottoValue3,
lottoValue4, lottoValue5, lottoValue6};
Приложения, использующие средства Windows Forms 1083
theButtons = buttons; // Сохранение дескриптора массива во
// внешней области определения
values = GetButtonValues (buttons); // Извлечение значений массива кнопок
// Подготовка диалогового окна к отображению
UserValueDialog->Values = values = GetButtonValues(buttons);
userValueDialog->LowerLimit = lottoUserMinimum;
userValueDialog-XJpperLimit = lottoUserMaximum;
userValueDialog->SetLabelText(lottoUserMinimum, lottoUserMaximum);
// Проверка, принадлежит ли кнопка групповой рамке euroValues
else if(euroValues->Controls->Contains(contextButton))
// Кнопка относится к группе Values...
array<ButtonA>A buttons = {euroValuel, euroValue2, euroValue3,
euroValue4, euroValue5};
theButtons = buttons; // Сохранение дескриптора массива во
// внешней области определения
values = GetButtonValues (buttons); // Извлечение значений массива кнопок
// Подготовка диалогового окна к отображению
userValueDialog->Values = values;
userValueDialog->LowerLimit = euroUserMinimum;
userValueDialog->UpperLimit = euroUserMaximum;
userValueDialog->SetLabelText(euroUserMinimum, euroUserMaximum);
// Проверка, принадлежит ли кнопка групповой рамке euroStars
else if(euroStars->Controls->Contains(contextButton))
11 Кнопка относится к группе Stars...
array<ButtonA>A buttons = { euroStarl, euroStar2 };
theButtons = buttons; 11 Сохранение дескриптора массива
// во внешней области определения
values = GetButtonValues (buttons); // Извлечение значений массива кнопок
// Подготовка диалогового окна к отображению
userValueDialog->Values = values;
userValueDialog->LowerLimit = euroStarsUserMinimum;
userValueDialog->UpperLimit = euroStarsUserMaximum;
userValueDialog->SetLabelText(euroStarsUserMinimum, euroStarsUserMaximum);
11 Отображение диалогового окна
if(userValueDialog->ShowDialog(this) == ::DialogResult::OK)
// Определение значения кнопки, которое нужно заменить
for(int i = 0 ; i<theButtons->Length ; i++)
if(contextButton == theButtons[i])
values[i] = userValueDialog->Value;
break;
Array::Sort(values); // Сортировка значений
// Установка значений всех кнопок
for(int i = 0 ; i<theButtons->Length ; i++)
theButtons[i]->Text = values[i].ToString();
1084 Глава 21
Вначале определяются две переменных типа массива: одна для хранения кнопок, а
вторая — для хранения значений кнопок. Эти переменные должны быть определены
именно здесь, поскольку массивы создаются внутри блока либо одного, либо другого
оператора if, и к ним требуется доступ извне блоков if.
Первые три оператора if определяют групповую рамку, содержащую кнопку, на
которой был выполнен щелчок правой кнопкой мыши для открытия контекстного
меню. Процесс, выполняемый внутри всех трех блоков if, по сути, одинаков, но
создаваемые при этом массивы будут различными. Массив кнопок создается из пере-
менных, содержащих дескрипторы того набора кнопок, к которому принадлежит
contextButton. Затем дескриптор массива сохраняется в члене theButtons, что
делает его доступным во внешней области. Затем вызывается функция, которую еще
предстоит добавить, GetButtonValues (), возвращающая массив целочисленных
значений кнопок. И, наконец, в блоке if устанавливаются значения трех свойств
объекта диалогового окна, после чего вызывается его функция SetLabelText () для
определения текста надписи в соответствии с предельными значениями. Дескриптор
contextButton должен принадлежать одной из трех групповых рамок, поскольку
контекстное меню доступно только для этих кнопок.
После выполнения того или иного блока if мы отображаем диалоговое окно, вы-
зывая его функцию ShowDialog () в соответствии с условием четвертого оператора
if. Если функция ShowDialog () возвращает значение : :DialogResult: :ОК, выпол-
няется код блока if. Вначале, сравнивая дескриптор contextButton с дескрипто-
рами, хранящимися в массиве theButtons, код определяет, значение какой кнопки
должно быть заменено. Как только соответствие найдено, соответствующий элемент
в массиве values заменяется новым значением и осуществляется выход из цикла.
После сохранения значений обновляются значения свойств Text для каждой кнопки
массива theButtons, и на этом действие кода завершается.
Реализация функции GetButtonValues () в классе Forml выглядит следующим об-
разом:
// Создание массива значений кнопок на основе массива кнопок
array<int>A GetButtonValues(array<ButtonA>A buttons)
array<int>A values = gcnew array<int>(buttons->Length);
for (int i = 0 ; i<values->Length ; i++)
values[i] = Int32::Parse(buttons[i]->Text);
return values;
Эта функция создает массив целочисленных значений такой же длины, как и мас-
сив дескрипторов кнопок, переданный в качестве аргумента. Затем массив значений
заполняется int-эквивалентами строки, возвращенной свойствами Text кнопок, и
возвращается дескриптор массива значений.
Еще раз скомпилировав проект, вы должны получить полностью работоспособ-
ное приложение. Оно позволяет генерировать записи для различных лотерей, как с
ограниченными, так и не ограниченными значениями. В записи можно также гене-
рировать новые случайные отдельные значения или вводить собственные значения.
Это приложение всегда работало, но мне ни разу не удалось с его помощью выиграть.
Естественно, критерием успеха приложения является его работоспособность.
Приложения, использующие средства Windows Forms 1085
0
Резюме
В этой главе было построено приложение Windows Forms, использующее элемен-
ты управления, которые, скорее всего, потребуются в большинстве программ. Теперь
вам должно быть ясно, что программы Windows Forms предназначены исключитель-
но для использования возможностей, предлагаемых средством Form Design. Весь код
класса помещается в определение класса, поэтому класс очень сложной формы будет
содержать множество строк кода. В реальных приложениях код состоит из ряда боль-
ших, достаточно слабо структурированных классов, которые трудно изменять и под-
держивать на уровне кода. Поэтому всегда, когда это возможно, для внесения изме-
нений следует использовать средство Form Design и окно Properties, а в тех случаях,
когда необходим доступ в код для принятия нестандартных решений, следует приме-
нять представление Class View.
Ниже перечислены ключевые моменты, с которыми вы познакомились в настоя-
щей главе.
□ Окно приложения — это форма, а форма определяется классом, производным
от класса System: :Form.
□ Диалоговое окно — это форма, значение свойства FormBorderStyle которой
установлено равным FixedDialog.
□ Диалоговое окно может быть создано как модальное путем вызова его функции
ShowDialog () или как немодальное с помощью его функции Show ().
□ Возможностью закрытия диалогового окна можно управлять, устанавливая зна-
чение свойства DialogResult объекта диалогового окна.
□ Элемент управления ComboBox сочетает в себе возможности элементов управ-
ления ListBox и TextBox и позволяет выбирать элемент из списка либо вво-
дить новое значение с клавиатуры.
□ Элемент управления NumericUpDown допускает ввод числовых данных из дан-
ного диапазона с определенным шагом.
□ Определение обработчика события Click для элемента управления можно до-
бавить, дважды щелкая на элементе управления во вкладке Form Design.
□ Существующую функцию можно указать в качестве обработчика данного собы-
тия элемента управления в окне Properties. Щелчок на кнопке Events в окне
Properties отображает список доступных событий для данного элемента управ-
ления.
□ Имена автоматически сгенерированных членов класса следует изменять только
в окне Properties, а не непосредственно в редакторе кода.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Измените код Ех21_01 так, чтобы он отображал диалоговое окно, созданное в
виде формы, отображаемой при щелчке на элементе меню Help1^About.
1086 Глава 21
2. Измените код Ех21_01 так, чтобы диалоговое окно, отображаемое для элемен-
та контекстного меню Choose, вместо текстового поля использовало элемент
управления ListBox и отображало полный набор доступных для выбора допу-
стимых значений.
3. Исследуйте свойства и функции, доступные для элемента управления WebBrowser,
и измените код Ех21_01 так, чтобы элемент управления WebBrowser отображал
станицу, URL-адрес которой был введен в элементе управления TextBox.
22
Доступ к источникам
данных в приложении
Windows Forms
В этой главе мы исследуем разработку приложений, построенных на основе форм,
которые будут отображать данные, полученные из различных источников. В частно-
сти, будет рассмотрено создание программ, построенных на основе форм, обращаю-
щихся к существующей базе данных.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
□ Виды классов, участвующие в инкапсуляции источника данных.
□ Использование элемента управления DataGridView для отображения своих
данных.
□ Настройка внешнего вида элемента управления DataGridView.
□ Функции, выполняемые компонентом Bindingsource, и его использование со-
вместно с элементом DataGridView.
□ Использование элемента управления BindingNavigator для перемещения по
данным источника данных, управляемого элементом Bindingsource.
□ Ускорение обновления базы данных с помощью элемента управления
BindingNavigator и компонента Bindingsource.
Visual C++ 2005 предоставляет большие возможности автоматизации создания приложе-
ний на основе форм, обращающихся к источникам данных, но мы начнем создание прило-
жения без использования средств автоматизации, дабы вы смогли получить представление
о программной работе с компонентами. Этот подход позволяет не только получить доста-
точно полное представление о работе компонентов, но и оценить преимущества, предостав-
ляемые средствами автоматизации.
1088 Глава 22
Работа с источниками данных
Источник данных может быть любым из числа существующих в приложении.
Реляционные базы данных, обращающиеся к данным Web-службы и объекты — все
они могут быть источниками данных. При разработке приложения, которое будет
работать с существующим источником данных, обычно придется идентифициро-
вать этот источник данных внутри разрабатываемого проекта. Это можно сделать в
окне Data Sources (Источники данных), отображаемого при выборе пункта Data1^
Show Data Sources (Данные1^ Показать источники данных) главного меню или при
нажатии комбинации клавиш <Shift+Alt+D>.
Источники данных представляются объектом класса, поэтому добавление источ-
ника данных в проект неизбежно приводит к добавлению определений ряда классов.
В табл. 22.1 дано краткое описание этих классов.
Таблица 22.1. Классы, связанные с источниками данных
Класс
Источник данных
Таблицы базы данных
Столбцы таблицы
Строки таблицы
Описание
Источник данных определяется классом, производным от класса DataSet,
который определен в пространстве имен System: :Data. Этот класс инкап-
сулирует в кэше все данные базы, доступные в текущем проекте.
Каждая таблица базы данных определяется вложенным классом класса
DataSet, который представляет базу данных, а класс, определяющий табли-
цу, является производным от класса System: :Data: :DataTable. Классы,
представляющие таблицы, определяют также события, которые сигнализи-
руют об изменениях данных в таблице и ее свойствах, обеспечивая доступ к
каждому значению записи текущей базы данных.
Каждая таблица в источнике данных идентифицируется членом класса
DataSet, являющимся дескриптором соответствующего объекта DataTable.
Каждый столбец в таблице базы данных идентифицируется членом класса
DataTable, который определяет таблицу. Члены, представляющие столбцы,
имеют тип System:: Data:: DataColumn и определяют такие характеристи-
ки столбцов, как имя столбца и тип данных, хранящихся в нем. Эти характе-
ристики обобщенно называют схемой столбца.
Строка в таблице представлена объектом типа System:: Data:: DataRow.
Объект DataRow содержит данные строки и состоит из множества элемен-
тов, являющихся столбцами в объекте DataTable.
Понятно, что при работе с базой данных, содержащей несколько таблиц, каждая
из которых имеет ряд столбцов, объем кода, сгенерированного для представления ис-
точника данных, будет достаточно большим. И действительно, код, состоящий из де-
сятков тысяч строк — отнюдь не редкое явление в реальных приложениях.
Классы, перечисленные в предыдущей таблице, предназначены исключительно
для инкапсуляции данных, поступающих из источника. Они не предоставляют ме-
ханизм для подключения к такому источнику данных, как база, и доступа к данным
внутри него. Эту возможность обеспечивает класс компонентов, называемый адапте-
ром таблицы, который будет генерироваться автоматически. Адаптер таблицы (table
adapter) устанавливает соединение с базой данных и выполняет команды или SQL-
операторы, действующие по отношению к базе данных. Для каждого члена DataTable
объекта DataSet создается один класс адаптера таблицы, поэтому, если приложение
рассчитано на работу с тремя таблицами базы данных, в нем будут определены три
класса адаптеров таблиц. Объект адаптера таблицы заполняет член DataTable объек-
та DataSet данными и при необходимости может обновлять таблицу в базе данных.
Доступ к источникам данных в приложении Windows Forms 1089
Доступ и отображение данных
В пространстве имен System::Windows::Forms определены три компонента, ко-
торые все вместе предназначены для доступа к данным и их отображения в приложе-
нии Windows Forms (табл. 22.2).
Таблица 22.2. Компоненты пространства имен System: : Windows::Forms
для доступа и отображения данных
Компонент
Описание
DataGridView
Bindingsource
BindingNavigator
Этот элемент управления может отображать буквально любой вид данных в
ячейках прямоугольной сетки. Этот элемент управления можно использовать в
значительной мере независимо от остальных двух компонентов.
Этот компонент служит для инкапсуляции данных, полученных из источника дан-
ных. Он позволяет управлять доступом к источнику данных и его обновлением, а
также может использоваться в качестве механизма отображения данных в эле-
менте управления DataGridView.
Этот элемент управления предоставляет панель управления, содержащую эле-
менты управления для навигации по данным в источнике — как правило, в ис-
точнике данных, инкапсулированном в элементе управления Bindingsource —
и манипулирования ими.
Компонент Bindingsource не является элементом управления, поскольку не
имеет графического представления, с которым пользователь может взаимодей-
ствовать, однако он призван дополнять и использоваться в приложении базы дан-
ных совместно с элементами управления DataGridView и BindingNavigator.
Компонент Bindingsource обеспечивает обмен информацией с источником дан-
ных, необходимый для выполнения запросов и команд обновления, элемент управ-
ления DataGridView предлагает интерфейс пользователя для просмотра и ввода
данных, а элемент управления BindingNavigator предоставляет панель инструмен-
тов, которая упрощает навигацию по данным. Применение элемента управления
BindingNavigator не обязательно. При желании записи можно изменять программ-
но, вручную.
Хотя эти три компонента спроектированы для совместного использования, эле-
мент управления DataGridView — особенно полезный инструмент и сам по себе,
поскольку его можно применять в значительной степени независимо от остальных
двух компонентов. Он обеспечивает поразительно широкие возможности изменения
внешнего вида сетки, отображающей данные. Прежде чем приступать к изучению
возможных способов использования этого элемента управления в сочетании с компо-
нентами Bindingsource и BindingNavigator, рассмотрим некоторые способы его
настройки.
Обратите внимание, что для получения доступа к источнику данных можно применять
также элементы управления SqlConnection, Sql Data Adapt er и DataSet. Если жела-
ете использовать эти элементы управления, вам, вероятно, придется добавить их в панель
инструментов ToolBox. Для этого необходимо выбрать пункт Tools^ Choose ToolBox
Items (Инструменты^ Выбрать элементы панели инструментов) главного меню и в списке
установить флажки напротив тех элементов управления, которые требуется добавить в
панель ToolBox.
1090 Глава 22
Использование элемента
управления DataGridView
Элемент управления DataGridView позволяет отображать и изменять прямоу-
гольный массив данных из множества различных источников данных. Его можно ис-
пользовать также для отображения практически любых данных, созданных непосред-
ственно в программе. По своей сущности это сложный элемент управления, который
обеспечивает огромную гибкость при его применении, и преимуществами множества
его функциональных возможностей можно воспользоваться через множество свойств,
функций и событий. В то же время применение элемента управления DataGridView
может быть поразительно простым. Можно не обращать внимания на внутреннюю
сложность и использовать его посредством инструмента Form Design (Конструктор
форм), которое берет на себя заботу обо всех основных характеристиках. Позднее в
этой главе будет показано, как создать завершенный пример программы для доступа к
базе данных North wind, вообще не прибегая к программированию вручную. Вся про-
грамма будет сгенерирована с помощью Form Design и путем определения свойств
компонентов, используемых в проекте.
Данные элемента управления DataGridView отображаются в прямоугольном
массиве ячеек, которые можно представить в виде коллекции строк или столбцов.
Каждый столбец ячеек имеет в верхней части ячейку заголовка, которая, как прави-
ло, содержит идентифицирующий этот столбец текст, а в начале каждой строки рас-
положена ячейка заголовка строки, как показано на рис. 22.1.
DataGridView"4 gridCntrl = gcnew DataGridView; // Создает элемент управления
Заголовки
строк
gridCntrl->Columns[3]
ссылка на отдельный столбец
Заголовки столбцов
gridCntrl->ColumnCount
количество столбцов
gridCntrl->Rows[2]
ссылка на
отдельную _
строку
gridCntrl->RowCount
количество г*
строк
ColumnO Column 1 L L Column2 Column3 Column4 1
RowO
Row1
Row2
Row3
gridCntrl->
Rows
ссылка на
коллекцию
строк
gridCntrl->Columns
ссылка на коллекцию
столбцов
gridCntrl - > Rows [2] >Се I Is [3]
ссылка на 4-ю ячейку
в 3-й строке
Рис. 22.1. Отображение данных в элементе управления DataGridView
Обращение к строкам и столбцам ячеек выполняется через свойства объекта
DataGridView. Свойство Rows возвращает значение типа DataGridRowCollection,
представляющее коллекцию всех строк, а обращение к конкретной строке выпол-
Доступ к источникам данных в приложении Windows Forms 1091
няется с помощью индекса, что можно видеть на рис. 22.1. Аналогично свойство
Columns элемента управления возвращает значение типа DataGridViewColumnColl
ection, которое также можно индексировать для обращения к конкретному столбцу.
Индексация строк и столбцов осуществляется, начиная с нуля. Свойство Cells объ-
екта DataGridRowCollection представляет коллекцию, содержащую ячейки строки,
и это свойство можно индексировать для получения доступа к конкретной ячейке в
строке. Пример ссылки на четвертую ячейку в третьей строке приведен на рис. 22.1.
Количество строк доступно как значение свойства RowCount, а свойство
ColumnCount возвращает количество столбцов. Вначале, когда элемент управления
еще не связан с источником данных, он не будет содержать ни строк, ни столбцов.
Количество столбцов и/или строк можно определить, устанавливая значения свойств
элемента управления, но при его использовании для отображения данных из источ-
ника данных это действие выполняется автоматически.
Элемент управления DataGridView применяется в трех различных режимах, кото-
рые описаны в табл. 22.3.
Таблица 22.3. Режимы элемента управления DataGridView
Режим Описание
Несвязанный режим
Связанный режим
Виртуальный режим
В несвязанном режиме передача данных элементу управления выполняется вруч-
ную, как правило, с помощью функции Add () применительно к свойству Rows
элемента управления. Этот режим следует использовать для отображения срав-
нительно небольших объемов данных.
В этом режиме посредством установки значения свойства Datasource элемента
управления необходимо указать источник предназначенных для отображения данных.
В виртуальном режиме элемент управления связан с кэш-памятью, заполняемой
данными из отдельного источника данных. Этот режим следует использовать для
отображения данных из источника, в котором в целях оптимизации производи-
тельности требуется управлять доступом к данным.
В несвязанном режиме элемент управления DataGridView можно использовать
для отображения в приложении любых данных, которые могут быть отображены в
табличном виде. Потому этот инструмент очень удобен для отображения данных во
множестве разнообразных приложений. В следующем разделе использование этого
элемента управления в несвязанном режиме рассматривается несколько подробнее.
Использование элемента управления
DataGridView в несвязанном режиме
Данные в элементе управления DataGridView хранятся в прямоугольной структу-
ре, определяемой свойствами Rows и Columns элемента управления. В несвязанном
режиме добавление данных в элемент управления выполняется с помощью функции
Add () применительно к свойству Rows. Но прежде чем в элемент управления мож-
но будет добавлять строки, потребуется определить столбцы — по меньшей мере, за-
дать количество элементов в строке. Программная установка свойства ColumnCount
элемента управления определяет количество столбцов и указывает, что элемент
управления будет работать в несвязанном режиме. Следующие операторы создают
элемент управления, обращение к которому выполняется посредством дескриптора
DataGridView, а затем устанавливают количество столбцов равным 3:
1092 Глава 22
DataGridView* dataGridView = dcnew DataGridView;
DataGridView->ColumnCount =3; // Устанавливает количество столбцов
При желании столбцы в элементе управления можно пометить путем установки
свойства Name для каждого столбца, указывая заголовки, идентифицирующие данные
в каждом из них. Это можно было бы выполнить так:
dataGridView->Columns [0] ->Name L"Name";
dataGridView->Columns [1] ->Name L’’Phone Number";
dataGridView->Columns[2]->Name L"Address";
Свойство Columns элемента управления — индексированное свойство, поэтому
доступ к отдельным столбцам можно получить с помощью значений индексов, начи-
нающихся с 0. Таким образом, эти три оператора снабжают метками три столбца в
элементе управления DataGridView. Заголовки столбцов можно определить также
в окне Properties (Свойства) элемента управления, как будет показано в следующем
действующем примере.
Свойство Rows возвращает значение, представляющее собой коллекцию типа
DataGridViewRowCollection, который определен в пространстве имен System: :
Windows : : Forms. Свойство Count упомянутой коллекции возвращает количество
строк. Кроме того, коллекция имеет также свойство начальной индексации, возвра-
щающее строку в позиции данного индекса. Коллекция строк обладает множеством
функций. Все они описываться здесь не будут, а лишь некоторые наиболее полезные,
предназначенные для добавления и удаления строк (табл. 22.4).
Таблица 22.4. Свойства для добавления и удаления строк коллекции
DataGr idV iewRowCollec tion
Функция
Описание
Add ()
Insert()
Clear ()
AddCopy()
Insertcopy()
Remove()
RemoveAt()
Добавляет в коллекцию одну или более строк.
Вставляет в коллекцию одну или более строк.
Удаляет все строки коллекции.
Добавляет копию строки, указанной в аргументе.
Вставляет копию строки, указанную первым аргументом, в позицию, которая
задана вторым аргументом.
Удаляет строку, указанную аргументом типа DataGridviewRow*.
Удаляет строку, указанную значением индекса, которое передано в качестве
аргумента.
Функция Add (), применяемая к значению, возвращенному свойством Rows, суще-
ствует в виде четырех перегруженных версий, которые позволяют добавлять в эле-
мент управления строку данных рядом различных способов (табл. 22.5).
Все версии функции Add () возвращают значение типа int, представляющее со-
бой индекс последней добавленной в коллекцию строки. Если значение свойства
SataSource элемента управления DataGridView не нулевое, или если элемент управ-
ления не содержит столбцов, все версии функции Add () генерируют исключение
типа System::InvalidOperationException.
Доступ к источникам данных в приложении Windows Forms 1093
Таблица 22.5. Перегруженные версии функции Add () коллекции
DataGridViewRowCollection
Функция Описание
Add () Добавляет в коллекцию одну новую строку.
Add(int rowCount) Добавляет в коллекцию rowcount новых строк. Если значение rowcount нулевое или отрицательное, функция гене- рирует исключение типа System: ’ArgumentOutOfRangeException.
Add(DataGridViewRow* row) Добавляет строку, указанную аргументом. Объект DataGridViewRow содержит коллекцию ячеек строки, а также параметры, которые опре- деляют внешний вид ячеек в строке.
Add(... Object* object) Добавляет новую строку и заполняет ячейки строки объектами, ука- занными аргументами.
Добавить новые строки в элемент управления dataGridView, содержащий три
столбца, можно было бы с помощью следующих операторов:
dataGridView->Rows->Add(L”Fred Abie", L”914 696 1200",
L"1235 First Street, AnyTown’’);
dataGridView->Rows->Add(L"May East", L"914 696 1399",
L"1246 First Street, AnyTown");
Каждый из этих операторов добавляет в коллекцию новую строку, а три аргумента
функции Add () соответствуют трем столбцам элемента управления. Элемент управле-
ния должен содержать достаточное количество столбцов, чтобы уместить все элемен-
ты, добавляемые в строку. При попытке добавления в строку элементов, количество
которых превышает количество столбцов в элементе управления, лишние элементы
игнорируются.
Вначале мы посмотрим, как несвязанный режим работает в примере, в котором
определение элемента управления DataGridView выполняется путем установки его
свойств на вкладке Form Design.
Практическое занятие
Элемент управления DataGridView
в несвязанном режиме
В этом примере отображается список книг, в котором каждая книга определена сво-
им номером ISBN, заглавием, автором и издателем. Создайте новый проект Windows
Form по имени Ех22_01. Добавьте в форму элемент управления DataGridView и щел-
кните на стрелке в верхнем правом углу элемента управления, чтобы открыть всплы-
вающее меню DataGridView Tasks (Задачи DataGridView), показанное на рис. 22.2.
Если щелкнуть на нижнем элементе меню — Dock in parent container (Пристыковать
к родительскому контейнеру) — элемент управления заполнит клиентскую область
формы. Верхний элемент меню предназначен для выбора источника данных, но в
данном случае мы не собираемся его указывать. Если щелкнуть на элементе меню Add
Column (Добавить столбец), откроется диалоговое окно Add Column (Добавление
столбца) ввода столбцов в элемент управления, показанное на рис. 22.3.
Переключатель Unbound column (Освободить столбец) выбран, поскольку ника-
кой источник данных не указан для элемента управления — в данном примере так и
должно быть.
1094 Глава 22
Рис. 22.2. Меню DataGridView Tasks
Add Column
Рис. 22.3. Диалоговое окно Add Column
Запись Name: (Имя:) — это значение свойства Name столбца, a Header text: (Текст
заголовка:) — значение свойства Header Text, которое соответствует тексту, отобража-
емому в элементе управления в качестве заголовка столбца. Если развернуть список,
предназначенный для выбора значения Туре: (Тип:), отобразится набор возможных
вариантов выбора типа столбца. В данном случае необходимо оставить значение, вы-
бранное по умолчанию — TextBoxColumn — поскольку в качестве данных для отобра-
жения мы будем добавлять строки. Остальные описанные в табл. 22.6 типы столбцов
создают в ячейках, представляющих данные, различные элементы управления.
Доступ к источникам данных в приложении Windows Forms 1095
Таблица 22.6. Типы столбцов DataGridView
Тип
Описание
DataGridViewButtonColumn
Этот тип служит для отображения кнопки в каждой ячейке столбца.
DataGridViewCheckBoxColumn
Этот тип используют, когда в ячейках столбца значения типа bool
(объекты System::Boolean) или объекты System::Windows::
Forms:: Checkstate необходимо хранить в виде флажков.
DataGridViewComboBoxColumn
Этот тип используют, когда в каждой ячейке столбца нужно отобра-
жать раскрывающийся список.
DataGridViewImageColumn
Этот тип выбирают, когда каждая ячейка столбца должна отобра-
жать изображение.
DataGridViewLinkColumn
Этот тип используют, когда каждая ячейка столбца предназначена
для отображения ссылки.
Чтобы добавить столбец в элемент управления, необходимо ввести значения
Name:, Header Text:, выбрать из списка значение Туре: (если требуется исполь-
зовать тип, отличающийся от выбранного по умолчанию) и щелкнуть на кнопке
Add (Добавить). В этом примере можно добавить столбцы с именами ISBN, Title
(Заголовок) и Publisher (Издатель). Значение поля Header Text: может в каждом
случае совпадать с соответствующим именем столбца. По завершении ввода столбцов
щелкните на кнопке Close (Закрыть), чтобы закрыть диалоговое окно (кнопка Close
отображается после добавления, по меньшей мере, одного столбца).
Столбцы можно изменять в любое время, щелкая на элементе Edit Columns
(Редактировать столбцы) всплывающего меню. В результате откроется диалоговое
окно Edit Columns (Редактирование столбцов), показанное на рис. 22.4.
Edit Columns
Selected Columns:
ISBN
] t?bi Title
1Ы Author
iabi Publisher
Unbound Column Properties
В Behavior
ContextMenuStrip
MaxInputLength
Readonly
Resizable
SortMode
□ Data
DataPropertyName
В Design
(Name)
ColumnType
Remove
(none)
32767
False
True _
Automatic
(none)
ISBN
D ata G ri d Vi eivT extB oxCo I ui v
(Name)
Indicates the name used in code to identify the
object.
*
Puc. 22.4. Диалоговое окно Edit Columns
1096 Глава 22
Диалоговое окно Edit Columns (Правка столбцов) позволяет изменять порядок сле-
дования существующих столбцов, добавлять новые столбцы или удалять их. Можно
также изменять любые свойства столбца.
Вернитесь на вкладку Design и измените значение свойства Text формы на Му
Book List (Перечень моих книг). Отобразив код формы, можно изменить конструк-
тор, чтобы обеспечить добавление данных в элемент управления DataGridView, ис-
пользуя функцию Add () применительно к свойству Rows:
Forml (void)
InitializeComponent();
//
// TODO: Добавьте здесь код конструктора
// Создание данных книг,
по одной в каждо
массиве
array<StringA>A bookl ® {L"0-09-174271-4", L"Wonderful Life”,
L"Stephen Jay Gould”, L”Hutchinson Radius”};
array<StringA>A book2 = {L"0-09-977170-5", L”The Emperor’s New Mind”,
L”Roger Penrose”, L”Vintage”};
array<StringA>A ЬоокЗ - (L”0-14-017996-8”,L”Metamagical Themas”,
L”Douglas R. Hofstadter", L"Penguin"};
array<StringA>A book4 = {L"0-201-36080-2", L"The Meaning Of It All”,
L”Richard P. Feynman”, L"Addison-Wesley"};
array<StringA>A book5 = {L"0-593-03449-X", L"The Walpole Orange",
L"Frank Muir", L"Bantam Press"};
array<StringA>A Ьоокб = {L"0-439-99358-X", L"The Amber Spyglass",
L"Philip Pullman", L”Scholastic Children’s Books”};
array<StringA>A book7 = {L"0-552-13461-9", L"Pyramids",
L"Terry Pratchett", L"Corgi Books"};
array<StringA>A book8 = (L"0-7493-9739-X”, L”Made In America",
L"Bill Bryson", L"Minerva"};
// Создание массива книг
array<array<String
books ={bookl, book2, ЬоокЗ, book4
Ьоок5, Ьоокб, book7, book8};
// Добавление всех книг в элемент управления
for each(array<StringA>A book in books )
dataGridViewl->Rows->Add(book);
Для каждой книги был создан массив типа array<StringA>, причем дескриптор
каждого массива хранится в переменной типа array<array<StringA>A>A. Каждый
из массивов строк состоит из четырех элементов, содержащих данные, которые со-
ответствуют столбцам элемента управления DataGridView. Таким образом, каждый
массив определяет книгу.
Для удобства мы объединили дескрипторы массивов строк в массив books. Из-за
повторяющихся символов А тип этого массива выглядит несколько непонятно, но
он представляет собой всего лишь дескриптор массива элементов, тип каждого из
которых — <stringA>A. Массив books позволяет определить все данные в элементе
управления в единственном цикле for each. Свойство Rows объекта DataGridView
возвращает дескриптор типа DataGridViewRowCollectionA, который ссылается на
коллекцию строк в элементе управления. Вызов функции Add () для объекта, который
был возращен свойством Rows, добавляет в коллекцию всю строку. Каждый элемент
массива, переданный в качестве аргумента, соответствует столбцу в элементе управ-
ления, а тип элемента данных должен соответствовать типу, выбранному для столбца.
В этом примере все столбцы имеют один и тот же тип, поэтому все ячейки строки
Доступ к источникам данных в приложении Windows Forms 1097
могут быть переданы в массив. Если бы столбцы имели различные типы, можно было
бы указать элемент для каждого столбца с помощью отдельного аргумента функции
Add () либо организовать массив элементов типа Object74.
Если скомпилировать и выполнить программу, нажав комбинацию клавиш
<Ctrl+F5>, должно открыться окно приложения, показанное на рис. 22.5.
Рис. 22.5. Элемент управления DataGridView в работе
Линейки прокрутки отображаются автоматически, поскольку элемент управления
DataGridView выходит за пределы границ клиентской области формы. При увеличе-
нии размеров окна линейки прокрутки рано или поздно исчезнут.
Было бы замечательно, если бы ширина столбцов была достаточно большой для
вмещения максимально длинного текста, который они содержат. Этого можно до-
биться, изменяя значение свойства AutoSizeColumnsMode в группе Layout (Макет).
Находясь на вкладке Forml [Design] панели Editor (Редактор) откройте окно Properties
(Свойства) для элемента управления DataGridView и измените значение свойства
AutoSizeColumnsMode на AllCells. Если вы выполните повторную компиляцию
программы и снова ее запустите, то увидите, что теперь окно приложение имеет вид,
показанный на рис. 22.6.
Рис. 22.6. Элемент управления DataGridView
с измененной шириной столбцов
1098 Глава 22
Ширина каждого столбца установлена так, чтобы в нем умещалась самая длинная
строка, присутствующая в данном столбце. В целом результирующее окно приложе-
ния выглядит не так уж плохо, но программные методы позволяют значительно по-
высить степень персонификации его внешнего вида.
Персональная настройка элемента
управления DataGridView
Как уже отмечалось ранее в этой главе, внешний вид элемента управления
DataGridView в значительной степени можно изменять в соответствии с личными
предпочтениями. Аспекты решения этой задачи мы будем исследовать, используя эле-
мент управления в несвязанном режиме, но все, что будет при этом описано, равно
применимо к использованию элемента управления и в связанном режиме. Внешний
вид каждой ячейки в элементе управления DataGridView определяется объектом
типа DataGridViewCellStyle, имеющим свойства, описанные в табл. 22.7.
Таблица 22.7. Свойства объекта DataGridViewCellStyle, влияющие на внешний вид ячейки
Свойство Описание
BackColor Это значение — объект System:: Drawing::Color, который определяет цвет
фона ячейки. В классе Color диапазон стандартных цветов определен в виде
статических членов. Значение, используемое по умолчанию — Color:: Empty.
ForeColor
SelectionBackColor
SelectionForeColor
Это значение — объект color, который определяет цвет изображения ячейки.
Значение, используемое по умолчанию — Color:: Empty.
Это значение — объект color, который определяет цвет фона выбранной
ячейки. Значение, используемое по умолчанию — Color:: Empty.
Это значение — объект Color, который определяет цвет изображения вы-
бранной ячейки. Значение, используемое по умолчанию — color:: Empty.
Font Это значение — объект System::Drawing::Font, определяющий шрифт,
который должен использоваться для отображения текста в ячейке. Значение,
используемое по умолчанию — null.
Alignment
WrapMode
Это значение определяет выравнивание содержимого ячейки. Допустимые
значения определены перечислением DataGridViewAlignment, поэто-
му значение может быть любой из следующих констант: Bottomcenter,
BottomLeft, BottomRight, Middlecenter, MiddleLeft, MiddleRight,
TopCenter, TopLeft, TopRight, NotSet. Значение, используемое по умол-
чанию — NotSet.
Это значение определяет переход текста в ячейке на следующую строку, если
он слишком длинен, чтобы уместиться в ячейке. Допустимое значение — одна
из констант, определенных перечислимым объектом DataGridViewTriState:
True, False, NotSet. Значение, используемое по умолчанию — NotSet.
Padding Это значение — объект типа System: -.Windows:: Forms:: Padding, который
определяет пробел между содержимым и краем ячейки. Конструктор класса
Padding требует передачи аргумента типа int, который представляет зна-
чение дополнения содержимого ячейки пробелами, измеренное в пикселях.
Значение, используемое по умолчанию, соответствует отсутствию дополнения
содержимого ячейки.
Format Это значение — строка формата, которая определяет способ форматирования
содержимого строки. Это форматирование аналогично используемому в функ-
ции Console:: WriteLine (). Значение, используемое по умолчанию — пу-
стая строка.
Доступ к источникам данных в приложении Windows Forms 1099
Приведенный в табл. 22.7 список включает далеко не все свойства объекта
DataGridViewCellStyle, а лишь те, которые связаны с внешним видом ячейки.
Определение внешнего вида конкретной ячейки — достаточно сложная задача, по-
скольку в элементе управления DataGridView можно устанавливать множество раз-
личных свойств, определяющих способ отображения данной ячейки или группы яче-
ек, причем некоторые из этих свойств могут действовать в любой заданный момент
времени. Например, можно определить значения свойств, которые указывают внеш-
ний вид строки или столбца ячеек или всех ячеек в элементе управления, причем все
эти свойства могут действовать одновременно. Очевидно, что поскольку строка и
столбец всегда пересекаются, все три значения свойств применимы к любой отдель-
ной ячейке, что ведет к возникновению явного конфликта.
Каждая ячейка в элементе управления DataGridView представлена объектом
System::Windows: : Forms:: DataViewCell, а внешний вид каждой конкретной ячей-
ки, включая ячейки заголовков, определяется значением ее свойства InheritedStyle.
Это значение для данной ячейки определяется путем просмотра всех доступных
свойств, которые возвращают значение, являющееся объектом DataGridViewStyle,
примененного к ячейке, и последующего упорядочения этих значений по приорите-
ту. Значение с наивысшим приоритетом будет установлено в качестве действующего.
Определение значения свойства InheritedStyle для ячеек заголовков строк и столб-
цов выполняется иначе, чем для остальных ячеек, поэтому они будут рассматриваться
отдельно, начиная с ячеек заголовков.
Настройка ячеек заголовков
Значение свойства InheritedStyle для каждой ячейки заголовка в элементе
управления определяется учетом значений свойств в представленной ниже последо-
вательности.
□ Свойства Style объекта DataGridViewCell, который представляет ячейку.
□ Свойства ColumnHeaderDefaultCellStyle или RowHeadersDefaultCellStyle
объекта элемента управления.
□ Свойства DefaultCellStyle объекта элемента управления.
Таким образом, если значение свойства Style объекта ячейки установлено, свой-
ству InheritedStyle ячейки присваивается это значение, которое и определяет ее
внешний вид. Если это значение не установлено, в действие вступает следующее зна-
чение, если оно установлено. Если и второе свойство не установлено, применяется
свойство DefaultCellStyle элемента управления.
Следует помнить, что значение свойства InheritedStyle — объект типа
DataGridViewCellStyle, который сам обладает свойствами, определяющими раз-
личные аспекты внешнего вида ячейки. Процесс учета приоритета применяется к
каждому из свойств объекта DataGridViewCellStyle, поэтому общая последователь-
ность приоритетов может включать в себя более одного свойства.
Настройка ячеек, не являющихся заголовками
Значение свойства InheritedStyle каждой ячейки элемента управления, не явля-
ющейся заголовком (то есть содержащей данные) определяется свойствами объекта
DataGridView в описанной далее последовательности.
1100 Глава 22
□ Свойством Style объекта DataGridViewCell, который представляет ячейку.
□ Свойством Defaultcellstyle объекта DatagridViewRow, который представля-
ет строку, содержащую ячейку. Как правило, ссылку на объект DatagridViewRow
нужно будет выполнять посредством индексации свойства Rows объекта эле-
мента управления.
□ Свойством AlternatingRowsDefaultCellStyle объекта элемента управления.
Это свойство применяется только к ячейкам строк с нечетными номерами ин-
дексов.
□ Свойством RowsDef aultCellStyle объекта элемента управления.
□ Свойством DefaultCellStyle объекта DataGridViewColumn, содержащего
ячейку. Как правило, обращение к объекту DataGridViewColumn будет выполнять-
ся посредством индексации свойства Columns объекта элемента управления.
□ Свойством DefaultCellStyle объекта элемента управления.
Вообще говоря, для каждой ячейки можно было бы использовать отдельный объ-
ект DataGridViewCellStyle, но для повышения эффективности количество таких
объектов следует сохранять минимальным.
В приведенном ниже примере определения объекта DataGridView рассмотрены
некоторые из упомянутых возможностей.
практическое занятие Определение внешнего вида элемента
управления
Используя шаблон Windows Forms, создайте новый проект CLR по имени Ех22_02.
На вкладке Design добавьте на форму элемент управления DataGridView и измените
значения его свойства (Name) на dataGridView. Это имя дескриптора в классе, ко-
торый ссылается на объект элемента управления. Можно также изменить значение
свойства Text формы на "Му Other Book List” (“Еще один перечень моих книг”).
Остальная часть примера связана с изменениями в коде конструктора.
Отображаемые данные аналогичны использованным в предыдущем примере, но
чтобы несколько расширить возможности приложения, в начало каждой строки,
определяющей книгу, мы добавим запись даты. Поэтому ячейки в первом столбце
будут содержать ссылки на объекты типа System: :DateTime, а остальные столбцы
будут строками. Класс DateTime определяет момент времени, который, как правило,
указывают в виде сочетания даты и времени. В данном примере интерес представля-
ет только дата, поэтому мы используем конструктор, который принимает только три
аргумента: год, месяц и день.
Определение данных
Прежде всего, необходимо подготовить данные, которые будут отображать-
ся. Добавьте следующий код в конструктор класса Forml после вызова функции
InitializeComponent():
// Создание данных книг, по одному массиву для каждой книги
array<ObjectA>A bookl = {gcnew DateTime(1999,11,5) , L"0-'09-174271-4",
L”Wonderful Life”, L”Stephen Jay Gould1’, L”Hutchinson Radius”};
array<ObjectA>A book2 = {gcnew DateTime (2001,10,25) , L"0-09-977170-5",
L"The Emperor’s New Mind", L"Roger Penrose", L"Vintage"};
array<ObjectA>A book3 = {gcnew DateTime(1993,1,15) , L"0-14-017996-8",
L"Metamagical Themas", "Douglas R. Hofstadter", L"Penguin"};
Доступ к источникам данных в приложении Windows Forms 1101
array<ObjectA>A book4 = {gcnew DateTime(1994,2,7), L"0-201-36080-2”,
L"The Meaning Of It All”, L”Richard P. Feynman", L"Addison-Wesley"};
array<ObjectA>A book5 = {gcnew DateTime(1995,11,6), L"0-593-03449-X",
L"The Walpole Orange", "Frank Muir", L"Bantam Press"};
array<ObjectA>A Ьоокб = {gcnew DateTime(2004,7,16), L"0-439-99358-X",
L"The Amber Spyglass", L"Philip Pullman",
L"Scholastic Children’s Books"};
array<ObjectA>A book7 = {gcnew DateTime(2002,9,18), L"0-552-13461-9",
L"Pyramids", L"Terry Pratchett", L"Corgi Books"};
array<ObjectA>A book8 = {gcnew DateTime(1998,2,27), L"0-7493-9739-X",
L"Made In America", L"Bill Bryson", L"Minerva"};
// Создание массива книг
array<array<ObjectA>A>A books = {bookl,
book2, ЬоокЗ, book4,
book5, Ьоокб, book7, book8};
Общий принцип работы этого кода таков же, как в предыдущем примере.
Различия обусловлены лишь тем, что теперь спецификация каждой книги содержит
дополнительный элемент типа DateTime, поэтому тип элементов массива — Object*.
Вспомните, что класс Object — базовый класс для любого класса С++/СЫ; поэтому в
элементе типа Object* можно хранить дескриптор объекта класса любого типа.
Затем в конструктор можно поместить следующий оператор:
array<StringA>A headers =
{L"Date", L"ISBN", L"Title", L"Author", L"Publisher"};
В результате будет создан массив, содержащий текст, который должен отображать-
ся в элементе управления в качестве заголовков столбцов. Эти заголовки можно до-
бавить в элемент управления, добавляя в конструктор такой код:
dataGridView->ColumnCount = headers->Length; // Установка количества столбцов
for (int i = 0 ; i<headers->Length ; i++)
dataGridView->Columns[i]->Name = headers[i];
Первый оператор указывает количество столбцов в элементе управления путем
установки значения свойства ColumnCount. Одновременно он переводит элемент
управления в несвязанный режим. Цикл for устанавливает в качестве значения
свойства Name объекта каждого столбца соответствующую строку в массиве headers.
Свойство Columns элемента управления возвращает ссылку на коллекцию столбцов,
и для выполнения ссылки на конкретный столбец достаточно индексировать эту кол-
лекцию.
Для добавления строк в элемент управления можно использовать еще один цикл:
for each(array<0bjectA>* book in books)
dataGridView->Rows->Add(book);
Цикл for each выбирает элементы из массива books в том порядке, в каком они
передаются методу Add () применительно к ссылке, возвращенной свойством Rows
элемента управления. Каждый элемент в массиве books представляет собой массив
строк, количество которых равно числу столбцов в элементе управления.
Теперь элемент управления загружен данными, поэтому количество строк и столб-
цов определено, а содержимое заголовков столбцов указано. Теперь можно присту-
пить к настройке внешнего вида элемента управления.
1102 Глава 22
Настройка элемента управления
Нам необходимо, чтобы элемент управления размещался в фиксированной пози-
ции в клиентской области формы. Это можно осуществить, устанавливая значение
свойства Dock:
dataGridView->Dock = Dockstyle:
В качестве значения свойства Dock должна быть установлена одна из констант,
определенных перечислением Dockstyle. Другими допустимыми значениями это-
го свойства являются Тор (Вверху), Bottom (Внизу), Left (Слева), Right (Справа) и
None (Нет), которые указывают стороны элемента управления, привязанные к пози-
ции размещения.
Привязку позиции элемента управления к клиентской области формы можно
выполнять также посредством установки свойства Anchor элемента управления.
Значение этого свойства указывает края элемента управления, которые должны быть
привязаны к клиентской области формы. Значение является побитовой комбинацией
констант, определенных перечислением Anchorstyles, и может принимать любые
или все из значений Top, Bottom, Left и Right. Например, чтобы привязать верх-
нюю и левую стороны элемента управления, в качестве значения нужно было бы
указать Anchorstyles: :Тор & Anchorstyles: :Left. Определение свойства Anchor
ведет к фиксации позиции элемента управления и его линеек прокрутки внутри кон-
тейнера заданного размера. Поэтому при изменении размера окна приложения эле-
мент управления и его линейки прокрутки сохраняют свои размеры. Если установить
значение свойства Dock так, как в предыдущем операторе, при изменении размера
окна приложения отображаемая в нем часть элемента управления будет изменяться
с соответствующим изменением линеек прокрутки. Поэтому теперь работать с при-
ложением значительно удобнее.
Нам требуется, чтобы ширина столбцов автоматически изменялась, обеспечивая
отображение полных строк данных в ячейках. Для этого можно вызывать функци-
ю AutoResizeColumns():
dataGridView->AutoResizeColumns ();
Этот оператор подбирает ширину всех столбцов в соответствии с текущим со-
держимым, в том числе в соответствии с содержимым ячеек заголовков. Обратите
внимание, что эта настройка выполняется во время вызова функции, поэтому к мо-
менту ее вызова содержимое уже должно существовать. При последующем изменении
содержимого ширина столбца не изменяется. Если требуется, чтобы ширина столбца
автоматически изменялась при каждом изменении содержимого ячеек, для элемента
управления нужно установить также свойство AutoS izeColumnsMode:
dataGridView->AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode::AllCells;
Значением этого свойства должна быть одна из констант, определенных пере-
числением DataGridViewAutoSizeColumnsMode. Другими возможными значениями
являются ColumnHeader (Заголовок столбца), AllCellsExceptHeader (Все ячейки,
кроме заголовка), DisplayedCells (Отображаемые ячейки), DisplayedCellsExcep
tHeader (Отображаемые ячейки, кроме заголовка), Fill (Заполнение) и None (Нет).
Естественно, эти значения отображаются также в списке значений этого свойства на
странице Properties элемента управления DataGridView.
В некоторых ситуациях требуется, чтобы автоматический подбор ширины при из-
менении содержимого выполнялся только для определенных столбцов. В этом случае
значение свойства AutoSizeMode необходимо устанавливать для объекта столбца.
Доступ к источникам данных в приложении Windows Forms 1103
Существует еще две перегруженных версии функции AutoSizeColumns (). Одна
принимает аргумент типа DataGridViewAutoSizeColumnsMode, определяющий ячей-
ки, на которые функция оказывает воздействие. Вторая перегрузка является защи-
щенной и, следовательно, предназначена для использования в производном классе.
Она принимает дополнительный аргумент типа bool, который указывает, должна ли
высота ячейки учитываться при вычислении новой ширины.
Используемый по умолчанию цвет фона всех ячеек элемента управления можно
установить следующим образом:
dataGridView->DefaultCellStyle->BackColor =,Color::Pink;
Этот оператор устанавливает в качестве цвета фона стандартный цвет Pink
(Розовый), который определен в качестве статического члена класса Color (Цвет).
Свойства DefaultCellStyle объекта элемента управления определяют только те
атрибуты стиля, которые применяются к ячейке при отсутствии какого-либо другого
действующего стиля ячейки, имеющего более высокий приоритет.
Можно также определить применяемый по умолчанию цвет изображения всех
ячеек:
dataGridView->DefaultCellStyle->ForeColor = Color::DarkBlue;
Для визуального определения выбранных ячеек можно указать цвета изображения
и фона выбранных ячеек. Эти цвета можно было бы определить следующим образом:
dataGridView->DefaultCellStyle->SelectionBackColor = Color::Green;
Естественно, смысл программного определения свойств в том, что установленные
значения применяются во время выполнения, что позволяет определять значения
в зависимости от условий и значений данных, существующих во время выполнения
приложения. Значения свойств, определяемые в панели Properties среды IDE, уста-
навливаются раз и навсегда — если только вы не располагаете кодом, который изме-
няет их впоследствии.
Пока закончим настройку всего элемента управления и займемся настройкой за-
головков столбцов.
Настройка заголовков столбцов
Если хотите самостоятельно определить внешний вид заголовков столбцов, значе-
ние свойства EnableHeadersVisualStyles потребуется установить равным false:
dataGridView->EnableHeadersVisualStyles = false;
Обычно элементы управления в приложении Windows Forms отображаются в со-
ответствии с действующей темой визуальных стилей, которая и определяет внешний
вид элементов управления. При выполнении приложения в среде Windows ХР эле-
менты управления отображаются в соответствии с текущей темой Windows ХР. Когда
значением свойства EnableHeadersVisualStyles является true, визуальные стили
заголовков столбцов будут установлены согласно действующей для приложения теме
визуальных стилей, а стили, определенные непосредственно в приложении, будут иг-
норироваться .
Мы собираемся определить несколько свойств, определяющих внешний вид
заголовков столбцов, и простой способ достижения этого — создание объекта
DataGridViewStyle, для которого можно определить необходимые свойства, и по-
следующая установка этого объекта в качестве определяющего стили заголовков.
Объект DataGridViewStyle можно создать так:
DataGridViewCellStyleA headerstyle = gcnew DataGridViewCellStyle;
1104 Глава 22
Было бы прекрасно, если бы заголовок отображался более крупным шрифтом, и
его можно определить, устанавливая значение свойства Font:
headerStyle->Font = gcnew System::Drawing::Font("Times New Roman”, 12,
Fontstyle::Bold);
Теперь текст заголовка отображается символами шрифта limes New Roman с по-
лужирным начертанием и размером в 12 пунктов.
Для ячеек заголовков можно определить также цвета фона и изображения:
headerStyle->BackColor - Color::AliceBlue;
headerStyle->ForeColor = Color::BurlyWood;
Текст отображается цветом BurlyWood на фоне AliceBlue. Если вы предпочи-
таете какие-либо другие цвета, класс Color предоставляет множество возможностей,
и средство Intellisense должно отобразить их список по завершении ввода операции
разрешения области определения.
Чтобы внешний вид ячеек заголовков соответствовал свойствам, установленным
для объекта headerstyle, необходимо добавить следующий оператор:
dataGridView->ColumnHeadersDefaultCellStyle - headerstyle;
Он устанавливает в качестве значения свойства ColumnHeadersDefaultCellStyle
элемента управления дескриптор header Style. Тем самым заменяется существующий
объект DataGridViewCellStyle, который действовал по отношению к заголовкам.
Применительно к заголовкам столбцов необходимо выполнить еще одну опера-
цию. Более крупный шрифт требует соответствующей подстройки высоты ячеек.
Вызов функции AutoResizeColumnHeadersHeight () элемента управления настраи-
вает высоту ячеек заголовков в соответствии с их текущим содержимым:
dataGridView->AutoResizeColumnHeadersHeight();
В результате высота всех ячеек заголовков будет автоматически подобрана так,
чтобы вмещать наибольшее по высоте содержимое. Если требуется автоматически на-
строить высоту заголовка только конкретного столбца, можно воспользоваться пере-
груженной версией функции, которая принимает аргумент, указывающий индекс на-
страиваемого столбца.
Если нужно, чтобы заголовки строки или столбца были невидимыми, это-
го можно достичь, устанавливая значение свойств Ro wHe a de г s Visible и/или
ColumnHeadersVisible элемента управления равными false.
Форматирование столбца
Первый столбец содержит дескрипторы объекта DateTime. В настоящий момент
для получения каких-либо данных для отображения приложение просто вызывает
функцию ToString () для объектов, но мы можем его усовершенствовать. Для свой-
ства DefaultCellStyle столбца можно установить свойство Format, и затем эта спе-
цификация формата будет использоваться для отображения содержимого ячеек:
dataGridView->Columns[0]->DefaultCellStyle->Format = L"y";
Этот оператор устанавливает в качестве значения свойства Format строку, содер-
жащую спецификацию формата у для объекта DateTime, которая представляет его в
краткой форме даты в виде месяца и года. Для объектов DateTime существует еще не-
сколько спецификаторов формата, которые можно было бы использовать. Например,
спецификатор D отображает день, месяц и год, а спецификаторы f и F наряду с датой
отображают и время.
Доступ к источникам данных в приложении Windows Forms 1105
Если вы добавили в конструктор класса Forml весь описанный код, пора скомпи-
лировать и запустить пример. Должно открыться окно приложения, подобное пока-
занному на рис. 22.7.
Рис. 22.7. Элемент управления DataGridView после форматирования
Ширина окна, представленного на рис. 22.7, была увеличена, чтобы показать боль-
ше столбцов. В этой книге окно приложения отображено в оттенках серого, но на
экране оно должно отображаться в ярких цветах Technicolor.
Если щелкнуть на заголовке одной из строк в левой части элемента управления,
строка должна быть выделена, как показано на рис. 22.8.
Му Other Book List
Рис. 22.8. Выделение строки
Цвет фона ячеек строки определен в свойстве SelectionBackColor свойства
DefaultCellStyle. Можно также выбрать отдельную ячейку, щелкая на ней. При
этом ее цвет фона изменится на зеленый.
Возможность сортировки строк по любому из столбцов встроена в элемент управ-
ления DataGridView. Можете щелкнуть на заголовке столбца и убедиться, что строки
сортируются по выбранному столбцу. Если вторично щелкнуть на заголовке столб-
ца, порядок сортировки строк изменится на противоположный. К каждому столбцу
1106 Глава 22
можно добавить контекстную подсказку, описывающую возможности сортировки.
Добавленный в конструктор класса Forml цикл выглядит следующим образом:
for each(DataGridViewColumnA column in dataGridView->Columns)
column->ToolTipText = L”Click to\nsort rows”;
Значение свойства Columns — коллекция столбцов, в которой каждый столбец —
объект типа DataGridViewColumn. Цикл последовательно выбирает каждый из столб-
цов и устанавливает для него значение свойства ToolTipText (Текст контекстной
подсказки). Контекстная подсказка для одного из заголовков столбцов показана на
Рис. 22.9. Контекстная подсказка для одного из заголовков столбцов
Эти контекстные подсказки отображаются только при помещении указателя мыши
над ячейкой заголовка столбца. Контекстную подсказку можно определить для любой
ячейки с данными, устанавливая свойство ToolTipText для объекта ячейки.
Настройка внешнего вида чередующихся строк
При отображении множества одинаковых по виду строк может оказаться затруд-
нительным фиксировать взгляд на нужной строке.
облегчения этой задачи можно
чередовать цвета строк, устанавливая другой цвет свойства BackColor (Цвет фона)
для свойства AlternatingRowsDefaultCellStyle объекта управления:
dataGridView->AlternatingRowsDefaultCellStyle->BackColor - Color::Blue;
Вероятно, для обеспечения приемлемого контраста между текстом и фоном по-
требуется изменить также значение свойства ForeColor для свойства Alternating-
RowsDefaultCellStyle:
dataGridView->AlternatingRowsDefaultCellStyle->ForeColor = Color::White;
Теперь чередующиеся строки отображаются поочередно на розовом и синем
фоне, как показано на рис. 22.10 (естественно, на рисунке строки различаются от-
тенком серого).
Теперь строки легко различать, а белый текст на синем фоне отчетливо виден. По-
прежнему строки можно выбирать, щелкая на заголовке строки — при этом выбран-
ная строка выделяется зеленым цветом. Щелчок на ячейке, расположенной слева от
заголовков столбцов, приводит к выбору всех строк.
доступ к источникам данных в приложен:
i Windows Forms 1107
Рис. 22.10. Чередование фона строк
Динамическое определение стилей ячеек
Существует несколько возможностей изменения внешнего вида ячеек через об-
работку событий элемента управления DataGridView. Событие CellFormatting
элемента управления DataGridView запускается, когда содержимое ячейки должно
быть сформатировано в вид, готовый для отображения, поэтому, добавляя обработ-
чик этого события, внешний вид любой ячейки можно настраивать в соответствии
с ее содержимым. Рассмотрим, как можно было бы расширить пример Ех22_02 для
решения этой задачи.
Например, предположим, что в примере Ех22_02 цвет фона ячеек в столбце Date
необходимо изменить на красный, если значение даты меньше 2000 года. Вспомните,
что, как было сказано в разделе 9 главы, посвященном событиям, для регистрации
обработчика нужно добавить экземпляр делегата события. Тип делегата события
CellFormatting — DataGridViewCellFormattingEventHandler, и этот тип ожи-
дает передачи ему двух параметров: первого, типа Object*, который идентифи-
цирует источник события, и второго
CellFormattingEventArgs. Второй аргумент, переданный обработчику события
CellFormatting, предоставляет дополнительную информацию о событии с помощью
свойств, описанных в табл. 22.8.
;ескриптора объекта типа DataGridView
Таблица 22.8. Свойства объекта DataGridViewCellFormattingEventArgs
Свойство Описание
Value
Значение этого свойства — дескриптор содержимого ячейки, которая должна
быть форматирована.
DesiredType
Значение этого свойства — дескриптор объекта типа Туре, который идентифи-
цирует тип содержимого форматируемой ячейки.
Cellstyle
Извлекает или устанавливает стиль ячейки, связанной с событи-
ем форматирования, потому это значение — дескриптор объекта типа
DataGridViewCellStyle.
Columnindex
Значение этого свойства — индекс столбца форматируемой ячейки.
Rowindex
FormattingApplied
Это значение — индекс строки форматируемой ячейки.
Значение этого свойства, которое может принимать значение true или false,
указывает, применялось ли форматирование к содержимому ячейки.
1108 Глава 22
Эти свойства позволяют выяснять все необходимые сведения о форматированной
ячейке — индекс ее строки и столбца, действующий стиль и содержимое ячейки.
Первый шаг по обработке события CellFormatting состоит в определении для
него функции обработчика. Код обработчика должен быть максимально кратким и
эффективным, поскольку функция вызывается для каждой ячейки элемента управле-
ния каждый раз, когда возникает необходимость форматирования ячейки. В класс
Forml можно добавить следующую функцию обработчика события CellFormatting:
private:
void OnCellFormatting(ObjectА sender, DataGridViewCellFormattingEventArgsA e)
// Проверка того, является ли данный столбец столбцом даты
if(dataGridView->Columns[e->Column!ndex]->Name == L”Date”)
// Если содержимое ячейки — не null, и значение года меньше 2000,
// цвет фона - красный
if(e->Value != nullptr && safe_cast<DateTimeA>(e->Value)->Year < 2000)
e->CellStyle->BackColor = Color::Red;
e->FormattingApplied = false; // Мы не форматировали данные
Вначале мы проверяем, является ли текущий столбец столбцом даты (Date), по-
скольку нас интересует изменение ячеек только этого столбца. Для ячеек столбца
Date мы проверяем действительное существование содержимого, и, если в них объ-
ект DateTime присутствует, то меньше ли значение года, чем 2000. В этом случае зна-
чение свойства объекта, возращенного свойством Cellstyle, мы устанавливаем рав-
ным Color: :Red. Значение свойства FormattingApplied мы устанавливаем равным
false для указания того, что форматированное содержимое отсутствует. Последнее
действие не обязательно, поскольку исходное значение этого свойства — false. Его
нужно было бы установить равным true, если бы в обработчике выполнялось фор-
матирование содержимого. Это препятствовало бы последующему форматированию
посредством значения свойства Format.
Чтобы зарегистрировать эту функцию в качестве обработчика события CellFor-
matting, в конец конструктора класса Forml нужно добавить следующий оператор:
dataGridView-XellFormatting +=
gcnew DataGridViewCellFormattingEventHandler(this, &Forml::OnCellFormatting);
Первый аргумент делегата — дескриптор объекта, содержащего функцию обработ-
чика и являющегося текущим объектом Forml. Второй аргумент — адрес функции но-
вого обработчика события. Если теперь повторить компиляцию и выполнение приме-
ра, вы должны увидеть, что цвет фона всех ячеек в столбце Date, в которых значение
даты предшествует 2000 году, изменился на красный.
Элемент управления DataGridView определяет события CellMouseEnter и
CellMouseLeave, запускаемые при помещении указателя мыши на ячейку или при пе-
ремещении его от нее. Обработчики этих событий можно было бы реализовать так,
чтобы ячейка с помещенным на нее указателем мыши выделялась за счет изменения
цвета фона. Обработчик события CellMouseEnter мог бы устанавливать новые цве-
та изображения и фона, а обработчик события CellMouseLeave — восстанавливать
первоначальные цвета. Решение этой задачи имеет несколько сложных аспектов, по-
этому заслуживает особого рассмотрения.
Доступ к источникам данных в приложении Windows Forms 1109
Практическое занятие
Выделение ячеек под указателем мыши
Этот пример создается как расширение примера Ех22_02 с учетом последнего до-
бавленного в него обработчика события CellFormatting, то есть он не создается с
нуля. Старые цвета фона и изображения должны быть где-то сохранены, поэтому до-
бавим в класс Forml следующие приватные члены данных:
// Сохранение в обработчике события помещения указателя мыши
//на ячейку старых цветов ячейки для их последующего восстановления
//в обработчике события перемещения указателя мыши из ячейки
private: Color oldCellBackColor;
private: Color oldCellForeColor;
В классе конструктора Forml оба эти члена данных нужно инициализировать зна-
чением Color: :Empty.
Первый параметр делегата любого из событий CellMouseEnter или CellMouse-
Leave — это дескриптор объекта, запускающего событие, которым в данном случае
является элемент управления DataGridView. Второй параметр обоих делегатов — де-
скриптор объекта типа DataGridViewCellEventsArg, хранящий дополнительную
информацию о событии. Этот объект имеет свойства Rowindex и Columnindex; их
значения позволяют устанавливать ячейку, в которую помещается указатель мыши
или которую он покидает. Первое свойство можно использовать для индексирования
свойства Rows, чтобы выделять строку, содержащую ячейку, а второе — для индекси-
рования свойства Cells для выбора самой ячейки внутри строки. Следует отметить
один нюанс — значение свойства Rowindex равно -1, когда указатель мыши располо-
жен в строке заголовков столбцов, а значение свойства Column Index равно -1, ког-
да указатель мыши находится над одним из заголовков строк- Эти значения требуют
проверки, поскольку попытка использования отрицательного индекса со свойством
Rows, как и попытка индексации свойства Cells для строки с отрицательным значе-
нием, приводит к генерации исключения.
Теперь приватную функцию обработчика события CellMouseEnter в классе Forml
можно определить следующим образом:
private:
void OnCellMouseEnter(ObjectА sender, DataGridViewCellEventArgsA e)
{
if(e->ColumnIndex >= 0 && e->RowIndex >= 0) // Проверка того, что индексы
// не отрицательны
{
// Идентификация ячейки, в которую помещен указатель
DataGridViewCellA cell =
dataGridView->Rows[e->Row!ndex]->Cells[e->ColumnIndex];
// Сохранение любых старых установленных цветов
oldCellBackColor = cell->Style->BackColor;
oldCellForeColor = cell->Style->ForeColor;
/I Установка цветов выделения
cell->Style->BackColor = Color::White;
cell~>Style->ForeColor = Color: .’Black;
}
}
После проверки того, что значения обоих индексов неотрицательны, мы извле-
каем дескриптор ячейки, в которую помещен указатель мыши. Для этого мы вначале
выбираем строку, индексируя значение свойства Rows элемента управления с помо-
1110 Глава 22
(ью значения свойства Rowindex, переданного в параметре е. Затем мы индексируем
свойство Cells строки, используя свойство Columnindex, переданное в параметре е,
чтобы выбрать ячейку внутри строки.
После того, как мы получили в свое распоряжение дескриптор ячейки, можно
легко сохранить значения свойств BackColor и ForeColor свойства Style ячейки
и установить новые цвета для отображения в ней белого текста на черном фоне.
Свойство Style может быть даже не определено. В этом случае обращение к зна-
чению свойства создает новый объект DataGridViewCellStyle, значения свойств
BackColor и ForeColor которого определены как Color::Empty. Если же значение
свойства Style ячейки установлено, мы получим объект, содержащий любые ранее
установленные для него свойства.
Функция обработчика события CellMouseLeave должна восстанавливать перво-
начальные цвета ячейки. Эту функцию обработчика можно реализовать следующим
образом:
private:
void OnCellMouseLeave(ObjectA sender, DataGridViewCellEventArgsA e)
if(e->ColumnIndex >=0 && e->Row!ndex >=0)
// Определение покидаемой ячейки
DataGridViewCellA cell -
dataGridView->Rows[e->RowIndex]->Cells[e->ColumnIndex];
// Восстановление сохраненных значений цвета
cell->Style->BackColor = oldCellBackColor;
cell->Style->ForeColor = oldCellForeColor;
// Сброс хранимых значений цвета к "пустому” цвету
oldCellForeColor = oldCellBackColor - Color::Empty;
Как и ранее, прежде чем выполнять какие-либо действия, необходимо проверить,
не отрицательны ли значения индексов. После определения ячейки, подобно тому,
как это было сделано в предыдущем обработчике, в свойстве Style ячейки мы вос-
станавливаем сохраненные ранее цвета. Способ определения цветов изображения и
фона ячейки определяется ранее рассмотренным списком приоритета. Если свойство
Style не было установлено для ячейки, восстанавливаемые значения равны Color::
Epmty и игнорируются при определении цветов ячеек. Таким образом, к ячейке при-
меняются исходные цвета. Если же свойства ForeColor и BackColor установлены
для свойства Style, именно они будут восстановлены и определят цвета форматиро-
вания ячейки.
Чтобы зарегистрировать функции обработчиков, в конец конструктора класса
Forml добавьте следующие операторы:
dataGridView->CellMouseEnter +=
gcnew DataGridViewCellEventHandler(this, &Forml::OnCellMouseEnter);
dataGridView->CellMouseLeave +=
gcnew DataGridViewCellEventHandler(this, &Forml::OnCellMouseLeave);
Ко всем событиям ячейки применяется один и тот же делегат, поэтому делегат
DataGridViewCellEventHandler служит для регистрации обоих обработчиков.
Если теперь еще раз скомпилировать и выполнить программу, вы должны увидеть
выделение ячеек в действии, как показано на рис. 22.11.
Доступ к источникам данных в приложении Windows Forms 1111
Рис. 22.11. Выделение ячеек
Что ж, эта программа работает — по крайней мере, в большинстве случаев. Однако
ячейки красного цвета в столбце Date не выделяются. В чем же дело?
Причина возникновения этой проблемы кроется в последовательности событий.
Событие CellFormatting запускается после события CellMouseEnter, поэтому по-
следнее слово остается за функцией обработчика, которая устанавливает красный
цвет фона, и она замещает результат обработчика события CellMouseEnter своими
значениями. Поэтому необходимо, чтобы обработчик события CellFormatting рас-
познавал выделенную ячейку и не предпринимал в этом случае никаких действий.
Для этого в класс Forml можно было бы добавить еще один член для хранения де-
скриптора выделенной в текущий момент ячейки:
private: DataGridViewCell74 highlightedCell; //Выделенная в текущий момент ячейка
В конструкторе класса Forml этот новый член нужно также инициализировать
значением nullptr.
Теперь необходимо дополнить обработчик события CellMouseEnter сохранением
дескриптора выделенной ячейки в новом члене:
void OnCellMouseEnter (Object74 sender, DataGridViewCellEventArgs74 e)
if(e->ColumnIndex >® 0 && e->RowIndex >= 0) // Проверка того, что индексы
//не отрицательны
// Идентификация ячейки, в которую помещен указатель
highlightedCell = dataGridView->Rows[e->RowIndex]->Cells[e->ColumnIndex];
Il Сохранение любых старых установленных цветов
oldCellBackColor == highlightedCell->Style->BackColor;
oldCellForeColor = highlightedCell->Style->ForeColor;
// Установка цветов выделения
hightLightedCell->Style->BackColor = Color::White;
highlightedCell->Style->ForeColor = Color::Black;
Реализацию обработчика события CellMouseLeve потребуется изменить:
void OnCellMouseLeave(Object74 sender, DataGridViewCellEventArgs* e)
if(e->ColumnIndex >=0 && e->RowIndex >=0)
1112 Глава 22
11 Восстановление сохраненных значений цвета
highlightedCell->Style->BackColor = oldCellBackColor;
highlightedCell->Style->ForeColor = oldCellForeColor;
// Сброс значения переменой хранения цвета к "пустому” значению
oldCellForeColor = oldCellBackColor = Color::Empty;
highlightedCell = nullptr;
11 Сброс дескриптора выделенной ячейки
Специальное определение дескриптора ячейки больше не требуется, так как
он доступен в члене highlightedCell класса Forml. Его даже не нужно прове-
рять, поскольку событию CellMouseLeave всегда должно предшествовать событие
СеllMouseEnter. Мы сбрасываем дескриптор выделенной ячейки в нулевое значе-
ние , потому что выполнение этого действия — полезная привычка.
Теперь можно усовершенствовать обработчик событий CellFormatting:
void OnCellFormatting(ObjectА sender, DataGridViewCellFormattingEventArgsA e)
// Проверка того, выделена ли ячейка
if(dataGridView->Rows[e->Row!ndex]->Cells[e->ColumnIndex] == highlightedCell)
return;
// Проверка того, является ли данный столбец столбцом даты
if(dataGridView->Columns[e->ColumnIndex]->Name == L"Date")
// Если содержимое ячейки не является нулевым, и значение года
// меньше 2000, делаем цвет фона красным
if(е~>Value != nullptr && safe_cast<DateTimeA>(e->Value^->Year < 2000)
e->CellStyle->BackColor = Color::Red;
e->FormattingApplied = false; // Мы не форматировали данные
Если теперь заново компилировать и выполнить программу примера, все выбран-
ные ячейки будут выделены.
Теперь вы должны иметь достаточно полное представление о том, как реализовать
обработчик события для элемента управления DataGridView. Существует множество
других событий, поэтому вы обладаете огромными возможностями по динамической
настройке элемента управления в соответствии с потребностями приложения.
Использование связанного режима
В связанном режиме источник данных, отображаемых элементом управления
DataGridView, указан значением его свойства DataSource, которое в общем случае
может быть установлено любым объектом ref class, тип которого реализует любой
из интерфейсов, перечисленных в табл. 22.9.
Понятно, что можно создать собственные классы, которые реализуют тот или
иной из описанных интерфейсов, а затем их можно будет применять в качестве ис-
точника данных для элемента управления DataGridView в связанном режиме. Однако
в большинстве ситуаций требуется доступ к существующему источнику данных без не-
обходимости заботиться о создании собственных классов для получения доступа к
данным. В этом случае, скорее всего, целесообразно остановить свой выбор на ком-
поненте Bindingsource.
Доступ к источникам данных в приложении Windows Forms 1113
Таблица 22.9. Доступные интерфейсы
Интерфейс Описание
System: :Collections:: Hist Класс, который реализует этот интерфейс, пред- ставляет собой коллекцию объектов, доступных через единый индекс. Все одномерные массивы C++/CLI реализуют этот интерфейс, поэтому в качестве источника отображаемых данных объект DataGridView может использовать любой одно- мерный массив. Интерфейс IList наследует члены классов интерфейсов ICollection и lEnumerable, которые определены в пространстве имен System:: Collections.
System::ComponentModel::IListSource Класс, который реализует этот интерфейс, обеспе- чивает доступность данных в виде списка, являюще- гося объектом IList. Список может содержать объ- екты, которые также реализуют интерфейс IList.
System::ComponentModel::IBindingList Этот класс интерфейса расширяет интерфейс IList, обеспечивая возможность обработки более сложных ситуаций связывания данных.
System::ComponentModel::IBindingListView Добавляет в интерфейс IBindingList возможности сортировки и фильтрации. Класс Bindingsource, который вы встретите в этой главе немного позже, определяет элемент управле- ния, реализующий этот интерфейс.
Компонент Bindingsource
Компонент Binding Sou гее служит промежуточным звеном между элементами
управления формы и таблицей в источнике данных. Этот компонент можно связать
с элементом управления DataGridView, отображающим содержимое таблицы, или
с набором отдельных элементов управления, каждый из которых отображает один
из столбцов таблицы. Можно также программно добавлять данные в компонент
Bindingsource — в этом случае он действует как источник данных и, по сути, ведет
себя подобно списку.
Вначале рассмотрим применение компонента Bindingsource в качестве источ-
ника данных элемента управления DataGridView. Создание программы, которая
использует компонент Bindingsource, можно в значительной степени автоматизи-
ровать. Но чтобы вы получили представление о способе связывания компонента с
элементом управления, вначале соберем компоненты формы вручную. Чтобы сделать
компонент BindingSource источником данных элемента управления DataGridView,
значение свойства DataSource элемента управления потребуется определить в виде
дескриптора, который ссылается на компонент BindingSource.
Практическое занятие
Использование компонента BindingSource
В этом примере будет написана простая программа для просмотра таблицы базы
данных. Процесс описан, исходя из предположения, что вы используете базу данных
Northwind, которая применялась при работе с интерфейсом MFC, но можно работать
с любой другой базой данных из числа доступных в системе.
1114 Глава 22
Создайте проект Windows Forms по имени Ех22__3 и измените свойство Text фор-
мы на какой-либо понятный текст, например, "Using a Binding Source Component”
(“Использование компонента связывания источника данных”). Добавьте в клиентскую
область формы элемент управления DataGridView и измените значение его свойства
Name на dataGridView. Можно также установить значение свойства Dock элемента
управления равным FILL. На данный момент элемент управления DataGridView не свя-
зан ни с одним источником данных, поэтому в проект необходимо добавить источник
данных, который можно будет связать с элементом управления. В ходе этого процесса
будет создан компонент BindingSource. Щелкните на небольшой стрелке в правом
верхнем углу элемента управления, чтобы открыть его меню, щелкните на стрелке рас-
крытия списка, расположенной рядом с элементом меню Choose Data Source (Выберите
источник данных), а затем щелкните на ссылке Add Project Data Source (Добавить ис-
точник данных проекта). Откроется диалоговое окно Data Source Configuration Wizard
(Мастер конфигурирования источников данных), показанное на рис. 22.12.
Это же диалоговое окно отображается при выборе пункта Data=>Add New Data
Source (Данные^Добавить новый источник данных) в главном меню, однако это дей-
ствие лишь добавляет источник данных в проект, но не связывает его с элементом
управления. Как видите, для источника данных можно выбрать одно из двух место-
положений, но в данном случае щелкните на опции Database (База данных). Вторая
опция позволяет указать объект, который предоставляет источник данных, опреде-
ленный либо внутри проекта, либо внутри какой-то другой созданной вами сборки.
После выбора опции Database и щелчка на кнопке Next (Далее) откроется окно
Choose Your Data Connection (Выберите подключение к данным) мастера Data Source
Configuration Wizard, показанное на рис. 22.18.
Установленные подключения к существующим данным отображаются в раскрыва-
ющемся списке, из которого потребуется выбрать нужное. Можно также щелкнуть на
кнопке New Connection (Новое подключение), чтобы установить новое подключение
к источнику данных.
Data Source Configuration Wizard
Choose a Data Source Type
Where wil the < |>< hcation get data from?
Database Object
Lets you connect to a database and choose the database objects for your application This option creates a
dataset
Cancel
Puc. 22.12. Диалоговое окно Data Source Configuration Wizard
Доступ к источникам данных в приложении Windows Forms 1115
В результате откроется диалоговое окно Add Connection (Добавление подключе-
ния), изображенное на рис. 22.14.
Рис. 22.13. Окно Choose Your Data Connection мастера
Data Source Configuration Wizard
Add Connection
Enter information to connect to the selected data source or click
"Change" to choose a different data source and/or provider.
Data source:
Microsoft ODBC Data Source (ODBC)
Data source specification
Change...
О Lise user or system data source name:
••••••••aIIBBBII
Refresh
I
Use connection string:
Login information
User name:
Password:
^2
Advanced...
4 — —
I
Test Connection
Puc. 22.14. Диалоговое окно Add Connection
1116 Глава 22
В этом диалоговом окне можно ввести спецификацию источника данных в виде
имени либо, выбирая второй переключатель, в виде строки подключения к источни-
ку данных. После ввода любой необходимой регистрационной информации работу
подключения можно проверить, щелкая на кнопке Test Connection (Проверить под-
ключение). В моей системе источник данных ODBC выбран по умолчанию, но если
отображаемый по умолчанию источник данных вам не подходит, можно щелкнуть
на кнопке Change (Изменить), чтобы открыть диалоговое окно Change Data Source
(Изменение источника данных), показанное на рис. 22.15.
Рис. 22.15. Диалоговое окно Change Data Source
В этом диалоговом окне отображены доступные типы источников данных. Выбе-
рите тип источника данных, с которым желаете работать, и щелкните на кнопке
ОК. После этого произойдет возврат к диалоговому окну, показанному на рис. 22.14,
в котором можно дополнить и проверить идентификационные данные, прежде чем
щелкнуть на кнопке ОК и возвратиться в диалоговое окно, приведенное на рис. 22.18.
После щелчка на кнопке Next скорее всего откроется окно сообщения с вопросом о
необходимости добавления источника данных в проект. Щелчок на кнопке Yes (Да)
приведет к открытию окна Choose Your Database Objects (Выберите объекты базы
данных) мастера Data Source Configuration Wizard, показанного на рис. 22.16, где мож-
но выбрать объекты в базе данных, с которыми требуется работать.
В этом диалоговом окне можно выбирать объекты базы данных, которые долж-
ны быть добавлены в проект. Для этого необходимо развернуть дерево, чтобы мож-
но было выбирать отдельные таблицы или даже отдельные поля внутри таблицы.
Для данного примера можно упростить задачу и просто выбрать таблицу Customers
(Клиенты). Можно также изменить имя набора данных
Customers (Клиенты). После щелчка на кнопке Finish (Готово) мастер создаст код,
необходимый для получения доступа к базе данных.
База данных инкапсулирована в классе, производном от класса Dataset. Выбор
каждой таблицы базы данных приводит к определению класса, производного от
DataTable, в качестве внутреннего класса DataSet. В классе DataSet для каждого
класса DataTable определен также класс адаптера таблицы, который выполняет за-
дачу подключения к базе данных и загрузки данных из таблицы базы данных в соот-
ветствующий член DataTable объекта DataSet. Это ведет к генерации достаточно
большого объема кода. При выборе в базе данных Northwind одной только табли-
лично я изменил его на
Доступ к источникам данных в приложении Windows Forms 1117
цы Customers мы получаем около 2000 строк кода. Кроме того, мастер добавляет
в проект компонент BindingSource, который предоставляет интерфейс между та-
блицей Customers базы данных Northwind и элементом управления DataGridView.
Вкладка Design в окне редактирования должна выглядеть подобно изображенной на
Data Source Configuration Wizard
Choose Your Database Objects
Which database objects do you want in your dataset?
J Employees
□ order oetai Is
Products
suppliers
5? Stored Procedures
I* Functions
QatoSet name:
Customers
< Erevious
Eini^h
Cancel
Puc. 22.16. Окно Choose Your Database Objects мастера Data
Source Configuration Wizard
Puc. 22.17. Вкладка Design после выбора таблицы Customers
1118 Глава 22
Как видите, добавление источника данных привело к добавлению в проект трех
объектов: NorthWindDataSet, инкапсулирующего базу данных, CustomersTable-
Adapter, который осуществляет доступ к данным в таблице Customers базы данных,
и CustomersBindingSource, управляющего обменом данными между базой данных и
элементом управления DataGridView.
Теперь приложение готово, а мы пока не написали ни одной строки кода. Это до-
статочно удивительно, учитывая количество функций, выполняемых программой.
Однако при запуске программы заголовки столбцов выглядят несколько зауженными.
Я предпочитаю, чтобы ширина столбцов автоматически изменялась в соответствии с
текстом строк, поэтому не смог удержаться от того, чтобы не добавить в конструктор
класса Forml следующие две строки кода:
dataGridView->AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode::AllCells;
dataGridView~>AutoResizeColumnHeadersHeight ();
Теперь окно приложения выглядит, как показано на рис. 22.18.
Using a Binding Source Component „ п|
ContactName
Puc. 22.18. Отображение содержимого таблицы Customers
Customerl D Company Name
МММММйНМав
I Alfreds Futterkiste
ALFK1
ANATR
ANTO N
ARC UT
BERGS
BLAUS
BLONP
BO LID
BO NAP
BOTTM
Mana Anders
Ала Trujillo Emparedados у helados
Antonio Moreno Taqueroa
Around the Hom
Berglunds snabbkop
Blauer See Delikatessen
Blondel pere et fils
Bolido Comidas preparadas
Bon app1
Battom-Dollar MarKets
Ana Trujillo
Antonio Moreno
Thomas Hardy
Christina Berglund
Hanna Moos
Frederique Cjteaux
------------ - “
Mart'n Sommer
Laurence Lebihan
Elizabeth Lincoln
Для перемещения по данным можно использовать линейки прокрутки, а колесо
мыши должно выполнять прокрутку строк. Элемент управления BindingNavigator
мог бы несколько улучшить ситуацию, поэтому рассмотрим, как можно его использо-
вать.
Использование элемента
управления BindingNavigator
Элемент управления BindingNavigator специально предназначен для рабо-
ты с компонентом Bindingsource. Использование элемента управления Binding-
Navigator для навигации по данным, полученным из источника данных, вряд ли мог-
ло бы быть проще. Достаточно добавить элемент управления в форму и в качестве
значения свойства Bindingsource элемента управления установить переменную,
которая инкапсулирует компонент Bindingsource. В следующем примере программа
Ех22_03 соответствующим образом расширяется.
Доступ к источ:
:кам данных в приложении Windows Forms 1119
Практическое занятие
Использование элемента управления
BindingNavigator
Щелкните на элементе управления BindingNavigator в окне Toolbox, а затем в
клиентской области формы, чтобы добавить элемент управления в проект. Открыв
окно Properties элемента управления, установите для свойства Bindingsource зна-
чение CustomersBindingSource, которое представляет собой имя дескриптора эле
мента управления Bindingsource, инкапсулирующего таблицу Customers. Вот и все.
Если скомпилировать и запустить приложение, можно видеть, что теперь окно содер-
жит линейку прокрутки данных, как показано на рис. 22.19.
Рис. 22.19. Добавление элемента управления BindingNavigator
Для перемещения по записям данных в прямом и обратном направлении можно
использовать кнопки со стрелками. Окно содержит также кнопки для перемещения
к первой или последней записи. Если ввести порядковый номер записи в текстовом
поле панели инструментов и нажать клавишу <Enter>, программа выполнит переход
непосредственно к указанной записи. Возможность перемещения по записям не за-
висит от их порядка. Щелкая на заголовке столбца Country (Страна), можно было бы
упорядочить записи по странам, после чего перемещение по записям можно будет вы-
полнять именно в этом порядке. Элемент управления BindingNavigator предлагает
также кнопки для добавления и удаления записей.
Каждая из кнопок, предоставляемых элементом управления BindingNavigator,
устанавливает связь с членом объекта Bindingsource, в котором выполняется нави-
гация (табл. 22.10).
Таким образом, щелчок на кнопке навигационной панели инструментов иницииру-
ет действие в объекте Bindingsource, управляющем источником данных, но ни одна
из применяемых по умолчанию операций не изменяет базу данных, которой управ-
ляет Bindingsource. Чтобы выполнить какие-либо изменения, потребуется написать
определенный код.
1120 Глава 22
Таблица 22.10. Кнопки элемента управления BindingNavigator
Элемент управления панели инструментов Действие
Move First (Перейти к первой записи) Вызывает функцию MoveFirst () применительно к объекту эле- мента управления Bindingsource, которая изменяет указатель текущей записи используемого источника данных так, чтобы он указывал на первую запись.
Move Previous (Перейти к предыдущей записи) Вызывает функцию Move Previous () применительно к объекту элемента управления BindingSource, которая изменяет указа- тель текущей записи используемого источника данных так, чтобы он указывал на предыдущую запись, если таковая существует.
Current Position (Текущая позиция) Соответствует значению свойства Current объекта Bindingsource, которым является текущая запись используемо- го источника данных.
Total Number of Items (Общее количество элементов) Соответствует значению свойства Count объекта BindingSource, которое соответствует количеству записей в используемом источ- нике данных.
Move Next (Перейти к следующей записи) Вызывает функцию MoveNext () применительно к объекту эле- мента управления Bindingsource, которая изменяет указатель текущей записи используемого источника данных так, чтобы он указывал на следующую запись, если таковая существует.
Move Last (Перейти к последней записи) Вызывает функцию MoveLast () применительно к объекту эле- мента управления Bindingsource, которая изменяет указатель текущей записи используемого источника данных так, чтобы он указывал на последнюю запись.
Add New (Добавить новую запись) Вызывает функцию AddNew () для объекта BindingSource. Эта функция вызывает функцию EndEdit () для выполнения любых отложенных операций редактирования в используемом источнике данных и создает новую запись в списке, поддерживаемом объ- ектом BindingSource. Это не приводит к обновлению используе- мого источника данных.
Delete (Удалить) Вызывает функцию Removecurrent () применительно к объекту Bindingsource для удаления текущей записи из списка. Это не приводит к изменению используемого источника данных.
Практическое занятие ОбНОВЛвНИв бЭЗЫ ДЭННЫХ
Нам необходимо, чтобы при щелчке пользователя на кнопке в элементе управле-
ния BindingNavigator для добавления или удаления записи программа выполняла
определенные дополнительные действия. Этого можно достичь, реализовав функцию
обработчика события Click для кнопок.
Как вы уже знаете, обработчик события Click для кнопки можно добавить, дваж-
ды щелкнув на ней во вкладке Design. Поэтому добавьте функции обработчиков для
кнопок Add New (Добавить новую запись) и Delete (Удалить) панели инструментов.
На данный момент при щелчке на любой из кнопок компонент BindingSource содер-
жит все необходимое для обеспечения обновления базы данных. Нам остается только
вызвать функцию Update () применительно к объекту адаптера обновляемой табли-
цы. Эта функция генерирует исключение при наличии какой-либо ошибки, поэтому
Доступ к источникам данных в приложении Windows Forms 1121
ее вызов должен быть помещен в блок try, что позволит перехватывать любые воз-
можные исключения. Функцию обработчика события Click кнопки Add New можно
реализовать следующим образом:
System: :Void bindingNavigatorAddNewItem_Click(System::ObjectA sender,
System::EventArgsA e)
try
CustomersTableAdapter->Update(Customers->_Customers);
catch (Exception74 ex)
MessageBox: :Show(Ь”Обновление неудачно!\n"+ex,
Ь’’Ошибка обновления записи базы данных",
MessageBoxButtons::0К,
MessageBoxIcon::Error);
Аргументом функции Update () должно быть имя таблицы данных, которая содер-
жит значения, предназначенные для записи в базу данных, поэтому в данном случае
им является член —Customer объекта Customers класса Forml. В случае возникнове-
ния каких-то проблем функция обработчика отображает окно сообщения с пояснени-
ем сути проблемы. Окно сообщения отображает текст, полученный из сгенерирован-
ного объекта Exception и поясняющий причину возникновения проблемы.
Реализация обработчика события Click кнопки Delete практически идентична
предыдущей:
private: System::Void bindingNavigatorDeleteItem_Click(System::Object74 sender,
System: zEventArgs74 e)
try
CustomersTableAdapter->Update(Customers->_Customers);
catch (Exception74 ex)
{
MessageBox::Show(I/’Удаление неудачно!\n"+ex,
Ь"Ошибка удаления записи базы данных”,
MessageBoxButtons::0К,
MessageBoxIcon::Error);
Единственное различие между этими функциями — в тексте окна сообщения.
Теперь, при наличии этих двух функций обработчиков, мы должны иметь возмож-
ность добавлять новые записи клиентов и удалять существующие.
Привязка к отдельным элементам управления
Можно создать также приложение Windows Forms, которое связывает каждый
столбец таблицы базы данных с отдельным элементом управления; более того, соз-
дание такого приложения проще и быстрее, нежели выполнение предыдущего при-
мера. Начните с создания нового проекта CLR по имени Ех22_04, используя шаблон
Windows Forms Application (Приложение Windows Forms). Следующий шаг — добавле-
1122 Глава 22
ние в проект источника данных. Как и в предыдущем примере, мы будем работать
только с одной таблицей. Нажмите комбинацию клавиш <Shift+Alt+D>, чтобы открыть
окно Data Sources (Источники данных), и щелкните на кнопке Add New Data Source
(Добавить новый источник данных). Можно использовать базу данных Northwind или
любую другую базу данных по своему выбору, но следует помнить, что форма будет
содержать отдельные элементы управления для каждого столбца выбранной таблицы.
Чтобы количество элементов управления оставалось в разумных пределах, в случае
применения базы данных Northwind я предлагаю выбрать таблицу Order Details
(Сведения о заказе), как показано на рис. 22.20, поскольку она содержит всего лишь
пять столбцов.
Data Source Configuration Wizard
Choose Your Database Objects
Which database objects do you want in your data set?
_> Tables
□ J Categories
□ □ Customers
• □ _J Employees
0 3 order Detai is
0 Л OrderlD
0 =) ProductTO
0 3 UnitPrice
0 i] Quantity
0 Ц Discount
* □ _ Orders
♦ □ 2Э Products
• □ Shippers
6 LI U Suppliers
± 0 4. Views
[ j _*> Stored Procedures
П 1 Functions
pataSet name:
DataSet 1
finish
Cancel
Puc. 22.20. Выбор таблицы Order Details
После щелчка на кнопке Finish окно Data Sources отобразит в качестве источника
данных базу данных Northwind с единственной доступной таблицей Order Details.
Щелкните на имени таблицы Order Details, чтобы выбрать ее, а затем щелкните на
расположенной справа от нее стрелке вниз, чтобы открыть меню, приведенное на
рис. 22.21.
Три верхних элемента меню позволяют выбирать элементы управления для ис-
пользования с таблицей при добавлении источника данных в форму. Если щелкнуть
на элементе меню DatagridView, этот элемент управления будет выбран как отобража-
ющий таблицу, в то время как щелчок на элементе Details указывает, что для каждого
столбца таблицы требуется отдельный элемент управления. Если же щелкнуть на эле-
менте [None] ([Нет]), это свидетельствует, что при добавлении источника данных в
форму создание элементов управления не требуется. Последний элемент меню откры-
вает диалоговое окно, которое позволяет изменять элемент управления, используе-
мый для таблицы по умолчанию (в настоящий момент им является DataGridView), и
изменять выбор элементов управления в соответствии со своими потребностями. В
данном случае следует просто щелкнуть на элементе Details, поскольку нам требуется
по одному элементу управления для каждого столбца таблицы.
Доступ к источникам данных в приложении Windows Forms 1123
Data Sources
Э IbS1! DataSerl
iii Order Details
[None]
Customize
Puc. 22.21. Меню, связанное с таблицей
расположенном слева от имени таблицы. Если затем щелкнуть
Теперь необходимо решить, какой элемент управления применять для каждого
столбца таблицы. Дерево имен столбцов таблицы Order Details можно развернуть,
щелкая на символе
на имени первого столбца для его выбора, можно отобразить и его меню, щелкая на
стрелке вниз. Это меню показано на рис. 22.22.
Data Sources
.a a- ЬФ ,
I g ia9| DataSetl
g Order Details
labi] Order© = v
- "J
TextBox
NumericUpDown
ComboBox
Label
LinkLabel
ListBox
[None]
|ЭЫ|
abl
abl
зЫ
A
V
Customize
WReso
^Soluti
Рис. 22.22. Меню, связанное со столбцом
Можно выбрать любой из элементов управления, приведенных в меню, но я по-
лагаю, что для столбца OrderlD (Идентификатор заказа) больше всего подходит
элемент управления TextBox. Процесс необходимо повторить для каждого столб-
ца таблицы. Для столбца Quantity (Количество) можно выбрать элемент управле-
ния NumericUpDown, а для каждого из остальных столбцов — элементы управления
TextBox.
1124 Глава 22
Обратите внимание, что выбор опции меню Customize (Настроить) в панели Data
Sources (Источники данных), показанной на рис. 22.22, приводит к открытию диало-
гового окна Options (Параметры), в котором можно изменять набор элементов управ-
ления, доступных для отображения различных типов данных. Можно изменить также
выбор элемента управления по умолчанию при перетаскивании элемента с панели
Data Sources на форму. Диалоговое окно Options показано на рис. 22.23.
Options
i± Environment
♦ Performance Tools
14- Projects and Solutions
14- Source Control
i* TextEditor
i й Database Tools
it Debugging
it Device Tools
it html Designer
rt Microsoft office Keyboard settings
re Test Tools
| - Windows Forms Designer
General
Data UI Customization
Data type:
[List]
Associated controls:
E DataGridView (default)
□ Chart
Ei ComboBox
L2 Image
□ Line
Г1 List
: ] Rectangle
□ Subreport
□ Table
П T extbox
Set Default
Clear Default
r
r->"i how to add custom rnrerwh,.
I '
Cancel
Puc. 22.23. Диалоговое окно Options
элемента управления могут быть
На рис. 22.23 видны элементы управления, связанные с отображением списка,
причем для каждого доступного для выбора типа данных существует отдельный спи-
сок связанных с ним элементов управления. Кроме выбранного по умолчанию элемен-
та управления DataGridView были установлены флажки для элементов управления
ComboBox и ListBox. Таким образом, все эти
выбраны для отображения списка элементов данных. Другие типы данных можно вы-
брать в раскрывающемся списке, расположенном в верхней части диалогового окна,
для которого требуется изменить набор доступных элементов управления.
Заключительный шаг по созданию программы связан с перетаскиванием таблицы
Order Details из окна Data Sources в клиентскую область окна формы. После этого
вкладка Design панели Editor будет выглядеть так, как показано на рис. 22.24.
Свойство Text формы было изменено, чтобы выделить текст в строке заголовка, а
также слегка изменил расположение элементов управления, чтобы более равномерно
разместить их внутри клиентской области. Кроме того, изменено расположение эле-
ментов в серой области в нижней части формы, чтобы они все были видны в панели
Design. Как видите, пять элементов управления, соответствующие пяти столбцам та-
блицы, были автоматически добавлены в форму, вместе с надписями, указывающими,
с каким столбцом таблицы связан каждый элемент управления. Верхняя часть клиент-
ской области содержит элемент управления BindingNavigator, предназначенный
для навигации по данным таблицы. Под формой отображены элементы, которые по-
казывают, что приложение содержит компонент Bindingsource, связывающий базу
данных с элементами управления, и классы DataSet и адаптера таблицы.
Если нажать комбинацию клавиш <Ctrl+F5> для сборки и запуска программы, вы
получите готовую работающую программу для просмотра и редактирования таблицы
Доступ к источникам данных в приложении Windows Forms 1125
Order Details базы данных Northwind. В панель инструментов BindingNavigator
была добавлена кнопка Save (Сохранить), служащая для сохранения любых выполнен-
ных изменений в таблице базы данных. Окно приложения приведено на рис. 22.25.
i-orml.h [DesignJ
DataSetl
Order_DetailsTableAdapter
V7 Order_DetailsBindingSource
4113 Order_DetailsBindingNavigator
Рис. 22.24. Вкладка Design панели Editor после
перетаскивания таблицы Order Details
Рис. 22.25. Работа приложения просмотра и
редактирования Order Details
По сути, точно так же можно было бы создать программу, которая использует эле-
мент управления DataGridView для отображения всей таблицы. Если хотите прове-
рить это, просто создайте еще один проект и добавьте в него источник данных, как
было сделано в предыдущем примере. Если затем перетащить таблицу из окна Data
Sources на форму, приложение будет готово. Перед компиляцией и запуском програм-
мы нужно будет только установить значение свойства Text формы для отображения
текста строки заголовка и определить значение свойства Dock элемента управления
DataGridView.
1126 Глава 22
Работа с множеством таблиц
Создание приложения, которое работает с несколькими таблицами, не намно-
го сложнее создания предыдущего примера. В этом примере применяется элемент
управления с вкладками, чтобы можно было получать доступ к трем различным табли-
цам базы данных Northwind. Создайте новый проект Windows Forms, присвоив ему
имя Ех22_05, и нажмите комбинацию клавиш <Shift+Alt+D> для открытия окна Data
Sources. Добавьте в проект базу данных Northwind, выбрав ней таблицы Customers
(Клиенты), Products (Продукты) и Employees (Сотрудники).
Добавьте в окно Toolbox формы элемент управления Tab Control из группы
Containers (Контейнеры) и установите Fill для его свойства Dock. Щелчок на
стрелке в левом верхнем углу элемента управления позволит добавить в него третью
страницу вкладки. Перемещение между вкладками элемента управления TabControl
можно выполнять с помощью клавиши <ТаЬ> или же двойным щелчком на метке
страницы вкладки. Постарайтесь не щелкнуть дважды на элементе управления, по-
скольку это приведет к генерации для него функции обработчика. Перейдите к пер-
вой странице вкладки и установите Employees в качестве значения ее свойства Text.
Затем для значения свойства Text остальных двух вкладок установите, соответствен-
но, Customers и Products.
На каждую страницу вкладки потребуется также добавить элемент управления
Panel (Панель), установив Fill в его свойстве Dock. Это необходимо для правиль-
ного размещения элементов управления BindingNavigator и DataGridView на стра-
нице вкладки. Если их добавить непосредственно на вкладку, устанавливая значение
свойства Dock элемента управления DataGridView равным Fill при элементе управ-
ления BindingNavigator, привязанном к верхнему краю страницы вкладки, заголов-
ки столбцов элемента управления DataGridView будут скрыты элементом управления
BindingNavigator. Использование контейнера Panel позволяет избежать такой
ситуации. Можно также изменить свойство Text формы на какой-нибудь понятный
текст вроде "Accessing Multiple Tables" (“Доступ к множеству таблиц”).
Теперь можно перетащить таблицу Customers из окна Data Sources на панель стра-
ницы вкладки, названной Customers (Клиенты). Элемент управления DataGridView
добавлен в панель, поэтому элемент управления BindingNavigator виден в верхней
части формы. Нам необходимо, чтобы каждая страница вкладки обладала собствен-
ным элементом управления BindingNavigator, поэтому его размещение на форме
не удобно. Нужно будет переместить его в панель на странице вкладки. Это можно
выполнить, вначале установив None в качестве значения свойства Dock элемента
управления BindingNavigator, а затем перетащив его в панель на странице вкладки
Customers.
После добавления таблицы на страницу вкладки Customers перейдите на следую-
щую вкладку, прежде чем перетаскивать следующую таблицу в ее панель из окна Data
Sources. После добавления соответствующих таблиц в панели остальных двух стра-
ниц вкладок каждая из них будет содержать элемент управления DataGridView, но
не элемент управления BingingNavigator — мы не добавляли его, поскольку один
такой элемент управления уже был добавлен при добавлении таблицы Customers.
Элементы управления BingingNavigator можно было бы добавить на эти страни-
цы из окна Toolbox, но существует комбинация клавиш быстрого доступа, которой
можно воспользоваться. Щелкните на элементе управления BingingNavigator на
странице вкладки Customers и нажмите <Ctrl+C>, чтобы скопировать его в буфер
обмена. Перейдите к одной из остальных страниц вкладок, выберите ее панель и на-
Доступ к источникам данных в приложении Windows Forms 1127
жмите комбинацию клавиш <Ctrl+V>, чтобы вставить элемент управления из буфера
обмена в панель данной страницы вкладки. Перейдите на третью страницу вкладки,
выберите ее панель и снова нажмите комбинацию клавиш <Ctrl+V>, чтобы добавить
элемент управления BingingNavigator в панель этой страницы вкладки. Затем в
качестве значения свойства Dock каждого из трех элементов управления Binding
Navigator установите Тор, а в качестве значения Dock каждого из элементов управ-
ления DataGridView — Fill.
Значения свойства Bindingsource двух элементов управления BindingNavigator,
которые были созданы с помощью копирования, установлены неправильно. Поэтому
выберите поле значения этого значения каждого из этих элементов управления и выбе-
рите требуемый компонент Bindingsource из раскрывающегося списка. Для элемента
управления на странице вкладки Employees нужно выбрать EmployeesBindingSource,
а для страницы вкладки Products — ProductsBindingSource.
Если теперь нажать комбинацию клавиш <Ctrl+F5> для сборки и запуска програм-
мы примера, должно открыться окно приложения, показанное на рис. 22.26.
Accessing Multiple Tables
i Employees Customers Products
of 5
Employee ID Last Name
Davolio
Fuller
Leveding
Peacock
Buchanan
6 Suyama
7 King
First Name
Nancy
Andrew
Janet
Margaret
1 Steven
Michael
Robert
. Till*
e |Г
Sal
I- '
I Vic
Sal R
|Sal
Sal —
(Sal
Sale
Puc. 22.26. Приложение, работающее с множеством таблиц
Как видите, работающее приложение было получено без написания даже единой
строчки кода. Думаю, вы согласитесь с тем, что описанная методика — замечательная
возможность генерации приложений для получения доступа к источникам данных.
Резюме
Эта глава служит кратким введением в существующие возможности получения до-
ступа к источникам данных в приложениях Windows Forms. Реальные возможности
значительно шире описанных, тем не менее, вы получили хорошее представление о
применении средств проектирования для компоновки элементов управления на фор-
ме и о совместной работе элементов управления при обращении к данным. В случае
необходимости перехода к другим областям применения у вас не должно возникать
особых проблем.
Ниже перечислены ключевые моменты, с которыми вы познакомились в настоя-
щей главе.
1128 Глава 22
□ Источником данных может быть реляционная база данных, Web-служба или
объект.
□ Класс, производный от System::Data: :DataSet, используется для инкапсуля-
ции источника данных, а класс, производный от System: :Data: :DataTable
инкапсулирует таблицу источника данных.
□ Строка данных в объекте DataTable представлена объектом типа System: :
Data: :DataRow, а схема столбца описывается объектом типа System: :Data: :
DataColumn.
□ Подключение к источнику данных и команды доступа к данным инкапсулирова-
ны в компоненте, который называется адаптером таблицы.
□ Элемент управления DataGridView используется в форме для отображения
данных в виде прямоугольной таблицы.
□ Элемент управления DataGridView можно привязывать к источнику данных
для отображения содержимого таблицы. Этот элемент управления можно ис-
пользовать также в несвязанном режиме для отображения данных, создавае-
мых непосредственно в программе.
□ Элемент управления DataGridView можно настраивать для изменения способа
отображения строк, столбцов, заголовков и отдельных ячеек.
□ Компонент Bindingsource предоставляет интерфейс между источником дан-
ных и элементами управления формы. Этот компонент может связывать столб-
цы таблицы с отдельными элементами управления формы или содержимое
всей таблицы с одним элементом управления DataGridView.
□ Элемент управления BindingNavigator предоставляет панель инструментов
для навигации по данным, доступ к которым осуществляется с помощью компо-
нента Bindingsource.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издатель-
ства.
1. Измените код примера Ех22_04 так, чтобы заголовки столбов отображались
шрифтом размером 12 пунктов с курсивным начертанием.
2. Измените код примера Ех22_05 так, чтобы столбцы стали достаточно широки-
ми для вмещения текста каждой из ячеек.
Измените код примера Ех22_05 так, чтобы чередующиеся строки данных на
каждой из страниц вкладок отображались затененными.
4. Создайте приложение Windows Forms, которое будет отображать содержимое
таблицы Suppliers (Поставщики) базы данных Northwind.
A
Ключевые слова C++
В языке C++ ключевым словам придается особое значение, поэтому они не должны
использоваться в программах в качестве идентификаторов. Компилятор Visual C++
2005 распознает программы, написанные как на языке ISO/ANSI C++, так и предна-
значенные для работы в среде CLR, которые соответствуют спецификации C++/CLL
Поэтому компилятор распознает как ключевые слова ISO/ANSI C++, так и дополни-
тельный набор ключевых слов, определенный в спецификации C++/CLI. При напи-
сании программ для среды CLR следует учитывать оба набора ключевых слов.
Ключевые слова ISO/ANSI C++
Ниже перечислены ключевые слова, определенные в спецификации языка ISO/
ANSI C++.
asm false sizeof
auto float static
bool for static_cast
break friend struct
case goto switch
catch if template
char inline this
class int throw
const long true
const_cast mutable try
continue namespace typedef
default new typeid
ИЗО Приложение А
Окончание таблицы
delete
do
double
dynamic_cast
else
enum
explicit
export
extern
operator
private
protected
public
register
reinterpret_cast
return
short
signed
typename
union
unsigned
using
virtual
void
volatile
wchar_t
while
Ключевые слова C++/CLI
В спецификации языка C++/CLI определены следующие ключевые слова в допол-
нение к тем, которые определены спецификацией ISO/ANSI C++:
enum class enum struct for each gcnew
interface class interface struct nullptr ref class
ref struct value class value struct
Обратите внимание, что пары слов являются ключевыми словами, но отдельные
слова могут и не быть ими. Например, for each — ключевое слово, a each — нет.
В спецификации языка C++/CLI определен также набор идентификаторов, кото-
рые не являются ключевыми словами, но в ряде ситуаций имеют контекстно-зависи-
мое значение. Эти идентификаторы перечислены ниже.
abstract delegate event finally
generic in initonly internal
literal override property sealed
where
В принципе, эти идентификаторы можно использовать в коде в качестве имен,
поскольку ситуации, в которых они имеют специальное значение, определяются кон-
текстом. Однако рекомендуется относиться к ним как к ключевым словам и не приме-
нять их для других целей. Это позволит избежать возможной неправильной трактов-
ки кода другим читающим его человеком.
Б
Коды ASCII
Первые 32 символа ASCII (American Standard Code for Information Interchange —
американский стандартный код обмена информацией) выполняют управляющие
функции. В табл. Б.1 показано только первых 128 символов ASCII. Остальные 128
символов представляют специальные символы и буквы национальных наборов симво-
лов, поэтому существует множество их наборов, предназначенных для использования
в широком множестве языковых контекстов.
Таблица Б.1. Коды ASCII
Десятичный код Шестнадцатеричный код Символ Управляющая функция
ООО 00 null NUL
001 01 © SOH
002 02 • STX
003 03 ЕТХ
004 04 ♦ EOT
005 05 * ENQ
006 06 АСК
007 07 • BEL (Audible bell)
008 08 Забой
009 09 НТ
010 0А LF (перевод строки)
011 ОВ VT (вертикальная табуляция)
012 ОС FF (перевод страницы)
013 0D CR (возврат каретки)
014 0Е SO
015 0F п SI
1132 Пресложение Б
Десятичный код
Шестнадцатеричный код
Символ
Управляющая функция
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
10
11
12
13
14
15
16
17
18
19
1А
1В
1С
1D
1Е
1F
20
21
22
23
24
25
26
27
28
29
2А
2В
2С
2D
2Е
2F
30
31
32
33
34
35
36
37
#
$
%
&
с
(
о
1
2
3
4
5
DLE
DC1
DC2
DC3
DC4
NAK
SYN
CAN
ЕМ
SUB
ESC (Escape)
FS
GS
RS
US
пробел
Коды ASCII 1133
Десятичный код
Шестнадцатеричный код
Символ
Управляющая функция
*
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
38
39
ЗА
ЗВ
ЗС
3D
ЗЕ
3F
40
41
42
43
45
46
47
48
49
4А
4В
4D
4Е
4F
50
51
52
53
54
55
56
57
58
59
5А
5В
5С
5D
5Е
5F
А
В
С
D
Е
G
Н
J
К
м
N
О
Р
R
S
U
V
W
X
Y
Z
[
1134 Приложение Б
Десятичный код
Шестнадцатеричный код
Символ
Управляющая функция
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
60
61 a
62 b
63 c
64 d
65 e
67 g
68 h
69 i
6D
6E
6F
70
71
72
73
m
n
0
p
q
s
74 t
75 u
76 v
77 w
78 x
79 у
7A z
7B {
7C |
7D }
7E
127
7F
delete
удаление
Коды Unicode, числовые значения которых совпадают с кодами ASCII, приведен-
ными в этой таблице, представляют такие же символы. Подробную информацию о
системе кодировки Unicode можно найти на сайте http: //www. unicode. org.
Предметный указатель
А
Application Wizard, 41
ASCII (American Standard Code for
Information Interchange), 81
c
CLI (Common Language Infrastructure), 28
CLR (Common Language Runtime), 28; 62
CTS (Common Type System), 29
D
DDV (Dialog Data Validation), 838
DDX (Dialog Data Exchange), 837
DLL (Dynamic Link Library), 898
E
ECMA (European Association for
Standardizing Information and
Computer Systems), 28
G
GDI (Graphical Device Interface), 711
I
IDE (Integrated Development
Environment), 27; 34
lvalue (left value — левое значение), 93
м
MDI (Multiple Document Interface), 629;
651
MFC (Microsoft Foundation Classes), 29;
36; 642
MSDN (Microsoft Development Network), 39
MSIL (Microsoft Intermediate Language), 29
N
.NET Framework, 28
0
ODBC, 928
Property Manager, 37
R
RDBMS (relational database management
system), 921
rvalue (right value — правое значение), 93
s
SDI (Single Document Interface), 651
Solution Explorer, 37; 57; 59
SQL (Structured Query Language), 922
w
Windows Forms, 646; 1027
A
Автоматическая переменная, 113
Адаптер таблицы, 1088
Аргумент, 127; 250
Арифметическое выражение, 73
Ассоциативность, 103
Атрибут, 711
Б
База данных
обновление, 983
Библиотека, 36
Windows Forms, 36
динамически подключаемая (DLL), 898
классов
MFC, 29; 36; 642
.NET Framework, 28
разработчика MSDN, 39
стандартная C++, 36
Блокировка записей, 978
Блок операторов, 75
1136 Предметный указатель
в
Ввод, 187
с клавиатуры, 88; 131
строковый, 187
Венгерская запись, 622
Время жизни
статическое, 116
Время хранения, 112
автоматическое, 113
статическое, 119
Вывод
в командную строку, 88
командной строки C++/CLI, 128
форматированный, 89; 128; 130
Выражение
арифметическое, 73
д
Делегат, 540
несвязанный, 544
связанный, 544
Деструктор, 412; 517
виртуальный, 517
класса, 411
по умолчанию, 412
Джордж Буль, 83
Диалог, 815
Диалоговое окно, 815
Add Column, 1094
Add Connection, 1115
Add New Item, 68; 1054
Change DataSource, 1116
Data Source Configuration Wizard, 1.114
Edit Columns, 1095
Items Collection Editor, 1035
MFC ODBC Consumer Wizard, 949
New Project, 41
Options, 55; 1124
Select Database Object, 933; 950
Select Data Source, 933
String Collection Editor, 1068
Tab Page Collection Editor, 1038
Toolbox
создание, 1053
Динамическое связывание
во время выполнения, 901
во время загрузки, 901
Директива
#include, 44; 70
using, 120
Диспетчеризация сообщения, 632
Диспетчер свойств, 37
Документ, 652
Документация, 39
3
Заголовок функции, 251
Запись
блокировка, 978
набор записей snapshot (снимок), 934
сортировка, 927
И
Идентификатор, 77
Инициализация, 79
массива, 184
Инкапсуляция, 349
Интегрированная среда разработки, 27; 34
Интерфейс, 652
DLL, 909
ODBC, 928
графических устройств (GDI), 711
класса, 446
многодокументный, 652
однодокументный, 652
Исключение, 294
возбуждение, 296
обработка в MFC, 298
перехват, 297
Источник данных, 1088
К
Карта сообщений, 680
Кисть, 718; 721
Класс, 349; 481
CArchive, 868
САггау, 761
CCircle, 744; 877
CCmdUI, 700
CCurve, 747; 774
CDatabase, 928; 979
CDBException, 928
CDC, 715
CDialog, 822
CElement, 737
Предметный указатель 1137
CFieldExchange, 928
CGdiObject, 723
CLine, 738; 877
CList, 763; 773
CObject, 761; 870
CPen, 719
CPenDialog, 826
CPrintlnfo, 883; 884
CProductset, 945
CRecordset, 928; 976
CRecordView, 928; 940; 945
CRectangle, 743
CSketcherDoc, 867
CSketcherView, 726
CText, 877
CTypedPtrList, 771
CWinApp, 643
Debug, 605
Form, 1029
Forml, 1029
Trace, 605
абстрактный, 512
базовый, 483
вложенный, 522
интерфейсный, 530
коллекции, 556
регистрация класса окна, 628
типа значения, 124
Ключ
вне:
ний, 922
первичный, 921
Ключевое слово, 78
const, 95
enum, 133
typedef, 85
Ключевые слова
C++/CLI, ИЗО
ISO/ANSI C++, 1129
Код
C++, 30
неуправляемый, 30
управляемый, 30
объектный, 36
Коллекция, 760
карта, 760; 769
массив, 760; 762
список, 760; 764
Комментарий, 70
Компилятор, 36
Компонент BindingSource, 1113
Компоновка, 898
статическая, 898
Компоновщик, 36
Консольное приложение, 32
CLR, 33
Win32, 33
Константа
перечислимая, 87
с плавающей точкой, 83
Конструктор
класса, 356
копирования, 370; 417; 497
статический, 406
Контейнер, 1028
Контекст устройства, 711
Конфигурация, 46
Координаты, 787
клиентские, 787
логические, 787; 840
страничные, 840
устройства, 840
экранные, 840
Куча, 211
Л
Литерал, 84
М
Макрос, 667; 680
ASSERT_VALID (), 752
BEGIN_MESSAGE_MAP(), 680
DECLARE_DYNAMIC(), 870
DECLARE_DYNCREATE(), 668; 867; 870
DECLARE_MESSAGE_MAP(), 667; 680
DECLARE-SERIALO, 870
END_MESSAGE_MAP(), 680
IMPLEMENT_DYNCRETE(), 867
ON_COMMAND, 682
Манипулятор, 89
Массив, 81; 180
CLR, 219
зубчатый, 230
инициализация, 184
массивов, 230
многомерный, 189; 227
объявление, 181
1138 Предметный указатель
символьный, 186
указателей, 199
Мастер
Add Member Function Wizard, 460
Add Member Variable Wizard, 736; 836;
1005
Application Wizard, 41; 42; 656; 887; 935
C++ Class Wizard, 735
Data Source Configuration Wizard, 1114
Event Handler Wizard, 691; 991
Generic C++ Class Wizard, 454
MFC Application Wizard, 659
MFC Class Wizard, 821
MFC ODBC Consumer Wizard, 949
Меню
системное, 618
управляющее, 618
Метка, 155
Метод, 349
Многозадачность, 632
вытесняющая, 632
кооперативная, 632
Модификатор
const, 95; 266
signed, 82
Модификация исходного кода, 44
н
Набор записей, 923
dynaset (динамический набор), 934
snapshot (снимок), 934
добавление фильтра, 956
настройка, 955
сортировка, 947
Наследование в классах, 483
Настройка опций Visual C++ 2005, 54
О
Область, 845
видимости, 112
блока, 113
локальная, 113
переменной, 112; 116
обновления, 732
Draw(), 737
GetBoundRect(), 737
Общая система типов, 29
Общеязыковая исполняющая среда
(CLR), 28; 62
Объединение, 421
анонимное, 421
в классах и структурах, 421
Объект, 133
Объектно-ориентированное
программирование (ООП), 349
Объектный
код, 36
файл, 36
Объявление using, 71
Окно
дочернее, 617
обрамляющее, 653
проводника решений, 37
редактора, 37
родительское, 617
Оператор, 72; 141
tfinclude, 71
continue, 162
do-while, 167
for, 158
for each, 175
if, 141
вложенный, 142
расширенный, 144
if-else, 145
вложенный, 146
int, 78
return, 253
switch, 152
using, 120
while, 166
вывода, 73
присваивания, 73; 93
составной, 75
Операция, 139
!=, 140
&, 107; 194
*, 93; 194
+, 93
++, 100
„101
-, 93
-, 100
->, 346
/,93
Предметный указатель 1139
117
<, 140
«, 73; 88; 107; 110
<=, 140
==, 140
>, 140; 423
>=, 140
», 88; 107; 110
Л, 107
1,107
~, 107
delete, 212
new, 212
safe_cast, 132
sizeof, 201
арифметическая, 93
битовая, 106
сдвига, 110
ввода-вывода, 88
декремента, 100
запятой,101
инкремента, 100
логическая, 147
И, 148
ИЛИ, 149
НЕ, 149
отношения, 139
получения адреса, 194
порядок выполнения операций, 102
разрешения контекста, 71; 117
разыменования, 194
тернарная, 150
унарная, 94
условная, 150
Откат, 979
Отладка, 567
Ошибка
семантическая, 569
синтаксическая, 569
п
Панель инструментов, 38
плавающая, 39
стандартная, 39
стыкуемая, 39
Параметр, 250
Перегрузка операции, 385; 422; 424; 470
декремента, 437; 475
инкремента, 437; 475
Перегрузка функций, 300
Переменная, 72; 76
автоматическая, 113
булевская, 83
глобальная, 115
инициализация, 79
логическая, 83
объявление, 72; 78
символьная, 81
статическая, 119; 276
с определенным набором значений, 86
тип, 72
функциональная нотация, 79
целочисленная, 80
элемента управления, 836
Перечисление, 86
Перечислитель, 86
Перо, 719
Печать документа, 881; 890
многостраничного, 885
Поле, 349; 920
литеральное, 389; 404
Поток, 73
Представление, 652; 653
Приведение типов, 97; 103
в операторах присваивания, 105
в старом стиле, 106
неявное, 97
правила приведения операндов, 103
явное, 97; 105
Приложение
MFC, 55
Windows Forms, 58; 647; 1028
консольное, 32
CLR, 33
Win32, 33
Пробел, 74
Проводник решений, 37
Программа
"родная", 28
CLR, 28
выполнение, 47
отладочная, 46
рабочая, 46
управляемая событиями, 34
Проект, 40; 62
определение, 40
1140 Предметный указатель
Пространство имен, 71; 119
System "Windows:: Forms, 1089
библиотеки .NET, 1029
System, 1029
System "Collections, 1029
System::ComponentModel, 1029
System: :Data, 1029
System:: Drawing, 1029
System:: Windows: :Forms, 1029
множественное, 122
объявление, 121
Прототип функции, 253
Распаковка, 124
Регистрация класса окна, 628
Редактор, 35
ресурсов, 656
Режим отображения, 711
MM_ANISOTROPIC, 712
MM_HIENGLISH, 712
MM_HIMETRIC, 712
MMJSOTROPIC, 712
MM_LOENGLISH, 712
MM_LOMETRIC, 712
ММ_ТЕХТ, 711
MM_TWIPS, 712
сборка, 46
Сборка (assembly), 51; 62
DLL-библиотеки, 910
решения, 46
Свойство, 392; 687
индексированное, 393; 399
скалярное, 393; 396
только для записи, 393
только для чтения, 392
Сериализация, 668; 738; 761; 866
документа, 874
классов элементов, 875
Система управления реляционными
базами данных (СУРБД), 921
Событие, 33; 540
Сокет, 905
Сообщение
диспетчеризация, 632
командное, 682
очередизованное, 630
очередь, 619
подкачка, 630
Сортировка записей, 927; 947
Спецификатор
static, 119
unsigned, 82
формата, 130
Среда
.NET Framework, 28
общеязыковая исполняющая, 28
Ссылка, 216; 265
Стандартная библиотека C++, 36
Стандарт
C++/CLI, 31; 32
C++ ISO/ANSI, 31
ЕСМА, 28
ISO/ANSI, 32
Стек вызовов, 588
Строка, 233
модификация, 237
объединение, 234
поиск, 239
строковый ввод, 187
Структура, 338
доступ к членам структуры через
указатель, 345
Счетчик с плавающей точкой, 165
Таблица
добавление строк в таблицу, 995
истинности, 107
соединение таблиц, 922
с помощью SQL, 924
Текущая позиция, 715
Тело функции, 252
Тип
данных, 79
bool, 83; 84
char, 81
double, 84
float, 83; 84
int, 80; 84
long, 80; 84
long double, 84
long long, 124
Предметный указатель 1141
short, 80; 84
signed char, 82; 84
signed int, 82
signed long, 82
unsigned char, 84
unsigned int, 82; 84
unsigned long, 84
unsigned long long, 124
unsigned short, 84
wchar_t, 84
char, 84
фундаментальный, 79
переменной, 72
Транзакция, 979
Указатель, 193; 287
char *,197
this, 370
внутренний, 242
инициализация, 196
объявление, 193
Упаковка, 124
Управляющая последовательность, 48; 90
\”, 91
V, 91
\?, 91
\\,91
\а, 91
\Ь, 91
\п, 91
\t, 91
Утечка памяти, 273; 600
Утилита Character Мар, 82
Ф
Файл, 36; 46
.dll, 912
.ехе, 46
.h, 912
.idb, 46
.ilk, 46
.lib, 912
.obj, 46
.pch, 46
.pdb, 46
<iomanip>, 90
<iostream>, 70
tchar.h, 76
заголовка, 70
объектный, 36
ресурсный, 618
Форма, 1027
Windows, 646
Функциональная нотация, 79
Функция, 64; 177; 250
-член, 353
Add(), 1092
AddHead(), 764; 772
AddNew(), 976; 977
AddOrder(), 1020; 1022
AddOrderDetails (), 1022
AddSegment (), 776
AddTail(), 764; 772
Arc(), 718; 746
AutoResizeColumnHeadersHeight(), 1104
AutoResizeColumns(), 1102
AutoSizeColumns(), 1103
BeginPrint(), 637
BeginTrans(), 980
BeginWaitCursor(), 958
CancelUpdate Q, 976; 994
CanUpdate(), 978; 994
cell(), 897
Char::IsLetter(), 176
Char::ToLower(), 177
CheckDlgButton0, 828
CheckMenuItem(), 799
Clear0, Ю92
CommitTrans(), 980
CompareElements(), 766
ConstructElements(), 763; 768
Create(), 644
CreateElement(), 750
CreateHatchBrush (), 722
CreateNewOrderID(), 1012
CreatePen(), 720
CreateSolidBrush(), 722
CreateWindow(), 628
DDX_(), 984
DDX_FieldText(), 944
DDX.TextO, Ю12
Delete(), 976
DestructElements0, 763; 768
DisplayMessage(), 418
DllMain(), 902; 906
1142 Предметный указатель
DoDataExchange(), 837; 939; 944; 954; 1007
DoFieldExchange(), 984
DoFileExchange(), 939
DoModal(), 829
DoPreparePrintingO, 887
Draw(), 747
Edit(), 976; 994
Ellipse(), 718; 746
Enable(), 991
EndWaitCursor (), 958
extract(), 319
Find(), 772
FindNext(), 772
get(), 1069
GetAt(), 772
GetCapture(), 756
GetCount(), 767; 772
GetDefaultConnect(), 936
GetDefaultSQL(), 938
GetDeviceCaps (), 842
GetDlgItem(), 838
GetDocument(), 714
GetFromPage(), 884
GetHead(), 772
GetHeadPosition(), 766; 772
getline(), 187
GetMaxPage (), 884
GetMessage(), 631
GetMinPage(), 884
GetNext(), 765; 772
GetPenWidth(), 830
GetPrev(), 772
GetRecordset(), 1000; 1002
GetTail(), 772
GetTailPosition(), 766; 772
GetToPage(), 884
GetValues(), 1050
HashKeyO, 770
InflateRect(), 742
InitializeComponent(), 1030
InitializeView(), 1019
Initlnstance(), 671
Insert(), 1092
InsertAfter(), 765; 772
InsertBefore(), 765; 772
InsertCopy(), 1092
InvalidateRect(), 732
IsEmptyQ, 767; 772
IsEOF(), 964
IsOnFirstRecord(), 992
LineTo(), 744
LPtoDP(), 843
main(), 64; 72; 267; 1031
max(), 777
min(), 777
Move(), 984; 1014
MoveElement(), 879
MoveFirst(), 1014
MoveLast(), 1014
MovePrev(), 1014
MoveTo(),715
OnActivateView(), 964; 969
OnBeginPrintingO, 896
OnCancel(), 825
OnContextMenu(), 799
OnDraw(), 714
OnEndPrinting(), 896
OnGetRecordset(), 955; 1002
OnlnitDialogO, 838
OnInitialUpdate(), 844; 968; 1001
OnLButtonDown(), 728
OnLButtonUpO, 728
OnMouseMove(), 728
OnOKO, 825
OnOrders(), 968
OnPrepareDC(), 841; 896
OnPreparePrinting(), 886; 896
OnPrint(), 896
OnProducts(), 968
PFX_(), 939
PolyLine(), 744
Read(), 131
ReadKeyO, 131
ReadLine(), 131
Rectangle(), 744
Remove(), 1092
RemoveAll(), 772
RemoveAt(), 772; 1092
RemoveHead(), 772
RemoveTail(), 772
Report Error (), 994
RFX_(),984
Rollback(), 980
Run(), 673
SelectObject(), 720
SelectStockObject(), 723
Предметный указатель 1143
SELECTView(), 959 X
SelectView(), 1008; 1015 SendToBack(), 812 Serialize(), 867; 868; 872; 876 SerializeElementsO, 763 set(), 1069 SetAt(), 766; 772 SetCapture(), 755 SetFieldType(), 939 SetMaxPage(), 884 SetMinPage(), 884 SetROP2(), 748 SetScrollSizes(), 786 SetTextAllign(), 893 SetValues(), 1051 Show(), 1062 ShowDialog(), 1060 ShowWindow(), 990 ToStringO, 1104 TrackPopupMenu(), 795 Update(), 976; 977; 993; 1121 UpdateAllViews(), 783 UpdateData(), 1015; 1022 WindowProc(), 623; 630; 635 WinMain(), 624; 632; 635 Write(), 127 WriteLine(), 127 виртуальная, 503; 509 чистая, 511 вспомогательная, 763 встроенная, 355 дружественная, 367 заголовок, 251 обобщенная, 323 перегрузка функций, 300 прототип, 253 рекурсивная, 278 статически связываемая, 897 тело,251 шаблон,303 Хеширование, 768 ц Цикл, 155 do-while, 167 for, 156 for each, 175 while, 166 бесконечный, 160 вложенный, 168 ш Шаблон документа, 654 класса, 438; 442 функции, 303 Штриховка, 722 э Экземпляр, 438 Экспорт объекта, 909 Элемент управления, 816 BindingNavigator, 1118 Button, 1041 ComboBox, 1068 ContextMenuStrip, 1045 DataGridView, 1090 GroupBox, 1039 MenuStrip, 1033 TabControl, 1036 WebBrowser, 1043 дружественный, 832 Я Язык MSIL, 29 SQL, 922
Научно-популярное издание
Айвор Хортон
Visual C++ 2005:
базовый курс
Верстка Т.Н. Артеменко
Художественный редактор В.Г. Павлютин
Издательский дом “Вильямс”
127055, г. Москва, ул. Лесная, д. 43, стр. 1
Подписано в печать 31.01.2007. Формат 70x100/16.
Гарнитура Times. Печать офсетная.
Усл. печ. л. 92,88. Уч.-изд. л. 73,05.
Тираж 3000 экз. Заказ № 9.
Отпечатано по технологии CtP
в ОАО “Печатный двор” им. А. М. Горького
197110, Санкт-Петербург, Чкаловский пр., 15.
ОСВОЙ САМОСТОЯТЕЛЬНО C++
ЗА 24 ЧАСА
ЧЕТВЕРТОЕ ИЗДАНИЕ
Джесс Либерти
Дэвид Б. Хорват, ССР
www.williamspublishing.com
Эта книга поможет
самостоятельно изучить
язык C++, его принципы и
концепции. Здесь изложены
фундаментальные основы
программирования,
описаны принципы
управления вводом-
выводом, циклы, массивы,
объектно-ориентированные
подходы, а также создание
полнофункционального
приложения. Все главы
содержат листинги
программ, результаты
их выполнения и анализ
кода. Приведены ответы на
часто задаваемые вопросы,
а также упражнения и
контрольные вопросы.
Изложение книги не
предполагает наличия у
читателя предварительных
знаний в области C++,
а четкая организация
материала позволит быстро
и просто изучить язык.
ISBN 5-8459-0949-Х
в продаже
освой
САМОСТОЯТЕЛЬНО C++
ЗА 21 ДЕНЬ,
пятое издание
Джесс Либерти,
Брэдли Джонс
www.williamspublishing.com
Книга поможет самостоятельно
изучить язык C++, его принципы
и концепции. Здесь изложены
фундаментальные основы
программирования, управление
вводом-выводом, циклы, массивы,
объектно-ориентированные
подходы, а также создание
полнофункционального
приложения. Все главы содержат
листинги программ, результаты
их выполнения и анализ кода.
Приведены ответы на часто
задаваемые вопросы, а также
упражнения и контрольные
вопросы. Изложение не
предполагает наличия у читателя
каких либо знаний в области
C++, а четкая организация книги
позволит быстро и просто
изучить язык.
ISBN 5-8459-0926-0
в продаже
C++: БАЗОВЫЙ КУРС
третье издание
Герберт Шилдт
www.williamspublishing.com
В этой книге описаны все
основные средства языка C++:
от элементарных понятий
до супервозможностей.
После рассмотрения основ
программирования на C++
(переменных, операторов,
инструкций управления,
функций, классов и объектов)
читатель освоит такие более
сложные средства языка,
как механизм обработки
исключительных ситуаций
(исключений), шаблоны,
пространства имен,
динамическая идентификация
типов, стандартная
библиотека шаблонов (STL),
а также познакомится
с расширенным набором
ключевых слов, используемым
в программировании для
.NET. Автор справочника —
общепризнанный авторитет
в области программирования
на языках С и C++, Java
и C# — включил в текст своей
книги и советы программистам,
которые позволят повысить
эффективность их работы.
ISBN 5-8459-0768-3
в продаже
язык
ПРОГРАММИРОВАНИЯ C++.
ВВОДНЫЙ КУРС
Стэнли Б. Липпман,
Жози Лажойе, Барбара Му
www.williamspublishing.com
Нынешнее издание столь
популярного вводного курса
стандартного языка C++
было полностью переписано
так, чтобы помочь быстрее
и эффективнее научиться
программировать на этом
языке. Теперь стандартная
библиотека C++ описана с
самого начала, что позволяет
читателю сразу приступить
к созданию работоспособных
программ, еще до изучения
подробностей языка. Здесь
содержатся полезные советы,
которые помогут облегчить
создание программ, а также
повысить их эффективность.
Примеры, в которых
используются возможности
библиотек, позволяют
продемонстрировать
достоинства языка C++, а
также наиболее эффективные
приемы его применения.
Как и в предыдущих
изданиях, здесь обсуждаются
фундаментальные концепции
и методы языка C++, что
делает книгу ценнейшим
ресурсом даже для опытных
программистов.
ISBN 5-8459-1121-4 в продаже
C++ ДЛЯ "ЧАЙНИКОВ"
5-Е ИЗДАНИЕ
С. Дэвис
www.dialektika.com
Книга представляет
собой введение в язык
программирования C++.
Основное отличие данной
книги от предыдущих изданий
C++ для “чайников” в том,
что это издание не требует
от читателя каких-либо
дополнительных знаний, в
то время как предыдущие
издания опирались на
знание читателем языка
программирования С. Книга
отличается также тем,
что несмотря на простоту
изложения материала, он
подан достаточно строго.
Поэтому, изучив основы
программирования на
C++, читателю не придется
пересматривать свои знания
при дальнейшем изучении
языка.Книга отличается
широким охватом тем — от
простейших объявлений
переменных до концепций
объектно-ориентированного
программирования,
вопросов перегрузки
операторов и множественного
наследования, причем весь
материал снабжен множеством
практических примеров.
ISBN 5-8459-0723-3
в продаже
полный
СПРАВОЧНИК
по C++,
4-Е ИЗДАНИЕ
Герберт Шилдт
www.williamspublishing.com
В четвертом издании "Полного
справочника по C++"полностью
описаны и проиллюстрированы
все ключевые слова, функции,
классы и свойства языка C++,
соответствующие стандарту
ANSI/ISO. Информацию,
изложенную в книге, можно
использовать во всех современных
средах программирования.
Освещены все аспекты языка C++,
включая его основу — язык С.
Справочник состоит из пяти
частей:
1) подмножество С;
2) язык C++;
3) библиотека стандартных
функций;
4) библиотека стандартных
классов;
5) приложения на языке C++.
Для широкого круга
программистов.
язык
ПРОГРАММИРОВАНИЯ C++
ЛЕКЦИИ И УПРАЖНЕНИЯ
5-е издание
Стивен Прага
www.williamspublishing.com
Книгу отличает простой
и доступный стиль
изложения, изобилие
примеров и множество
рекомендаций по написанию
высококачественных
программ. Подробно
рассматриваются такие
вопросы, как представление
данных, операции и
операторы, управляющие
структуры и функции.
Немалое внимание уделяется
работе с классами, шаблонами
и пространствами имен, а
также генерации и обработке
исключений. Исчерпывающие
сведения о концепциях
объектно-ориентированного
программирования
дадут возможность
максимально успешно и
эффективно создавать
живучий программный код.
Приводимые в конце каждой
главы вопросы и упражнения
для самостоятельной
проработки позволят надежно
закрепить полученные знания.
Книга рассчитана на
программистов разной
квалификации, а также
будет полезна для студентов
и преподавателей.
ISBN 5-8459-1127-3
в продаже
C++ ДЛЯ ПРОФЕССИОНАЛОВ
Николас А. Солтер
Скотт Дж. Клепер
www.dialektika.com
В этом практическом
руководстве с большим
количеством примеров
представлены все
грани разработки С++-
приложений, включая
этапы проектирования,
тестирования и отладки.
Здесь описаны простые, но
мощные методы, используемые
С++-профессионалами,
малознакомые, но весьма
полезные средства и
многократно применяемые
шаблоны проектирования.
В книге демонстрируются
различные методики
и хороший стиль
программирования, а
также предлагаются пути
повышения качества
кода и эффективности
программирования в целом.
Книга предназначена
для программистов и
разработчиков, которые
хотят поднять свои навыки
С++-программирования на
профессиональный уровень.
ISBN 5-8459-1065-Х
в продаже
Программистам от программистов
Айвор Хортон
Базовыйкурс
Visual С++2ОО5
Написанное одним из самых выдающихся
авторов, специализирующихся на языках
программирования, новое издание этой ставшей
бестселлером книги полностью учитывает
особенности новой версии среды Visual C++,
ориентированной на .NET. Будучи великолепным
введением в стандартный язык C++ и в его версию
C++/CLI для .NET, книга также воплощает в себе
традиционный уникальный подход Айвора Хортона
к обучению языкам программирования.
Вы изучите основы Visual C++ 2005 и получите
хорошее введение в технологии доступа
к источникам данных, которые используются
в приложениях Microsoft® Foundation Classes
и Windows® Forms. Предлагаемые в конце
большинства глав упражнения помогут надежно
закрепить полученные знания, а также существенно
ускорят ваше продвижение по пути к эффективному
программированию.
Из книги вы узнаете:
• как использовать возможности Visual C++ 2005
для создания приложений
• уникальные аспекты и новые средства
Visual C++ 2005
• базовые идеи и технологии
применяемые для отладки
• способы построения графического интерфейса
пользователя для ваших приложений
• как структурированы приложения
Microsoft Windows
• советы, помогающие разобраться с нюансами
языка C++ без необходимости погружаться
в дебри программирования графики для Windows
Серия Базовый курс от Wrox призвана
обучать пользователей языкам и технологиям
программирования с максимально возможной
простотой, предлагая хорошо структурированный
формат, который помогает эффективно осваивать
все необходимые технологии.
Для кого предназначена эта книга
Эта книга предназначена для начинающих программистов, которые планируют заниматься
написанием на языке C++ приложений для MicrosoftWindows Предварительные навыки
программирования не требуются
Категория
Предмет рассмотрения
языки программирования
Visual C++ 2005
Издательство “Диалектика
www.dialektika.com
p2p.wrox.com
Информационный цемтр
для программистов
wrcx
www. wrox. com
Этот файл был взят с сайта
http://all-ebooks.com
Данный файл представлен исключительно в
ознакомительных целях. После ознакомления с
содержанием данного файла Вам следует его
незамедлительно удалить. Сохраняя данный файл
вы несете ответственность в соответствии с
законодательством.
Любое коммерческое и иное использование кроме
предварительного ознакомления запрещено.
Публикация данного документа не преследует за
собой никакой коммерческой выгоды.
Эта книга способствует профессиональному росту
читателей и является рекламой бумажных изданий.
Все авторские права принадлежат их уважаемым владельцам.
Если Вы являетесь автором данной книги и её распространение
ущемляет Ваши авторские права или если Вы хотите
внести изменения в данный документ или опубликовать
новую книгу свяжитесь с нами по email.