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; // Указатель на 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.