Книга
Книга
“Используя эту книгу вместе с Visual C++ 2005, вы сможете понять фундаментальные основы языка C++ и научиться программировать для Microsoft Windows. Сначала вы изучите принципы, положенные в основу C++, и освоите стандарты ISO/ANSI C++ и C++/CLI. Затем, уже обладая базовыми знаниями, вы посмотрите, как создавать Windows-приложения
с использованием библиотеки Microsoft Foundation Classes из традиционных программ на C++, и как добавлять к ним возможности Windows Forms и C++/CLI в среде .NET.
Зы ознакомитесь с основами технологий доступа к базам данных из традиционных приложений на C++, а также из приложений для .NET
то задача непростая, но вы получите немало удовольствия, решая ее. Совершите свой первый шаг на пути в мир реального программирования. ”
Айвор Хортон
Visual C++
2005
Базовый курс
Обновления, исходные коды, поддержка от издательства Wrox на сайте www.wrox.com
www.dialektika.com
Ivor Horton's
BeginningVisual C++*’ 2005
Ivor Horton
WILEY
Wiley Publishing, Inc.
Visual С** 2005
Базовый курс
Айвор Хортон
“Диалектика”
Москва • Санкт-Петербург • Киев2007
ББК 32.973.26-018.2.75
Х82
УДК 004.438
Компьютерное издательство “Диалектика”
Зав. редакцией С.Н. Тригуб
Перевод с английского Ю.И. Корниенко, Н.А. Мухина
Под редакцией Ю.Н, Артеменко
По общим вопросам обращайтесь в издательство “Диалектика” по адресу:info@dialektika.com, http://www.dialektika.com115419, Москва, а/я 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 associated 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 (англ.)ISBN 978-5-8459-1016-5 (pyc.)ISBN 0-7645-7197-4 (англ.)© Компьютерное изд-во “Диалектика”, 2007, перевод, оформление, макетирование
ISBN 978-5-8459-1016-5 (pyc.)
ISBN 0-7645-7197-4 (англ.)
ISBN 978-5-8459-1016-5 (pyc.)
ISBN 0-7645-7197-4 (англ.)
© Copyright © by Ivor Horton, 2006
Оглавление
Введение22
Глава 1. Программирование в Visual C++ 200527
Глава 2. Данные, переменные и вычисления63
Глава 3. Решения и циклы139
Глава 4. Массивы, строки и указатели179
Глава 5. Структурная организация программ249
Глава 6. Дополнительные сведения о структурах программ287
Глава 7. Определение собственных типов данных337
Глава 8. Дополнительные сведения о классах411
Глава 9. Наследование классов и виртуальные функции481
Глава 10. Технологии отладки567
Глава 11. Концепции программирования для Windows615
Глава 12. Программирование для Windows с использованием MFC 651
Глава 13. Работа с меню и панелями инструментов679
Глава 14. Рисование в окне709
Глава 15. Создание документа и усовершенствование представления 759
Глава 16. Работа с диалогами и элементами управления815
Глава 17. Сохранение и печать документов865
Глава 18. Написание собственных DLL-библиотек897
Глава 19. Подключение к источникам данных919
Глава 20. Обновление источников данных975
Глава 21. Приложения, использующие средства Windows Forms1027
Глава 22. Доступ к источникам данных в приложении Windows Forms 1087
Приложение А. Ключевые слова C++1129
Приложение Б. Коды ASCII1131
Предметный указатель1135
Содержание
Об авторе21
Благодарности21
Введение22
Для кого предназначена эта книга22
О чем эта книга23
Как построена эта книга23
Что необходимо для работы с этой книгой25
Соглашения25
Исходные коды26
От издательства26
Глава 1. Программирование в Visual C++ 200527
Среда .NET Framework28
Общеязыковая исполняющая среда (CLR)28
Написание приложений на C++29
Изучение программирования для Windows31
Изучение C++31
Стандарты C++32
Консольные приложения32
Концепции программирования для Windows33
Что такое интегрированная среда разработки?34
Компоненты системы35
Использование IDE37
Опции панели инструментов38
Стыкуемые панели инструментов39
Документация39
Проекты и решения40
Настройка опций Visual C++ 200554
Создание и выполнение Windows-приложений55
Создание приложения Windows Forms58
Резюме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++/CLI123
Специфика C++/CLI: фундаментальные типы данных124
Вывод командной строки C++/CLI128
Специфика C++/CLI — форматирование вывода128
Клавиатурный ввод в C++/CLI131
Применение safe_cast132
Перечисления C++/CLI132
Резюме135
Упражнения137
Глава 3. Решения и циклы139
Сравнение значений139
Оператор if141
Вложенные операторы if142
Расширенный оператор if144
Вложенные операторы if-else146
Логические операции и выражения147
Условная операция150
Оператор switch152
Безусловное ветвление155
Повторение блока операторов155
Что такое цикл?155
Вариации цикла for157
Цикл while166
Цикл do-while167
Вложенные циклы168
Программирование на C++/СЫ171
Цикл for each175
Резюме177
Упражнения178
Глава 4. Массивы, строки и указатели179
Обработка множества однотипных элементов данных180
Массивы180
Объявление массивов181
Инициализация массивов184
Символьные массивы и обработка строк186
Многомерные массивы189
Косвенный доступ к данным192
Что такое указатель?193
Объявление указателей193
Использование указателей194
Инициализация указателей196
Операция sizeof201
Константные указатели и указатели на константы203
Указатели и массивы204
Динамическое выделение памяти211
Свободное хранилище, псевдоним “куча”211
Операции new и delete212
Динамическое распределение памяти для массивов212
Динамическое распределение многомерных массивов215
Использование ссылок216
Что такое ссылка?216
Объявление и инициализация ссылок216
Программирование на C++/CLI217
Отслеживаемые дескрипторы217
Массивы CLR219
Строки233
Отслеживающие ссылки242
Внутренние указатели242
Резюме245
Упражнения246
Глава 5. Структурная организация программ249
Что такое функции250
Зачем нужны функции?251
Структура функции251
Использование функций253
Передача аргументов в функцию257
Механизм передачи по значению257
Указатели как аргументы функций259
Передача массивов в функцию260
Ссылки как аргументы функции264
Использование модификатора const266
Аргументы main ()267
Прием функцией переменного количества аргументов269
Возврат значений функциями271
Возврат указателя271
Возврат ссылки274
Статические переменные в функциях276
Рекурсивные вызовы функции278
Использование рекурсии280
Программирование на C++/CLI281
Функции, принимающие переменное количество аргументов282
Аргументы ma i п ()283
Резюме284
Упражнения284
Глава 6. Дополнительные сведения о структурах программ287
Указатели на функции287
Объявление указателей на функции288
Указатель на функцию в качестве аргумента290
Массивы указателей на функции292
Инициализация параметров функций293
Исключения294
Возбуждение исключений296
Перехват исключений297
Обработка исключений в MFC298
Обработка ошибок выделения памяти299
Перегрузка функций300
Что такое перегрузка функций?301
Когда нужно перегружать функции303
Шаблоны функций303
Использование шаблона функции304
Пример использования функций306
Реализация калькулятора306
Удаление пробелов из строки309
Вычисление выражения310
Получение значения элемента312
Анализ числа314
Собираем программу вместе316
Расширение программы318
Извлечение подстроки319
Запуск модифицированной программы321
Программирование C++/CLI322
Что такое обобщенные функции323
Программа калькулятора для CLR328
Резюме334
Упражнения335
Глава 7. Определение собственных типов данных337
Структуры в C++338
Что такое структура?338
Определение структуры338
Инициализация структуры339
Доступ к членам структуры339
Поддержка средства Intellisense при работе со структурами343
Структура RECT344
Использование указателей со структурами344
Типы данных, объекты, классы и экземпляры346
Первый класс348
Операции с классами348
Терминология349
Что такое класс?349
Определение класса350
Определение объектов класса350
Доступ к данным-членам класса351
Функции-члены класса353
Расположение определения функции-члена355
Встроенные функции355
Конструкторы классов356
Что такое конструктор?356
Конструктор по умолчанию358
Присваивание параметрам в классе значений по умолчанию360
Использование списка инициализации в конструкторе363
Приватные члены класса363
Доступ к приватным членам класса366
Дружественные функции класса367
Конструктор копирования по умолчанию369
Указатель this370
const-объекты класса373
const-функция-член класса373
Определения функций-членов вне класса374
Массивы объектов класса375
Статические члены класса377
Статические данные-члены класса377
Статические функции-члены класса379
Указатели и ссылки на объекты классов380
Указатели на объекты класса380
Ссылки на объекты класса383
Программирование на C++/CLI384
Определение типов классов значений385
Определение типов ссылочных классов390
Свойства классов392
Поля initonly404
Статические конструкторы406
Резюме407
Упражнения408
Глава 8. Дополнительные сведения о классах411
Деструкторы классов411
Что такое деструктор?412
Деструктор по умолчанию412
Деструкторы и динамическое распределение памяти414
Реализация конструктора копирования417
Разделение памяти между переменными419
Определение объединений420
Объединения в классах и структурах421
Перегрузка операций422
Реализация перегруженной операции422
Реализация полной поддержки операции425
Перегрузка операции присваивания429
Перегрузка операции сложения433
Перегрузка операций инкремента и декремента437
Шаблоны классов438
Определение шаблона класса439
Создание объектов из шаблона класса441
Шаблоны классов с множественными параметрами444
Использование классов446
Понятие интерфейса класса446
Определение проблемы446
Реализация класса СВох447
Определение класса СВох455
Использование класса СВох464
Организация кода программы467
Именование программных файлов469
Программирование на C++/CLI470
Перегрузка операций в классах значений470
Перегрузка операций инкремента и декремента475
Перегрузка операций в ссылочных классах476
Резюме478
Упражнения479
Глава 9. Наследование классов и виртуальные функции481
Базовые идеи объектно-ориентированного программирования481
Наследование в классах483
Что такое базовый класс?483
Наследование классов от базового класса484
Управление доступом при наследовании487
Работа конструктора в производном классе490
Объявление членов класса как protected493
Уровень доступа унаследованных членов класса496
Конструктор копирования в производном классе497
Члены класса как друзья501
Дружественные классы503
Ограничения отношения дружественности классов503
Виртуальные функции503
Что такое виртуальная функция?505
Использование указателей на объекты классов507
Использование ссылок с виртуальными функциями509
Чистые виртуальные функции511
Абстрактные классы512
Непрямые базовые классы514
Виртуальные деструкторы517
Приведение между типами классов521
Вложенные классы522
Программирование на C++/CLI525
Наследование классов в C++/СЫ525
Интерфейсные классы530
Определение интерфейсных классов531
Классы и сборки535
Функции, специфицированные как new539
Делегаты и события540
Деструкторы и финализаторы в ссылочных классах550
Обобщенные классы552
Резюме562
Упражнения563
Глава 10. Технологии отладки567
Что такое отладка?567
Ошибки в программах569
Распространенные ошибки570
Базовые операции отладки571
Установка точек прерывания573
Установка точек трассировки575
Запуск отладки576
Изменение значения переменной580
Добавление отладочного кода581
Использование утверждений581
Добавление собственного отладочного кода583
Отладка программы588
Стек вызовов588
Переход к ошибке590
Тестирование расширенного класса593
Поиск следующей ошибки595
Отладка динамической памяти596
Функции проверки свободного хранилища596
Управление отладочными операциями свободного хранилища598
Отладочный вывод свободного хранилища599
Отладка программ C++/CLI604
Использование классов Debug и Trace605
Резюме613
Глава 11. Концепции программирования для Windows615
Основы программирования для Windows616
Элементы окна617
Windows-программы и операционная система619
Программирование, управляемое событиями619
Сообщения Windows619
Windows API620
Типы данных Windows621
Нотация программ Windows621
Структура Windows-программы623
Функция W i nMa i п ()624
Функции обработки сообщений635
Простая Windows-программа639
Организация Windows-программ640
Библиотека Microsoft Foundation Classes642
Нотация MFC642
Как структурирована программа MFC643
Использование Windows Forms646
Резюме649
Глава 12. Программирование для Windows с использованием MFC 651
Концепция “документ-представление” в MFC652
Что такое документ?652
Документные интерфейсы652
Что такое представление?653
Связь документа с его представлениями654
Ваше приложение и MFC655
Создание приложений MFC656
Создание SDI-приложения658
Вывод мастера MFC Application Wizard663
Создание MDI-приложения673
Резюме676
Упражнения676
Глава 13. Работа с меню и панелями инструментов679
Взаимодействие с Windows679
Что такое карты сообщений?680
Категории сообщений683
Обработка сообщений в ваших программах684
Расширение программы Sketche г685
Элементы меню686
Создание и редактирование ресурсов меню686
Добавление обработчиков сообщений меню691
Выбор класса для обработки сообщений меню692
Создание функций сообщений меню692
Кодирование функций сообщений меню694
Добавление обработчиков сообщений для обновления пользовательского интерфейса699
Добавление кнопок панели инструментов702
Редактирование свойств кнопки панели инструментов703
Испытание кнопок панели инструментов704
Добавление всплывающих подсказок705
Резюме706
Упражнения707
Глава 14. Рисование в окне709
Основы рисования в окне709
Клиентская область окна710
Интерфейс графических устройств Windows711
Механизм рисования в Visual C++713
Класс представления в вашем приложении714
Класс CDC715
Практическое рисование графики723
Программирование для мыши726
Сообщения от мыши726
Обработчики сообщений мыши728
Рисование с помощью мыши730
Испытание программы S ke t che г754
Запуск примера754
Захват сообщений мыши755
Резюме756
Упражнения757
Глава 15. Создание документа и усовершенствование представления 759
Что такое классы коллекций?760
Типы коллекций760
Безопасные к типам классы коллекций761
Коллекции объектов761
Типизированные коллекции указателей771
Использование шаблонного класса CLis t773
Рисование кривой773
Определение класса CCurve774
Реализация класса CCurve776
Испытание класса CCurve777
Создание документа778
Использование шаблона CTypedPtrList778
Усовершенствование представления783
Обновление множественных представлений783
Прокрутка представлений785
Использование режима отображения MM_LOENGLI SH790
Удаление и перемещение фигур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
Отображение кнопки счетчика839
Использование показателя масштаба839
Масштабируемые режимы отображения840
Установка размера документа841
Установка режима отображения841
Реализация прокрутки с масштабированием843
Работа с панелями состояния845
Добавление панели состояния в обрамляющее окно846
Использование окна списка849
Удаление диалога масштаба850
Создание элемента управления — окна списка850
Использование элемента управления — поля редактирования853
Создание ресурса поля редактирования853
Создание класса диалога855
Добавление пункта меню Text857
Определение текстового элемента858
Реализация класса С Text858
Создание текстового элемента860
Резюме861
Упражнения863
Глава 17. Сохранение и печать документов865
Что такое сериализация?865
Сериализация документа866
Сериализация в определении класса документа866
Сериализация в реализации класса документа867
Функциональность классов, базирующихся на COb j ect870
Как работает сериализация871
Как реализовать сериализацию класса872
Применение сериализации872
Запись изменений в документе872
Сериализация документа874
Сериализация классов элементов875
Испытание сериализации878
Перемещение текста879
Печать документа881
Процесс печати882
Реализация многостраничной печати885
Получение полного размера документа886
Сохранение данных печати886
Подготовка к печати887
Очистка после печати889
Подготовка контекста устройства889
Печать документа890
Получение печатного вывода документа894
Резюме895
Упражнения896
Глава 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
Немного об SQL922
Извлечение данных с использованием SQL923
Соединение таблиц с помощью SQL924
Сортировка записей927
Поддержка баз данных в MFC927
Классы MFC для поддержки ODBC928
Создание приложения базы данных929
Регистрация базы данных ODBC929
Генерация программы MFC ODBC932
Структура программы935
Тестирование примера945
Сортировка набора записей947
Модификация заголовка окна947
Использование второго объекта набора записей948
Добавление класса набора записей949
Добавление класса представления для набора записей952
Настройка набора записей955
Доступ к многотабличным представлениям959
Просмотр заказов для продукта964
Просмотр подробностей о заказчике965
Добавление набора записей заказчиков965
Создание ресурса для диалога заказчика966
Создание класса представления заказчиков966
Добавление фильтра968
Реализация параметра фильтра970
Связывание диалога заказов с диалогом информации о заказчике971
Испытание программы просмотра базы данных973
Резюме974
Упражнения974
Глава 20. Обновление источников данных975
Операции обновления975
Операции обновления CRecordset976
Проверка допустимости операций978
Блокировка записей978
Транзакции979
Операции транзакций класса CDatabase979
Простой пример обновления981
Настройка приложения982
Управление процессом обновления985
Реализация режима обновления987
Активизация и отключение полей редактирования987
Изменение надписи кнопки989
Управление видимостью кнопки Cancel990
Отключение меню Record991
Фактическое выполнение обновления993
Реализация операции отмены994
Добавление строк в таблицу995
Процесс ввода заказа997
Создание ресурсов997
Создание наборов записей998
Создание представлений наборов записей999
Добавление элементов управления в ресурсы диалогов1003
Реализация переключения диалоговых окон1007
Создание идентификатора заказа1011
Выбор продуктов для включения в заказ1018
Резюме1025
Упражнения1026
Глава 21. Приложения, использующие средства Windows Forms1027
Общее представление о Windows Forms1027
Общее представление о приложениях Windows Forms1028
Изменение свойств формы1030
Запуск приложения1031
Индивидуальная настройка графического интерфейса пользователя1032
Добавление элементов управления в форму1033
Добавление элемента управления с вкладками1036
Использование элементов управления GroupBox1039
Использование элементов управления Button1041
Использование элемента управления WebBrowser1043
Работа приложения Winning Application1045
Добавление контекстного меню1045
Создание обработчиков событий1046
Обработка событий меню Limits1052
Создание диалогового окна1053
Использование диалогового окна1059
Добавление второго диалогового окна1065
Реализация элемента меню Help1^About1074
Обработка щелчка на кнопке1074
Реакция на щелчок в контекстном меню1077
Резюме1085
Упражнения1085
Глава 22. Доступ к источникам данных в приложении Windows Forms 1087
Работа с источниками данных1088
Доступ и отображение данных1089
Использование элемента управления DataGridView1090
Использование элемента управления DataGridView в несвязанном режиме1091
Персональная настройка элемента управления DataGridView1098
Настройка ячеек заголовков1099
Настройка ячеек, не являющихся заголовками1099
Динамическое определение стилей ячеек1107
Использование связанного режима1112
Компонент Bindingsource1113
Использование элемента управления BindingNavigator1118
Привязка к отдельным элементам управления1121
Работа с множеством таблиц1126
Резюме1127
Упражнения1128
Приложение А. Ключевые слова C++1129
Ключевые слова ISO/ANSI C++1129
Ключевые слова C++/CLI1130
Приложение Б. Коды ASCII1131
Предметный указатель1135
Эта книга посвящается Александру Гилби (Alexander Gilbey).Надеюсь получить его комментарии, хотя вероятно, ждать придется долго.
Об авторе
Айвор Хортон (Ivor Horton) получил математическое образование и затем увлекся информационными технологиями, поскольку они обещали значительную отдачу от достаточно небольших усилий. Реальность оказалась таковой, что, как выяснилось, в этой области требуется приложить весьма значительные усилия, чтобы добиться относительно средних результатов, однако это не помешало ему продолжать работу с компьютерами вплоть до настоящего времени. В разное время ему доводилось заниматься программированием, проектированием систем, консультациями и управлением реализацией проектов высокой сложности.
Хортон обладает многолетним опытом проектирования и реализации компьютерных систем применительно к инженерному проектированию и выполнению операций во многих отраслях промышленности. Ему приходилось разрабатывать множество полезных приложений на широком разнообразии языков программирования, а также обучать этому ученых и инженеров. За более чем 10 лет он написал ряд книг по программированию, среди которых руководства по С, C++ и Java. В настоящее время Хортон, когда не пишет очередную книгу и не рецензирует чужие публикации, он тратит свое время на рыбалку, путешествия и изучение французского.
Благодарности
Я хотел бы выразить признательность издательству John Wiley 8с 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++. Для достижения успеха вам понадобится хотя бы приблизительное представление о том, как работает компьютер, включая устройство его памяти и способы хранения данных и команд.
О чем эта книга
Моей целью при написании этой книги было научить вас сути программирования на 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 для печати документов и сохранению их на диске.
Глава 18 научит вас всему, что понадобится для создания собственных библиотек, использующих МЕС. Вы узнаете о различных видах библиотек и напишете работающие примеры таких библиотек, которые затем будете развивать и совершенствовать на протяжении целых шести глав.
В главах 19 и 20 рассматривается доступ к источникам данных из приложений МЕС. Вы приобретете опыт доступа к базам данных в режиме только для чтения, а затем изучите фундаментальные приемы программирования, необходимые для обновления базы данных с использованием МЕС. Эти примеры используют базу данных 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++/CL1, в заранее определенном контексте. Причина такой организации материала состоит в том, что 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++ параллельно.
Что необходимо для работы с этой книгой
Чтобы использовать эту книгу, вам понадобится либо 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++.
Соглашения
Чтобы помочь вам получить от текста большую отдачу и следить за тем, что происходит, в этой книге принят ряд соглашений.
Практическое занятие | Здесь ПОмеЩаеТСЯ уПраЖНвНИв, КОТОрОв вы должны выполнить, следуя инструкциям, приведенным в книге.
Описание полученных результатов
После нескольких разделов “Практическое занятие” в этом разделе рассмотренный ранее код объясняется во всех подробностях.
Врезки вроде этой содержат важную информацию, которую не следует упускать из виду, и которая непосредственно касается текста, где размещена.
Советы, подсказки, трюки и сноски выделены курсивом.
Ниже описаны стили текста книги.
Новые термины и важные слова, которые встречаются впервые, выделены полужирным.
Клавишные комбинации представлены как <Ctrl+A>.
Имена файлов, URL-адреса и код внутри текста выделен моноширинным шрифтом: persistence.properties.
□ Код представляется двумя разными способами:
В примерах полужирным выделяются новые и важные части кода.
Выделение не используется для кода, который менее важен в текущем контексте либо уже был представлен ранее.
Исходные коды
Исходные коды примеров и упражнений вместе с решениями доступны для загрузки на Web-сайте издательства.
От издательства
Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами. Нам интересно услышать и любые другие замечания, которые вам хотелось бы высказать в наш адрес.
Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас.
Посылая письмо или сообщение, не забудьте указать название книги и ее авторов, а также ваш обратный адрес. Мы внимательно ознакомимся с вашим мнением и обязательно учтем его при отборе и подготовке к изданию последующих книг.
Наши координаты:
E-mail:info@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?
Итак, включайте свой ПК, запускайте 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 — это, по сути, спецификация среды виртуальной машины, которая позволяет приложениям, написанным на разнообразных высокоуровневых языках програм-
мирования, выполняться в различных системах без изменения и перекомпиляции оригинального исходного кода. 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 появился задолго до того, как на сцену вышел
язык 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++"Родной" C++"Родной" C++MFCMFCКаркасные классыКаркасные классыОбщеязыковая исполняющая средаОбщеязыковая исполняющая среда
Родной" C++
Родной" C++
"Родной" C++
"Родной" C++
MFC
MFC
Каркасные классы
Каркасные классы
Общеязыковая исполняющая среда
Общеязыковая исполняющая среда
Операционная система
Аппаратное обеспечение
Рис. 1.1. Основные варианты выбора, доступные при разработке приложений на C++
На рис. 1.1 представлена не полная картина. Приложение может состоять частично из управляемого C++ и частично — из родного кода, то есть вы не привязаны к одной или другой среде. Конечно, смешивая разнородный код, вы кое-что теряете, поэтому поступать так нужно только при необходимости, например, когда вы собираетесь перенести существующие родное приложение C++ под управление CLR. Понятно, что вы не сможете получить выгоды, присущие управляемому C++ в родном коде C++, к тому же взаимодействие между управляемыми и неуправляемыми компонентами программы влечет за собой определенные накладные расходы. Однако возможность смешения управляемого и неуправляемого кода может оказаться чрезвычайно полезной, когда нужно разработать или расширить существующий неуправляемый код, но при этом в некоторых местах использовать преимущества CLR. Конечно, при
разработке новых приложений нужно в самом начале решить, должен быть его код управляемым или нет.
Изучение программирования для 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++/CLI.
Написание программ на 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++.
Стандарты 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++. Поэтому в начальных главах нашей книги, со-
средоточенных на основах работы C++, мы потратим некоторое время на рассмотрение нескольких “легковесных” консольных приложений, прежде чем перейдем к “тяжеловесному” коду мира Windows.
Таким образом, изучая C++, вы сможете сосредоточить внимание на языковых средствах, не заботясь о среде, в которой они работают. В консольных приложениях, которые вы разработаете в процессе изучения, вы будете иметь дело только с текстовым интерфейсом, но этого вполне достаточно для понимания C++, поскольку в определении языка отсутствует описание каких-либо средств графики. Естественно, я представлю всю необходимую информацию о программировании графического пользовательского интерфейса, когда очередь дойдет до написания программ под Windows с применением MFC на “родном” C++ и с применением Windows Forms — на C++/CLI.
Существует два вида консольных приложений, и вы будете иметь дело с обоими. Консольные приложения Win32 компилируются в родной код, и их мы будем рассматривать при изучении средств ISO/ANSI C++. Консольные приложения CLR ориентированы на CLR, поэтому7 их мы будем использовать для работы со средствами C++/CLI.
Концепции программирования для Windows
Наш подход к Windows-программированию будет основан на полном использовании инструментов, представленных Visual C++ 2005. Средства создания проекта, встроенные в Visual C++ 2005, позволяют автоматически сгенерировать скелет кода для широкого диапазона различных прикладных программ, включая базовые Windows-программы. Создание проекта — это начальная точка при разработке всех приложений и компонентов в Visual C++ 2005, и для того, чтобы получить представление о том, как это делается, в настоящей главе вы ознакомитесь с процессом создания некоторых примеров, включая набросок программы для Windows.
Программы Windows имеют структуру, отличающуюся от обычных консольных программ, которые запускаются из командной строки, к тому же они значительно сложнее. В консольной программе вы можете получать ввод с клавиатуры и отправлять вывод непосредственно в командную строку, в то время как Windows-программа может работать со средствами ввода и вывода компьютера только через функции, предоставленные операционной системой Windows; никакого прямого доступа к аппаратным ресурсам не допускается. Поскольку в Windows могут быть активными одновременно несколько программ, Windows определяет, какая из них должна получить ввод — щелчок мыши или нажатие клавиши — и соответственно оповещает нужную программу. То есть общее управлением всем взаимодействием с пользователем осуществляется операционной системой Windows.
К тому же природа интерфейса между пользователем и приложением Windows такова, что в любой момент времени возможно самое разнообразное воздействие на программу. Пользователь может выбрать любой из пунктов меню, щелкнуть на кнопке панели инструментов или же щелкнуть кнопкой мыши в любом месте окна приложения. Хорошо спроектированное приложение Windows должно быть готовым обрабатывать ввод любого рода в любой момент времени, поскольку заранее не может быть известно, какое именно воздействие произойдет. Все эти действия пользователя принимаются в первой инстанции операционной системой, и рассматриваются ею как события (events). Событие, воспринятое пользовательским интерфейсом вашего приложения, обычно приводит к выполнению определенного кусочка кода программы. Таким образом, что именно делает программа — определяется последовательное-
тью действий пользователя. Программы, работающие таким образом, называются программами, управляемыми событиями (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, а затем постепенно будут раскрываться детали, в контексте изучаемого материала.
События:
Рис. 1.2. Структура типичной ЖпсЬи$трограммы
Компоненты системы
Список фундаментальных составляющих Visual C++ 2005, поставляемых как части IDE, включает в себя редактор, компилятор, компоновщик и библиотеки. Это — основные инструменты, необходимые для написания и исполнения программ C++. Их назначение описано ниже.
Редактор
Редактор представляет собой интерактивную среду, в которой вы можете создавать и редактировать исходный код C++. Наряду с обычными средствами вроде вырезания и вставки фрагментов, с которыми вы наверняка знакомы, редактор также обеспечивает цветовое выделение различных элементов языка. Редактор автоматически распознает ключевые конструкции языка C++ и окрашивает их в соответствии с их значением. Это не только помогает сделать код более читабельным, но также ясно указывает на ошибки при вводе некоторых слов.
Компилятор
Компилятор преобразует ваш исходный код в объектный код, обнаруживает и извещает об ошибках в процессе компиляции. Компилятор может обнаружить широкий диапазон ошибок, связанных с некорректным или нераспознаваемым программным кодом, а также структурные ошибки, как, например, части программы, которые никогда не будут выполнены. Выходной объектный код, созданный компилятором, помещается в так называемые объектные файлы. Существуют два типа объектного кода, производимого компилятором. Файлы с объектным кодом обычно имеют имена с расширением . оЬ 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. В свое время вы узнаете, как ее использовать при разработке приложений.
Использование IDE
Вся разработка и выполнение программ, описанных в этой книге, будет осуществляться внутри IDE. При запуске Visual C++ 2005 вы увидите окно приложения, подобное тому, что показано на рис. 1.3.
Start PayeStart PayeL-1L-1▼ я х▼ я хSolution ExplorerSolution ExplorerMiC-VSiAVisual Studio 2ооьMiC-VSiAVisual Studio 2ооьPure C • : Invo* ing Generic or Function TemplatesMon, 19 Sep 2Ь05 20:51 25 GMT - This month, Stan Lippman walks thnuugh the access of def inf ng and intcjcng a generic or template function under С++\Ш.C+ +at Wo* writing, ding, nd а «4*ig Mug-insMen, 19 Sep 2DD5 20:5D:31 GMT - Paul DiLasua answers a number of renders' questions this month on writrg, Joading, and accessing plug-ins With C++ n .NET.OpenMP and C++: Reap the Benefits of Multithreading without A11 Mon, 19 Sep 2005 19:17:53 GMT - P your application is not coded to take advant -qe of con puters with multiple processors, you may r ot reap the benefit::. OpenMP helps you create multithreaded C++ applications quickly so that the benefits or multithreading can be realized.See the New Technologies Announced at PDC '05Mor. 12 ^en 2005 23:139:59 GMT - See what is in stnrp for dew Innt-rs in fhpPure C • : Invo* ing Generic or Function TemplatesMon, 19 Sep 2Ь05 20:51 25 GMT - This month, Stan Lippman walks thnuugh the access of def inf ng and intcjcng a generic or template function under С++\Ш.C+ +at Wo* writing, ding, nd а «4*ig Mug-insMen, 19 Sep 2DD5 20:5D:31 GMT - Paul DiLasua answers a number of renders' questions this month on writrg, Joading, and accessing plug-ins With C++ n .NET.OpenMP and C++: Reap the Benefits of Multithreading without A11 Mon, 19 Sep 2005 19:17:53 GMT - P your application is not coded to take advant -qe of con puters with multiple processors, you may r ot reap the benefit::. OpenMP helps you create multithreaded C++ applications quickly so that the benefits or multithreading can be realized.See the New Technologies Announced at PDC '05Mor. 12 ^en 2005 23:139:59 GMT - See what is in stnrp for dew Innt-rs in fhpOpen:Project... |Web Sit..Create:Project.. I Web Sit..Open:Project... |Web Sit..Create:Project.. I Web Sit.._7soiut... lciUasr 5iProp.._7soiut... lciUasr 5iProp..What's Neto In 'visual Г++ Security Вей Practices far C++ Creating and Managing Visual CL.What's Neto In 'visual Г++ Security Вей Practices far C++ Creating and Managing Visual CL.
Start Paye
Start Paye
L-1
L-1
▼ я х
▼ я х
Solution Explorer
Solution Explorer
MiC-VSiA
Visual Studio 2ооь
MiC-VSiA
Visual Studio 2ооь
Pure C • : Invo* ing Generic or Function Templates
Mon, 19 Sep 2Ь05 20:51 25 GMT - This month, Stan Lippman walks thnuugh the access of def inf ng and intcjcng a generic or template function under С++\Ш.
C+ +at Wo* writing, ding, nd а «4*ig Mug-ins
Men, 19 Sep 2DD5 20:5D:31 GMT - Paul DiLasua answers a number of renders' questions this month on writrg, Joading, and accessing plug-ins With C++ n .NET.
OpenMP and C++: Reap the Benefits of Multithreading without A11 Mon, 19 Sep 2005 19:17:53 GMT - P your application is not coded to take advant -qe of con puters with multiple processors, you may r ot reap the benefit::. OpenMP helps you create multithreaded C++ applications quickly so that the benefits or multithreading can be realized.
See the New Technologies Announced at PDC '05
Mor. 12 ^en 2005 23:139:59 GMT - See what is in stnrp for dew Innt-rs in fhp
Pure C • : Invo* ing Generic or Function Templates
Mon, 19 Sep 2Ь05 20:51 25 GMT - This month, Stan Lippman walks thnuugh the access of def inf ng and intcjcng a generic or template function under С++\Ш.
C+ +at Wo* writing, ding, nd а «4*ig Mug-ins
Men, 19 Sep 2DD5 20:5D:31 GMT - Paul DiLasua answers a number of renders' questions this month on writrg, Joading, and accessing plug-ins With C++ n .NET.
OpenMP and C++: Reap the Benefits of Multithreading without A11 Mon, 19 Sep 2005 19:17:53 GMT - P your application is not coded to take advant -qe of con puters with multiple processors, you may r ot reap the benefit::. OpenMP helps you create multithreaded C++ applications quickly so that the benefits or multithreading can be realized.
See the New Technologies Announced at PDC '05
Mor. 12 ^en 2005 23:139:59 GMT - See what is in stnrp for dew Innt-rs in fhp
Open:Project... |Web Sit..
Create:Project.. I Web Sit..
Open:Project... |Web Sit..
Create:Project.. I Web Sit..
_7soiut... lciUasr 5iProp..
_7soiut... lciUasr 5iProp..
What's Neto In 'visual Г++ Security Вей Practices far C++ Creating and Managing Visual CL.
What's Neto In 'visual Г++ Security Вей Practices far C++ Creating and Managing Visual CL.
▼ V X▼ V Xoutput
▼ V X
▼ V X
Show • Lit
Pac. 13. Окно Visual C++ 2005
Часть окна слева на рис. 1.3 называется окном проводника решений (Solution Explorer), правое верхнее окно, содержащее стартовую страницу (Start page) — это окно редактора (Editor), а окно в нижней части называется окном вывода (Output). Окно проводника решений позволяет осуществлять навигацию по программным файлам, отображать их содержимое в окне редактора, а также добавлять новые файлы к вашей программе. Окно проводника решений содержит три дополнительных вкладки (на рис. 1.3 показаны только две из них), которые отображают Class View (представление классов), Resource View (представление ресурсов) и Property Manager (диспетчер свойств). Вы можете указать, какие именно представления отображаются, через меню View (Вид). Окно редактора — это место, где вы вводите и модифицируете исходный код и другие компоненты своей программы. Окно вывода отображает сообщения, полученные при компиляции и компоновке вашей программы.
Опции панели инструментов
Выбрать, какие панели инструментов (toolbars) должны отображаться, можно, выполнив щелчок правой кнопкой мыши на области панелей инструментов. При этом появляется всплывающее меню со списком доступных панелей (рис. 1.4), в котором отображаемые в данный момент панели помечены галочками.
v Build
v Class Designer
Data Design
Database Diagram
I I Debug
Debug Location
Device
Dialog Editor formatting
Help
HTML Source Editing
Image Editor
Layout
Query Designer
Report Borders
Report Formatting
Source Control
v Standard
Style Sheet
Table Designer
Text Editor
Рис. 1.4. Всплываюидее меню co списком доступных панелей инструментов
Здесь вы принимаете решение относительно того, какие панели инструментов должны быть видимы в любой момент времени. Вы можете выбрать такой же набор инструментов, как показан на рис. 1.3, пометив пункты меню Build (Сборка), Class Designer (Конструктор классов), Debug (Отладка), Standard (Стандартная) и View Designer (Конструктор представлений). Щелчок в серой области слева от списка панелей инструментов помечает их либо снимает метку, если она уже стоит. Помеченные панели отображаются, не помеченные — скрываются.
Вам не нужно загромождать окно IDE панелями инструментов, которые, как вы думаете, могут понадобиться когда-то в будущем. Некоторые из них появляются автоматически, когда возникает в них необходимость, поэтому, скорее всего, вы согласитесь с тем, что выбор панелей по умолчанию является наиболее удобным. Во время разработки приложения время от времени вам понадобится доступ к панелям, которые не отображаются в данное время. В любой момент вы можете изменить набор видимых
инструментальных панелей, просто выполнив правый щелчок мыши в поле панелей и выбрав требуемые панели в контекстном меню.
Подобно многим другим Windows-приложениям, панели инструментов Visual C++ 2005 включают всплывающие подсказки. Для того чтобы увидеть их, просто наведите курсор мыши на соответствующую кнопку, подождите секунду или две и вы увидите белую метку с кратким описанием функции этой кнопки.
Стыкуемые панели инструментов
Стыкуемая (dockable) панель инструментов — это такая панель, которую с помощью мыши вы можете перетаскивать в любое удобное место внутри окна. Когда она размещается у одной из четырех границ окна приложения, говорят, что она при- стывкована (docked). При этом она выглядит точно так же как панели инструментов в верхней части окна IDE. Панель в верхней части, содержащая пиктограмму с изображением дискеты, и текстовое поле справа от кнопки с биноклем — это стандартная панель инструментов. Вы можете установить курсор мыши на ее поверхность, нажать левую кнопку, перетащить в нужное место и оставить там, отпустив кнопку мыши. Затем она будет выглядеть как отдельно окно, которое можно разместить где угодно.
Если вы перетащите любую стыкуемую панель прочь от границы окна приложения, она будет выглядеть как стандартная панель, показанная на рис. 1.5 — в виде отдельного окна с собственным заголовком. В этом состоянии она называется плавающей (floating) панелью. Все панели инструментов, которые вы видите на рис. 1.3, являются стыкуемыми, поэтому вы можете поэкспериментировать с перетаскиванием любой из них. Вы можете поместить их затем в их нормальное положение либо присоединить к любой границе окна.
4f I. Jzrd
Рис. 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.
Рис. 1.6. Диалоговое окно New Project (Новый проект)
Это также идентифицирует мастер создания приложений, который наполнит проект начальным содержимым. Правая панель отображает список шаблонов, доступных для выбранного слева типа проектов. Выбранный шаблон используется мастером приложения при создании файлов, составляющих проект. В следующем диалоговом окне вам представится возможность настроить файлы, созданные в результате щелчка на кнопке ОК данного диалогового окна. Для большинства опций типа/шаблона базовый набор исходных модулей программы создастся автоматически.
После этого вы можете ввести соответствующее имя проекта в редактируемом поле Name: (Имя:). Например, вы можете назвать его Extl_01 либо выбрать другое имя проекта по своему усмотрению. Visual C++ 2005 поддерживает длинные имена файлов, поэтому ваш выбор ничем не ограничен. Имя папки решения появляется в нижнем редактируемом поле и по умолчанию совпадает с именем проекта. При желании можете его изменить. Диалоговое окно также позволяет модифицировать местоположение решения, содержащего ваш проект — для этого предназначено редактируемое поле Location: (Расположение:). Если вы просто введете имя проекта, то папка решения будет автоматически установлена в папку с этим именем, в пути, указанном в поле Location:. По умолчанию папка решения создается, если она не существует. Если вы хотите указать другой путь для папки решения, просто введите его в поле Location:. Альтернативно вы можете использовать кнопку Browse (Обзор) для выбора другого пути размещения вашего решения. Щелчок на кнопке ОК вызовет диалоговое окно мастера Application Wizard (Мастер создания приложений), показанное на рис. 1.7.
Диалоговое окно отобразит текущие активные установки. Если вы щелкнете на кнопке Finish (Iotobo), мастер создаст файлы проекта на их основе. В данном случае вы можете щелкнуть на ссылке Application Settings (Настройки приложения) в левой части, чтобы отобразить страницу настроек приложения, показанную на рис. 1.8.
Рис. 1.7. Мастер создания приложений
Рис. 1.8. Страница Application Settings (Настройки приложения) мастера Application Wizard
Страница 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. Я увеличил ширину панели, чтобы можно было видеть внизу полные имена вкладок.
Solution Explorer - Exl_01▼ 3- X
- d l3 Solution 'Exl_01' (1 project]
-J ^ Exl_Ol
Header Files । h“| stdafx.h a Resource Files 9 • Source Files
*У Exl_01.cpp
-^ i stdafx.cpp
Ц] ReadMe.txt
^Solution Explorer;^,Class Viev. Property Manager ^Resource Viev»
Pac. 1.9. Левая панель проводника решений
Вкладка Solution Explorer (Проводник решений) предоставляет обзор всех проектов текущего решения и файлов, которые они содержат; в данном случае, конечно, проект только один. Вы можете отобразить содержимое любого файла в отдель-
ной вкладке окна редактора, просто дважды щелкнув на его имени в поле 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 сгенерированная программа не делает ничего, поэтому для того, чтобы сделать ее несколько более интересной, в нее потребуется внести изменения. Если файл Exl Ol. срр еще не отображен в панели редактора, выполните двойной щелчок на его имени в панели Solution Explorer. Этот файл — главный исходный файл программы, который сгенерирован мастером Application Wizard, и выглядит он так, как показано на рис. 1.10.
Если в вашей системе не отображаются номера строк, выберите в главном меню пункт Tools’^ Options (Сервис1^ Параметры), чтобы отобразить диалоговое окно Options (Параметры). Если вы развернете опцию C/C++ в правой панели и выберете General (Общие) в расширенном дереве, то сможете выбрать Line Numbers (Номера строк) в правой панели диалогового окна. Сначала я дам вам общее представление о том, что делает код на рис. 1.10, а подробности вы узнаете позже.
Первые две строки — просто комментарии. Все, что следует в строке за “/ / ”, компилятором игнорируется. Когда вы хотите добавить описательные комментарии в строке, предварите текст символами “//”.
В строке 4 находится директива #include, добавляющая содержимое файла stdaf х. h в то место данного файла, где она расположена. Это стандартный способ добавления содержимого исходных файлов . h в исходные файлы . срр программ на C++.
Е*1_М р start р »е 1-^ IГ" (Global Scope)* 'v^moin(int orgc, _TCHAR 2orgv)6 LЕ*1_М р start р »е 1-^ IГ" (Global Scope)* 'v^moin(int orgc, _TCHAR 2orgv)6 Lv Xv X7-0,1 IC _СЯДШ(1ПЕ ЙГ;С,7-0,1 IC _СЯДШ(1ПЕ ЙГ;С,tchar* arav[])tchar* arav[])
Е*1_М р start р »е 1-^ IГ" (Global Scope)* 'v^moin(int orgc, _TCHAR 2orgv)
6 L
Е*1_М р start р »е 1-^ IГ" (Global Scope)* 'v^moin(int orgc, _TCHAR 2orgv)
6 L
v X
v X
7-0,1 IC _СЯДШ(1ПЕ ЙГ;С,
7-0,1 IC _СЯДШ(1ПЕ ЙГ;С,
tchar* arav[])
tchar* arav[])
1Г }
12
Pac. 1.10. Содержимое файла Exl_01. cpp
Строка 7 — первая строка исполняемого кода в данном файле и начало функции tmain (). Эта функция — просто именованный элемент исполняемого кода программы на C++; каждая программа C++ состоит, по крайней мере, из одной, а чаще — из множества функций.
Строки 8 и 10 содержат левую и правую фигурные скобки соответственно, которые ограничивают исполняемый код функции _tmain (). Таким образом, исполняемый код состоит из единственной строки 9, и все, что он делает — это завершает программу.
Теперь вы можете добавить следующие две строки кода в окне редактора.
// Ех1_01.срр : Определяет точку входа в консольное приложение.
#include "stdafx.h"
#include <iostream>
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. Все, что находится между парой двойных кавычек, выводится программой в командной строке.
Сборка решения
Чтобы построить решение, нажмите клавишу <F7> или выберите пункт меню Builds Build Solution (Сборка^Собрать решение). Альтернативно вы можете щелкнуть на кнопке в панели инструментов, соответствующей этому пункту меню. Кнопки панели для меню Build могут не отображаться, но вы легко исправите это, щелкнув правой кнопкой мыши в области панелей инструментов и выбрав панель Build в выпадающем меню. После этого программа должна успешно скомпилироваться. Если будут какие-то ошибки, проверьте внимательно две строки, которые вы добавили в исходный код.
Файлы, создаваемые при сборке консольного приложения
После того, как пример собран без ошибок, загляните в папку проекта, используя Windows Explorer, чтобы увидеть новую вложенную папку по имени Debug. Эта папка содержит вывод только что выполненной вами сборки проекта. Обратите внимание, что в ней находится несколько файлов.
Помимо файла . ехе, который представляет вашу программу в готовом к выполнению виде, вам не нужно ничего знать о том, что собой представляют эти файлы. Если же вы любопытны, то в табл. 1.1 дано краткое описание наиболее интересных из них.
Таблица 1.1. Файлы, создаваемые в результате сборки консольного приложения
Расширение файла Описание
ехеИсполняемый файл программы. Вы получите этот файл только в том случае,
если и компиляция, и компоновка завершились успешно.
objКомпилятор генерирует эти объектные файлы, содержащие машинный код ис
ходных файлов вашей программы. Они используются компоновщиком, вместе с библиотечными файлами, чтобы создать файл .ехе.
ilkЭтот файл используется компоновщиком, когда вы перестраиваете ваш про
ект. Он позволяет компоновщику инкрементно связывать объектные файлы, сгенерированные компилятором из модифицированного исходного кода в существующий файл . ехе. Благодаря этому нет необходимости заново выполнять всю компоновку при каждом изменении программы.
pchЭто предварительно скомпилированный файл заголовка. Благодаря таким
файлам значительная часть кода, не являющегося субъектом модификации (в частности, код библиотек C++), может быть обработана один раз и помещена в файл .pch. Применение этих файлов существенно снижает затраты времени, необходимого для повторного построения ваших программ.
»pdbЭтот файл включает отладочную информацию, используемую при выполнении
программы в режиме отладки. В этом режиме вы можете динамически инспектировать информацию, которую генерирует исполняющаяся программа.
idbСодержит информацию, необходимую для перестройки всего решения.
Отладочная (Debug) и рабочая (Release) версии программы
Вы можете установить широкий диапазон разнообразных опций проекта, выбрав пункт меню ProjectsЕх1 01 Properties (Проект^Свойства Ех1_01). Эти опции определяют, как обрабатывается ваш исходный код на стадиях компиляции и компоновки. Набор опций, который порождает конкретную исполняемую версию вашей программы, называется конфигурацией. Когда вы создаете новое рабочее пространство
проекта. Visual C++ 2005 автоматически создает конфигурации для построения двух версий вашего приложения. Одна из них, называемая отладочной (Debug), включает информацию, помогающую в отладке программы. Если что-то идет не так, то, запуская эту версию, вы можете выполнять код программы построчно, проверяя значение данных, с которыми работает программа. Другая версия, называемая рабочей (Release), не имеет в себе никакой отладочной информации и содержит оптимизированный компилятором машинный код, что позволяет обеспечить максимальную эффективность исполняемого модуля. Этих двух конфигураций будет достаточно для всех примеров, которые мы рассмотрим в книге, но когда вам понадобится настроить другую конфигурацию приложения, вы можете сделать это через меню Builds Configuration Manager (Сборка^Диспетчер конфигурации). Обратите внимание, что этот элемент меню не появляется, если в данный момент в IDE-среду не загружен какой-то проект. Очевидно, что в таком поведении нет никакой проблемы, но может быть, это смутит вас, когда вы будете просматривать все доступные опции меню.
Вы можете выбирать текущую конфигурацию программы, с которой собираетесь работать, указывая ее в выпадающем списке Active solution configuration (Активная конфигурация решения) в диалоговом окне Configuration Manager (Диспетчер конфигурации), как показано на рис. 1.11.
Рис. 1.11. Выбор текущей конфигурации программы
Выберите в списке конфигурацию, с которой собираетесь работать, и затем щелкните на кнопке Close (Закрыть). В процессе разработки приложения вы будете работать с отладочной конфигурацией. После того как приложение тщательно протестировано в отладочной конфигурации, и вы убедились, что оно работает правильно, обычно вы пересобираете его в рабочей конфигурации; это порождает оптимизированный код без возможностей отладки и трассировки, в результате чего программа работает быстрее и требует меньше памяти.
Выполнение программы
После успешной компиляции и компоновки решения вы можете выполнить программу, нажав <Ctrl+F5>. При этом вы должны увидеть окно, показанное на рис. 1.12.
Рис. 1.12. Выполнение консольной программыРис. 1.12. Выполнение консольной программы
Рис. 1.12. Выполнение консольной программы
Рис. 1.12. Выполнение консольной программы
Как видите, в командной строке выводится текст, заключенный в двойные кавычки в исходном коде. Фрагмент “\п”, который указан в конце текста — это специальная последовательность, которая называется управляющей последовательностью и означает символ новой строки. Управляющие последовательности служат для представления символов в текстовой строке, которые невозможно непосредственно ввести с клавиатуры.
Практическое занятиеПрактическое занятиеI
Практическое занятие
Практическое занятие
Создание пустого консольного проекта
Предыдущий проект содержит в себе некоторый излишний багаж, который не нужен при работе с простыми примерами на C++. Опция предварительной компиляции заголовков, выбранная по умолчанию, приводит к созданию в проекте заголовочного файла stdafx.h. Это механизм, повышающий эффективность процесса компиляции, когда программа состоит из большого количества файлов, но не являющийся необходимым для большинства наших примеров. В их случае вы будете начинать с пустого проекта, к которому станете добавлять свои собственные исходные файлы. Теперь вы увидите, как это делается, создав новый проект и новое решение консольной программы Win32 по имени Ext 10 2. После того, как вы введете имя проекта и щелкнете на кнопке ОК, выполните щелчок на 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;// Вернуться в операционную систему
}
Win32 AppL zat on Wizard - Ex1_02
Application Settings
OverviewApplication SettingsOverviewApplication Settings
Overview
Application Settings
Overview
Application Settings
App'ication type:
Windows application
Console application
DLL
Static library
Additional options:
0 ^rnptY.proiecJ
Add common header files for:
< Previous
Net! ;=■
Finish
Cancel
Puc. 1.13. Настройка приложения Extl_02
Рис. 1.14. Добавление к проекту нового файла Extl__02
Обратите внимание на автоматический отступ, выполненный при вводе вашего кода. C++ использует отступы, чтобы увеличить читабельность программ, и редактор автоматически выполняет отступ для каждой вводимой вами строки кода, основываясь на содержимом предыдущих строк. В процессе ввода исходного кода вы также увидите в действии автоматическое выделение цветом синтаксиса. Некоторые элементы программы будут показаны в разных цветах, потому что редактор автоматически раскрашивает элементы языка в зависимости от их значения.
Приведенный выше код составляет завершенную программу. Возможно, вы отметите ряд отличий по сравнению с кодом, сгенерированным мастером Application Wizard для предыдущего примера. Нет директивы #include для файла stdafx.h. Этот файл не входит в проект, поскольку вы не используете средство предварительной компиляции заголовков. Функция теперь называется main; ранее она называлась —tmain. Фактически все программы на ISO/ANSI C++ начинают выполнение с функции по имени main (). Microsoft также называет эту функцию wmain, когда применяется кодировка символов Unicode, и имя _tmain определено либо как main, либо как wmain, в зависимости того, используется ли кодировка Unicode. В предыдущем примере имя _tmain определено “за кулисами” как main. Во всех примерах ISO/ANSI C++ вы будете применять имя main.
Операторы вывода также несколько отличается. Первый оператор в main () выглядит так:
std::cout « "This is a simple program that outputs some text." « std::endl;
Здесь вы видите два появления операции «, и каждое посылает то, что за ним следует, в std: : cout — стандартный поток вывода. Сначала в поток отправляется строка между двойными кавычками, затем — std: :endl, где std: :endl определено в стандартной библиотеке как символ новой строки. Ранее вы уже использовали управляющую последовательность \п для обозначения символа новой строки внутри строки в двойных кавычках. То есть, последний оператор можно было бы записать и так:
std: :cout « "This is a simple program that outputs some text.\n";
Я должен пояснить, почему эта строка затенена, в то время как предыдущая — нет. Когда я повторяю строку кода без изменений, то оставляю ее не затененной. Данная последняя строка кода — новая, и ранее в примерах она не появлялась, поэтому я выделил ее полужирным.
Теперь вы можете построить проект точно так же, как в предыдущем случае. Обратите внимание, что все открытые в панели редактора исходные файлы сохраняются автоматически, если вы не сделали этого раньше. Когда программа будет успешно скомпилирована, нажмите <Ctrl+F5>, чтобы ее выполнить. Вы увидите окно, показанное на рис. 1.15.
Рис. 1.15. Выполнение программы Extl_02
Обращение с ошибками
Конечно, если вы неправильно введете текст программы, то получите сообщения об ошибках. Чтобы увидеть, как это работает, вы можете внести ошибку преднамеренно. Если вы уже получили какие-то сообщения об ошибках, то можете использовать их для этого упражнения. Вернитесь в панель редактора и удалите точку с запятой в конце предпоследней строки между фигурными скобками (в строке 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; там есть спецификации используемых типов
данных, информация о версии кода, а также информация, указывающая, должно ли содержимое данной сборки быть доступно другим сборкам. Короче говоря, сборка — это фундаментальный строительный блок программ CLR.
Рис. 1.16. Создание нового приложения CLR
Рис. 1.17. Файлы консольного проекта CLR
Если исходный код файла Ех1_03. срр еще не отображается в окне редактора, выполните двойной щелчок на имени файла в панели Solution Explorer. Окно редактора должно выглядеть, как показано на рис. 1.18.
Рис. 1.18. Исходный код Ех1 03. срр
Как видите, здесь есть те же директивы #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, не указывая имени пространства имен в качестве префикса. Если у вас случится конфликт между определенным вами именем и именем, определенным в библиотеке, вы можете решить эту проблему исключением
оператора using namespace из текста программы и указанием полного имени библиотечного компонента вместе с префиксом — наименованием пространства имен. Подробнее о пространствах имен вы узнаете в главе 2.
Вы можете скомпилировать и выполнить программу, нажав <Ctrl+F5>. Результат показан на рис. 1.19.
Рис. 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 (Параметры), которое отображается после выбора пункта Tools1^Options (Сервис^ Параметры) в главном меню. Диалоговое окно Options показано на рис. 1.20.
Щелчок на символе (+) у любого элемента в левой панели отображает список его подразделов. На рис. 1.20 показаны опции подраздела General (Общие), относящегося к разделу Projects and Solutions (Проекты и решения). Правая панель отображает опции, которые вы можете настроить для раздела, выбранного в левой панели. Пока вам достаточно рассмотреть лишь несколько из них, но возможно, вы сочтете полезным потратить некоторое время на просмотр всех опций, доступ к которым обеспечивает это диалоговое окно. Щелчок на кнопке получения справки (с символом ‘?’) в правом верхнем углу диалогового окна отобразит пояснение к текущим выбранным опциям.
Возможно, при создании нового проекта вы решите использовать путь по умолчанию, и вы можете сделать это через первую опцию, показанную на рис. 1.20. Просто укажите путь к папке файловой системы, в которой вы хотите размещать свои решения и проекты.
Вы можете установить опции, прилагаемые к каждому проекту C++, выбрав раздел Projects and Solutionsc>VC++ Project Settings (Проекты и решения^Настройки проекта VC++) в левой панели. Также вы можете установить опции, специфичные для
данного проекта через пункт Projects Properties (Проект^Свойства) главного меню. Этот пункт меню будет дополнен именем текущего проекта.
Op nsEnvironmentF^nrrmance TodsProjects and soli Jtions■Gpnfl»"7llBuM an d RunVB DefaultsVC-м- DirectoriesVC+f Project Settings+■ Source ControlText EditorF Database ToolsDebugging* Device Foal sHTL'L designer• Microsoft Office Keyboard Settings+ Te£t Toolsr Windows Ferns DesignerOp nsEnvironmentF^nrrmance TodsProjects and soli Jtions■Gpnfl»"7llBuM an d RunVB DefaultsVC-м- DirectoriesVC+f Project Settings+■ Source ControlText EditorF Database ToolsDebugging* Device Foal sHTL'L designer• Microsoft Office Keyboard Settings+ Te£t Toolsr Windows Ferns DesignerVisual Studio projects location:D:\peglrning Visual C-n- 2JOS'.ExamplesVisual Studio projects location:D:\peglrning Visual C-n- 2JOS'.Examplesvisual studio use project templates loceton:C:\Doc .marts and Setting^rorEiorto^My Documente^Vrsual Studio 2visual studio use project templates loceton:C:\Doc .marts and Setting^rorEiorto^My Documente^Vrsual Studio 2Visual Studio user item templates location:C:\pocuments a nd Settings^or Hertor^My Docurnents^Visual Studio 2Visual Studio user item templates location:C:\pocuments a nd Settings^or Hertor^My Docurnents^Visual Studio 2i I Always show Error List if build finishes with errors□ Track Active Item in Solution ExplorerPl Show cdvanctd bull configurationsE Always show solution0 Sax e ne^ projects when created0 Warn user when the project location is not trusted0Show Output window when build starts□Prompt for 3m belie renaming when renaming filesi I Always show Error List if build finishes with errors□ Track Active Item in Solution ExplorerPl Show cdvanctd bull configurationsE Always show solution0 Sax e ne^ projects when created0 Warn user when the project location is not trusted0Show Output window when build starts□Prompt for 3m belie renaming when renaming filesCancelCancel
Op ns
Environment
F^nrrmance Tods
Projects and soli Jtions
■Gpnfl»"7ll
BuM an d Run
VB Defaults
VC-м- Directories
VC+f Project Settings
+■ Source Control
Text Editor
F Database Tools
Debugging
* Device Foal s
HTL'L designer
• Microsoft Office Keyboard Settings
+ Te£t Tools
r Windows Ferns Designer
Op ns
Environment
F^nrrmance Tods
Projects and soli Jtions
■Gpnfl»"7ll
BuM an d Run
VB Defaults
VC-м- Directories
VC+f Project Settings
+■ Source Control
Text Editor
F Database Tools
Debugging
* Device Foal s
HTL'L designer
• Microsoft Office Keyboard Settings
+ Te£t Tools
r Windows Ferns Designer
Visual Studio projects location:
D:\peglrning Visual C-n- 2JOS'.Examples
Visual Studio projects location:
D:\peglrning Visual C-n- 2JOS'.Examples
visual studio use project templates loceton:
C:\Doc .marts and Setting^rorEiorto^My Documente^Vrsual Studio 2
visual studio use project templates loceton:
C:\Doc .marts and Setting^rorEiorto^My Documente^Vrsual Studio 2
Visual Studio user item templates location:
C:\pocuments a nd Settings^or Hertor^My Docurnents^Visual Studio 2
Visual Studio user item templates location:
C:\pocuments a nd Settings^or Hertor^My Docurnents^Visual Studio 2
i I Always show Error List if build finishes with errors
□ Track Active Item in Solution Explorer
Pl Show cdvanctd bull configurations
E Always show solution
0 Sax e ne^ projects when created
0 Warn user when the project location is not trusted
0Show Output window when build starts
□Prompt for 3m belie renaming when renaming files
i I Always show Error List if build finishes with errors
□ Track Active Item in Solution Explorer
Pl Show cdvanctd bull configurations
E Always show solution
0 Sax e ne^ projects when created
0 Warn user when the project location is not trusted
0Show Output window when build starts
□Prompt for 3m belie renaming when renaming files
Cancel
Cancel
Puc. 1.20. Диалоговое окно Options (Параметры)
Создание и выполнение Windows-приложений
Только для того, чтобы показать, насколько это просто, создадим два работающих Windows-приложения. Сначала будет построено родное приложение C++ с использованием библиотеки MFC, а затем — приложение Windows Forms, работающее под CLR. Обсуждение этих программ откладывается до тех пор, пока у вас не появится достаточного запаса знаний, чтобы понять их во всех подробностях. Пока просто демонстрируется тот факт, что создавать Windows-приложения достаточно просто.
Создание приложения MFC
Для начала, если у вас открыт существующий проект (на что указывает имя проекта в заголовке главного окна Visual C++ 2005), вы можете выбрать пункт меню File^ Close Solution (Файл1^Закрыть решение). В качестве альтернативы вы можете просто создать новый проект, что автоматически закроет текущее решение.
Чтобы создать Windows-программу, выберите пункт меню File1^ New^ Project (Файл1^ Создать1^ Проект) или нажмите комбинацию клавиш <Ctrl+Shift+N>, затем выберите тип проекта MFC, а в качестве шаблона проекта — MFC Application (Приложение MFC). После этого можете ввести имя проекта Ех1_04, как показано на рис. 1.21.
Когда вы щелкнете на кнопке ОК, появится диалоговое окно мастера создания приложений MFC (MFC Application Wizard). В нем вы найдете множество опций, которые позволяют указать, какие средства необходимо включить в новое приложение. Они идентифицируются элементами списка в правой части диалогового окна, как показано на рис. 1.22. Многие из них будут использоваться в дальнейших примерах.
Пока что вы можете проигнорировать все эти опции, и просто принять настройки по умолчанию, поэтому щелкните на кнопке Finish (Готово) для создания проекта с настройками по умолчанию. Панель Solution Explorer в окне IDE будет выглядеть так, как показано на рис. 1.23.
Рис. 1.21. Создание нового проекта приложения MFC
OverviewApplication T ypeCompound Document SupportDocument Template StringsDatabase SupportUser Interface FeaturesAdvanced FeaturesGenerated ClassesOverviewApplication T ypeCompound Document SupportDocument Template StringsDatabase SupportUser Interface FeaturesAdvanced FeaturesGenerated Classes
Overview
Application T ype
Compound Document Support
Document Template Strings
Database Support
User Interface Features
Advanced Features
Generated Classes
Overview
Application T ype
Compound Document Support
Document Template Strings
Database Support
User Interface Features
Advanced Features
Generated Classes
MFC Application Wizard - Ex1„04
Welcome to the MFC Application Wizard
These are the current project settings:
Multiple document interface
No database support
No compound document support
Click Push from any window to accept me current settings.
After you create the project, see the project's readme.txt file for information about the project features and files that are generated.
< Pre s
Next >
Finish
Cancel
Рас. 1.23. Панель Solution Explorer для проекта Ех1_04
Обратите внимание, что я скрыл вкладку Property Manager (Диспетчер свойств), щелкнув на ней правой кнопкой мыши и выбрав в контекстном меню пункт Hide (Скрыть), поэтому она не появляется на рис. 1.23. Список содержит множество автоматически созданных файлов. Вам потребуется достаточно места на жестком диске, если вы собираетесь писать программы для Windows! Файлы с расширением . срр содержат исполняемый исходный код C++, а файлы . h хранят код C++, состоящий из определений, используемых исполняемым кодом. В файлах . ico находятся пиктограммы. Как видите, все файлы сгруппированы в нескольких вложенных папках, чтобы облегчить к ним доступ. Однако, это не настоящие папки, и они не появляются в папке проекта на вашем диске.
Если теперь вы заглянете в папку решения Ех1_04 с помощью Windows Explorer либо любого другого средства, которым вы пользуетесь для работы с файловой системой на жестком диске, то вы обнаружите там 24 сгенерированных файла. Три из них находятся в папке решения, еще 17 — в папке проекта и 4 — в папке res, вложенной в папку проекта. Файлы в этой папке содержат ресурсы, используемые программой, такие как меню и пиктограммы. Все это вы получаете после простого ввода имени нового проекта. С учетом такого большого числа файлов, создаваемых автоматически, вам должно стать ясно, что хранение каждого проекта в отдельном каталоге — более чем просто хорошая идея.
Один из файлов в каталоге проекта Ех1_04 называется ReadMe. txt, и в нем содержатся пояснения назначения каждого из файлов, сгенерированных мастером МЕС Application Wizard. Если хотите, загляните в него с помощью Notepad, WordPad или даже редактора Visual C++ 2005. Чтобы увидеть его в окне редактора IDE, дважды щелкните на нем в панели Solution Explorer.
Сборка и запуск приложения MFC
Прежде чем вы сможете запустить программу, вам необходимо собрать проект, то есть скомпилировать исходный код и скомпоновать программные модули. Это
делается точно так же, как вы уже делали при сборке консольных приложений. Для экономии времени, чтобы собрать проект и запустить готовую программу, нажмите <Ctrl+F5>.
После того, как проект будет успешно собран, в окне Output не появится никаких сообщений об ошибках, и программа будет запущена. Окно сгенерированной вами программы будет выглядеть, как показано на рис. 1.24.
Рис. 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.
Рис. 1.25. Создание нового проекта приложения Windows Forms
В этом случае больше нет никаких опций для выбора, поэтому щелкните на кнопке ОК для создания проекта.
Панель Solution Explorer на рис. 1.26 отображает файлы, сгенерированные для нового проекта.
Рис. 1.26. Панель Solution Explorer для проекта Ех1__05
Как видите, в этом проекте файлов сравнительно меньше — если вы заглянете в каталог, то обнаружите, что в решение включено всего 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 (Вид1^ Панель инструментов), то увидите дополнительное окно Toolbox (Панель инструментов), показывающее список компонентов GUI (рис. 1.28).
Окно Toolbox предлагает список стандартных компонентов, которые вы можете добавить в приложение Windows Forms. Можете добавить несколько кнопок в окно Ех1_05. Для этого щелкните на элементе Button (Кнопка) в списке окна Toolbox, а затем щелкните в клиентской области окна приложения Ех1_05, которое отображено в окне редактора, в том месте, куда хотите поместить кнопку. Затем можете подкорректировать размер кнопки, передвигая ее границы, а также переместить ее в любое место окна приложения. Вы также можете изменить метку кнопки, просто набрав ее на клавиатуре; попробуйте набрать Start (Пуск) и нажать клавишу <Enter>. Метка кнопки изменится, и наряду с этим появится другое окно, в котором будут отображены свойства этой кнопки. Пока я не буду погружаться в это, только отмечу, что свойства кнопки — это спецификация ее внешнего вида, который вы можете изменить, как того требует приложение. Попробуйте добавить еще одну кнопку, например, с меткой Stop (Стоп). Окно редактора будет выглядеть, как показано на рис. 1.29.
Программирование в Visual C++ 200561
Toolbox▼ - X
ti Ail Windows Forms ^ - Common Controls -t Pointer ® Button 0 CheckBox 15 Checked ListBox 3 Com bo Box
DateTimePicker A Label A LinkLabel 5 ListBox “■ Listview *-. MaskedTextBox Mo nth Calendar ^ Notifylcon 13 NumericUpDown lU Picture Box ® ProgressBar Ra dio Button
■^ RichTextBox abii TextBox ^ ToolTip X:_ TreeView - , WebBrowser - Containers t Pointer ^ FlowLayoutPanel GroupBox
□ Panel
Ш Splitcontainer
TahCnntml2^
Puc. 1.28. Панель инст^ментов для построения графического интерфейса пользователя
Рис. 1.29. Окно редактора для приложения Windows Forms после добавления двух кнопок
Вы можете графически редактировать любые компоненты 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++.
Определение переменных и констант.
Базовый ввод с клавиатуры и вывод на экран.
Выполнение арифметических вычислений.
Приведение операндов.
Область видимости переменных.
Структура программы 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.
Когда вызывается функция,
Рис. 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.
Рис. 2.2. Создание нового проекта консольного приложения Win32
OverviewApplication SettingsOverviewApplication Settings
Overview
Application Settings
Overview
Application Settings
Win32 Applicati on W zard - Ех2_01
Welcome to the Win32 Application Wizard
These are the current project settings:
• Console application
Click Fnish from any window to accept tie current settings.
After you create the project; see the projects readme, txt file for information about the project features and files that are generated.
Next >
Finish
Cancel
Рис. 2.3. Мастер создания приложений
Рис. 2.4. Дополнительные опции консольного приложения Win32
Настройкой по умолчанию будет Console application (Консольное приложение), что включает в себя файл, содержащий версию функции main () по умолчанию, но вы должны начать с самой базовой структуры проекта, поэтому выберите Empty project (Пустой проект) в наборе дополнительных опций и щелкните на кнопке Finish (Готово). Теперь ваш проект создан, но не содержит никаких файлов. Вы можете видеть, что входит в проект на панели Solution Explorer в левой части главного окна Visual C++ 2005, как показано на рис. 2.5.
5_0
Solution 'Ех2_01' (1 project)
Ei ‘^■ExFoi
Header Files
Resource Files
Source Files
Solution Explorer |l^ Class View ^Resource View
Puc. 2.5. Состав проекта Ex2_01
Начните с добавления к проекту нового исходного файла, для чего щелкните правой кнопкой мыши на Source Files (Исходные файлы) в панели Solution Explorer и выберите в контекстном меню пункт Add^New Item (Добавить1^Новый элемент). При этом появится диалоговое окно Add New Item (Добавить новый элемент), подобное тому, что показано на рис. 2.6.
Рис. 2.6. Диалоговое окно Add New Item (Добавить новый элемент)
Убедитесь, что выделен шаблон C++ File (.срр) (Файл C++ (.срр)), щелкнув на нем, и введите имя файла, как показано на рис. 2.6. Файл автоматически получит расширение . срр, поэтому набирать его не нужно. Не будет никаких проблем, если имя файла совпадет с именем проекта. Файл проекта имеет расширение .vcproj, поэтому его полное имя будет отличаться от имени исходного файла
Щелкните на кнопке Add (Добавить) для создания файла. Затем вы можете набрать следующий код в панели редактора окна IDE:
/ / Ех2_01. срр
/ / Простой пример программы
#include <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;// Выход из программы
}
Цель этого примера— проиллюстрировать некоторые способы, применяемые для написания операторов C++, не являюгциеся, однако, демонстрацией хорошего стиля программирования.
Как только на основании расширения файл идентифицирован как файл исходного кода C++, ключевые слова, распознаваемые редактором, окрашиваются соответствующим образом. Вы сразу увидите ошибку, если введете Int там, где должны ввести int, потому что слово Int не будет окрашено в тот же цвет, который применяется для выделения ключевых слов языка в исходном коде.
Если вы посмотрите на панель Solution Explorer, где открыт ваш новый проект, то увидите вновь созданный исходный файл. Solution Explorer всегда показывает все файлы проекта. Если щелкнуть на вкладке Class View (Представление классов) в нижней части панели Solution Explorer, будет отображено представление Class View. Эта панель состоит из двух частей: верхней, показывающей глобальные функции и макросы, определенные в проекте (и еще классы, если это проект, включающий в себя классы), и изначально пустой нижней. Функция main () появится в нижней панели, если вы выберете элемент Global Functions and Variables (Глобальные функции и переменные) в верхней панели Class View, как показано на рис. 2.7. Чуть позже я объясню подробно, что все это значит, а пока отмечу, что по сути глобальными являются функции и/или переменные, доступные в любом месте программы.
Class View▼ Д X
< Search'▼ . j
е 79 Ex2_oi
label functions and Variable ^ Maci os and Constants
* main(void)
L ^Solution Explorer|l^Class View L^Resource View
Puc. 2.7. Представление Class View
Существуют три способа компиляции и компоновки программы; вы можете выбрать пункт Build Ех2_01 (Собрать Ех2_01) в меню Build (Сборка) либо нажать функциональную клавишу <F7>, или же выбрать соответствующую кнопку на линейке инструментов — найти ее можно, перемещая курсор над кнопками с небольшой задержкой. Если предположить, что операция сборки программы завершилась успешно, вы можете запустить программу, нажав комбинацию клавиш <Ctrl+F5> или вы- брав пункт Start Without Debugging (Запустить без отладки) в меню Debug (Отладка). В результате вы должны получить следующий вывод в окне командной строки:
Апельсины - не единственные фрукты...
- и всего у нас 11 фруктов.
Press any key to continue . . .
Для продолжения нажмите любую клавишу . . .
Первые две строки выдала программа, а последняя указывает, что вы можете завершить выполнение и закрыть окно командной строки.
Комментарии к программе
Первых две строки программы — это комментарии. Комментарии рассматриваются как важная часть программы, однако они не являются исполняемым кодом — это просто подсказки для читателей-людей. Все комментарии компилятором игнорируются. В любой строке кода два последовательных слеша //, если они не содержатся внутри текстовой строки (что такое текстовые строки, вы увидите позже), говорят о том, что остальная часть строки справа от них представляет собой комментарий.
Как видите, несколько строк в программе содержат комментарии вместе с операторами самой программы. Вы также можете применять альтернативную форму комментариев, ограничивая их комбинациями символов /* и */. Например, первая строка программы могла бы выглядеть следующим образом:
/* Ех2_01.срр */
Комментарий, начинающийся с //, включает только часть строки справа от него, в то время как форма /*. . . */ определяет, что все, находящееся между /* и */, является комментарием, и может распространяться на несколько строк. Например, вполне можно написать так:
/*
Ех2_01.срр
Простой пример программы
*/
Здесь все четыре строки являются комментариями. Если вы хотите выделить некоторые строки комментариев, то всегда можете украсить их рамкой:
у****************************
Ех2_01.срр*
Простой пример программы *
****************************у
Как правило, всегда нужно стараться снабжать программы исчерпывающими комментариями. Комментарии должны быть достаточными, чтобы другой программист или вы сами через какое-то время, смогли понять назначение конкретной части кода и то, как он работает.
Директива #include — файлы заголовков
Вслед за начальными комментариями в программе находится директива #include:
#include <iostream>
Она называется директивой, поскольку указывает компилятору сделать что-то — в данном случае вставить содержимое файла <iostream> в программу перед ее компиляцией. Файл <iostream> называется файлом заголовков, потому что обычно он появляется в начале программного файла. В действительности точнее было бы назвать <iostream> заголовком, так как согласно стандарту ANSI C++ заголовок не обязательно должен содержаться в файле. Заголовочный файл <iostream> включает определе-
ния, которые понадобятся вам для того, чтобы можно было использовать операторы ввода и вывода 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 — символ новой строки.
Чуть позднее в этой главе вы узнаете больше об этом, включая и то, как определять свои собственные пространства имен.
Функция main ()
Функция main () в последнем примере состоит из заголовка main () и всего остального, начиная с открывающей фигурной скобки ({) и до соответствующей закрывающей фигурной скобки (}). Фигурные скобки заключают в себе исполняемые операторы функции, которые все вместе называются телом функции.
Как вы убедитесь, любая функция состоит из заголовка, определяющего (помимо всего прочего) ее имя, за которым следует тело функции, включающее множество операторов, заключенных в пару скобок. Тело функции может не содержать в себе вообще никаких операторов — в таком случае функция ничего не делает.
Функция, которая ничего не делает, может показаться излишней, но когда вы пишете крупную программу, то можете изначально отобразить полную структуру программы на функции, однако опустить код многих из них, оставляя их тела пустыми либо с минимальным содержимым. Поступая так, вы обеспечиваете возможность компиляции и выполнения всей программы, со всеми ее функциями в любой момент времени, с последовательным инкрементным добавлением кода этих функций в процессе разработки.
Операторы программы
Каждый из операторов программы, образующих тело функции main () , завершается точкой с запятой. Этот символ помечает конец оператора, а не конец строки. Следовательно, один оператор может распространяться на несколько строк, если это помогает понять код, либо несколько операторов могут находиться в одной строке. Оператор программы — базовый элемент, определяющий то, что делает программа. Он в некоторой степени похож на предложение в абзаце текста, где каждое из них выражает действие или идею, но при этом комбинируется с другими предложениями абзаца для определения более общей идеи. Оператор — самодостаточное определение действия, которое может выполнять компьютер, но которое может комбинироваться с другими операторами с целью определения более сложного действия или вычисления.
Действие функции всегда выражается набором операторов, каждый из которых завершается точкой с запятой. Взгляните на операторы в последнем примере, просто чтобы почувствовать, как они работают. Каждый из этих операторов позднее в этой главе будет описан более подробно.
Вот первый оператор в теле функции main ():
int apples, oranges; // Объявление двух целочисленных переменных
Этот оператор объявляет две переменные — apples и oranges. Переменная — это просто именованный фрагмент памяти компьютера, который вы можете использовать для сохранения данных, а оператор, представляющий имена одной или более переменных, называется объявлением переменной. Ключевое слово int в предыдущем примере означает, что переменные с именами apples и oranges предназначены для хранения целочисленных значений. Везде, где в программе объявляется имя новой переменной, всегда должен указываться вид данных, которые она может сохранять, и это называется типом переменной.
Следующий оператор объявляет другую целочисленную переменную — fruit:
int fruit;// . . .и еще одной
Хотя и можно объявлять несколько переменных в одном операторе, как это сделано в предыдущей строке с 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 предназначено для того, чтобы представлять стандартный выходной поток, вы не должны использовать это имя для других целей, а потому не можете применять его, например, в качестве имени переменной. Очевидно, что использование одного и того же имени для различных вещей может стать причиной путаницы.
Второй из трех операторов вывода распространяется на две строки:
cout « "Апельсины - не единственные фрукты. . . " « endl « "- и всего у нас " « fruit « " фруктов.";
Как упоминалось ранее, вы можете размещать один оператор программы в стольких строках, в скольких хотите, если это сделает код более ясным. Конец оператора всегда отмечается точкой с запятой, а не концом строки. Последовательные строки читаются и комбинируются компилятором в один оператор, пока он не обнаружит точку с запятой, означающую его конец. Конечно, это значит, что если вы забудете поставить точку с запятой в конце оператора, компилятор будет считать следующую строку частью того же оператора и соединит их вместе. Обычно в результате получается нечто такое, что компилятор не может понять, поэтому вы получите сообщение об ошибке.
Приведенный выше оператор посылает в командную строку текст “Апельсины - не единственные фрукты. . . ”, за которым следует еще один символ новой строки (endl), потом — еще одну часть текста “- и всего у нас ”, затем значение переменной fruit и завершающий текст “ фруктов. ”. В такой последовательности сущностей, отправляемой в выходной поток, не кроется никаких проблем. Оператор выполняется слева направо, при этом каждый элемент отправляется на cout по очереди. Обратите внимание, что каждому отправленному на вывод элементу при этом предшествует собственная операция «.
Третий и последний оператор вывода просто посылает на экран еще один символ новой строки. Так эти три оператора формируют вывод программы, который вы и наблюдаете.
Последний оператор программы выглядит следующим образом:
return 0; // Выход из программы
Этот оператор прекращает выполнение функции main () и завершает программу. Управление возвращается операционной системе. Об этом операторе будет рассказано позже.
Операторы программы выполняются в том порядке, в котором они записаны, если только специальные управляющие операторы не изменяют естественный порядок их выполнения. В главе 3 вы ознакомитесь с операторами, изменяющими последовательность выполнения.
Пробелы
Пробел (whitespace) — термин, используемый в C++ для обозначения символов пробела, табуляции, новой строки, новой страницы и комментариев. Пробелы служат разделителями одной части оператора от другой и позволяют компилятору идентифицировать, где заканчивается один элемент оператора, такой как int, и начинается другой. В остальных случаях пробелы игнорируются и никакого влияния не оказывают.
Посмотрим, к примеру, на следующий оператор:
int fruit; // . . .и еще одной
Должен быть, по крайней мере, один пробельный символ (обычно — пробел) между int и fruit, чтобы компилятор мог различить их, но если пробелов будет больше, они игнорируются.
С другой стороны, взгляните на такой оператор:
fruit = apples + oranges; // Получить сумму
Ни один пробельный символ не является необходимым между f ruit и =, или между = и apples, хотя при желании вы можете их включить сюда. Дело в том, что = — это не буква и не цифра, поэтому компилятор может отделить его от окружения. Аналогично никакие пробельные символы не нужны вокруг знака +, но вы можете включить их, если хотите сделать код более читабельным.
Как я сказал, помимо применения в качестве разделителей между элементами оператора, которые в противном случае могли бы быть спутаны, пробелы компилятором игнорируются (конечно, если только они не являются частью строки между двумя кавычками). Поэтому вы можете включить в свою программу сколько угодно пробелов, если считаете, что это повысит ее читабельность, как это и было сделано в приведенном выше примере, где один оператор распространялся на несколько строк программы. Помните, что в C++ концом оператора является точка с запятой.
Блоки операторов
Вы можете заключить несколько операторов в фигурные скобки. В этом случае они образуют блок, или составной оператор. Примером блока может служить тело функции. Такой составной оператор можно воспринимать как единственный оператор (в чем вы убедитесь, изучая управляющие операторы C++ в главе 3). Фактически, всякий раз, когда вы помещаете отдельный оператор в C++, вы можете вместо него использовать блок операторов в фигурных скобках. Как следствие, блоки могут быть включены в другие блоки. Фактически блоки могут быть вложенными один в другой на любую глубину.
Блок операторов также оказывает важное влияние на переменные, но я отложу дискуссию на эту тему до того момента, когда позднее в этой главе обращусь к теме видимости переменных.
Автоматически сгенерированные консольные программы
В последнем примере вы настроили проект как пустой (Empty project), без исходных файлов, а затем последовательно добавили их. Если же вы позволите мастеру Application Wizard сгенерировать проект, как это делалось в главе 1, то проект изначально будет содержать несколько файлов, и вам стоит немного глубже разобраться в их содержимом. Создайте новый проект консольной программы Win32 по имени Ех2_01А, на этот раз позволив мастеру Application Wizard сделать свою работу, не устанавливая никаких опций в диалоге Application Settings (Настройки приложения). Проект будет включать в себя три файла с кодом: Ех2_01А. срр и stdaf х. срр, содержащие исходный код, и заголовочный файл stdafx.h. Они предназначены для обеспечения базовых средств, необходимых работающей консольной программе, которая не делает ничего. Если у вас есть открытый проект, вы можете закрыть его, выбрав пункт меню File«=>Close Solution (Файл1^ Закрыть решение). Вы можете создать новый проект, не закрывая старый — при этом он будет закрыт автоматически, если только вы не предпочтете добавить новый к существующему решению.
Прежде всего, содержимое Ех2_01А.срр будет таким:
#include "stdafx.h”
int _tmain(int argc, _TCHAR* argv[])
{
return 0;
}
Это совершенно отличается от предыдущего примера. Присутствует директива #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++.
Определение переменных
Фундаментальное назначение всех компьютерных программ — манипулировать некоторыми данными и получать некоторые ответы. Существенным условием этого процесса является наличие в вашем распоряжении фрагмента памяти, к которому можно обращаться по некоторому осмысленному имени, и где можно хранить элемент данных. Каждый фрагмент памяти, специфицированный таким образом, называется переменной.
Как вы уже знаете, каждая переменная сохраняет данные определенного вида, и тип сохраняемых данных фиксируется при объявлении переменной в программе. Одна переменная может хранить только целые числа, и вы не можете использовать ее для хранения дробных значений. Конкретное значение, хранимое в переменной в каждый отдельный момент времени, определяется операторами вашей программы, и конечно, как правило, ее значение будет меняться много раз в процессе вычислений, выполняемых программой.
В следующем разделе мы поговорим о правилах именования переменных при объявлении их в программах.
Именование переменных
Имя, которое вы присваиваете переменной, называется идентификатором, или проще — именем переменной. Имена переменных могут включать буквы A-z (в верхнем или нижнем регистре), цифры 0-9 и знаки подчеркивания. Никакие другие символы не допускаются, и если вы нечаянно укажете какой-то другой символ, то получите сообщение об ошибке при попытке скомпилировать программу. Имена переменных должны начинаться либо с буквы, либо с подчеркивания. Обычно выбор имен переменных обусловлен видом хранимой в них информации.
Поскольку имена переменных в Visual C++ 2005 могут иметь длину до 2048 символов, вам предоставляется достаточно свободы в именовании ваших переменных. Фактически, помимо переменных есть несколько других вещей в C++, которые имеют собственные имена, и они тоже могут содержать до 2048 символов, подчиняясь тем же правилам, что и имена переменных. Применение имен максимальной длины может затруднить чтение ваших программ, и если только у вас нет изумительных навыков работы на клавиатуре, их очень трудно набирать. Но более серьезное обстоятельство, которое следует принять во внимание при выборе имен — это то, что не все компиляторы поддерживают такие длинные имена. Если вы намереваетесь компилировать свой код в других средах, то желательно ограничиться именами не длиннее 31 символа. Обычно этого достаточно, чтобы выбирать осмысленные значащие имена, и позволяет избежать проблем, связанных с ограничениями на длину имен, присущими другим компиляторам.
Хотя разрешается использовать имена переменных, начинающиеся со знака подчеркивания, например, _this и __that, все же этого лучше избегать, потому что это чревато потенциальными конфликтами со стандартными системными переменными, имеющими такую же форму. По той же причине следует избегать применения имен переменных, начинающихся с двух знаков подчеркивания.
Ниже представлены примеры хороших имен переменных.
price
discount
pShape
value_
COUNT
Имена вроде 8_Ball, 7Up и 6_pack не допускаются, равно как и Hash! или Маг у-Ann. Последний пример демонстрирует распространенную ошибку, хотя имя Магу_Апп со знаком подчеркивания вместо тире вполне приемлемо. Конечно, Магу Ann — тоже неправильно, поскольку пробелы в именах переменных не разрешены. Обратите внимание, что имена переменных republican и Republican трактуются как различные, потому что имена переменных зависят от регистра, то есть символы верхнего и нижнего регистра различаются. Конечно, пробельные символы вообще не могут появляться в именах, и если вы нечаянно включите пробельные символы, то получите два или более имен вместо одного, что обычно заставляет компилятор выдавать соответствующее сообщение.
Обычное соглашение, принятое в C++, заключается в том, что имена, начинающиеся с заглавных букв, резервируются для именования классов, а начинающиеся с прописных — для именования переменных. О классах речь пойдет в главе 8.
Ключевые слова в 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.
Термин объявление вы будете применять для представления имени вашей программе, вместе с информацией о том, для чего это имя используется. Термин определение описывает выделение компьютерной памяти, связанной с этим именем. В случае переменных вы можете
совместить объявление и определение в одном операторе, как показано в предыдущей строке кода. Причина столь педантичного различия между объявлением и определением состоит в том, что позднее вы столкнетесь с операторами, которые являются объявлениями, но не определениями.
Вы должны объявлять переменную в некоторой точке, находящейся между началом вашей программы и тем местом, где она будет впервые задействована. В 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 также определяет фундаментальные типы дан-
ных, которые не являются частью 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 = OL;
Эти операторы объявляют переменные 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.
В других компиляторах 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 А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, предшествующий символьной константе * Z *, сообщает компилятору, что это 16-битное значение кода символа.
Для инициализации переменных char (как и других целых типов) вы также можете использовать шестнадцатеричные константы, что во многих случаях удобнее. Шестнадцатеричное число записывается с применением стандартного представления шестнадцатеричных цифр: от 0 до 9 и от А до F (или от а до f), означающие в десятичном эквиваленте цифры от 0 до 15. Кроме того, они предваряются сочетанием Ох (или ОХ) для того, чтобы отличать их от десятичных значений. Таким образом, чтобы получить тот же результат, вы могли бы переписать оператор, приведенный выше, следующим образом:
char letter = 0x41; // Эквивалент А
Не записывайте десятичные целочисленные значения с ведущим нулем. Компилятор интерпретирует их как восьмеричные, поэтому значение, записанное как 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— ключевые слова, поэтому их нельзя применять в качестве имен переменных.
Булевский тип
Булевские переменные — это такие переменные, которые могут хранить только два значения: 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Д25х102, что равно 112,5.
Константы с плавающей точкой должны включать десятичную точку, либо экспоненту, либо и то, и другое. Если вы записываете числовое значение без них, то получаете целое.
Вы можете специфицировать переменную с плавающей точкой, используя ключевое слово double, как в следующем операторе:
double in_to_mm = 25.4;
Переменная типа double занимает 8 байт памяти и сохраняет значения, точность которых определяется примерно 15 десятичными знаками. Диапазон их значений намного шире, чем можно выразить 15-ю знаками — начиная от 1,7x10'308 и заканчивая 1,7x10308, положительные и отрицательные.
Если вам не требуется 15-значная точность и не нужны очень большие диапазоны значений, которые обеспечивает тип double, можете воспользоваться ключевым словом float для объявления переменных с плавающей точкой, занимающих 4 байта.
Например:
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 C++
В табл. 2.1 представлен список всех фундаментальных типов ISO/ANSI C++, а также диапазоны их допустимых значений в Visual C++ 2005.
Таблица 2.1. Фундаментальные типы ISO/ANSI C++
Тип
Размер в байтах
Диапазон значений
Bool
1
true или false
Char
1
По умолчанию — то же, что и signed char: от -128 до +127. Необязательно вы можете задать для char тот же диапазон, что и у типа unsigned char.
signed char
1
от -128 до +127
unsigned char
1
от 0 до 255
wchar_t
2
от 0 до 65535
short
2
от -32768 to +32767
unsigned short
2
от 0 до 65535
int
4
от -2147483648 до 2147483647
unsigned int
4
от 0 до 4294967295
long
4
от -2147483648 до 2147483647
unsigned long
4
от 0 до 4294967295
float
4
± 3,4x10±38, примерно с 7-значной точностью
double
8
± 1,7х10±308, примерно с 15-значной точностью
long double
8
± 1,7х10±308, примерно с 15-значной точностью
Литералы
Я уже использовал в этой книге множество явных значений для инициализации переменных. В C++ фиксированные значения любого рода называются литералами. Литерал — это значение определенного типа, поэтому 23, 3.14159, 9.5f и true — примеры литералов типа int, типа double, типа float и типа bool, соответственно. Литерал "Samuel Beckett" — это пример строкового литерала, но обсуждение этого типа мы отложим до главы 4. В табл. 2.2 представлены примеры записи литералов различных типов.
Таблица 2.2. Примеры литералов различных типов
Тип
Примеры литералов
char, signed char или unsigned char
'A', 'Z', ' 8 ', '*'
wchar_t
L’A’, L' Z ', L' 8 ’, L' * '
int
-77, 65, 12345, 0x9FE
unsigned int
10U, 64000U
long
-77L, 65L, 12345L
unsigned long
5UL, 999999999UL
float
3.14f, 34.506f
double
1.414,2.71828
long double
1.414L, 2.71828L
bool
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, то можете применять оба спецификатора типа в одной и той же программе для объявления различных переменных, которые в итоге будут восприняты компилятором как однотипные.
Поскольку 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 как перечисление, которое может принимать значения, специфицированные ранее. Вы даже можете пропустить ключевое
слово enum в объявлении переменной, поэтому предыдущий оператор может быть переписан так:
Week nextWeek;
При желании вы можете присвоить определенные значения всем перечислителям. Например, можно определить следующее перечисление:
enum Punctuation {Comma = *, *, Exclamation = •I*, 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=’Fl, Saturday=’S’, Sunday=,S'};
Теперь значения констант немного лучше отображают то, что они представляют, хотя не делают различия между Thursday и Tuesday или между Saturday и Sunday. Нет никаких проблем в том, что разные константы имеют одно и то же значение, но имена, конечно, должны быть уникальными.
А вот пример перечисления с константами типа bool:
enum State : bool { On = true, Off};
Поскольку On имеет начальное значение true, то Off будет false. Если бы были еще указаны последующие константы перечисления, они по умолчанию получили бы чередующиеся значения.
Базовые операции ввода-вывода
Здесь вы только поверхностно ознакомитесь с вводом и выводом в “родном” C++ — настолько, чтобы использовать его в примерах в процессе дальнейшего изучения языка. Это совсем не трудно, даже наоборот. А при программировании для Windows оно и вовсе не понадобится. Ввод и вывод C++ вращается вокруг понятия потоков данных; данные можно вставлять в выходной поток и принимать из входного. Вы уже видели, что стандартный выходной поток ISO/ANSI C++ в командную строку на экране называется cout. Дополняющий его входной поток с клавиатуры известен под именем cin.
Ввод с клавиатуры
Вы получаете ввод с клавиатуры через поток cin, используя для этого операцию извлечения из потока ». Чтобы прочитать два целых значения с клавиатуры в переменные 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, и для передачи данных в него используется операция вставки «. Эта операция также “указывает” в направлении движения данных. Вы уже применяли ее для вывода текстовой строки, заключенной в кавычки. Я могу продемонстрировать процесс вывода значения переменной на примере простой программы.
Практическое занятие I
р I Вывод в командную строку
Предположим, что вы уже создали новый пустой проект, добавили к нему новый исходный файл и построили исполняемую программу. Ниже показан код, который необходимо поместить в исходный файл после создания проекта Ех2_02.
// Ех2_02.срр
// Упражнение по выводу
#include <iostream>
using std::cout;
using std::endl;
int main() {
int numl = 1234, num2 = 5678;
cout « endl;// Начать новую строку
cout « numl « num2; // Вывести две переменные
cout « endl;// Завершить строку
return 0;// Завершить программу
}
Описание полученных результатов
Первый оператор в теле main () объявляет и инициализирует две целочисленных переменных — numl и num2. За ним следуют два оператора вывода, первый из которых перемещает позицию экранного курсора на новую строку. Поскольку операторы вывода выполняются слева направо, второй оператор отображает значение переменной numl, за которым следует значение num2.
Когда вы скомпилируете и запустите приведенный выше код, то получите на экране следующий вывод:
12345678
Вывод правильный, но не слишком полезный. На самом деле вам нужно увидеть два отдельных значения, разделенных хотя бы одним пробелом. По умолчанию выходной поток просто отображает десятичные цифры, представляющие значение, что не предполагает какого-либо разделения выводимых значений пробелами. А потому у вас нет никакой возможности сказать, где заканчивается одно значение и начинается другое.
Форматирование вывода
Проблему, связанную с отсутствием пробелов между значениями, можно исправить довольно просто — вставив пробел в поток вывода между двумя значениями. Это можно сделать, заменив следующий оператор вашей оригинальной программы:
cout « numl « num2;cout « numl « num2;// Вывести два значения// Вывести два значения
cout « numl « num2;
cout « numl « num2;
// Вывести два значения
// Вывести два значения
Просто подставьте вместо него оператор:
cout « numl « * * « num2; // Вывести два значения
Конечно, если у вас несколько строк вывода, и вы хотите выровнять колонки, то вам понадобятся какие-то дополнительные возможности, поскольку вы не знаете, сколько знаков будет в каждом значении. С этой ситуацией можно справиться, используя то, что называется манипуляторами. Манипулятор модифицирует способ управления выводом данных в поток (или вводом из потока).
Манипуляторы определены в заголовочном файле <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 (б) и его операцию вставки из последнего примера.
Управляющие последовательности
Когда вы пишете символьную строку, заключенную в двойные кавычки, то можете включить в нее специальные символы, называемые управляющими последовательностями (escape sequences). Они так называются потому, что позволяют поместить в строку символы, которые не могут быть представлены иным образом, за
счет того, что они избегают (escaping) обычного процесса интерпретации символов. Управляющая последовательность начинается с символа обратного слеша \, который заставляет компилятор интерпретировать следующий за ним символ особым образом. Например, символ табуляции записывается, как \t, так что t понимается компилятором как табуляция в строке, а не буква Ч’. Взгляните на следующие два оператора вывода:
cout « endl « "Это вывод.";
cout « endl « "\t3TO вывод после табуляции.";
Они выведут на экран следующие строки:
Это вывод.
Это вывод после табуляции.
Комбинация \t во втором операторе вывода сдвигает следующий за ней текст в первую позицию табуляции.
Фактически, вместо использования endl вы можете применять управляющую последовательность символа новой строки \п в каждой строке, поэтому предыдущие операторы можно переписать так:
cout « "\пЭто вывод.";
cout « "\п\ГЭто вывод после табуляции.";
В табл. 2.3 даны некоторые управляющие последовательности, которые могут вам пригодиться.
Таблица 2.3. Некоторые полезные управляющие последовательности
Управляющая последовательность
Что делает
\а
Выдает звуковой сигнал
\п
Символ новой строки
V
Одиночная кавычка
\\
Обратный слеш
\ь
Забой
\t
Символ табуляции
\"
Двойная кавычка
\?
Знак вопроса
Очевидно, что если вы хотите включить обратный слеш или двойную кавычку в строку, вы должны использовать соответствующую управляющую последовательность, чтобы представить их. В противном случае обратный слеш будет интерпретирован как начало другой управляющей последовательности, а двойная кавычка — как конец символьной строки.
Вы также можете применять символы, специфицированные управляющими последовательностями, в инициализации переменных типа char, например:
char Tab = *\t’; II Инициализировать символом табуляции
Поскольку символьный литерал ограничивается символами одиночной кавычки, вы должны использовать управляющую последовательность, чтобы специфицировать символьный литерал, представляющий саму одиночную кавычку, то есть ’ \ ’ ’.
Практическое занятие | ИСПОЛЬЗОВаНИв УЯЗВЛЯЮЩИХ последовательностей
Ниже приведен текст программы, использующей некоторые управляющие последовательности из предыдущей таблицы:
// Ех2_04.срр
// Применение управляющих последовательностей
#include <iostream>
#include <iomanip> using std::cout;
int main() {
char newline = ’\n’; // Управляющая последовательность - символ новой строки cout « newline; И Начать новую строку
cout « ”\’’We\’ll make our escapes in sequence\”, he said.’’;
cout « ”\n\tThe programVs 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 выводится строка, которая применяет управляющие последовательности для представления символов двойной (\ ”) и одиночной (V) кавычек. Вы не обязаны использовать управляющую последовательность для одиночной кавычки, потому что строка ограничена двойными кавычками, и компилятор воспринимает одиночную кавычку внутри нее так, как она есть, а не в качестве разделителя. Однако внутри этой строки для представления двойной кавычки применять управляющую последовательность необходимо. Строка начинается с управляющей последовательности — символа новой строки, за которым идет управляющая последовательность символа табуляции, поэтому выходная строка сдвигается на расстояние табуляции вправо. Строка также заканчивается двумя экземплярами управляющих последовательностей, выдающих звуковой сигнал, поэтому вы можете услышать два подряд звуковых сигнала из динамика вашего компьютера.
Вычисления в C++
Здесь вы действительно начнете делать что-то с введенными данными. Теперь вы знаете, как организовать простой ввод и вывод; теперь обратимся к тому, что посредине — той части программы C++, которая занята обработкой данных. Все аспекты C++, связанные с вычислениями, достаточно интуитивно понятны, так что изучение этой темы пойдет у вас, как по маслу.
Операторы присваивания
Вы уже видели примеры присваивающих операторов. Типичный такой оператор выглядит следующим образом:
whole = parti + part2 + part3;
Оператор присваивания позволяет вычислить значение выражения, стоящего справа от знака равенства — в данном случае сумму parti, part2 и part3 — и сохранить результат в переменной, указанной слева от знака равенства — в данном случае, переменной whole. В показанном операторе whole — просто сумма частей, и ничего более.
Обратите внимание, что оператор, как всегда, завершается точкой с запятой.
Вы можете также писать повторяющиеся присваивания, как здесь:
а = Ь = 2;
Это эквивалентно присваиванию значения 2 переменной Ь с последующим присваиванием значения Ь переменной а. В результате обе переменных получают значение 2.
Понятия lvalue и rvalue
lvalue (left value — левое значение) — это нечто такое, что ссылается на адрес памяти и называется так потому, что любое выражение, дающее в результате lvalue, может быть поставлено слева от знака равенства в операторе присваивания. Большинство переменных являются lvalue, потому что они специфицируют место в памяти. Однако, как вы вскоре убедитесь, существуют переменные, которые не являются lvalue, и не могут появляться слева в операторах присваивания, потому что их значения определены как константные.
Переменные а и Ь, которые вы видели в предыдущем разделе, являются lvalue, в то время как результат вычисления выражения а+Ь не может быть таковым, поскольку для этого результата не определено место в памяти, где он должен быть сохранен. Результат выражения, не являющегося lvalue, называют rvalue (right value — правое значение).
Термин lvalue еш,е не раз появится в этой книге— иногда там, где вы менее всего будете его ожидать, поэтому запомните его определение.
Арифметические операции
Базовые арифметические операции, которые предоставлены в ваше распоряжение— сложение, вычитание, умножение и деление — обозначаются символами +, -, * и / соответственно. В основном они работают так, как и можно было ожидать, за исключением деления, в поведении которого, как вы увидите, существуют некоторые отклонения, когда оно применяется к целым или константам. Вы можете писать операторы вроде следующего:
netPay = hours * rate - deductions;
Здесь будет вычислено произведение hours и rate, затем из полученного результата вычтено deductions. Операции умножения и деления выполняются перед операциями сложения и вычитания, чего и следовало ожидать. Позднее в этой гла-
ве я расскажу подробнее о порядке выполнения различных операций в выражениях. Общий результат вычисления выражения 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;
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, а потому не может помещаться в левой части оператора присваивания.
Вы можете убедиться в этом, добавив в текст программы где-нибудь после объявления г oil width оператор вроде такого:
rollwidth = 0;
После этого программа перестанет компилироваться, а будет выдана ошибка ’error С2166: 1-value specifies const object’ (ошибка C2166: lvalue специфицирует константный объект).
Иногда бывает очень удобно определять константы, используемые в программе, снабжая модификатором const типы переменных, в частности, когда вы используете некоторые константы в программе несколько раз. Во-первых, это гораздо лучше, чем разбрасывать по всей программе литералы, назначение которых не очевидно. Например, значение 42 в программе может означать все что угодно, но если вы используете константную переменную по имени myAge, имеющую значение 42, то ее назначение сразу становится совершенно очевидным. Кроме того, если вам понадобится изменить значение применяемой константной переменной, то вам придется сделать это один-единственный раз, в одном исходном файле, дабы гарантировать, что это изменение автоматически появится везде, где упомянутая переменная используется. Эту технику вы будете использовать очень часто.
Константные выражения
Константная переменная roll length также инициализируется арифметическим выражением (12.0*33.0). Возможность использования константных выражений для инициализации переменных избавляет вас от необходимости вычислять их вручную при написании программы. К тому же такие выражения могут оказаться более информативными, потому что, например, 33 фута по 12 дюймов каждый более ясно указывает на смысл величины, чем если просто написать 396. Обычно компилятор точно вычисляет константные выражения, в то время как если вы станете делать это вручную, то в зависимости от сложности выражения и ваших способностей к вычислениям, появляется вероятность того, что оно будет вычислено неверно.
Вы можете использовать любое выражение, которое в результате вычисления даст константное значение во время компиляции, включая константные объекты, которые уже определены выше. Поэтому, например, если это пригодится в программе, вы можете объявить площадь стандартного рулона обоев так:
const double rollarea = rollwidth*rolllength;
Этот оператор должен быть помещен после объявления двух переменных const, использованных при инициализации rollarea, потому что все переменные, которые появляются в константном выражении, должны быть известны компилятору в той точке исходного файла, где появляется константное выражение.
Ввод программы
После объявления некоторых целочисленных переменных следующие четыре оператора программы обрабатывают ввод с клавиатуры:
cout « endl// Начать новую-строку
« "Введите высоту комнаты в дюймах: "; cin » height;
cout « endl// Начать новую строку
« "Введите длину и ширину в дюймах: "; cin » length » width;
Здесь выводится текст в cout, приглашающий пользователя ввести необходимую информацию с клавиатуры, используя cin, который представляет собой стандартный входной поток. Сначала вы получаете значение height, а затем последовательно читаете length и width. В реальной программе вам понадобилось бы проверить введен-
ные данные на предмет возможных ошибок и убедиться, что прочитанные значения имеют смысл, но пока у вас недостаточно знаний для этого!
Вычисление результата
В рассматриваемой программе присутствует четыре оператора, участвующих в вычислении количества стандартных рулонов обоев, необходимых для оклейки комнаты:
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> () и других типах явного приведения типов.
Обратите внимание на то, как вычисляется периметр комнаты в следующем операторе. Чтобы умножить сумму length и width на два, выражение сложения заключается в скобки. Это гарантирует, что сложение будет выполнено первым, а результат будет умножен на 2.0, чтобы получить правильное значение периметра. С помощью скобок вы можете гарантировать, что вычисление будет выполнено именно в том порядке, в каком нужно, потому что выражения в скобках всегда выполняются первыми. Если есть несколько вложенных друг в друга выражений со скобками, то эти выражения вычисляются последовательно — от внутренних скобок к внешним.
Третий оператор, вычисляющий количество полос обоев, необходимых для оклейки комнаты, использует тот же эффект, что вы видели в первом операторе. Результат округляется в меньшую сторону до ближайшего целого, поскольку он должен быть присвоен целочисленной переменной strips_reqd. Но это не то, что вам нужно на самом деле. Было бы лучше округлить в сторону большего, но пока у вас нет достаточных знаний о C++, чтобы сделать это. Прочитав следующую главу, вы сможете вернуться и внести соответствующие исправления.
Последнее арифметическое выражение вычисляет количество необходимых рулонов, разделив количество полос (как целое) на количество полос в рулоне (тоже как целое). Поскольку вы делите одно целое на другое целое, результат также будет целым числом, и любой остаток игнорируется. То же самое случится, даже если nrolls будет переменной с плавающей точкой. Целое значение, полученное от выражения деления, будет преобразовано в значение с плавающей точкой и сохранено в nrolls. Полученный результат, по сути, будет тем же, как если бы вы получили значение с плавающей точкой и округлили его в сторону ближайшего меньшего целого. Опять- таки, это не то, что вам нужно, поэтому если вы хотите использовать эту программу, ее придется откорректировать.
Отображение результата
Результат вычисления отображается с помощью следующего оператора:
cout « endl
« "Для оклейки вашей комнаты понадобится " « nrolls « " рулонов обоев." « endl;
Это один оператор вывода, разнесенный на три строки кода. Сначала оно выводит символ новой строки, затем текстовую строку "Для оклейки вашей комнаты понадобится ". После этого следует значение переменной nrolls, за которым — еще одна текстовая строка " рулонов обоев. ". Как видите, операторы вывода на C++ писать очень легко.
Программа завершается следующим оператором:
return 0;
Здесь 0 — это возвращаемое значение, которое в данном случае передается операционной системе. Подробнее о возвращаемых значениях вы узнаете в главе 5.
Вычисление остатка
Вы видели в последнем примере, что деление одного целого значения на другое дает целое с игнорирированием остатка, поэтому если 11 разделить на 4, то в результате получится 2. Поскольку остаток от деления может представлять значительный интерес, например, когда вы делите печенье между детьми, C++ предусматривает для этого специальную операцию — %. То есть, проблему деления печений можно решить, написав следующие операторы:
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 и с, а результат присвоен а.
Операции инкремента и декремента
Теперь я представлю несколько необычные арифметические операции, называемые операциями инкремента и декремента, и уверен, что вы сочтете их весьма полезными по мере серьезного применения 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—;
установит в total значение 11. Обе операции обычно применяются к целым, в частности, в контексте циклов, как будет показано в главе 3. Позже в других главах вы узнаете, что они также могут применяться и к другим типам данных C++, например, к переменным, хранящим адреса памяти.
Практическое занятие | ОпераЦИЯ ЗЭПЯТОЙ
Операция запятой (,) позволяет специфицировать несколько выражений там, где обычно может присутствовать только одно. Лучше всего это пояснить на примере, который демонстрирует, как это работает.
// Ех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
Это не требует пояснений. Переменная пшп4 принимает значение последнего из трех присваиваний, а присваиваемым значением является значение, которое получает его левая часть. Скобки в присваивании 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. Этот пример нацелен на то, чтобы проиллюстрировать эффект от операции запятой, но не является примером того, как следует писать хороший код.
Последовательность вычислений
До сих пор я не говорил о том, как повлиять на последовательность вычислений, участвующих в выражении. В основном это согласуется с тем, что вы изучали в школе, когда знакомились с основными арифметическими операциями, но в C++ присутствует множество других операций. Чтобы понять, что произойдет с ними, нужно рассмотреть механизм, используемый C++ для определения этой последовательности. Это то, что называется приоритетом операции.
Порядок выполнения операций
Порядок выполнения операций упорядочивает операции в порядке приоритетов. В любом выражении операции с более высоким приоритетом всегда выполняются первыми, за ними выполняются операции со следующим по возрастанию приоритетом и так далее, вплоть до тех, чей приоритет самый низкий. Порядок выполнения операций C++ представлен в табл. 2.4.
Таблица 2.4. Порядок выполнения операций C++
Операции
Ассоциативность
• •
Левая
0 [] -> .
Левая
! - +(унарная) -(унарная) ++ — &(унарная) *(унарная)
Правая
(приведение типа) static_cast const_cast dynamic_cast
reinterpret_cast sizeof new delete...typeid
. *(унарная) ->*
Левая
* / %
Левая
+ -
Левая
« >>
Левая
< <= > >=
Левая
== 1 =
Левая
&
Левая
A
Левая
|
Левая
Левая
| |
Левая
?: (условная операция)
Правая
= *= /= %= += -= &= A= |= «= »=
Правая
!
Левая
Здесь приведено множество операций, с которыми вы пока не знакомы, но до конца книги вы узнаете их все. Вместо того чтобы разбрасывать их по главам, я собрал все операторы C++ в одну таблицу порядка выполнения, чтобы вы всегда могли обратиться к ней, когда не уверены в том, как соотносится приоритет одной операции с приоритетом другой.
Операции с наивысшим приоритетом находятся в верхней части таблицы. Все операции, которые указаны в одной ячейке таблицы, имеют одинаковый приоритет. Если в выражении нет скобок, операции с равным приоритетом выполняются в последовательности, определяемой их ассоциативностью. То есть, если ассоциативность “левая”, то самая левая операция в выражении выполняется первой, затем последовательно выполняются операции всего выражения — слева направо. Это значит, что выражение вроде а + Ь + с + d выполняется, как записано, то есть как (((а + Ь) + с) + d), потому что бинарная операция + имеет левую ассоциативность.
Обратите внимание, что когда операция имеет и унарную (работающую с одним операндом), и бинарную (с двумя операндами) формы, то унарная форма всегда имеет более высокий приоритет, а потому выполняется первой.
Вы всегда можете изменить приоритеты операций в выражении с помощью скобок. Поскольку в C++ очень много операций, иногда бывает затруднительно понять, каким должен порядок вычисления сложного выражения. Поэтому хорошей идеей будет применять скобки, чтобы обрести уверенность. Дополнительная выгода от них проявляется в том, что это часто облегчает чтение кода.
Типы переменных и приведения
Вычисления в C++ могут выполняться только между однотипными значениями. Когда вы пишете выражение, включающее переменные или константы разных типов, то для каждой выполняемой операции компилятор должен выполнить преобразование типа одного операнда к типу другого. Процесс преобразования типов называется приведением. Например, если вы хотите прибавить значение типа double к значению целочисленного типа, то целочисленное значение сначала преобразовывается в double, после чего выполняется сложение. Конечно, сама переменная, содержащая исходное значение, не изменяется. Компилятор сохраняет преобразованное значение во временной памяти, которая теряется по завершении вычислений.
Выбор операнда, подлежащего преобразованию в любой операции, регулируется строгими правилами. Любое выражение, которое должно быть вычислено, разбивается на серии операций с двумя операндами. Например, выражение 2 * 3 - 4 + 5 состоит из следующих серий: 2 * 3 в результате дает 6, затем 6-4, которая в результате дает 2 и, наконец, 2 + 5, результат которого — 7. Таким образом, правила приведения операндов необходимы только для принятия решений относительно пар операндов. Поэтому для любой пары операндов разного типа проверяются описанные ниже правила, в том порядке, как они записаны. Если правило применимо к конкретной паре, оно используется.
Правила приведения операндов
Если любой из операндов имеет тип long double, то второй преобразуется в тип long double.
Если любой из операндов имеет тип double, то второй преобразуется в тип double.
Если любой из операндов имеет тип float, то второй преобразуется в тип float.
Любой операнд типа char, signed char, unsigned char, short или unsigned short преобразуется в тип int.
Перечислимый тип преобразуется сначала в int, unsigned int, long или unsigned long, в зависимости от того, какого из них достаточно, чтобы вместить диапазон перечислителей.
Если один из операндов типа unsigned long, то другой преобразуется в unsigned long.
Если один из операндов типа long, а другой — типа unsigned int, то оба операнда преобразуются к типу unsigned long.
Если любой из операндов типа 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.
Хотя чтение этой последовательности несколько напоминает “песню аукциониста”, основную идею вы поняли.
Приведения в операторах присваивания
Как вы видели ранее в этой главе на примере Ех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 тоже подходит.
Явные приведения
Когда смешанные выражения включают базовые типы, то компилятор при необходимости выполняет нужные приведения, но вы также можете принудительно указать приведение одного типа к другому, используя явные приведения. Чтобы привести значение выражения к определенному тип)7, необходимо написать так:
s t at i с_сas t<тип_ к_которому_привести>(выражение)
Ключевое слово 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, порожденные приведением, сохраняются лишь временно, для использования в вычислении суммы, а затем теряются. Хотя оба приведения приводят при вычислении к потере информации, компилятор предполагает, что вы знаете, что делаете, применяя явное приведение.
Кроме того, как я писал в Ех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_cats<> () (а также другие операции приведения: const casto (), dynamic_cast<> () и reinterpret_ casto (),o которых мы поговорим позднее), явное приведение результата выражения к другому типу записывалось следующим образом:
(тип_к_которому_привести) выражение
Здесь результат выражения приводится к типу, указанному в скобках. Например, оператор вычисления strips_per_roll из предыдущего примера мог быть записан так:
strips_per_roll = (int)(rolllength / height);
// Получить количество полос в рулоне
По сути, существуют четыре разных вида приведений, и синтаксис приведений старого стиля покрывал все. По этой причине код, использующий приведения в старом стиле, более подвержен ошибкам — компилятору не всегда были ясны ваши намерения, и вы не получали ожидаемый результат. Хотя вы встретите в унаследованном коде интенсивное использование приведений старого стиля (это все еще часть языка, и их много в коде MFC — по причинам исторического характера), я настоятельно рекомендую в новом коде применять только новые приведения.
Битовые операции
Битовые операции трактуют свои операнды как последовательности индивидуальных битов, а не числовые значения. Они работают только с целочисленными переменными или целыми константами в качестве операндов, поэтому с ними могут использоваться только типы данных short, int, long, signed char и char, а также их беззнаковые варианты. Битовые операции удобны для программирования аппаратных
устройств, где состояние устройства часто представляется серией индивидуальных флагов (то есть, каждый бит может описывать состояние определенного аспекта данного устройства), или же их можно применять в ситуациях, когда желательно упаковать набор флагов “включено/выключено” в единую переменную. Вы увидите их в действии, когда будете подробно изучать средства ввода-вывода, где отдельные биты служат для управления различными опциями управления потоками данных.
Существует шесть битовых операций.
& битовое И| битовое ИЛИА битовое исключающее ИЛИ
~ битовое НЕ» сдвиг вправо« сдвиг влево
В следующем разделе мы посмотрим, как работает каждая из них.
Битовое И
Битовое И (&) — это бинарная операция, которая комбинирует соответствующие биты своих операндов определенным образом. Если в обоих операндах соответствующие биты равны 1, то и в результате будет 1, если же оба или один из битов операндов равен 0, то и в результате соответствующий бит будет равен 0.
' Эффект применения конкретной битовой операции часто показывают с помощью таблицы истинности. В ней показаны все возможные комбинации битов операндов и соответствующий результирующий бит, полученный в результате применения оператора. Таблица истинности для & представлена в табл. 2.5.
Таблица 2.5. Таблица истинности для операции битового И
Битовое И
0
1
0
0
0
1
0
1
Для каждой комбинации строки и столбца результат применения & находится на их пересечении. Как это работает, можете увидеть на примере.
char letterl = * А*, letter2 = ’ Z*, result = 0;
result = letterl & letter2;
Для того чтобы понять, что произойдет, нужно взглянуть на битовые шаблоны. Буквы ‘А’ и ‘Z’ соответствуют шестнадцатеричным значениями 0x41 и 0х5А, соответственно (см. в приложении Б таблицу кодов ASCII). Работа операции битового И с этими двумя значениями показана на рис. 2.8.
result: 0x400 1 0 00 0 0 0
Рис. 2.8. Работа операции битового И
Вы можете проверить это, пользуясь таблицей истинности операции &, которая была приведена выше. После присваивания result будет иметь значение 0x40, что соответствует символу 4@\
Поскольку & дает 0, если любой из соответствующих битов операндов равен 0, вы можете применять его в тех случаях, когда в переменной определенные биты должны были установлены в 0, независимо от их исходного значения. Это достигается созданием так называемой “маски” и комбинацией ее с исходной переменной с помощью &. Вы создаете маску, специфицируя значение 1 в тех битах, в которых старое значение нужно сохранить, и 0 — в тех битах, значение которых нужно сбросить в 0. Результатом применения И к маске и другому целому будет бит 0 в тех позициях, где стоит 0 в маске, и неизменное значение бита исходной переменной там, где в маске стоит 1. Предположим, что имеется переменная letter типа char, где в целях демонстрации вы хотите сбросить в 0 старшие 4 бита. Это очень просто достигается установкой значения маски 0х0Е и комбинацией ее со значением letter посредством &:
letter = letter & 0x0F;
или, более коротко:
letter &= 0x0F;
Если изначально значение letter равно 0x41, то в результате любого из этих двух операторов оно станет равным 0x01. Эта операция проиллюстрирована на рис. 2.9.
result: 0x01result: 0x01letter: 0x41letter: 0x41mask: OxOFmask: OxOF0 0 0 00 0 0 0Рис. 2.9. Применение маски к значению переменнойРис. 2.9. Применение маски к значению переменной
result: 0x01
result: 0x01
letter: 0x41
letter: 0x41
mask: OxOF
mask: OxOF
0 0 0 0
0 0 0 0
Рис. 2.9. Применение маски к значению переменной
Рис. 2.9. Применение маски к значению переменной
Нулевые биты маски сбрасывают в ноль соответствующие биты letter, а биты маски, равные 1, оставляют соответствующие биты letter неизмененными.
Аналогично вы можете использовать маску 0xF0, чтобы сохранить 4 старших бита и сбросить в ноль 4 младших.
Таким образом, следующий оператор:
letter &= 0xF0;
даст в результате изменение значения letter с 0x41 на 0x40.
Битовое ИЛИ
Битовое ИЛИ (I), иногда называемое включающим ИЛИ, комбинирует соответствующие биты операндов так, что результат равен 1, если хотя бы один из битов операндов в данной позиции равен 1, и 0, если оба бита равны 0. Таблица истинности для битового ИЛИ показана в табл. 2.6.
Таблица 2.6. Таблица истинности для операции битового ИЛИ
Битовое ИЛИ
0
1
0
0
1
1
1
1
Вы можете проверить это на примере установки индивидуальных флагов, упакованных в переменную типа int. Предположим, что имеется переменная по имени style типа short, которая содержит 16 индивидуальных 1-битных флагов. Предположим далее, что вам нужно установить отдельные флаги в переменной style. Одним из способов сделать это является определение значений, которые можно скомбинировать операцией ИЛИ для установки определенных битов в 1. Чтобы установить крайний правый бит, вы можете определить переменную:
short vredraw = 0x01;
Чтобы установить второй справа бит, вы должны определить вторую переменную hredraw:
short hredraw = 0x02;
Тогда для установки двух правых крайних битов переменной style в 1 можно воспользоваться следующим оператором:
style = hredraw | vredraw;
Эффект от этого оператора проиллюстрирован на рис. 2.10.
hredraw: 0x02hredraw: 0x02vredraw: 0x01vredraw: 0x01
hredraw: 0x02
hredraw: 0x02
vredraw: 0x01
vredraw: 0x01
style: 0x03
Рис. 2.10. Работа операции битового ИЛИ
Поскольку операция ИЛИ дает в результате 1, когда любой из двух битов операндов равен 1, то объединение с ее помощью двух переменных дает в результате установку обоих бит.
Очень часто в программах предъявляется требование иметь возможность установить набор определенных флагов, не трогая остальных, которые могут устанавливаться где-то в другом месте. Вы можете легко сделать это с помощью такого оператора:
style |= hredraw > vredraw;
Этот оператор установит два крайних правых бита переменной style в 1, оставляя нетронутыми все остальные.
Битовое исключающее ИЛИ
Битовое исключающее ИЛИ (А), так называется потому, что работает подобно обычному ИЛИ, но когда оба бита операндов установлены в 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;
0
1
0
0
0
0
0
0
0
0
0
0
0
0
1
1
Десятичное 16 387 в двоичном виде:
край и теряются
=12
= 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-байтного целого.
Вы можете подумать, что между операциями сдвига и операциями, используемыми для ввода и вывода, возникает конфликт. До тех пор, пока это рассматривает компилятор, значение операции в каждом конкретном случае всегда ясно из контекста.
Если же нет, то компилятор выдаст сообщение, но все же вы должны быть осторожны. Например, если вы хотите вывести результат сдвига переменной 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, поскольку бит знака повторяется. При операциях с беззнаковыми целочисленными типами, конечно, бит знака не повторяется, поэтому в первой позиции появляется ноль.
Время хранения и область видимости
Все переменные имеют ограниченное время жизни при выполнении программы. Они появляются в точке, где вы их объявили, а затем в некоторой точке они исчезают — не позднее момента завершения программы. Насколько долго существует конкретная переменная, определяется свойством, называемым временем хранения (storage duration). Существуют три разных вида времени хранения переменных.
автоматическое;
статическое;
□ динамическое.
Какое из них будет иметь переменная — зависит от того, как вы ее создаете. Пока я отложу дискуссию о динамическом времени хранения до главы 4, но характеристики двух других будут раскрыты в этой главе.
Другое свойство, присущее переменным — это область видимости (scope). Область видимости переменной — это просто часть программы, на протяжении которой имя данной переменной определено. Вне этой области видимости вы не можете ссылаться на ее имя — любая попытка сделать это вызовет ошибку компиляции. Обратите внимание, что переменная все еще может существовать вне этой области видимости, даже несмотря на то, что вы не можете обратиться к ней по имени. Чуть позднее будут даны примеры подобной ситуации.
Все переменные, которые вы до сих пор объявляли в примерах, имели автоматическое время хранения, а потому назывались автоматическими переменными. Давайте рассмотрим их первыми.
Автоматические переменные
Все переменные, которые вы объявляли до сих пор, объявлялись внутри блока — то есть, внутри пары фигурных скобок. Такие переменные называются автоматическими, и о них говорят, что у них локальная область видимости, или область видимости блока. Автоматическая переменная “видима”, начиная с точки, в которой она объявлена, и до конца блока, содержащего ее объявление. Пространство, которое занимает автоматическая переменная, выделяется автоматически в области памяти, называемой стеком, которая специально предназначена для этой цели. По умолчанию размер стека составляет 1 Мбайт, чего достаточно для большинства случаев, хотя если его не хватает, вы можете увеличить размер стека, установив опцию проекта /STACK в необходимое значение по своему выбору.
Автоматическая переменная “рождается” в момент ее определения, и для нее выделяется пространство в стеке, а прекращает свое существование в конце блока, содержащего ее определение. Это происходит в точке, где находится закрывающая фигурная скобка, которая соответствует первой открывающей фигурной скобке, предшествующей объявлению этой переменной. Каждый раз, когда выполняется блок операторов, содержащий объявление автоматической переменной, переменная создается вновь, и если вы специфицируете начальное значение автоматической переменной, она повторно инициализируется при каждом создании. Когда автоматическая переменная, исчезает, занятая ею память в стеке освобождается для использования другими автоматическими переменными.
Существует ключевое слово auto, которое вы можете применять для спецификации автоматических переменных, но оно редко используется, поскольку предполагается по умолчанию. Ниже приведен пример, иллюстрирующий то, что я до сих пор рассказал об области видимости.
практическое занятие Автоматические переменные
Эффект области видимости автоматических переменных демонстрируется на следующем примере.
// Ех2_07.срр
// Демонстрация области видимости
#include <iostream>
using std::cout;
using std::endl;
int main()
{ // Область видимости функции начинается здесь
int countl = 10;
int counts = 50; cout « endl
« "Значение внешней countl = " « countl « endl;
{ // Здесь начинается новая область видимости...
int countl = 20; // Это скрывает внешнюю countl
int count2 = 30;
cout « "Значение внутренней countl = " « countl « endl;
countl += 3; // Это изменяет внутреннюю countl count3 += count2;
} // ...а здесь она заканчивается cout « "Значение внешней countl = " « countl « endl
« "Значение внешней count3 = " « count3 « endl;
// cout « count2 « endl; // уберете комментарий - получите ошибку return 0;
} // Область видимости функции здесь заканчивается
Вывод этого примера:
Значение внешней countl =10
Значение внутренней countl =20
Значение внешней countl =10
Значение внешней count3 = 80
Описание полученных результатов
Первые два оператора объявляют и определяют две целочисленных переменных, countl и count3, с начальными значениями 10 и 50 соответственно. Обе эти переменные существуют, начиная с этой точки и до закрывающей скобки в конце программы. Область видимости этих переменных также распространяется до закрывающей скобки в конце main ().
Помните, что время жизни и область видимости переменной — разные вещи. Важно не питать эти два понятия. Время жизни — это период во время выполнения программы, начиная с момента первого объявления переменной до момента ее уничтожения и освобождения занятой ею памяти для другого использования. Область видимости переменной — это часть программного кода, в которой переменная доступна.
Вслед за определением переменной значение countl выводится на экран, что дает первую из строк, показанных выше. Далее идет новая открывающая фигурная скобка, с которой начинается новый блок. Две переменных, countl и count2, определены в этом блоке, со значениями 20 и 30 соответственно. Переменная countl, объявленная здесь, отличается от первой переменной countl. Первая countl все еще существует, но ее имя замаскировано второй переменной countl. Любое упоминание имени countl после объявления внутри вложенного блока ссылается на countl, объявленную в этом блоке.
Я использовал дублированное имя переменной countl здесь только для того, чтобы проиллюстрировать, что произойдет. Хотя этот код совершенно легален, он не является примером правильного подхода к программированию. При разработке реальных программ это приведет к путанице, и если вы используете дублированные имена, то очень легко нечаянно скрыть переменные, определенные во внешней области видимости.
Значение во второй строке вывода демонстрирует, что внутри вложенного блока обращение к имени countl означает обращение к переменной, имеющей область видимости этого блока, то есть объявленной во внутренних фигурных скобках:
cout « "Значение внутренней countl = " « countl « endl;
Если бы здесь использовалась внешняя переменная count 1, то было бы выведено ее значение —10. Далее значение count 1 увеличивается следующим оператором:
countl += 3; // Это изменяет внутреннюю countl
Инкремент касается переменной, имеющей внутреннюю область видимости блока, поскольку внешняя все еще сокрыта. Однако count3, которая была определена во внешней области видимости, увеличивается в следующем операторе без всяких проблем:
count3 += count2;
Это доказывает, что переменные, объявленные в начале внешней области видимости, доступны также и во вложенной области. (Обратите внимание, что если бы count3 была объявлена после закрывающей скобки вложенного блока, она бы также существовала во внешней области видимости, но в этом случае она бы еще не существовала во вложенном блоке.)
После закрывающей скобки вложенного блока count2 и внутренняя переменная countl прекращают существовать. Переменные countl и count3 по-прежнему находятся во внешней области видимости, и отображаемые значения доказывают, что значение counts было увеличено во вложенном блоке.
Если вы снимите комментарий со следующей строки:
// cout « count2 « endl; // уберете комментарий - получите ошибку
то программа не станет корректно компилироваться, потому что попытается обратиться к несуществующей переменной. В этом случае вы получите сообщение об ошибке вроде следующего:
c:\microsoftvisual studio\myprojects\Ex2_07\Ex2_07.срр(29) : error С2065: *count2’ : undeclared identifier
с:\microsoft visual studio\myprojects\Ex2_07\Ex2_07.cpp (29) :
ошибка C2065: 1 count21 : необъявленный идентификатор
Это потому, что в этой точке count2 вышла из своей области видимости.
Размещение объявлений переменных
Вам предоставляется значительная свобода в том, где помещать объявления ваших переменных. Наиболее важный аспект, который следует при этом учитывать — это какова должна быть область видимости этой переменной. Помимо этого, обычно следует размещать объявление переменной поближе к тому месту, где она будет впервые использована в программе. Вы должны писать свои программы так, чтобы максимально облегчить их понимание другими программистами, и объявление переменной непосредственно перед первым ее использованием весьма поможет в достижении этой цели.
Можно поместить объявление переменной вне любой функции, входящей в программу. В следующем разделе будет рассказано о последствиях таких объявлений.
Глобальные переменные
Переменные, которые объявлены вне всех блоков и классов (о классах речь пойдет позже), называются глобальными, и они имеют глобальную область видимости (которая также называется областью видимости глобального пространства имен или областью видимости файла). Это значит, что они доступны всем функциям в файле, начиная с точки, где они были объявлены. Если вы объявляете их в самом начале ваше программы, то они будут доступны в любом месте файла.
Глобальные переменные также по умолчанию имеют статическое время жизни. Глобальные переменные со статическим временем жизни существуют с момента начала выполнения программы и до момента ее завершения. Если вы не специфицируете начальное значение глобальной переменной, то по умолчанию она инициализируется нулем. Инициализация глобальных переменных происходит перед началом выполнения функции main (), поэтому они всегда доступны в любой части кода, находящейся внутри области видимости переменной.
На рис. 2.12 показано содержимое исходного файла Expamp 1е. срр, и стрелками указана область видимости каждой переменной.
Переменная valuel, которая появляется в начале файла, объявлена с глобальной областью видимости, как и value4, которая появляется после функции main (). Область видимости каждой глобальной переменной простирается от точки ее определения до конца файла. Даже несмотря на то, что value4 существует в момент начала выполнения программы, к ней нельзя обратиться из тела main (), потому что main () не находится в ее области видимости. Для того чтобы main () могла обратиться к переменной value4, ее объявление следует переместить в начало файла. Обе переменные — и valuel, и value4 — по умолчанию будут инициализированы нулем, что отличает их от автоматических переменных. Обратите внимание, что локальная переменная по имени valuel в function () скрывает глобальную переменную с тем же именем.
Ц Example.cpp long valuel;ч—int main(){int value2;{int value3;}}<—int value4;<—int function(int n) {long values;int valuel;}<<—Ц Example.cpp long valuel;ч—int main(){int value2;{int value3;}}<—int value4;<—int function(int n) {long values;int valuel;}<<—value3value3valuelvaluelvalue2value2value4value4Ivaluesvaluel |Ivaluesvaluel |
Ц Example.cpp long valuel;
ч—
int main()
{
int value2;
{
int value3;
}
}
<—
int value4;
<—
int function(int n) {
long values;
int valuel;
}
<
<—
Ц Example.cpp long valuel;
ч—
int main()
{
int value2;
{
int value3;
}
}
<—
int value4;
<—
int function(int n) {
long values;
int valuel;
}
<
<—
value3
value3
valuel
valuel
value2
value2
value4
value4
Ivalues
valuel |
Ivalues
valuel |
Puc. 2.12. Области видимости переменных
Поскольку глобальные переменные продолжают существовать столько, сколько работает программа, это может вызвать вопрос: “Почему бы не сделать все переменные глобальными и избежать неприятностей, связанных с исчезновением локальных переменных?”. На первый взгляд это кажется привлекательным, но, как и в случае с Сиренами из мифологии, у такого решения были бы серьезные побочные эффекты, которые полностью перевешивают любые возможные преимущества.
Реальные программы обычно состоят из множества операторов, существенного количества функций и огромного числа переменных. Объявление всех переменные глобальными значительно увеличивает риск непреднамеренной их модификации, а также существенно затрудняет работу по их именованию. К тому же они будут занимать память в течение всего времени выполнения программы. Сохраняя переменные локальными по отношению к функции или блоку, вы можете быть уверенными, что они почти полностью защищены от внешних эффектов, потому что они существуют и занимают память, только начиная с точки их объявления и до завершения включающего блока. При этом становится легче управлять всем процессом разработки.
Если в 8взглянете на панель Class View (Представление классов) в правой части окна IDE для любого примера из тех, что были рассмотрены до сих пор, и развернете дерево классов проекта, щелкнув на знаке +, то увидите там элемент по имени Global Functions and Variables (Глобальные функции и переменные). Если щелкнуть на нем, можно увидеть список всего того в вашей программе, что имеет глобальную область видимости. Это включает все глобальные функции, а также все объявленные глобальные переменные.
| Практическое занятие J Опер^Я^аЗреШе!^^
Как вы уже видели, глобальная переменная может быть скрыта локальной переменной с тем же именем. Однако все же остается возможность получить доступ к такой глобальной переменной, используя операцию разрешения контекста (: :), которую вы уже видели в главе 1, когда речь шла о пространствах имен. Я продемонстрирую, как она работает, на измененной версии последнего примера:
// Ех2_08.срр
/ / Демонстрация области видимости переменных
#include <iostream>
using std::cout;
using std::endl;
int countl = 100; // Глобальная версия countl
int main ()
{ // Область видимости функции начинается здесь
int countl = 10;
int counts = 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;
countl += 3; Н Это изменяет внутреннюю count1 counts += counts;
} II ...а здесь она заканчивается.
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. Тем, что перед оператором не было указано имя пространства имен, вы сообщили компилятору, что для поиска имени он должен обратиться к глобальному пространству имен.
Вы узнаете еще больше об этой операции, когда в главе 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:rein » value;
std::cout « "\пВы ввели " « value
« std:; endl;
return 0;
}
Объявление переменной value находится вне определения main (). Об этом объявлении также говорят, как об области видимости глобального пространства имен, потому что объявление переменной находится вне какого-либо пространства имен. Переменная доступна в любом месте main (), равно как и из определения любой другой функции, которая может встретиться в том же исходном файле. Я поместил объявление value вне main (), просто чтобы продемонстрировать в следующем разделе, как его можно поместить в пространство имен.
Обратите внимание на отсутствие объявления using для cout и endl. В данном случае оно не нужно, поскольку имена из пространства имен std квалифицированы полностью. Может быть, поступать так и неразумно, но вы можете использовать cout в качестве имени своей переменной, и это не приведет ни к какой путанице, потому что cout будет отличаться от std: : cout. Таким образом, пространства имен предоставляют возможность отделить имена, используемые в одной части программы, от имен, применяемых в другой. Это чрезвычайно ценно, когда речь идет о крупном проекте, в работе над которым задействовано несколько групп программистов, занимающихся разными частями программы. Каждая команда может иметь собственное пространство имен, и в этом случае можно не беспокоиться о том, что они непреднамеренно используют одно и то же имя для различных функций.
Взгляните на следующую строку кода:
using namespace std;
Этот оператор называется директивой using.
Эффект от его применения состоит в том, что все имена из пространства std импортируются в ваш исходный файл, так что вы можете из своей программы ссылаться на все, что определено в этом пространстве имен, без квалифицированного имени. То есть, вы можете писать имя cout вместо std: :cout и endl вместо std: : endl. Недостатком такого применения директивы using является то, что сводится на нет основная причина применения пространств имен — то есть, предотвращение непреднамеренных конфликтов. Самый безопасный способ доступа к именам из пространства имен — это квалифицировать каждое имя явно вместе с наименованием пространства имен; к сожалению, это делает код очень многословным и снижает его читабельность. Другая возможность — представить объявлением using только те имена, которые вы используете в своем коде, например:
using std::cout; // Позволяет использовать cout без квалификации using std::endl; // Позволяет использовать endl без квалификации
Эти операторы называются объявлениями using. Каждый оператор представляет одно имя из указанного пространства и позволяет применять его без квалификации внутри последующего программного кода. Это обеспечивает более удобный способ импортирования из пространства имен, поскольку импортируются лишь те имена, которые действительно нужны в программе. Поскольку Microsoft установила прецедент импорта всего пространства имен System для кода C++/CLI, я буду придерживаться этого в примерах C++/CLI. Но вообще я рекомендую использовать в своем коде объявления using вместо директив using, когда вы пишете программы сколько-нибудь значительного размера.
Конечно, вы можете определить свое собственное пространство имен по своему выбору. В следующем разделе показано, как это сделать.
Объявление пространства имен
Для объявления пространства имен применяется ключевое слово 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>
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, без квалификации
}
Второе объявление пространства имен с тем же именем — это просто продолжение первого, поэтому вы можете обращаться к именам первого блока пространства имен из второго без необходимости квалифицировать их. Все они находятся в одном пространстве имен. Конечно, обычно вы не будете намеренно организовывать исходный файл подобным образом, но такое может случиться естественным образом с заголовочными файлами, которые вы включаете в свою программу. Например, у вас может быть нечто вроде:
#include <iostream>//Содержимое-в пространстве имен std
#include "myheader.h"//Содержимое-в пространстве имен myStuff
#include <string>//Содержимое-в пространстве имен std
/ / и так далее. . .
Здесь iostream и string — заголовочные файлы стандартной библиотеки ISO/ ANSI C++, a myheader. h — заголовочный файл, содержащий ваш программный код. То есть вы имеете ситуацию с пространствами имен, которая в точности повторяет предыдущий пример.
Все это должно дать вам представление о том, как работают пространства имен. Можно еще очень много сказать о пространствах имен, но того, что я показал здесь, достаточно, чтобы вы ухватили суть и были готовы без проблем узнать больше при необходимости.
Обратите внимание, что две формы директивы #include в предыдущем фрагменте кода заставляют компилятор искать включаемые файлы разными способами. Когда вы специфицируете включаемый файл между угловыми скобками, то тем самым сообщаете компилятору, что он должен искать его в пути, указанном опцией компилятора /I, а не найдя там, обратиться к переменной окружения INCLUDE. Эти пути указывают на библиотечные файлы C++, поэтому такая форма зарезервирована для библиотечных заголовков. Переменная окружения INCLUDE указывает на папку, содержащую библиотечные заголовки, а опция /I позволяет специфицировать дополнительные каталоги, содержащие библиотечные заголовки. Когда же имя файла указано между двойных кавычек, компилятор ищет папку, где находится файл, в котором встретилась данная директива #include. Если там файл не найден, поиск продолжается в библиотечных каталогах.
Программирование на C++/CLI
C++/CLI предлагает множество расширений и дополнительных возможностей вдобавок к тем, которые рассматривались до сих пор в этой главе. Прежде чем погрузиться в детали, я представлю краткий перечень этих дополнительных возможностей. Итак, ниже перечислены дополнительные возможности C++/CLI.
Все фундаментальные типы данных ISO/ANSI, о которых я говорил ранее, могут быть использованы в программах C++/CLI, но здесь они имеют некоторые дополнительные свойства в определенных контекстах, которые будут раскрыты позже.
C++/CLI предоставляет собственный механизм клавиатурного ввода и вывода в командную строку консольных программ.
C++/CLI предоставляет операцию safe cast, которая гарантирует, что операция приведения приведет к генерации проверяющего кода.
C++/CLI предоставляет альтернативную возможность перечисления на основе классов и обеспечивает большую гибкость, чем объявление enum из ISO/ANSI C++, которое вы видели ранее.
Вы узнаете больше о ссылочных типах классов CLR в начале главы 4, но поскольку я уже представил глобальные переменные родного C++, то здесь отмечу, что переменные ссылочных типов классов CLR не могут быть глобальными.
Я бы хотел начать с обзора фундаментальных типов данных C++/CLI.
Специфика C++/CLI: фундаментальные типы данных
Вы можете и должны использовать имена фундаментальных типов ISO/ANSI C++ в своих программах C++/CLI, и с арифметическими операциями они работают точно так же, как вы видели в родном C++. Кроме того, в C++/CLI определены два дополнительных целочисленных типа, которые описаны в табл. 2.8.
Таблица 2.8. Дополнительные целочисленные типы C++/CLI
ТипРазмер в байтах Диапазон значений
long long8от -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++/CLI. Когда необходимо, компилятор выполняет автоматические преобразования от исходного значения в ассоциированный тип класса и обратно; этот процесс называется упаковкой (boxing) и распаковкой (unboxing) соответственно. Это позволяет переменной любого из этих типов вести себя как простое значение либо как объект — в зависимости от обстоятельств. Подробнее о том, когда и как это происходит, вы узнаете в главе 6.
Таблица 2.9. Фундаментальные типы C++/CLI
Фундаментальный тип
Размер (в байтах)
Класс значений CLI
bool
1
System::Boolean
char
1
System::Sbyte
signed char
1
System::Sbyte
unsigned char
1
System::Byte
short
2
System::Intl6
unsigned short
2
System::Uintl6
int
4
System::Int32
unsigned int
4
System::Uint32
long
4
System::Int32
unsigned long
4
System::Uint32
long long
8
System::Int64
unsigned long long
8
System::UInt64
float
4
System::Single
double
8
System::Double
long double
8
System::Double
wchar_t
2
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.
Практическое занятиеПрактическое занятиеI
Практическое занятие
Практическое занятие
Консольная программа CLR
Создайте новый проект и выберите тип проекта CLR и шаблон CLR Console Application (Консольное приложение CLR). Затем вы можете ввести имя проекта Ех2_12, как показано на рис. 2.13.
Рис. 2.13. Создание консольного приложения CLRРис. 2.13. Создание консольного приложения CLR
Рис. 2.13. Создание консольного приложения CLR
Рис. 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. срр следующим образом:
// Ех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 (Ь"\пАпельсины - не единственные фрукты. .. ”) ;
WriteLine () — это функция C++/CLI, которая определена в классе Console пространства имен System. Подробнее о классах вы узнаете в главе 6, а пока что скажу, что класс Console предоставляет стандартные потоки ввода и вывода, соответствующие клавиатуре и командной строке консольного окна. То есть функция WriteLine () пишет все, что находится между скобками, следующими за ее именем, в командную строку, добавляя при этом символ новой строки, чтобы переместить курсор в начало следующей строки экрана. То есть предыдущий оператор выводит в командную строку текст "ХпАпельсины — не единственные фрукты. . . ”. Буква L, предшествующая строке, указывает на то, что это строка, состоящая из “широких” символов, где каждый занимает 2 байта.
Функция Write () класса Console — это, по сути, то же самое, что функция WriteLine (), с единственным отличием — она не добавляет к указанному выводу символ новой строки. Поэтому вы можете использовать Write (), когда нужно вывести два или более элемента данных в одну строку несколькими отдельными операторами.
Значение, которые вы помещаете между скобками, следующими за именем функции, называются аргументами. В зависимости от того, как написана функция, она
может принимать при ее вызове ноль, один или более аргументов. Когда вам нужно передать более одного аргумента, их следует разделять запятыми. Функции вывода класса 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"MMeeTcn {0} пакетов.", в которой фрагмент “ {0} ” отмечает место, куда будет помещен второй аргумент. Этот фрагмент заключает в себе форматную строку, применяемую для вывода второго аргумента, хотя в данном случае она предельно проста, и состоит из одного нуля. Аргументы, следующие в функции Console: :WriteLine () за первым, пронумерованы по порядку, начиная с нуля:
Ссылаться по :012 и так далее.
Console::WriteLine("Строка формата", arg2, агдЗ, агд4,... );
Таким образом, ноль между фигурными скобками в предыдущем фрагменте кода указывает на то, что аргумент 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 фунтов.
Существуют и другие спецификаторы формата, которые позволяют вам представить различные типы данных различными способами. Некоторые из наиболее часто используемых спецификаторов формата перечислены в табл. 2.10.
Таблица 2.10. Часто используемые спецификаторы формата
Спецификатор формата
Описание
С или с
Выводит значение в денежном формате.
вили d
Выводит целое в десятичном виде. Если указать более высокую точность, количество десятичных знаков в числе, то оно будет дополнено нулями слева.
Е или е
Выводит значение с плавающей точкой в научной нотации, то есть с экспонентой. Значение точности указывает количество десятичных разрядов после точки.
гили f
Выводит значение с плавающей точкой как число с фиксированной точкой в форме ±ddd.dd...
G ИЛИ g
Выводит значение в наиболее компактном виде, в зависимости от его типа и указанной точности. Если точность не указана, принимается значение точности по умолчанию.
N ИЛИ п
Выводит десятичное значение с плавающей точкой, используя при необходимости разделитель — запятую между группами по три разряда.
X ИЛИ х
Выводит целое в шестнадцатеричном виде. Шестнадцатеричные цифры выводятся в верхнем или нижнем регистре в зависимости от того, указан х или х.
Это даст вам достаточные знания относительно вывода, чтобы продолжить изучать примеры C++/CLI. Теперь посмотрим на кое-что из описанного в действии.
Практическое занятие форМаТИрОВвННЫЙ ВЫВОД
Ниже приведен демонстрационный пример вывода консольной программы CLR, вычисляющий цену коврового покрытия.
Н Ех2_13.срр : главный файл проекта.
// Вычисление цены коврового покрытия
#include "stdafx.h"
using namespace System;
int main(array<System::String A> 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;
}
Вывод этой программы должен быть таким:
Размер комнаты 8.25 ярдов на 4.50 ярдов
Площадь комнаты 37.13 квадратных ярдов
Цена коврового покрытия $1037.64
Press any key to continue . . .
Описание полученных результатов
Размеры комнаты указаны в футах, в то время как цена коврового покрытия определяется площадью в квадратных ярдах, поэтому вы определяете константу feet PerYard, чтобы использовать ее при пересчете футов в ярды. В выражении преобразования каждое измерение получается в результате деления значения типа double на int. Компилятор вставит код для преобразования типа int в double, прежде чем выполнит умножение. После преобразования размеров комнаты в ярды выполняется вычисление цены коврового покрытия перемножением размеров в ярдах, чтобы получить площадь, и умножением этой площади на цену за квадратный ярд.
Операторы вывода используют спецификатор формата F2, чтобы ограничить выходные значения двумя десятичными знаками после точки. Без этого в выводе может получиться больше знаков, что нежелательно, особенно когда речь идет о цене. Чтобы увидеть разницу, можете удалить спецификатор формата.
Обратите внимание, что оператор вывода площади использует арифметическое выражение во втором аргументе функции WriteLine (). Компилятор сначала вычислит это выражение, а затем передаст результат в виде аргумента функции. В общем случае вы всегда можете использовать выражение в качестве аргумента функции — до тех пор, пока тип полученного результата согласуется с типом параметра функции.
Клавиатурный ввод в C++/CLI
Возможности ввода с клавиатуры, которые имеются у консольных программ .NET Framework, несколько ограничены. Вы можете прочесть полную строку ввода как текстовую строку, используя для этого функцию Console: :ReadLine (), или же вы можете прочесть отдельный символ, применяя для этого функцию Console: :Read (). Можно также прочесть нажатие клавиши функцией Console:: ReadKey ().
Функция Console: :ReadLine () используется следующим образом:
StringA 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);
Аргумент 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 value1 = 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.
Обратите внимание на ключевое слово 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};
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(L"MacTb - {0} и значение -
{1} ",
suit, value);
suit = Suit::Spades;
value = safe_cast<int>(suit);
Console::WriteLine(Ь"Масть - {0} и значение -
{1} ",
suit, value);
int main(array<System::String Л> Aargs) {
Suit suit = Suit::Clubs;
int value = safe cast<int>(suit);
return 0;
}
Этот пример выдаст на консоль следующее:
Масть - Clubs и значение - 0
Масть - Diamonds и значение - 1
Масть - Hearts и значение - 2
Масть - Spades и значение - 3
Press any key to continue . . .
Описание полученных результатов
Поскольку это тип класса, перечисление Suit не может быть определено внутри функции main (), поэтому его определение помещено перед определением main (), а
потому относится к глобальному контексту. В примере объявляется переменная suit типа Suit и размещает в ней значение Suit: :Clubs следующим оператором:
Suit suit = Suit::Clubs;
Квалификация имени константы Clubs именем типа Suit существенна; без нее компилятор не может распознать Suit.
Если вы посмотрите на вывод, то увидите, что значение переменной suit отображается как имя соответствующей константы, в первом случае — Clubs. Чтобы получить константное значение, соответствующее объекту, вы должны явно привести его к типу int, как в следующем операторе:
value = safe_cast<int>(suit);
Из вывода программы видно, что константам перечисления присвоены значения, начиная с 0. Фактически вы можете изменить тип, используемый для перечислимых констант. В следующем разделе мы посмотрим, как это делается.
Спецификация типа перечислимых констант
Константы в перечислениях C++/CLI могут относиться к любому из следующих типов:
short
int
long
long long
signed char
char
unsigned
unsigned
unsigned
unsigned
unsigned
bool
short
int
long
long long
char
Чтобы специфицировать тип констант перечисления, название типа констант записывается после имени типа перечисления и отделяется двоеточием — как и в перечислениях “родного” 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 — в соответствии с явным присваиванием.
Значения, присвоенные перечислимым константам, не обязательно должны быть уникальными. Это дает возможность использовать значения констант для выражения некоторого дополнительного свойства. Например:
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. В этом случае она продолжает существовать в течение всего времени выполнения программы. Но доступ к ней открыт только в области видимости (блоке), где она определена.
Переменные могут быть объявлены вне любых блоков программы — в этом случае они имеют глобальную область видимости. Переменные, имеющие глобальную область видимости, доступны из любой ее точки, за исключением тех мест,
где существуют локальные переменные с теми же именами. Но даже там к ним можно обратиться, используя операцию разрешения контекста.
Пространство имен определяет область видимости, объявленные в которой имена квалифицированы дополнительным именем — именем пространства имен. Обращение к таким именам извне этого пространства имен требует обязательной квалификации (полного имени).
Стандартная библиотека 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.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Напишите программу ISO/ANSI C++, которая приглашает пользователя ввести число, а затем печатает его, используя локальную целочисленную переменную.
Напишите программу, которая читает целое значение, вводимое с клавиатуры, в переменную типа int и использует одну из битовых операций (но не %!) для определения положительного остатка от деления на 8. Например, 29 = (3 х 8) + 5 и -14 = (-2 х 8) + 2 имеют положительные остатки 5 и 2 соответственно.
Снабдите скобками следующие выражения, чтобы проиллюстрировать приоритеты и ассоциативность:
1 4-2 4-3 4-4
16 * 4 / 2 * 3
а > Ь? а: с > d? е: f a&b&&c&d
Создайте программу, которая вычисляет коэффициент пропорциональности (aspect ratio) экрана вашего компьютера по заданным значениям ширины и высоты в пикселях, используя следующие операторы:
int width = 1280;
int height = 1024;
double aspect = width / height;
Какой ответ вы получите в результате? Будет ли он удовлетворительным? Если нет — как следует модифицировать код, не добавляя новых переменных?
(Усложненное.) Не запуская на выполнение следующий код, можете ли вы сказать, какой результат он выдаст и почему?
unsigned s = 555;
int i = (s » 4) & ~(~0 « 3) ; cout « i;
Напишите консольную программу C++/CLI, которая использует перечисление для идентификации месяцев года значениями номеров месяцев от 1 до 12. Программа должна выдавать каждую константу перечисления и лежащее в основе значение.
Напишите консольную программу C++/CLI, которая вычислит площади трех комнат до ближайшего числа полных квадратных футов. Размеры комнат: Комната 1: 10,5 на 17,6, Комната 2: 12,7 на 18,9, Комната 3: 16,3 на 15,4
Программа также должна вычислить и вывести среднюю площадь этих трех комнат, а также их общую площадь, причем в каждом случае результат должен быть ближайшим целым числом квадратных футов.
3
Решения и циклы
В этой главе вы узнаете, как можно добавить в программы C++ средства принятия решений. Кроме того, вы научитесь тому, как заставить программу повторно выполнять набор действий до наступления определенного условия. Это позволит вам обрабатывать значительные объемы входной информации, а также выполнять проверку корректности прочтенных данных. Вы сможете писать программы, которые адаптируют свое поведение в зависимости от входных данных, и иметь дело с проблемами, где принятие решений основано на логике. После изучения этой главы вы узнаете следующие моменты.
Как сравнивать значения данных.
Как изменять последовательность выполнения программы на основе некоторого результата.
Как применять логические операции и выражения.
Как справляться с ситуациями с множеством вариантов выбора.
Как писать и использовать циклы в программах.
Начну я с одного из наиболее мощных и фундаментальных инструментов программирования: возможности сравнения переменных и выражений с другими переменными и выражениями, чтобы на основе полученного результата выполнять один или другой набор операторов.
Сравнение значений
Если только вы не хотите принимать решения наобум, то вам понадобится механизм сравнения сущностей. Это вовлекает в дело некоторые новые операции, называемые операциями отношений. Поскольку вся информация в компьютере в конечном итоге представлена в числовом виде (в предыдущей главе вы видели, как в числовом виде представляется символьная информация), сравнение числовых значений — суть
практически всего механизма принятия решений. Существует шесть фундаментальных операций для сравнения двух доступных значений:
меньше чем больше чем равноменьше чем больше чем равно<= меньше или равно >= больше или равно ! = не равно
меньше чем больше чем равно
меньше чем больше чем равно
Операция сравнения “равно” состоит из двух подряд знаков равенства. Это не то же самое, что операция присваивания, которая состоит лишь из одного знака равенства. Распространенная ошибка связана с использованием операции присваивания вместо операции сравнения, поэтому будьте внимательны.
Каждая из этих операций сравнивает значения двух своих операндов и возвращает одно из двух возможных значений типа bool: true — если сравнение истинно, и false — если нет. Вы можете увидеть, как это работает, рассмотрев несколько простых примеров сравнений. Предположим, что объявлены целочисленные переменные i и j со значениями 10 и -5 соответственно. Все представленные ниже выражения возвращают значение true:
i > j i != j j > -8 i <= j + 15
Далее предположим, что вы определили следующие переменные: char first = 'К', last = * Z';
Вот несколько примеров сравнений, использующих эти символьные переменные:
first == 65 first < last ’Е' <= first first != last
Все четыре выражения сравнивают значения кодов ASCII. Первое выражение воз- вращает true, потому что first инициализировано символом ’ А*, что эквивалентно десятичному числу 65. Второе выражение проверяет, меньше ли значение first, которое равно 'А’, чем значение last, которое равно * Z 1. Если заглянуть в таблицу кодов символов ASCII в приложении Б, то мы увидим, что заглавные буквы представлены последовательными числовыми величинами — от 65 до 90, при чем 65 представляет * А1, а 90 — * Z 1, поэтому второе сравнение также вернет true. Третье же выражение вернет false, потому что *Е' больше, чем значение first. Последнее выражение вернет true, поскольку 1 А* определенно не равно 1Z *.
Теперь рассмотрим несколько более сложные сравнения чисел. Имея переменные, определенные следующим образом:
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—1 равно 20, то есть тому же, что и j. Третье выражение возвращает true, потому что 3 + у чуть меньше, чем 3.
Вы можете использовать операции отношений для сравнения значений любого фундаментального типа, поэтому все, что вам нужно — какой-то практический способ использования результатов сравнения для модификации поведения программы.
Оператор if
Базовый оператор i f позволяет программировать выполнение единственного оператора или блока операторов, заключенных в фигурные скобки, если данное условное выражение оценено как истинное, или же пропустить оператор или блок операторов, если условие оценено как ложное. Это показано на рис. 3.1.
if (условие)
Рис. 3.1. Базовый оператор if
Вот простой пример оператора if:
if(letter == ' А')
cout « "Первая заглавная, в алфавитном порядке.";
Проверяемое условие помещается в скобки, следующие за ключевым словом if, после чего следует оператор, который должен быть выполнен, если условие возвращает true. Обратите внимание на то, где находится точка с запятой. Она идет после след^юг^его за if со скобками оператора; то есть, не должно быть точки с запятой сразу после проверочного условия в скобках, так как эти две строки, по сути, составляют единый оператор. Вы также можете видеть, как оператор, следующий за if, сдвинут, чтобы отметить, что он должен быть выполнен только в том случае, когда условие if вернет true. Такой отступ не является необходимым, чтобы программа компилировалась и исполнялась, но он помогает вам увидеть отношение между условием i f и оператором, который от него зависит. Оператор вывода в этом фрагменте кода выполняется только в том случае, если переменная letter содержит значение ‘А’.
Вы можете расширить этот пример, добавив оператор, изменяющий значение letter, если оно равно А’:
if(letter == 1 А*) {
cout « "Первая заглавная, в алфавитном порядке."; letter = ' а';
}
Блок операторов, управляемый оператором 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;II Для хранения введенного значения
cout « endl
« ’’Введите букву: "; II Приглашение ввода
cin » letter;// прочитать символ
if (letter >= ’А’)// Больше или равно ’А’
if (letter <= ’Z’)// Меньше или равно ’ Z’
{ cout « endl
« ”Вы ввели заглавную букву.”
« endl;
return 0; }
if (letter >= ’a’)/ / Больше или равно ’а’
if (letter <= *z’)// Меньше или равно ’z’
Описание полученных результатов
Программа начинается с обычных строк комментариев; затем идет оператор #include для включения заголовочного файла поддержки ввода-вывода и объявление using для cin, cout и endl из пространства имен std. Первое действие в теле main () — приглашение пользователю на ввод буквы. Введенная буква сохраняется в переменной типа char по имени letter.
Оператор if, который следует за вводом, проверяет введенный символ на предмет того, больше или равен он * А *. Поскольку коды ASCII для прописных букв (от 97 до 122) больше, чем коды заглавных букв (от 65 до 90), ввод прописной буквы заставит программу выполнить блок первого оператора if, потому что условие (letter >= * А*) возвращает true для всех букв. В этом случае выполняется вложенный оператор if, который проверяет, меньше или равен введенный символ 1 Z *. Если он равен * Z * или меньше, это значит, что введена заглавная буква, отображается соответствующее сообщение, и на этом программа завершается оператором return. Оба оператора заключены в фигурные скобки, поэтому оба они выполняются, если условие вложенного if возвращает true.
Следующие операторы if проверяют, является ли введенный символ буквой нижнего регистра с помощью, по сути, того же механизма, что и в первой паре if, отображают сообщения и завершают программу.
Если же введенный символ не является буквой, выполняется оператор вывода, следующий за последним блоком if. Он выдает сообщение о том, что введенный символ не был буквой. Затем выполняется return и программа завершается.
Как видите, отношения между вложенными if и оператором вывода гораздо проще отследить, имея отступы в строках кода.
Ниже показан пример выполнения этого примера.
Введите букву: Т
Вы ввели заглавную букву.
Можно легко организовать преобразование символа верхнего регистра в нижний, добавив только один дополнительный оператор к if, проверяющий верхний регистр:
if (letter >= ’А’)// Больше или равно ’А’
if (letter <= ’Z’)// Меньше или равно ’Z’
{ cout « endl
« "Вы ввели заглавную букву." « endl;
letter += ’а* - ’А’; И Преобразовать в нижний регистр return 0;
}
Здесь добавлено один дополнительный оператор. Он преобразует заглавную букву в прописную, увеличивая значение letter на разность * а * - ’ А ’. Это работает, потому что ASCII-коды от * А * до * Z * и от * а * до ’ z ’ представляют две непрерывных группы последовательных числовых кодов, поэтому выражение * а * - * А * представляет значение, которое должно быть добавлено к букве верхнего регистра, чтобы получить эквивалентную букву нижнего регистра.
С тем же успехом вы могли бы применить здесь эквивалентные коды ASCII, но за счет использования букв гарантируется, что программа будет работать и на компью- терах, где применяется кодировка символов, отличная от ASCII — до тех пор, пока множества прописных и заглавных букв представлены непрерывными последовательностями числовых значений.
Существует библиотечная функция ISO/ANSI C++ для преобразования букв в верхний регистр, поэтому обычно вам не придется программировать эту операцию самостоятельно. Она называется toupper () ,иее заголовок содержится в стандартном библиотечном файле <ctype>. Позднее вы узнаете больше о библиотечных средствах, когда мы поговорим о написании функций.
Расширенный оператор if
Оператор if, который вы использовали до сих пор, выполнял оператор или блок, если указанное условие возвращало true. Затем выполнение программы продолжалось со следующего оператора по порядку. Но есть и другая версия if, которая позволяет выполнить один оператор, когда условие if возвращает true, и другой — когда оно возвращает false. После этого выполнение программы продолжается со следующего оператора по порядку. Как вы уже видели в главе 2, блок операторов всегда может заменить один оператор, и это также касается расширенной версии if.
Практическое занятиеПрактическое занятиеРасширение оператора ifРасширение оператора if
Практическое занятие
Практическое занятие
Расширение оператора 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)
эквивалентно такому:
(number % 2L != 0)
Если остаток равен 1, условие равно true, и тогда выполняется оператор, следующий непосредственно за if. Если же остаток равен 0, то условие оценивается как false, и тогда выполняется оператор, следующий за ключевым словом else.
Условие оператора if может быть выражением, возвращающим значение любого из фундаментальных типов данных, с которыми вы познакомились в главе 2. Когда условное выражение возвращает числовое значение вместо bool, то компилятор вставляет автоматическое приведение результата такого выражения к типу bool. Приведение к bool ненулевого значения дает true, а нулевого— false.
Поскольку остатком от деления целого на 2 может быть только единица или ноль, я снабдил код комментарием, отражающим этот факт. После любого исхода оператор return завершает программу.
Ключевое слово else пишется без точки с запятой— так же, как часть оператора if. Опять-таки, отступы служат для визуального выделения отношений между операторами. Благодаря им, вы можете ясно видеть, какой оператор выполняется в случае true или ненулевого результата, а какое— в случае false или нуля. Вы всегда должны сдвигать операторы в своей программе, чтобы подчеркнуть ее логическую структуру.
Комбинация if-else предоставляет выбор между двумя возможностями. Общая логика if-else показана на рис. 3.2.
if( условие)
// И еще операторы
Рис. 3.2. Оператор if-else
Стрелочки на диаграмме указывают последовательность выполнения операторов, в зависимости от того, возвращает условие if значение true или false.
Вложенные операторы if-else
Как вы уже видели, можно вкладывать операторы if в другие операторы if. Точно так же можно вкладывать операторы if-else внутрь операторов if, а операторы 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 « "У нас есть кофе, но нет пончиков.";
Здесь скобки важны. Если их убрать, то 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 в одну конструкцию, то почти наверняка рано или поздно допустите ошибку. В следующем разделе я покажу, как упростить ситуацию.
Логические операции и выражения
Как видите, использование if может выглядеть несколько неуклюже, когда нужно проверить два или более взаимосвязанных условия. В предыдущем примере мы попробовали применить if для выяснения ситуации с кофе и пончиками, но на практике может понадобиться проверять намного более сложные условия. Вы можете искать в файле персонала кого-нибудь старше 21, но младше 35 лет, женского пола, с дипломом колледжа, не замужем и разговаривающую на хинди или урду. Определение
такого проверочного условия может породить монстроподобную конструкцию операторов if.
Простое и четкое решение обеспечивают логические операции. Применяя логические операции, вы можете комбинировать серии сравнений в одно логическое выражение, так что в конечном итоге понадобится всего один оператор if — почти независимо от сложности набора условий, до тех пор, пока он сводится в конечном итоге к выбору из двух альтернатив (true или false).
Существуют всего три логических операции.
&& Логическое И
I | Логическое ИЛИ
! Логическое НЕ
Логическое И
Операция логического И (&&) применяется тогда, когда есть два условия, и оба должны вернуть результат true, чтобы общий результат был равен true. Вы хотите быть богатым и здоровым. Например, вы можете использовать операцию && при проверке принадлежности символа к буквам верхнего регистра; проверяемое значение должно быть больше или равно ’ А ’ И меньше или равно ’ Z'. Оба условия должны вернуть true, чтобы значение относилось к заглавным буквам.
Как и ранее, условия, комбинируемые логическими операциями, могут возвращать числовые значения. Помните, что ненулевые значения приводятся к true, а нулевые— к false.
Если вернуться к примеру, в котором проверяется значение типа char, сохраненное в переменной letter, то можно было бы заменить проверку с двумя операторами if на одну, использующую операцию &&:
if ((letter >= 'А') && (letter <= ' Z')) cout « "Это заглавная буква.";
Скобки внутри проверочного выражения условия i f гарантируют, что операции сравнения, вне всяких сомнений, будут выполнены первыми, и это делает весь оператор яснее. Здесь вывод сообщения произойдет только в том случае, если оба условия, объединенные операцией &&, окажутся истинными.
Как и в случае с бинарными операциями, о которых мы говорили в предыдущей главе, эффект применения каждой логической операции можно представить таблицей истинности. Упомянутая таблица для && представлена в табл. 3.1.
Таблица 3.1. Таблица истинности для операции логического И
&&
falsetrue
false true
falsefalse
falsetrue
Заголовки строк слева и заголовки столбцов сверху представляют значения логических выражений, комбинируемых операцией &&. Таким образом, чтобы определить результат комбинации условия true с условием false, выберите строку с true слева, и столбец с false сверху, а на пересечении строки и столбца ищите результат (false). В действительности вам даже не нужна таблица истинности, поскольку все достаточно просто: операция && возвращает true, только если оба операнда равны true.
Логическое ИЛИ
Операция логического ИЛИ (| |) применяется тогда, когда имеются два условия и нужно получить результат true, если любое из них или оба возвращают true. Например, банк может положительно оценить вашу кредитоспособность для получения займа, если ваш доход составляет не менее $100 000 в год или же у вас есть $1 000 000 наличными. Это может быть проверено с помощью следующего оператора if:
if((income >= 100000.00) I I (capital >= 1000000.00)) cout « "Сколько вы хотели бы занять, сэр (кланяясь)?";
Банковский клерк проявит любезность, когда любое из двух условий выполнено либо оба сразу. (Более подходящим вопросом был бы: “Почему вы хоти те занять денег?”. Вообще-то странно, что банки готовы одалживать деньги только тогда, когда вы в них совершенно не нуждаетесь.)
Таблица истинности для операции | | представлена в табл. 3.2.
Таблица 3.2. Таблица истинности для операции логического ИЛИ
11
falset
true
false
false
true
true
true
true
Здесь также результат может быть выражен очень просто: результат false получается только тогда, когда оба операнда операции | | равны false.
Логическое НЕ
Третья логическая операция — НЕ (!) — принимает один операнд типа bool и инвертирует его значение. Поэтому если значением переменной test является true, то ! test получит значение false. Если же test равно false, то !test будет равно true. В качестве примера простого выражения, если х имеет значение 10, то выражение:
1 (х > 5)
будет false, поскольку х > 5 соответствует true.
Можно применить операцию ! к любимому выражению Чарльза Диккенса:
!(доходы > расходы)
Если это выражение возвращает true, то в результате вы получите нищету, по крайней мере, как только банк перестанет оплачивать подписанные вами чеки.
И, наконец, вы можете применять операцию ! к другим базовым типам данных. Предположим, что имеется переменная rate типа float, содержащая значение 3,2. По некоторой причине вы можете пожелать убедиться, что значение rate отличается от нуля, в этом случае можно использовать следующее выражение:
!(rate)
Значение 3,2 отличается от нуля, а потому преобразуется в значение true типа bool, и результат всего выражения будет false.
практическое занятие Комбинирование логических операций
Вы можете комбинировать условные выражения и логические операции так, как вам будет удобно. Например, вы можете сконструировать тест, определяющий, относится ли символ к буквам, применив единственный оператор 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 <= 1Z1)) ||
((letter >= * a*) && (letter <= 'z'))) // Проверка на вхождение в алфавит cout << endl
« "Вы ввели букву." « endl;
else
cout « endl
« "Вы ввели не букву." « endl; return 0;
}
Описание полученных результатов
Пример начинается так же, как ЕхЗ_01. срр — с чтения символа, введенного пользователем в ответ на приглашение. Что здесь интересно, так это условие оператора if — оно состоит из двух логических выражений, скомбинированных операцией | | (ИЛИ), так что если любое из них равно true, то все условие возвращает true, и на экран выдается сообщение:
Вы ввели букву.
Если же оба выражения возвращают false, то выполняется оператор else, который выдает сообщение:
Вы ввели не букву.
Каждое из логических выражений, объединенных | |, само состоит из комбинаций пары выражений, объединенных операцией && (И), поэтому каждое выражение, участвующее в этих парах, должно возвращать true, чтобы данная пара вернула true. Первое логическое выражение возвращает true, если введена буква верхнего регистра, а второе — если это буква нижнего регистра.
Условная операция
Условную операцию иногда называют тернарной операцией, потому что она работает с тремя операндами. Лучше всего объяснить ее на примере. Предположим,
что имеется две переменных — а и b — и вы хотите присвоить максимальное из этих двух значений третьей переменной с. Это можно сделать с помощью следующего оператора:
с = а > b ? а : Ь; // Присвоить с большее из двух значений а и b
Первый операнд условной операции должен быть выражением, которое возвращает значение типа bool — true или false, как в случае с а > Ь. Если это выражение возвращает true, то в качестве результирующего значения возвращается значение второго операнда — а. Если же первый аргумент равен false, то возвращается значение третьего операнда — то есть Ь. Таким образом, результатом условного выражения а > b ? а : b будет а, если а больше Ь, и b — в противном случае. Это значение записывается в переменную с операцией присваивания. Такое применение условной операции в операторе присваивания эквивалентно следующей конструкции if:
if(а > b) с = а;
else
с = Ь;
В общем виде условная операция может быть записана так:
условие 2 выражение! : выражение2
Если условие оценивается как true, результатом будет значение выражение!, если же оно равно false, то результатом будет значение выражение2.
Практическое занятие | ИСПОЛЬЯОВЯНИЯ УСЛОВНОЙ ОПЯрЯЦИИ При ВЫВОДЯ
Часто условная операция применяется для управления выводом, в зависимости от результата выражения или значения переменной. Вы можете варьировать сообщение, выбирая одну или другую текстовую строку, в зависимости от указанного условия.
// ЕхЗ_04.срр
// Условная операция для выбора вывода
#include <iostream>
using std::cout;
using std::endl;
int main()
{
int nCakes = 1; // Количество пирожных cout « endl
« "У нас есть " « nCakes « ” пирожн" « ((nCakes > 1) 2 "ых." : "oe.") « endl;
++nCakes;
cout « endl
« ”У нас есть " « nCakes « ” пирожн” « ((nCakes > 1) 2 "ых." : "oe.") « endl;
return 0;
}
Вывод этой программы:
У нас есть 1 пирожное.
У нас есть 2 пирожных.
Описание полученных результатов
Сначала вы инициализируете переменную 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; // Здесь сохранить выбранное значение
cout « endl
« "Электронная книга рецептов к вашим услугам." « endl
« "Вы можете выбрать любое из следующих изысканных блюд: "
« endl
« endl « "1 Вареные яйца”
« endl « ”2 Жареные яйца"
« endl « "3 Взбитые яйца"
« endl « "4 Запеченные яйца”
« endl << endl « "Укажите номер выбора: ";
cin » choice; switch(choice) {
cout « endl << "Сварить несколько яиц." « endl; break;
cout « endl « "Зажарить несколько яиц." « endl; break;
cout « endl « "Взбить несколько яиц." « endl; break;
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Разделение case
Практическое занятие
Практическое занятие
Разделение case
Разделение case
Каждое из выражений, которые вы специфицируете для идентификации вариантов case, должно быть константой, чтобы его значение могло быть определено во время компиляции, и должно бать уникальным целым числом. Причина в том, что
если бы два константы 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 1 a’:
case 1e1:
case 1i’:
case 'o':
case 1 u’ : cout « endl « "Вы ввели гласную.’1;
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. Вы можете видеть, что одно и то же действие может соот-
ветствовать нескольким последовательным меткам case, размещенным перед оператором, выполняющим это действие. Если же будет введена согласная буква, то выполнится оператор, следующий за меткой default.
Безусловное ветвление
Оператор if предоставляет вам возможность гибко выбирать один или другой набор операторов, в зависимости от указанного условия, то есть последовательность выполнения операторов варьируется в зависимости от данных программы. В отличие от этого, оператор goto — грубый инструмент. Он позволяет вам перейти к определенному оператору программы безо всяких условий. Оператор, на который выполняется переход, должен быть идентифицирован меткой — идентификатором, определенным в соответствии с теми же правилами, которым подчиняются имена переменных. Метка должна быть снабжена двоеточием и помещена перед оператором, к которому выполняется переход. Ниже представлен пример помеченного оператора.
myLabel: cout « "Активизирован переход на myLabel" « endl;
Оператор имеет метку myLabel, и безусловный переход к этому оператору в программе записывается так:
goto myLabel;
Где только возможно, вы должны избегать в своих программах применения goto. Это порождает запутанный код, который чрезвычайно трудно отследить.
Поскольку теоретически goto не является необходимым в программах — применению goto всегда существует альтернатива— большая часть программистов считает, что использовать его вообще никогда не следует. Я не подпишусь под этой крайней точкой зрения. В конце концов, это — законный оператор языка, и бывают случаи, когда применить его удобно. Однако я рекомендую, чтобы он использовался только тогда, когда очевидно его преимущество перед другими возможными вариантами организации кода. В противном случае вы рискуете получить запутанный, подверженный ошибкам код, который трудно понять и еще труднее сопровождать.
Повторение блока операторов
Возможность повторно выполнять группу операторов — фундаментальна для большинства приложений. Без этой возможности организации пришлось бы модифицировать программу начисления зарплаты всякий раз, когда нанимается новый сотрудник, и вам пришлось бы перезагружать Halo 2 всякий раз, когда вы хотите сыграть в другую игру. Поэтому давайте разберемся, как работают циклы.
Что такое цикл?
Цикл выполняет последовательность операторов до тех пор, пока истинно (или ложно) определенное условие. Вы можете написать цикл, используя лишь те операторы C++, с которыми вы познакомились до сих пор. Вам понадобится для этого только if и “страшный” goto. Взгляните на следующий пример.
// ЕхЗ_07.срр
// Создание цикла средствами if и goto
#include <iostream>
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 = •• « j
« endl; return 0;
}
В этом примере осуществляется накопление суммы целых чисел от 1 до 10. При первом выполнении последовательности операторов i, равное 1, добавляется к переменной sum, которая поначалу равна 0. В операторе if переменная i увеличивается до 2 и, до тех пор, пока она меньше или равна шах, выполняется безусловный переход к метке 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
« »i = ” « i
« endl; return 0;
}
Описание полученных результатов
Если вы скомпилируете и запустите это, то получите тот же вывод, что и в предыдущем примере, но как видите, здесь код заметно меньше. Условия, определяющие операцию цикла, появляются в скобках после ключевого слова for. В этих скобках содержится три выражения, разделенные точкой с запятой.
Первое выражение выполняется один раз, в начале, и устанавливает начальное условие цикла. В данном случае переменной i присваивается значение 1.
Второе выражение — логическое — определяет, до каких пор должен выполняться оператор цикла (или блок операторов). Если второе выражение истинно, цикл продолжает выполняться; когда оно ложно, он завершается и выполнение продолжается с оператора, расположенного за телом цикла. В данном случае оператор цикла в строке, следующей за for, выполняется до тех пор, пока значение i меньше или равно шах.
□ Третье выражение выполняется после оператора цикла (или блока операторов), и в данном случае оно увеличивает 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 используются довольно стандартным способом: первое из них инициализирует один или более счетчиков цикла, второе проверяет, должен ли цикл продолжаться, а третье увеличивает или уменьшает значение одного или более счетчиков. Однако вы не обязаны использовать выражения именно таким образом — на самом деле, возможны несколько вариаций.
Рис. 3.3. Оператор ^икла for
Выражение инициализации цикла for может также включать объявление переменной цикла. В предыдущем примере вы могли бы написать цикл так, чтобы в первом его управляющем выражении содержалось объявление переменной i.
for (int i = 1; i <= max; i++) И Спецификация цикла sum += i; И Оператор цикла
Естественно, исходное объявление i теперь должна быть исключена из программы. Если вы внесете это изменение в последний пример, то обнаружите, что программа не компилируется, потому что переменная цикла i исчезает после завершения цикла, и вы не можете обращаться к ней в операторе вывода. Цикл имеет область видимости, распространяющуюся от управляющих выражений for до конца его тела, которое может быть либо блоком кода в фигурных скобках, либо единственным оператором. Счетчик i теперь объявлен внутри области видимости цикла, и вы не можете обращаться к нему в операторе вывода, потому что оно находится за пределами этой области. По умолчанию компилятор C++ придерживается стандарта ISO/ANSI
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++)
практическое занятие | Применение множественных счетчиков
Вы можете использовать операцию запятой, чтобы включить несколько счетчиков в цикл 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, разделяя их операцией запятой и увеличивая значения каждой в секции инкремента.
Понятно, что таким образом в каждый из разделов for можно вставить столько выражений, сколько понадобится.
Можно даже специфицировать множество условий, разделенных запятыми во втором выражении, представляющем проверочную часть цикла for, которая определяет, должен ли он продолжаться. Однако при этом лишь самое правое из них будет управлять циклом, определяя, когда он должен завершиться.
Обратите внимание, что присваивания, задающие начальные значения i и power, являются выражениями, а не операторами. Оператор всегда заканчивается точкой с запятой.
При каждом увеличении i на 1 значение power удваивается за счет сложения с самим собой. Это порождает последовательность чисел, представляющих степени двойки. Результат работы программы выглядит следующим образом.
О 1 2 3 4 5 6 7 8 9 10О 1 2 3 4 5 6 7 8 9 101
О 1 2 3 4 5 6 7 8 9 10
О 1 2 3 4 5 6 7 8 9 10
2
4
8
16
32
64
128
256
512
1024
Манипулятор setw, с которым вы познакомились в предыдущей главе, используется для симпатичного выравнивания столбцов цифр. Чтобы использовать setw () без квалифицированного имени, в программу включен заголовочный файл <iomanip> и добавлено объявление using для имен из пространства std.
Практическое занятие | БеСКОНеЧНЫЙ ЦИКЛ fOE
Если вы пропустите второе управляющее выражение, которое специфицирует проверочное условие цикла 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;
int i = 0;// Счетчик значений
char indicator = ’ n’; II Продолжать или нет? for(;;)// Бесконечный цикл
{ cout « endl « "Введите значение: "; cin » value;// Читать значение
++i;// Увеличить счетчик
sum += value;// Добавить текущее к сумме
cout « endl
« "Хотите ввести еще одно значение (для завершения введите п)? "; cin » indicator;// Читать индикатор
if ( (indicator == 'n') || (indicator == 'N'))
break;// Выход из цикла
} cout « endl « "Среднее из " « i « " введенных значений равно " « sum/i « "." « endl;
return 0;
Описание полученных результатов
Данная программа вычисляет среднюю величину произвольного числа значений. После ввода каждого числа пользователь должен указать, желает ли он ввести еще одно значение, введя один символ — у или п. Типичный сеанс работы с этой программой будет выглядеть так:
Введите значение: 10
Хотите ввести еще одно значение (для завершения введите п)? у
Введите значение: 20
Хотите ввести еще одно значение (для завершения введите п)? у
Введите значение: 30
Хотите ввести еще одно значение (для завершения введите n)? п
Среднее из 3 введенных значений равно 20.
После объявления и инициализации переменных, которые вы собираетесь использовать, запускается цикл for без управляющих выражений, а, следовательно, и без гарантий завершения. Непосредственно следующий за этим блок — субъект цикла, который должен повторяться.
Блок цикла выполняет следующие базовые действия.
□ Читает значение.
□ Добавляет прочитанное из cin значение к sum.
□ Проверяет, желаете ли вы продолжить вводить значения.
Первое действие внутри блока приглашает вас ввести число, и читает введенное значение в переменную value. Введенное значение добавляется в sum, после чего задается вопрос — желаете ли вы продолжить ввод, и предлагается ввести ' п', если не желаете. Введенный символ помещается в переменную indicator для последующей проверки на равенство ’п’ или 'N' в операторе if. Если равенства нет, цикл продолжается, в противном случае выполняется break. Эффект от break в цикле подобен тому, что он имеет в контексте оператора switch. В данном случае он приводит к немедленному выходу из цикла с передачей управления оператору, следующему за закрывающей фигурной скобкой блока цикла.
И, наконец, выполняется вывод количества введенных значений и их средней величины, которая вычисляется делением 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;
for(i = 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.
Рис. 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;
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) // и как десятичное « п „ « gma]_]_// вывести 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 — соответственно от * а1 до ' z 1.
Тело цикла содержит единственный оператор вывода, растянутый на семь строк. Первая из них:
cout « endl
начинает новую строку на экране.
Следующие три строки таковы:
« "\t" « 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 выводится точно таким же образом в следующих трех строках оператора вывода:
« п п « sma]_i// вывести small как символ
« hex « setw(10) « static_cast<int> (small) // и как шестнадцатеричное число « dec « setw(10) « static_cast<int> (small) // и как десятичное
В результате программа генерирует следующий вывод:
А
41
65
а
61
97
В
42
66
b
62
98
С
43
67
с
63
99
D
44
68
d
64
100
Е
45
69
е
65
101
F
46
70
f
66
102
G
47
71
g
67
103
Н
48
72
h
68
104
I
49
73
i
69
105
J
4а
74
j
6a
106
К
4Ь
75
k
6b
107
L
4с
76
1
6c
108
М
4d
77
m
6d
109
N
4е
78
n
6e
110
0
4f
79
о
6f
111
Р
50
80
P
70
112
Q
51
81
q
71
113
R
52
82
r
72
114
S
53
83
s
73
115
Т
54
84
t
74
116
и
55
85
u
75
117
V
56
86
V
76
118
W
57
87
w
77
119
X
58
88
X
78
120
Y
59
89
У
79
121
Z
5а
90
z
7a
122
Счетчики с плавающей точкой
Вы можете также использовать в качестве счетчиков цикла значения с плавающей точкой. Ниже приведен пример цикла for со счетчиком такого типа:
double а = 0.3, b = 2.5;
for (double х = 0.0; х <= 2.0; х += 0.25) cout « "\n\tx = " « х
« "\ta*x + b = " « а*х + b;
Этот фрагмент кода вычисляет значение а*х+Ь для значений х от 0.0 до 2.0 с шагом 0.25; однако вы должны быть осторожны, применяя в циклах счетчик с плавающей точкой. Многие десятичные значения не могут быть представлены точно в двоичной форме с плавающей точкой, поэтому в аккумулируемых значениях может накапливаться погрешность. Это значит, что вы не должны кодировать цикл for так, чтобы его завершение зависело от достижения счетчиком с плавающей точкой точного значения. Например, следующий неправильно спроектированный цикл никогда не завершится:
for (double х = 0.0 ; х != 1.0 ; х += 0.2) cout « х;
Казалось бы, этот цикл должен отобразить значения х, изменяя их от 0.0 до 1.0; однако шаг 0.2 не может быть представлен точно в формате с плавающей точкой, поэтому значение х никогда не будет точно равно 1.0. Следовательно, второе управляющее циклом выражение всегда будет true, а потому цикл будет продолжаться бесконечно.
Цикл 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 1 = 0;// Счетчик значений
char indicator = *у*;// Продолжать или нет?
while (indicator = ’у’)// Повторять тело цикла, пока введено 'у'
{
cout « endl
« "Введите значение: ";
cin » value;// Читать значение
4-4-1;// Увеличить счетчик
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. Данный пример можно усовершенствовать, если сформулировать условие цикла так, чтобы для продолжения цикла можно было вводить ’ Y’ наряду с 1 у *. Для этого потребуется модифицировать оператор while следующим образом:
while ((indicator = ’у’) || (indicator = ’Y’))
Можно также создать потенциально бесконечный цикл while, используя условие, которое всегда true. Это можно написать так:
while(true)
{
}
Можно также записать управляющее выражение цикла как целочисленное значение 1, которое будет преобразовано в значение true типа bool. Естественно, здесь также остается в силе то же требование, что предъявляется к бесконечным циклам for, а именно: необходимо предусмотреть какой-нибудь способ выхода из цикла внутри его тела. Вы увидите в главе 4 другие примеры применения цикла while.
Цикл do-while
Цикл do-while подобен циклу while в том, что он выполняется до тех пор, пока указанное условие остается истинным. Главное отличие его состоит в том, что здесь условие проверяется в конце цикла — что отличает его от циклов for и while, где
оно проверяется в начале. Как следствие, оператор внутри цикла do-while всегда выполняется, по меньшей мере, один раз. Общая форма цикла do-while следующая:
do
{
оператор_внутри_цикла ;
} while (условие);
Логика этой формы цикла показана на рис. 3.6.
Рис. 3.6. Оператор цикла do-while
Вы можете заменить цикл while в последней версии программы вычисления среднего циклом do-while:
do
{ cout « endl
« "Введите значение: ";
cin » value;// Читать значение
++i;// Увеличить счетчик
sum += value; // Добавить текущее значение к сумме
cout « "Хотите ввести еще одно значение (для завершения введите п) ? "; cin » indicator; // Читать индикатор
} while((indicator = *у*) | | (indicator = fYr));
Между двумя последними версиями цикла нет особой разницы, за исключением того, что правильная работа этой версии не зависит от начального значения indicator. До тех пор, пока вы уверены, что будет введено хотя бы одно осмысленное значение, которое нужно обработать, применение данной версии цикла более предпочтительно.
Вложенные циклы
Циклы можно вкладывать друг в друга. Смысл этого станет более очевидным в главе 4: такая техника обычно применяется для повторения действий на разных уровнях классификации. Примером может быть вычисление суммы оценок каждого студента в классе с повторением процесса для каждого класса школы.
Практическое занятие | Вложенные циклы
Эффект от вложения одного цикла в другой можно увидеть на примере вычисления значений по простой формуле. Факториал целого числа — это произведение всех целых чисел от 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, как сообщает про-
грамма. Если вы введете еще большие входные значения, это приведет к потере старших десятичных разрядов в переменной factorial, и можно даже получить отрицательные значения факториала, как это произошло при вводе числа 22.
В этой ситуации не въедается никаких сообщений об ошибке, поэтому чрезвычайно важно при разработке программы быть уверенным в том, что значения, с которыми будет иметь дело программа, не выйдут за пределы допустимого диапазона для типа данных используемых переменных. Вы должны также предусмотреть обработку некорректных входных значений. Ошибки такого рода, происходящие “молча”, чрезвычайно трудно найти.
Внешний из двух циклов, цикл do-while, управляет завершением программы. До тех пор, пока вы вводите 1 у1 или ' 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 « ” |”;// Вывести метку строки
for(j = 1; j <= size; j++)// Внутренний цикл остальной части строки
cout « setw(3) « i*j « " "; // Конец вложенного цикла
} // Конец внешнего цикла
cout « endl;
return 0;
}
Ниже показан вывод этого примера.
Таблица умножения 12 на 12
5
6
7
8
9
10
11
12
1
1
2
3
4
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
12
16
20
24
28
32
36
40
44
48
5 I
5
10
15
20
25
30
35
40
45
50
55
60
6 1
6
12
18
24
30
36
42
48
54
60
66
72
7 |
7
14
21
28
35
42
49
56
63
70
77
84
8 1
8
16
24
32
40
48
56
64
72
80
88
96
9 1
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 |
12
24
36
48
60
72
84
96
108
120
132
144
Описание полученных результатов
Заголовок таблицы выдается первым оператором вывода программы. Следующий оператор вывода, вставленный в следующий за ним цикл, генерирует заголовки столбцов. Каждый столбец имеет ширину в пять символов, поэтому значения заголовков отображаются в поле шириной три символа, что указано манипулятором setw (3), за которым идет два пробела. Оператор вывода, предшествующий циклу, выводит четыре пробела и вертикальную линию перед заголовком первого столбца. Затем ниже идет серия символов подчеркивания, отделяющих заголовки от содержимого столбцов.
Вложенный цикл генерирует основное содержимое таблицы. Внешний цикл повторяется по разу для каждой строки, поэтому здесь 1 — номер столбца. Оператор вывода
cout « endl
« setw(3) « i « ” I";// Вывести метку строки
переходит на новую строку и затем выводит ее заголовок в виде значения 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.
практическое занятие | Программа CLR, использующая вложенные операторы if
Создайте консольную программу с кодом по умолчанию и модифицируйте функцию main() следующим образом:
И ЕхЗ_15.срр : главный файл проекта.
#include "stdafx.h"
using namespace System;
int main(array<System::String A> Aargs) {
wchar^t letter;// Соответствует типу C++/CLI Char
Console: :Write(L"Введите букву: ") ;
letter = Console: :Read() ;
if (letter >= ’A’)// Проверка letter равно или больше *A*
if (letter <= * Z *)M Проверка letter равно или меньше ' Z1
{
Console: :WriteLine(Ь"Вы ввели заглавную букву.") ; return 0;
}
if (letter >= * a*)/ / Проверка letter равно или больше ’a’
if (letter <= ’z’)/ / Проверка letter равно или меньше ’ z*
{
Console: :WriteLine(L"BM ввели прописную букву.") ; return 0;
}
Console: :WriteLine (Ь"Вы ввели не букву.") ; 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 (), проверяющие регистр символа. Вы передаете проверяемый символ этим функциям в виде аргумен-
та, а они возвращают значение bool в качестве результата. Это можно использовать, чтобы закодировать main () несколько иначе.
wchar_t letter;Н Соответствует типу C++/CLI Char
Console::Write(L"Введите букву: ") ;
letter = Console:: Read () ;
wchar_t upper = Char: :ToUpper(letter) ;
if (upper >= *A' && upper <= ’Z’) // Проверка на вхождение в диапазон от *А* до * Z * Console: :WriteLine(L"Вы ввели (0) букву.".
Char: :IsUpper (letter) ? "заглавную" : "прописную");
else
Console: : WriteLine (L"Bh ввели не букву.") ;
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} ", ekeyPress.Key, keyPress.KeyChar);
}while(keyPress.Key != ConsoleKey::Escape); return 0; )
Ниже показан результат работы этой программы.
Нажмите комбинацию клавиш — для выхода нажмите Esc. Вы нажали Shift, В что дает символ В Вы нажали Shift, Control, N что дает символ _ Вы нажали Shift, Control, Oeml что дает символ Вы нажали Oeml что дает символ ; Вы нажали ОешЗ что дает символ '
Вы нажали 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.
Как можно видеть из аргументов функции WriteLineO в последнем операторе вывода, для доступа к свойству объекта необходимо поместить наименование свойства следом за именем объекта, отделив его точкой. Здесь точка — это операция доступа к члену. Чтобы получить доступ к свойству KeyChar объекта keyPress, вы должны написать keyPress .KeyChar.
Программа работает очень просто. Внутри цикла вызывается функция ReadKey () для чтения нажатия клавиши, и ее результат помещается в переменную keyPress. Далее с помощью функции Write () начальная часть вывода помещается в командную строку. Поскольку в этом случае не выводится символ новой строки, последующий вывод продолжается в той же строке. Затем выполняется проверка свойства Modifiers на предмет того, больше ли оно нуля. Если это так, значит, были нажаты модифицирующие клавиши, и об этом выводится сообщение, в противном случае информация о модифицирующих клавишах пропускается. Возможно, вы помните, что перечислимые константы C++/CLI являются объектами, которые нужно явно привести к целочисленном типу, прежде чем использовать их как числовые значения — отсюда приведение к int в выражении if.
Интересен вывод Modifiers. Как видно из результирующего вывода программы, когда нажато более одной модифицирующей клавиши, вы получаете информацию о них всех в одном операторе вывода. Это потому, что перечисление Modifiers определено с атрибутом FlagsAttribute, который указывает на то, что этот перечислимый тип представляет набор отдельных битовых флагов. Благодаря этому переменные перечислимого типа могут состоять из нескольких флагов, объединенных вместе логическим И, а индивидуальные флаги распознаются и выводятся функциями Write () и WriteLine ().
Цикл продолжается до тех пор, пока истинно условное выражение 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 A> Aargs) {
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;
Запуск этого примера даст следующий вывод:
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-битные целые значения.
Далее определяется строка для анализа:
String* proverb = L"A nod is as good as a wink to a blind horse.";
Переменная proverb имеет тип StringA, который описывается как тип “дескриптора String” (handle to String); дескриптор применяется для хранения местоположения объекта в динамически распределяемой “куче” памяти под управлением CLR, очищаемой сборщиком мусора. Позже вы более подробно узнаете о дескрипторах и типе StringA, когда речь пойдет о типах классов 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:
if (Char::IsLetter(ch))
Функция Char: : IsLetter () возвращает значение true, если аргумент (в данном случае — ch) является буквой, и false — в противном случае. Таким образом, следующий за if блок выполняется только в том случае, если ch содержит букву. Это необходимо, поскольку вы не хотите, чтобы знаки препинания обрабатывались так, как если бы они были буквами.
Установив, что ch — на самом деле буква, с помощью следующего оператора она преобразуется в нижний регистр:
ch = Char::ToLower(ch);
Здесь применяется функция Char: : ToLower () из библиотеки .NET Framework, которая возвращает эквивалент своего аргумента (в данном случае — ch) в нижнем регистре. Если аргумент уже находится в нижнем регистре, функция просто возвращает его без изменений. Преобразование символа в нижний регистр позволяет избежать необходимости последовательно проверять его на вхождение в список гласных верхнего и нижнего регистра.
Определение того, является ли ch гласной или согласной буквой, выполняется внутри оператора switch:
switch(ch)
{
case * a*: case ’e’: case ’i’:
case ’ о ’ : case ’ u1 :
++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 позволяет выполнять свои операторы до тех пор, пока его
проверочное условие возвращает true. И, наконец, цикл do-while выполняет операторы как минимум один раз, а затем позволяет повторять их до тех пор, пока указанное условие истинно.
В C++/CLI, в дополнение к трем перечисленным операторам циклов, предусмотрена еще одна форма цикла — for each.
Циклы любого вида могут быть вложены в любые другие циклы.
Ключевое слово continue позволяет пропускать остаток операторов текущей итерации цикла и сразу переходить к следующей итерации.
Ключевое слово break обеспечивает немедленный выход из цикла. Оно также выполняет выход из switch в конце группы операторов case.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Напишите программу, которая читает из cin входные числа и суммирует их, прекращая процесс после ввода 0. Сконструируйте три версии этой программы, с использованием циклов while, do-while и for.
Напишите программу ISO/ANSI C++ для чтения символов с клавиатуры и подсчета гласных. Программа должна прекращать работу при вводе Q (или q). Используйте комбинацию бесконечного цикла для чтения символов с оператором switch для их подсчета.
Напишите программу для распечатки таблицы умножения размером от 2 до 12 столбцов.
Предположим, что необходимо установить в программе переменную “режима открытия файла” на базе двух атрибутов: типа файла, который может быть текстовым или двоичным, и способа открытия файла — для чтения, записи или добавления данных. Используя битовые операции (& и |), а также набор флагов, предложите метод, позволяющий в одной целочисленной переменной устанавливать любую комбинацию этих двух атрибутов. Напишите программу, которая установит значение этой переменной, а затем декодирует его, распечатав все установки флагов для всех возможных комбинаций атрибутов.
Воспроизведите пример ЕхЗ_2 в виде программы C++/CLI (можете использовать Console: : ReadKey () для чтения символов с клавиатуры).
Напишите консольную программу CLR, которая определяет строку (типа StringA) и затем анализирует ее символы для подсчета символов верхнего регистра, символов нижнего регистра, небуквенных символов и общего количества символов в строке.
4
Массивы, строкии указатели
Итак, до настоящего момента вы последовательно изучили все фундаментальные типы данных, и уже обладаете базовыми знаниями о том, как в программе выполняются вычисления и принимаются решения. В этой главе вы сможете расширить приложение основных приемов программирования, которые вы изучили до сих пор — от применения отдельных элементов данных к работе с целыми их коллекциями. В этой главе вы узнаете следующие моменты.
Что такое массивы и как они применяются.
Как объявлять и инициализировать массивы различных типов.
Как объявлять и использовать многомерные массивы.
Что такое указатели и как их использовать.
Как объявлять и инициализировать указатели различных типов.
Отношения между указателями и массивами.
Ссылки, как они объявляются и некоторые начальные представления об их использовании.
Как динамически выделять память для переменных в программах на “родном” C++.
Как работает динамическое распределение памяти в программе CLR.
Отслеживание дескрипторов и ссылок и зачем они нужны в программах CLR.
Как работать со строками и массивами в программах C++/CLI.
Что такое внутренние указатели, и как вы можете создавать и применять их.
В этой главе вы будете более интенсивно применять объекты, хотя вы пока и не знаете всех деталей их создания — поэтому не волнуйтесь, если что-то будет не совсем ясно. Изучение классов и объектов во всех подробностях начнется в главе 7.
Обработка множества однотипных элементов данных
Вы уже знаете, как объявлять и инициализировать переменные различных типов, каждая из которых содержит отдельный элемент информации. Я буду называть их элементами данных. Вы знаете, как создать отдельный символ в переменной типа char, отдельное целое число в переменной типа short, типа int, типа long, или же отдельное число с плавающей точкой в переменной типа float или типа double. Наиболее очевидное развитие этих идей — возможность ссылаться на несколько элементов данных определенного типа по одному имени переменной. Это позволило бы вам управлять приложениями в более широком контексте.
Рассмотрим пример того, где это может понадобиться. Предположим, что вам необходимо написать программу для ведения ведомости зарплат. Применение отдельных именованных переменных для каждой индивидуальной выплаты, связанных с ней налоговых отчислений и так далее было бы, мягко говоря, непростой задачей. Гораздо удобнее было бы справиться с этой проблемой, если бы можно было ссылаться на наемного работника по какому-то обобщенному имени — к примеру, employeeName — и также иметь другие обобщенные имена для различных данных, связанных с каждым работником, вроде pay (выплата), tax (налог) и так далее. И, конечно же, нужен какой-то способ указания на конкретного работника из общей группы, вместе с данными из обобщенных переменных, которые его касаются. Подобные требования возникают всегда, когда программа имеет дело с любыми коллекциями однотипных данных — будь то члены баскетбольной команды или группа боевых кораблей. Естественно, в C++ предусмотрены возможности для решения таких задач.
Массивы
Базовое решение всех этих проблем в ISO/ANSI C++ обеспечивают массивы. Массив — это просто некоторое множество мест в памяти, называемых элементами массива, или просто элементами, каждый из которых может хранить единицу данных определенного типа, и к которым можно обращаться по одному имени переменной. Имена сотрудников в программе платежной ведомости могут быть помещены в один массив, суммы выплат — в другой, а суммы налогов каждого сотрудника — в третий.
Индивидуальные элементы массива специфицируются значением индекса, который представляет собой просто целое число — порядковый номер элемента в массиве, причем первый имеет индекс 0, второй — 1 и так далее. Вы можете представлять индекс массива как смещение от его первого элемента. Первый элемент имеет смещение 0, а потому — индекс 0, а индекс 3 ссылается на четвертый элемент в массиве. Что касается платежной ведомости, вы можете упорядочить массивы таким образом, что если имя определенного сотрудника помещено в элемент массива employeeName с определенным значением индекса, то массивы pay и tax также должны хранить данные, ассоциированные с этим сотрудником, в элементах с тем же индексом.
Базовая структура массива проиллюстрирована на рис. 4.1.
На рис. 4.1 показан массив. Имя height содержит шесть элементов, каждый содержит отдельное значение. Это могут быть значения роста членов семьи, измеренные в дюймах. Поскольку имеется шесть элементов, значение индекса находится в пределах от 0 до 5. Чтобы сослаться на определенный элемент, вы пишете имя массива, за которым идет значение индекса определенного элемента, заключенное в квадратные скобки. Так, например, чтобы сослаться на третий элемент, нужно написать height [2]. Если вы представите индекс в виде смещения от первого элемента, то легко увидеть, что значение индекса четвертого элемента будет равно 3.
Общий объем памяти, необходимый для хранения каждого элемента, определяется его типом, и все элементы массива сохраняются в одном непрерывном блоке памяти.
Значение индекса для 5-го элементаИмя массиваЗначение индекса для 5-го элементаИмя массиваЗначение индекса для 2-го элемента
Значение индекса для 5-го элемента
Имя массива
Значение индекса для 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];double cubic_inches[10];double horsepower[10];Вы можете объявить массив любого типа. Например, чтобы объявить массив, предназначенный для хранения рабочего объема и выходной мощности серии двигателей, вы можете написать следующее:
double cubic_inches[10];
double horsepower[10];
double cubic_inches[10];
double horsepower[10];
// Объемы двигателей в кубических дюймах // Выходные мощности
Это позволит хранить рабочие объемы и мощности до 10 двигателей, ссылаясь на них по индексам от 0 до 9. Как вы уже видели это на примере других переменных, можно объявлять несколько массивов одного и того же типа в одном операторе, но на практике почти всегда лучше объявлять эти переменные в отдельных операторах.
Практическое занятие
Использование массивов
Для целей базового примера использования массивов предположим, что вы храните записи об объемах бензина, купленного для вашей машины, и показания одометра в каждом случае. Вы можете написать программу для анализа данных, чтобы увидеть, как изменяется показатель расхода бензина на милю пробега.
И Ех4_01.срр
II Вычисление расхода бензина
#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 == ’у* I | 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;
}
Программа предполагает, что вы наполняете бак каждый раз, так что потребленный объем соответствует записанному пробегу. Ниже показан пример вывода этой программы.
Введите объем бензина: 12.8
Введите показания одометра: 25832
Продолжить ввод (у или п) ? у
Введите объем бензина: 14.9
Введите показания одометра: 26337
Продолжить ввод (у или п) ? у
Введите объем бензина: 11.8
Введите показания одометра: 26598
Продолжить ввод (у или п) ? п
Куплено бензина = 14.9 пробег составил 33.8926 миль на галлон.
Куплено бензина = 11.8 пробег составил 22.1186 миль на галлон.
Описание полученных результатов
Поскольку вам нужно брать разницу между двумя показаниями одометра, чтобы вычислить пробег в милях на купленном объеме бензина, из первой пары значений используется только показание одометра, а объем купленного бензина игнорируется, поскольку он покрывает расход на расстояние, пройденное до первого замера.
Как видим, во время второго периода, отображенного программой на выходе, режим движения был весьма неэкономным — вы либо много стояли в пробках, либо часто парковались.
Размеры двух массивов gas и miles, использованных для хранения входных данных, определялись значением константы по имени МАХ. Изменяя значение МАХ, вы можете адаптировать программу для приема различного количества входных значений. Эта техника часто используется для обеспечения гибкости в отношении объема обрабатываемой информации. Конечно, весь код программы должен быть написан с учетом размеров массивов или любых других параметров, специфицированных переменными const. На практике это предоставляет некоторое неудобство, однако, нет причин, которые не позволили бы в определенной мере адаптировать такой подход. Чуть позже вы увидите, как можно выделять память для хранения данных во время выполнения программы, так что не придется фиксировать заранее определенный объем памяти, выделенный для данных.
Ввод данных
Значения данных читаются в цикле while. Поскольку переменная-счетчик цикла count может изменяться от 0 до МАХ - 1, мы не можем позволить пользователю программы ввести больше значений, чем может вместить массив. Вы инициализировали переменные count и indicator значениями 0 и 1 у1 соответственно, так что тело цикла while будет выполнено, по крайней мере, один раз. Перед каждым вводом данных выводится приглашение, и прочитанное значение записывается в соответствующий элемент массива. Элемент, используемый для сохранения конкретного значения, определяется переменной count, которая при первом вводе содержит значение 0. Элемент массива указывается в операторе cin посредством count в качестве индекса, и count затем увеличивается на единицу, готовя к приему значения следующий элемент.
После ввода каждого значения программа запрашивает подтверждение намерения продолжать ввод. Введенный символ читается в переменную indicator, затем проверяется в условии цикла. Цикл прерывается, если не будет введено ’ у ’ или 1Y1, или же count достигнет значения МАХ.
После завершения цикла (по любой причине) count содержит значение на единицу больше индекса последнего заполненного элемента каждого массива. (Вспомните, что вы инкрементируете его после ввода каждого нового элемента.) Это значение
проверяется, чтобы убедиться, что было введено не менее двух пар значений. Если это не так, программа завершается с соответствующим сообщением, поскольку для вычисления пробега необходимо два показания одометра.
Выдача результата
Вывод программы генерируется в цикле for. Управляющая переменная i изменяется от 0 до count-1, позволяя вычислять пробег как разницу между текущим показанием miles [i] и предыдущим miles [i-1]. Обратите внимание, что значение индекса может быть любым выражением, вычисление которого дает целочисленный результат — корректное значение от нуля до величины, на единицу меньшей, чем количество элементов в массиве.
Если значение индексного выражения окажется вне допустимого диапазона, соответствующего элементам массива, то вы обратитесь к несуществующим данным, которые могут содержать значения других переменных, или какой-то мусор, или даже код самой программы. Если обращение к такому элементу произойдет в выражении, то вы получите какие-то произвольные данные, которые, участвуя в вычислениях, породят некорректные результаты. Если же вы попытаетесь записать значение в элемент массива, указанный неправильным индексом, то перезапишете любые данные, которые окажутся в этом месте памяти. Если это будет программный код, результат окажется катастрофическим. Когда вы используете неправильное значение индекса, не выдается никаких предупреждений — ни во время компиляции, ни во время выполнения. Единственный способ предохраниться от такой ситуации — соответствующим образом писать код программы.
Для генерации вывода для всех введенных значений, за исключением первой, используется единственный оператор cout. Номер каждой выходной строки также генерируется значением управляющей переменной цикла i. Количество миль пробега на один галлон вычисляется непосредственно в операторе вывода. Вы можете использовать элементы массива в выражениях точно таким же образом, как любые другие переменные.
Инициализация массивов
Чтобы инициализировать массив в его объявлении, нужно поместить начальные значения элементов в ограниченный фигурными скобками список, разделенный запятыми, который находится справа от знака равенства, следующего за именем массива. Ниже показан пример того, как можно объявить и инициализировать массив:
int cubic_inches [5] = { 200, 250, 300, 350, 400 };
Массив называется cubic_inches, содержит пять элементов, каждый из которых хранит значение типа int. Элементы массива инициализированы списком значений в фигурных скобках, соответствующим последовательности значений индекса массива, поэтому в данном случае cubic_inches [0] получает значение 200, cubic_inches [1] — значение 250, cubic_inches [ 2 ] — значение 300 и так далее.
Вы не должны специфицировать больше значений, чем объявлено элементов в массиве, но меньше — указать можно. Если задано меньше значений, то они присваиваются последовательно элементам массива, начиная с первого — то есть элемента с индексом 0. Элементы массива, для которых не указаны начальные значения, инициализируются нулями. Это не то же самое, что происходит, если вообще не указать списка инициализации. Без списка инициализации элементы массива получают произвольные случайные значения. Кроме того, если вы включаете список инициализа-
ции, в нем должен присутствовать хотя бы один элемент, иначе компилятор генерирует сообщение об ошибке. Я могу проиллюстрировать это на следующем довольно ограниченном примере.
Практическое занятиеПрактическое занятиеИнициализация массиваИнициализация массива
Практическое занятие
Практическое занятие
Инициализация массива
Инициализация массива
// Ех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) « junk[i];
cout << endl;
return 0;
}
В этом примере объявляются два массива, первый из которых — value, инициализируется частично, а второй — junk, не инициализируется вообще. Программа выдает две строки вывода, которые на моем компьютере выглядят так:
12300
-858993460-858993460-858993460-858993460-858993460
Вторая строка (соответствующая значениям от j unk[0] до j unk[4]) на вашем компьютере может отличаться.
Описание полученных результатов
Первые три элемента массива value инициализируются указанными значениями, а остальные два — по умолчанию нулями. В случае junk все значения случайны, поскольку никаких начальных значений не указано вообще. Элементы этого массива содержат то, что осталось в данной области памяти от программы, которая использовала ее раньше.
Удобный способ инициализации всего массива нулевыми значениями — это просто указание единственного начального значения, равного 0, например:
long data[100] = {0}; // Инициализировать все элементы нулями
Этот оператор объявляет массив data размером 100 элементов, инициализированных нулями. Первый элемент инициализируется значением, указанным в фигурных скобках, а остальные инициализируются нулями по умолчанию, поскольку значения для них не указаны явно.
Вы можете также опустить размер массива числового типа, если указываете список значений инициализации. Количество элементов в массиве определяется количеством указанных инициирующих значений. Например, объявление массива:
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”;
Поскольку размер не указан, компилятор выделяет пространство, достаточное для размещения инициализирующей строки, плюс ограничивающий нулевой символ. В данном случае он выделит 14 элементов для массива president. Конечно, если вы хотите использовать данный массив позднее для хранения другой строки, ее длина (включая нулевой символ-ограничитель) не должна превышать 14 байт. В общем случае на вас ложится ответственность обеспечить достаточный размер массива, чтобы в него поместилась любая строка, которую вы в последствии захотите в нее поместить.
Строковый ввод
Заголовочный файл <iostream> содержит определения множества функций для чтения символов с клавиатуры. Одну их них мы рассмотрим здесь. Это — функция get line (), которая читает последовательность вводимых с клавиатуры символов и помещает ее в символьный массив в виде строки с ограничивающим нулем \0. Обычно вы будете использовать функцию get line () в операторах вроде следующего:
const int MAX = 80;// Максимальная длина строки, включая \0
char name [ММ];// Массив для хранения строк
cin. get line (name, MAX, ’ \n’); // Прочитать введенную последовательность как строку
В этом фрагменте сначала объявляется массив char размером в МАХ элементов, а затем в него читается строка из cin с помощью функции getline (). Источник данных — cin — записывается так, как показано, с точкой, отделяющей его от имени функции. Что именно означают аргументы функции getline (), можно видеть на рис. 4.3.
Максимальное количество символов для прочтения. Когда указанное количество прочитано, ввод прекращается.Имя массива типа char[], в который помещаются символы, прочтенные из cin.Максимальное количество символов для прочтения. Когда указанное количество прочитано, ввод прекращается.Имя массива типа char[], в который помещаются символы, прочтенные из cin.Символ, прекращающий процесс ввода. Вы можете специфицировать здесь любой символ, и первое его появление остановит ввод.Символ, прекращающий процесс ввода. Вы можете специфицировать здесь любой символ, и первое его появление остановит ввод.
Максимальное количество символов для прочтения. Когда указанное количество прочитано, ввод прекращается.
Имя массива типа char[], в который помещаются символы, прочтенные из cin.
Максимальное количество символов для прочтения. Когда указанное количество прочитано, ввод прекращается.
Имя массива типа char[], в который помещаются символы, прочтенные из cin.
Символ, прекращающий процесс ввода. Вы можете специфицировать здесь любой символ, и первое его появление остановит ввод.
Символ, прекращающий процесс ввода. Вы можете специфицировать здесь любой символ, и первое его появление остановит ввод.
cin.getline( name , MAX, *\n*);
Рис. 4.3. Аргументы функции getline ()
Поскольку последний аргумент, переданный функции getline () — это ’ \п’ (символ новой строки), а второй аргумент — МАХ, символы читаются из cin до тех пор, пока не будут прочитан символ ’ \п ’, или когда будет прочитано МАХ-1 символов — в зависимости от того, что произойдет раньше. Максимальное количество прочитанных символов равно МАХ-1, чтобы позволить дописать символ-ограничитель ’ \ 0 ’ к последовательности символов, сохраняемых в массиве. Символ ’ \п’ генерируется, когда вы нажимаете клавишу <Enter> на клавиатуре, а потому это — наиболее удобный символ для завершения ввода. Однако вы можете указать какой-то другой символ в качестве признака завершения ввода, изменив последний аргумент. Символ ’ \п ’ не сохраняется во входном массиве name, но как уже было сказано, в конец введенной строки, помещенной в массив, добавляется ’ \0 ’.
Практическое занятиеПрактическое занятиеПозднее, когда мы будем говорить о классах, вы узнаете больше об используемом здесь синтаксисе. А пока примем, его как данность и применим в примере.
Практическое занятие
Практическое занятие
Программирование со строками
// Ех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>
Буква “с” в имени заголовочного файла говорит о том, что этот файл содержит определения, относящиеся к библиотеке языка С, которая является частью библио-
теки 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 может быть определено как эквивалент наиболее подходящего типа в текущем аппаратном окружении.
В конце примера введенная строка и количество ее символов отображаются единственным оператором вывода. Обратите внимание на применение управляющей последовательности ' \'" для вывода двойной кавычки.
Многомерные массивы
Массивы, которые мы определяли до сих пор, имеют один индекс и называются одномерными массивами. Но массив может иметь и более одного индексного значения — в этом случае он называется многомерным массивом. Предположим, что у вас есть поле, на котором расположена плантация бобов, по 10 растений на грядке, и это поле содержит 12 таких грядок (то есть всего имеется 120 единиц растений). Вы можете объявить массив для записи веса бобов, собранных от каждого растения, используя следующий оператор:
double beans[12][10];
Здесь объявляется двумерный массив beans, где первый индекс — номер грядки, а второй — номер растения на грядке. Чтобы обратиться к любому конкретному элементу, необходимо указать два индекса. Например, вы можете установить значение элемента, соответствующего пятому растению в третьей грядке, следующим оператором:
beans[2] [4] = 10.7;
Напомним, что значения индексов начинаются с нуля, поэтому значение индекса третьей грядки — 2, а индекс пятого растения — 4.
Будучи успешным бобовым фермером, вы можете иметь несколько таких полей, засеянных бобами по тому же шаблону. Предполагая, что у вас 8 полей, можно определить трехмерный массив для записи данных об урожае следующим образом:
double beans[8][12][10];
Такая запись позволяет организовать учет каждого растения, растущего на всех этих полях, причем самый левый индекс ссылается на определенное поле. Если вы когда-нибудь займетесь выращиванием бобов в международном масштабе, то сможете использовать четырехмерный массив, где дополнительное измерение будет обозна-
чать страну. Если предположить, что вы — настолько же успешный продавец, как и фермер, то выращивание такого количества бобов, в конце концов, станет оказывать влияние на озоновый слой.
Массивы хранятся в памяти так, что самый правый индекс растет быстрее всего. То есть массив 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] = {
{ 1, 2, 3, 5 },
{ 7, 11, 13, 17 } };
То есть, инициализация значений каждой строки массива содержится внутри собственной пары фигурных скобок. Поскольку в каждой строке четыре элемента, в каждой группе присутствует по четыре значения инициализации, и поскольку строк всего две, внутри скобок находится две группы инициализирующих значение, разделенных запятой.
Вы можете пропустить инициализацию значений в любой строке, в этом случае остальные элементы массива инициализируются нулевыми значениями, например:
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;
}
Описание полученных результатов
Помимо развлекательного значения, главное, что интересно в этом примере — объявление массива 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++, не хранящих в себе данные, которые вы обычно вводите для вычислений, но значительно повышающих мощь и гибкость ваших программ. Переменные такого рода называются указателями.
Что такое указатель?
Каждое место в памяти, которое вы используете для хранения данных, имеет адрес. Адрес предоставляет возможность для оборудования компьютера обращаться к определенному элементу данных. Указатель — это переменная, которая сохраняет адрес другой переменной определенного типа. Переменная-указатель обладает именем, как и любая другая переменная, и также имеет тип, определяющий то, на какого рода данные она может указывать. Переменная, являющаяся указателем, содержащим адрес местоположения в памяти, которое хранит значение типа int, называется “указателем на int”.
Объявление указателей
Объявления указателей подобно объявлению обычных переменных, за исключением того, что имя дополняется впереди звездочкой, говорящей о том, что данная переменная является указателем. Например, чтобы объявить указатель рnumber типа 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. Но как получить адрес этой переменной?
Операция получения адреса
Что вам нужно для этого — операция получения адреса, &. Она представляет собой унарную операцию, возвращающую адрес переменной — своего операнда. Он также называется операцией ссылки — по причине, которую я поясню позже в этой главе. Чтобы присвоить значение адреса переменной указателю, вы можете написать следующий оператор присваивания:
pnumber * &number; Н Сохранить адрес number в pnumber
Результат выполнения этой операции можно видеть на рис. 4.5.
&number <
Адрес: 1008
numbernumber9999pnumber 1008 <
number
number
99
99
pnumber = &number;
Рис. 4.5. Результат выполнения операции получения адреса
Вы можете использовать операцию & для получения адреса любой переменной, но для сохранения его вам понадобится переменная-указатель соответствующего типа. Например, если нужно сохранить адрес переменной double, указатель должен быть объявлен с типом double *, то есть “указатель на double”.
Использование указателей
Получение адреса переменной и сохранение его в переменной-указателе — это очень хорошо, но более интересно, как его использовать. Основной смысл применения указателя состоит в возможности доступа к данным, на которые он указывает. Это делается с помощью операции разыменования (*).
Операция разыменования
Для доступа к переменной, на которую указывает указатель, вы будете применять операцию разыменования указателя (*), подставляя в качестве операнда имя переменной-указателя. Название “операция разыменования” или “операция косвенного доступа” говорит о том, что обращение к данным не прямое, и что для получения доступа к данным, на которые указывает указатель, его следует “разыменовать”.
Один аспект этой операции, который может показаться запутанным, связан с существованием разных применений одного и того же символа *. Во-первых, это — операция арифметического умножения, во-вторых — операция разыменования, и вдобавок она используется при объявлении переменных-указателей. Всякий раз, встречая символ *, компилятор определяет его значение в каждом конкретном случае из контекста. Когда вы перемножаете две переменных, например, А*В, не существует никакой другой осмысленной интерпретации этого выражения, кроме как операции умножения.
Зачем нужны указатели?
Вопрос, который обычно возникает при изучении указателей, звучит так: “Зачем они вообще нужны?”. В конце концов, получить адрес переменной, которая вам уже известна, сохранить его в указателе, чтобы потом разыменовать, выглядит излишним, то есть чем-то таким, без чего вполне можно обойтись. Однако существует несколько причин, объясняющих важность указателей.
Как вы вскоре увидите, нотацию указателей можно использовать для операций с данными, хранимыми в массиве; иногда это выполняется быстрее, чем в случае применения обычной нотации массивов. К тому же, когда позднее вы будете определять свои собственные функции, то увидите, что указатели интенсивно используются для обеспечения доступа функций к объемным блокам данных, таким как массивы, которые определены вне этих функций. Но как вы увидите позднее, еще более важно то, что память для переменных может выделяться динамически, то есть во время выполнения программы. Это средство позволяет программам адаптировать использование памяти к объему обрабатываемых данных. Поскольку при этом вы не можете знать наперед, сколько переменных придется создать динамически, основной способ работы с такой памятью — через указатели. Так что будьте уверены, что указатели вам пригодятся.
Практическое занятиеПрактическое занятиеИспользование указателейИспользование указателей
Практическое занятие
Практическое занятие
Использование указателей
Использование указателей
В следующем примере вы можете испытать различные аспекты операций с указателями.
//Ех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 = &numberl; // Сохранить адрес в указателе
*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
Описание полученных результатов
В этом примере нет ввода. Все операции выполняются с инициализированными значениями переменных. После сохранения адреса 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
Инициализируя указатель адресом другой переменной, помните, что эта переменная должна быть объявлена до объявления указателя.
Конечно, объявляя указатель, вы можете решить не инициализировать его адресом определенной переменной. В этом случае его можно инициализировать указателем, эквивалентным нулю. Для этого в 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++/CLI дела обстоят несколько иначе.
Для использования 0 в качестве инициализирующего значения указателя, нужно просто написать:
int* pnumber = 0; // Указатель, не указывающий ни на что
Чтобы проверить, содержит ли указатель корректный адрес, используйте следующий оператор:
if (pnumber == 0) cout « endl « "pnumber равен null.";
С тем же успехом можете применить и такой оператор:
if(!pnumber) cout « endl « "pnumber равен null.";
Этот оператор делает то же самое, что и предыдущий.
Конечно, вы также можете использовать и такую форму:
if(pnumber ’ = 0)
// Указатель правильный, делать что-то полезное
Адрес, на который указывает NULL-указатель, содержит “мусорное” значение. Никогда не пытайтесь разыменовывать нулевой указатель, поскольку это приведет к немедленному прерыванию работы вашей программы.
Указатели на char
Указатель типа char * обладает интересным свойством — он может быть инициализирован строковым литералом. Так, например, можно объявить и инициализировать указатель следующим оператором:
char* proverb = "A miss is as good as a mile.";
Это выглядит похожим на инициализацию массива char, однако есть небольшое отличие. Здесь создается строковый литерал (на самом деле, массив типа const char), содержащий символьную строку, заключенную в кавычки, ограниченную символом \0, и его адрес присваивается указателю proverb. Адресом литерала будет адрес его первого символа. Это показано на рис. 4.6.
Создается указатель proverb.
proverb
Создается константная строка, ограниченная \0. —>|1000
Address: 1000
A m Jssis as good as a mile
. \0
Адрес строки сохраняется в указателе.
Практическое занятиеПрактическое занятиеРис. 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;
case 2: cout « pstr << pstr2;
break;
cout « pstr « pstr3;
break;
cout « pstr « pstr4;
break;
cout « pstr « pstr5;
break;
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;
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, похожим на исходную версию примера. Оно позволяет отобразить либо имя выбранной звезды, либо соответствующее сообщение, если пользователь ввел неверное значение.
Общий объем памяти 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]);
Выражение (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;
}
Описание полученных результатов
Как видите, необходимое изменение в этом примере очень простое. Мы вычисляем количество элементов в массиве указателей 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"
В этом случае устраняется противоречие между константностью строк, на которые указывают элементы массива указателей и не константностью самих указателей. Если теперь попытаться изменить строки, компилятор обнаружит это и выдаст сообщение об ошибке во время компиляции.
Однако вы все еще можете написать такой оператор:
pstr[0] = pstr[1];
Те счастливчики, которые выберут мистера Редфорда (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.
Если имеются следующие объявления:
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]data[O]data[1]data[2]data[3]data[4]Каждый элемент занимает 8 байтКаждый элемент занимает 8 байтАдресАдресpdata+1pdata+1pdata+2pdata+2
data[O]data[1]data[2]data[3]data[4]
data[O]data[1]data[2]data[3]data[4]
Каждый элемент занимает 8 байт
Каждый элемент занимает 8 байт
Адрес
Адрес
pdata+1
pdata+1
pdata+2
pdata+2
pdata = &data[2];
Puc. 4.8. Операции над указателем
Другими словами, инкремент и декремент указателя работает в терминах типа объекта, на который он указывает. Увеличение на единицу указателя на 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>
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
Описание полученных результатов
Здесь присутствуют обычные операторы #include с заголовочным файлом <iostream> для ввода и вывода, а также с <iomanip>, поскольку используются манипуляторы потока для установки ширины полей при выводе.
Константа МАХ определяет количество простых чисел, которые нужно получить от программы. Массив primes, в котором сохраняются результаты, инициализирован первыми тремя простыми числами, чтобы было с чего запустить процесс. Вся работа выполняется в двух циклах: внешнем do-while, который указывает следующее проверяемое значение и добавляет найденное значение в массив primes, если оно является простым числом, и внутренним циклом for, который в действительности проверяет значение на принадлежность к множеству простых чисел.
Алгоритм цикла for очень прост и основан на том факте, что если число не простое, то оно должно делиться на одно из ранее найденных простых — каждое из которых меньше проверяемого, поскольку все числа являются либо простыми, либо произведениями простых. Фактически, нужно проверить только деление на простые числа, меньшие или равные корню квадратному из проверяемого числа, поэтому данный пример еще не настолько эффективен, насколько он мог бы быть.
found = (trial % * (primes + i) ) == 0;// Истинно при делении без остатка
Этот оператор присваивает переменной found значение 1, если нет остатка от деления значения trial на текущее простое число * (primes + 1) (напомним, что это эквивалентно 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;
Это начинает новую строку, когда 1 получает значения 0, 5, 10 и так далее.
Практическое занятие | Подсчет символов'
Чтобы увидеть, как обрабатываются строки в нотации указателей, можно написать версию программы, рассмотренной ранее, которая предназначалась для подсчета символов в строке:
// Ех4_10.срр
// Подсчет символов строки с использованием указателя
#include <iostream>
using std::cin;
using std::cout;
using std::endl;
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
« "Строка V" « buffer
« "\" содержит " « pbuffer - buffer « " символов.";
cout « endl;
return 0;
}
Вот пример типичного вывода этого примера:
Введите строку не длиннее 80 символов:
Ничего на свете лучше нету, чем бродить друзьям по белу свету.
Строка "Ничего на свете лучше нету, чем бродить друзьям по белу свету, содержит 62 символ (-а, -ов) .
Описание полученных результатов
Здесь программа работает с указателем pbuffer, а не с именем массива buffer. Не нужна переменная count, поскольку указатель увеличивается в цикле while, пока не найдет \0. Когда обнаруживается символ \0, pbuffer содержит адрес его положения в строке. Счетчик количества символов строки, таким образом, вычисляется, как разница между адресом, записанным в pbuffer, и адресом начала массива, который обозначен buffer.
Вы могли бы также выполнять инкремент указателя в цикле следующим образом:
while(*pbuffer++); // Считать до \0
Этот цикл не содержит в себе никаких операторов, а только проверочное условие. Это должно работать адекватно, за исключением того факта, что значение счетчика увеличивается после достижения \0, поэтому адрес будет на единицу больше, чем последняя позиция в строке. Поэтому в данном случае количество символов в строке должно быть вычислено как pbuffer — buffer — 1.
Обратите внимание, что здесь вы не можете применять имя массива так же, как используете указатель. Выражение buf fer++ совершенно неправильно, поскольку нельзя модифицировать значение адреса, которое представляет имя массива. Даже несмотря на то, что вы можете использовать имя массива в выражениях вместо указателя, все же это — не указатель, поскольку адрес, который оно представляет, фиксирован.
Применение указателей с многомерными массивами
Применение указателя для хранения адреса одномерного массива относительно просто, но когда речь идет о многомерных массивах, все несколько усложняется. Если вы не собираетесь делать это, можете пропустить настоящий раздел, так как он немного запутан; однако если у вас есть предварительный опыт использования языка С, на него стоит взглянуть.
Если вам нужно применять указатель с многомерными массивами, то нужно ясно представлять, что будет происходить. В качестве иллюстрации можете взять массив, объявленный следующим образом:
double beans[3][4];
Вы можете объявить и присвоить значение указателю pbeans так:
double* pbeans;
pbeans = &beans[0][0];
Здесь вы устанавливаете указатель на адрес первого элемента массива, который имеет тип double. Вы также можете установить указатель на адрес первой строки массива с помощью следующего оператора:
pbeans = beans[0];
Это эквивалентно применению имени одномерного массива, который заменяется его адресом. Мы использовали это в предыдущих дискуссиях; однако поскольку 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. Таким образом, полное выражение ссылается на конкретный элемент массива.
Если вы действительно хотите все запутать (хотя это не рекомендуется), то следующие два оператора, в которых смешивается нотация массивов с нотацией указателей, также легально ссылаются на тот же элемент массива:
*(beans[i] + j )
(*(beans + i) ) [j]
Существует еще один аспект применения указателей, который на самом деле важнее всех остальных: возможность динамического выделения памяти для переменных. Мы рассмотрим это в следующем разделе.
Динамическое выделение памяти
Работа с фиксированным набором переменных в программе может серьезно стеснять разработчика. Часто в приложениях возникает необходимость в оперативном принятии решений относительно динамического выделения места для размещения переменных различных типов непосредственно во время выполнения — в зависимости от входных данных, полученных программой. Для одного набора данных может быть разумно применять большой массив целых чисел, в то время как другой набор входных данных может потребовать большого массива чисел с плавающей точкой. Понятно, что поскольку динамически распределенные переменные не могут быть определены во время компиляции, они не имеют имен в исходном тексте программы. Когда они создаются, то идентифицируются адресами в памяти, которые сохраняются в указателях. Благодаря мощности указателей и средствам динамического управления памятью в Visual C++ 2005, написание программ, обладающих такого рода гибкостью, выполняется легко и быстро.
Свободное хранилище, псевдоним “куча”
Во многих случаях, когда выполняется ваша программа, у компьютера есть неиспользуемая память. Эта неиспользуемая память в C++ называется кучей или, иногда, свободным хранилищем. Вы можете выделить память внутри этого хранилища для новой переменной заданного типа с помощью специальной операции C++, которая возвращает адрес выделенного пространства. Этой операцией является new, и ее дополняет операция delete, которая освобождает память, ранее выделенную new.
Вы можете выделить память в свободной хранилище для некоторой переменной в одной части программы, а затем освободить выделенное пространство и вернуть его в свободное хранилище после того, как необходимость в этой переменной отпадет. Это позволяет позже в той же программе повторно задействовать эту память для других динамически распределяемых переменных.
Память из свободного хранилища используется всякий раз, когда возникает потребность в выделении памяти для элементов, которые могут быть определены лишь во время выполнения. Одним из примеров может служить выделение памяти для строк, вводимых пользователем вашего приложения. Нет способа заранее знать, насколько большими должны быть эти строки, поэтому имеет смысл выделять память для них во время выполнения, используя для этого операцию new. Позднее вы ознакомитесь с примером применения свободного хранилища для динамического распределения памяти массива, где измерения массива задаются пользователем во время работы программы.
Это может быть очень мощная техника, которая позволяет расходовать память очень эффективно, и во многих случаях разрабатывать программы, способные решать намного более масштабные проблемы, обрабатывая большие объемы данных, чем это возможно без ее применения.
Операции new и delete
Предположим, что вам необходимо пространство для переменной типа double. Вы можете определить указатель на тип double и затем запрашивать память во время выполнения. Вы можете делать это, используя операцию new в следующих операторах:
double* pvalue = NULL; // Указатель, инициализированный null pvalue = new double; H Запрос памяти для переменной 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 символов
Это выделяет память для символьного массива размером в 20 символов и сохраняет указатель на него в pstr.
Чтобы освободить распределенный таким образом массив и вернуть память в свободное хранилище, вы должны применить операцию delete. Оператор должен выглядеть так:
delete [] pstr; // Удалить массив, на который указывает pstr
Обратите внимание на квадратные скобки, свидетельствующие о том, что то, что вы удаляете — массив. Когда освобождается массив, распределенный в свободном хранилище, всегда нужно указывать квадратные скобки, иначе результат будет непредсказуемым. Обратите внимание, что при этом не нужно указывать размер массива — достаточно просто скобок [ ].
Конечно, указатель pstr после этого будет содержать адрес памяти, которая может быть уже распределена для каких-то других целей, поэтому, конечно же, он не должен никак использоваться. Поэтому когда вы применяете операцию delete для того, чтобы освободить некоторую память, которая была распределена ранее, то всегда должны сбрасывать значение указателя в 0, как показано ниже:
Практическое занятиеПрактическое занятиеpstr = 0; // Установить указатель в null
Практическое занятие
Практическое занятие
Использование свободного хранилища
Чтобы увидеть на практике, как работает динамическое выделение памяти, вы можете переписать программу вычисления простых чисел, используя память из свободного хранилища для их размещения.
// Ех4_11.срр
II Вычисление простых чисел с применением динамического распределения памяти #include <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;II Счетчик найденных простых чисел
int found = 0;// Признак обнаружения простого числа
int max = 0;/ / Количество требуемых простых чисел
cout « endl
« "Введите количество простых чисел, которые хотите получить (минимум 4) : ";
cin >> max;cin >> max;// Число необходимых простых чисел
cin >> max;
cin >> max;
if(max < 4) // Проверка ввода пользователя, чтобы значение было не меньше 4 max = 4; // Принудительное указание количества, если указано меньше
pprime = new long[max];
// Вставить три начальных // простых числа// Вставить три начальных // простых числа*pprime = 2;
// Вставить три начальных // простых числа
// Вставить три начальных // простых числа
* (pprime + 1) = 3;
* (pprime + 2) =5;
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
Описание полученных результатов
Фактически эта программа очень похожа на предыдущую версию. После получения необходимого количества простых чисел в i nt-переменной max, вы распределяете массив этого размера в свободном хранилище, используя операцию new. Обратите внимание, что программа гарантирует, что max будет не меньше 4. Это потому, что ей необходимо распределить пространство в свободном хранилище, как минимум для трех начальных простых чисел, плюс еще одно. Вы специфицируете необходимый размер массива, помещая переменную max между квадратными скобками, которые следуют за спецификацией типа массива:
pprime = new long[max];
Вы помещаете адрес области памяти, выделенной операцией new в указатель pprime. Если память не удастся выделить, то в этой точке программа должна быть прервана по исключению.
После того, как память для хранения значений простых чисел успешно выделена, первым трем элементам массива присваиваются значения первых трех простых чисел:
*pprime = 2;// Вставить три начальных
(pprime + 1) = 3;// простых числа
(pprime + 2) =5;
Вы используете операцию разыменования для доступа к первым трем элементам массива. Как было показано ранее, скобки во втором и третьем операторе необходимы потому, что приоритет операции * выше, чем операции +.
Вы не можете специфицировать начальные значения элементов массива, который распределен динамически. Если нужно установить начальные значения элементов такого массива, это следует сделать явным присваиванием.
Вычисление простых чисел выполняется точно так же, как и раньше; единственное отличие в том, что вместо имени массива primes, использованного в предыдущей версии, подставляется указатель pprime. Процесс вывода — тот же. Динамическое получение пространства памяти — вообще не проблема. После того, как память выделена, она никак не влияет на методику вычислений.
По завершении работы с массивом он освобождается операцией delete. Не забудьте о квадратных скобках, чтобы указать, что удаляется массив.
delete [] 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 можно указывать переменную. Это касается двух и более измерений, но с тем ограничением, что переменной можно задавать лишь размер самого левого измерения.
Все прочие измерения должны быть заданы константами или константными выражениями. Поэтому вы можете записать следующим образом:
pBigArray = new double [max] [10] [10] ;
где max — переменная; однако спецификация переменной для всех других измерений массива кроме крайнего левого, вызовет сообщение компилятора об ошибке.
Использование ссылок
Ссылка (reference) во многих отношениях подобна указателю — вот почему я представляю ее здесь, хотя на самом деле это совсем другая вещь. Истинная ценность ссылок становится ясной, только когда рассматривается их применение с функциями, в частности в контексте объектно-ориентированного программирования. Не позволяйте ввести себя в заблуждение их кажущейся простотой и тривиальностью концепции. Как вы увидите позже, ссылки являются чрезвычайно мощным средством, и в некоторых контекстах позволяют достичь таких результатов, которые без них вообще были бы невозможными.
Что такое ссылка?
Ссылка — это псевдоним для другой переменной. Она имеет имя, которое может использоваться вместо исходного имени переменной. Поскольку это псевдоним, а не указатель, переменная, для которой она определена, должна быть указана, когда ссылка объявляется, и в отличие от указателя, ссылка не может быть изменена, чтобы представлять другую переменную.
Объявление и инициализация ссылок
Предположим, вы объявили переменную следующим образом:
long number = 0;
После этого вы можете объявить ссылку на эту переменную с помощью такого оператора объявления:
long& rnumber = number; // Объявление ссылки на переменную number
Знак амперсанд, следующий за именем типа long и предшествующий имени переменной rnumber, говорит о том, что это объявление ссылки, а имя переменной, которую она представляет — number — специфицировано как инициализирующее значение, следующее за знаком равенства; таким образом, переменная rnumber имеет тип “ссылка на long”. После этого вы можете использовать ссылку вместо имени исходной переменной. Например, оператор:
rnumber += 10;
в качестве эффекта дает увеличение значения переменной number на 10.
Сравните ссылку rnumber с указателем pnumber, объявленным в операторе:
long* pnumber = &number; // Инициализация указателя адресом
Здесь объявляется указатель pnumber, который инициализируется адресом переменной number. Это позволяет увеличивать значение number оператором вроде:
*pnumber += 10;*pnumber += 10;// Инкремент number через указатель
*pnumber += 10;
*pnumber += 10;
Есть существенная разница между использованием указателя и использованием ссылки. Указатель должен быть разыменован, и адрес, который он содержит, служит для доступа к переменной, участвующей в выражении. В случае ссылки нет необходимости в разыменовании. В некотором смысле ссылка подобна указателю, который уже разыменован, хотя его и нельзя заставить ссылаться на другую переменную. Ссылка полностью эквивалентна переменной, на которую она ссылается. Ссылка может показаться просто альтернативной нотацией для данной переменной, и здесь она определенно ведет себя именно таким образом. Однако когда я буду говорить о функциях 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++, однако есть и существенные отличия. Дескриптор хранит адрес, и адрес, который в нем содержится, автоматически обновляется сборщиком мусора, если объ-
ект, на который он ссылается, перемещается во время сжатия кучи. Однако вы не можете применять арифметику адресов к таким указателям, как вы делаете это с “родными” указателями, кроме того, не разрешается приведение дескрипторов.
Все объекты, созданные в куче CLR, должны снабжаться дескрипторами. Все объекты ссылочных типов классов сохраняются в куче и потому созданные вами переменные, которые ссылаются на такие объекты, должны быть дескрипторами. Например, тип класса String — это ссылочный тип класса, поэтому переменные, которые ссылаются на объекты String, должны быть отслеживаемыми дескрипторами. Память для типов классов значений по умолчанию выделяется в стеке, но вы можете также разместить их в куче, используя для этого операцию gcnew. Стоит также напомнить то, о чем говорилось в главе 2: переменные, размещенные в куче CLR, включая ссылочные типы CLR, не могут быть объявлены в глобальном контексте.
Объявление отслеживаемых дескрипторов
Вы специфицируете дескриптор для типа, помещая символ А (часто называемый “шляпой”) следом за именем типа. Например, вот как можно объявить отслеживаемый дескриптор по имени proverb, который может хранить адрес объекта String:
StringA proverb;
Это определяет переменную proverb как отслеживаемый дескриптор типа StringA. Когда вы объявляете дескриптор, он автоматически инициализируется нулем, поэтому ни на что не ссылается. Для явной установки дескриптора в ноль служит ключевое слово nullpt г:
proverb = nullptr; // Установить дескриптор в null
Обратите внимание, что вы не можете здесь использовать 0, как это можно делать с родными указателями. Если вы инициализируете дескриптор нулем, то значение О преобразуется в тип объекта, на который будет ссылаться дескриптор, и в него будет помещен адрес этого нового объекта.
Конечно, можно явно инициализировать дескриптор при его объявлении. Вот другой оператор, определяющий дескриптор объекта String:
String^ saying = Ъ”Некоторая длинная строка”;
Этот оператор создает в куче объект String, который содержит строку справа от операции присваивания; адрес нового объекта помещается в saying. Типом строкового литерала здесь является wchar_t*, а не String. Определение класса String дает возможность такому литералу быть использованным для создания объекта типа String.
А вот как создается дескриптор для типа значения:
intA value = 99;
Этот оператор создает дескриптор value типа intА, и значение в куче, на которое он указывает, инициализируется числом 99. Помните, что вы создаете разновидность указателя, поэтому value не может участвовать в арифметических операциях без его разыменования. Для разыменования отслеживаемого дескриптора служит операция * — точно так же, как это делается с указателями. Например, ниже представлен оператор, использующий значение, указанное отслеживаемым дескриптором в арифметической операции:
int result = 2*(*value)+15;
Выражение *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>A data;
Переменная массива data может хранить ссылку на одномерный массив элементов типа int.
Вы можете создать массив CLR, используя операцию gcnew, одновременно с объявлением переменной массива:
array<int>A data = gcnew array<int>(100);// Создать массив для
// хранения 100 целых чисел
Этот оператор создает одномерный массив по имени 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 ссылается на каждый элемент массива по очереди. Первый оператор в теле цикла заменяет значение текущего элемента удвоенным старым его значением плюс единица. Второй оператор цикла выводит новое значение, выравнивая его вправо в поле шириной пять символов, поэтому в результате получается следующий вывод:
711131713
Переменная массива может хранить адрес любого массива того же ранга (ранг — это количество измерений, которое в случае массива 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};
Размер массива определяется количеством инициирующих значений, заключенных в фигурные скобки, в данном случае — 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();
// Вывести содержимое 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();
}
fl Найти максимальное значение
double max = 0;
for each (double sample in samples) if(max < sample)
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>A samples = gcnew array<double> (50);
Переменная массива samples должна быть отслеживаемым дескриптором, потому что массивы CLR создаются в управляемой сборщиком мусора куче.
Вы наполняете массив псевдослучайными значениями типа double в следующих операторах:
RandomA generator = gcnew Random;
for(int i = 0 ; i< samples->Length ; i++) samples[i] = 100.0*generator->NextDouble ();
Первый оператор создает объект типа Random в куче CLR. Объект Random включает функции, генерирующие псевдослучайные значения. Здесь вы в цикле используете функцию NextDouble (), которая возвращает случайное значение типа double, лежащее в пределах от 0,0 до 1,0. Умножая это значение на 100,0, вы получаете значение между 0,0 и 100,0. Цикл for сохраняет случайное значение в каждом элементе массива samples.
Объект Random также имеет функцию Next (), которая возвращает случайное неотрицательное значение типа int. Если передать целочисленный аргумент при вызове функции Next О, то она вернет случайное неотрицательное значение, меньшее значения этого аргумента. Вы можете также передать два целочисленных аргумента, представляющих минимальное и максимальное значения, в пределах которых должно находиться случайное число.
Следующий цикл выводит содержимое массива, по пять элементов в строке:
Console::WriteLine(Ь"Массив содержит следующие значения:");
for(int 1=0; К samples->Length ; i++) {
Console::Write(L"{0,10: F2} ", samples[i]);
if((i+l)%5 == 0)
Console::WriteLine();
1
Внутри цикла значение каждого элемента выводится в поле шириной 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>A 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();
Вызов функции Sort () переупорядочивает значения элементов массива samples по возрастанию. Результат выполнения этого фрагмента будет таким:
231116182754
Можно также сортировать диапазон элементов массива, передав в двух аргументах функции Sort () индекс первого элемента и количество элементов, подлежащих сортировке. Например:
array<int>A samples = { 27, 3, 54, 11, 18, 2, 16};
Array::Sort(samples, 2, 3) ; II Сортировать элементы с 2 по 4
Этот оператор сортирует три элемента массива samples, начиная с позиции 2. После выполнения этих операторов элементы массива будут иметь следующие значения:
273111854216
Практическое занятиеПрактическое занятиеСуществует также несколько других версий функции 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) ;II Сортировать массивы
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;
}
Вывод этой программы:
AlBillEveJillMaryTed
176180115103128168
Press any key to continue . . .
Описание полученных результатов
Значения массива weights соответствуют весу персон в тех же позициях индекса, что и их имена в массиве names. Вызываемая здесь функция Sort () сортирует оба массива, используя первый из них (names) в качестве ключа сортировки, определяющего порядок обоих массивов. Вы можете видеть по выходной информации, что после сортировки имени каждого лица соответствует его правильный вес во втором массиве.
Поиск в одномерном массиве
В классе Array также предусмотрены функции поиска элементов в одномерном массиве. Версии функции BinarySearch () используют алгоритм бинарного поиска для нахождения позиции индекса заданного элемента во всем массиве либо в указанном диапазоне его элементов. Алгоритм бинарного поиска требует, чтобы элементы были упорядочены, поэтому перед выполнением поиска в массиве их следует отсортировать.
Вот как можно выполнять поиск во всем массиве:
array<int>A 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 () с четырьмя аргументами. Первый аргумент — дескриптор массива, в котором выполняется поиск, второй — позиция индекса элемента, с которого нужно начинать поиск, третий аргумент — количество элементов, среди которых нужно искать, а четвертый — собственно, искомое значение. Вот как это можно использовать:
array<int>A values = { 23, 45, 68, 94, 123, 127, 150, 203, 299};
int toBeFound = 127;
int position = Array::BinarySearch(values, 3, 6, toBeFound);
Здесь выполняется поиск в массиве, начиная с его четвертого элемента и до последнего. Как и предыдущая версия BinarySearch (), эта возвращает позицию индекса найденного элемента или отрицательное значение, если поиск не удался.
Рассмотрим пример.
Практическое занятие | ПоиС|< в маССИве
Это — вариант предыдущего примера с добавлением операции поиска.
// Ех4_14.срр : главный файл проекта.
II Поиск в массиве
#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,
76, 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);
Этот оператор создает двумерный массив из четырех строк и пяти столбцов, так что всего он вмещает 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() ; // Вывести новую строку
/ / Вывести верхнюю строку таблицы 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 ; KSIZE ; 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() ; // Вывести новую строку }
II Вывести горизонтальную разделительную линию
for(int i = 0 ; i <= SIZE ; i++) Console: :Write (L"”) ;
Console: :WriteLine() ; // Вывести новую строку return 0;
Этот пример должен сгенерировать следующий вывод:
Таблица умножения размером 12:
1 1 1
11
2 I
1
3
1 4 |
11
5 |
1
6
1 7 |
11
8
1 9
1
1 ю
1
1 11
1
1 12
1
1
1 1 1
2 |
3
1 4 |
5 |
6
1 7 |
8
1 9
1 ю
1 11
1 12
2
1 2 |
4 1
6
1 8 |
Ю |
12
1 14 |
16
1 18
1 20
1 22
I 24
3
1 3 |
6 1
9
1 12 |
15 |
18
1 21 |
24
I 27
| 30
1 33
I 36
4
1 4 |
8 1
12
1 16 |
20 |
24
1 28 |
32
1 36
| 40
| 44
| 48
5
1 5 |
10 |
15
1 20 |
25 !
30
1 35 |
40
1 45
I 50
I 55
1 60
6
1 6 |
12 |
18
1 24 |
30 |
36
1 42 |
48
1 54
I 60
I 66
| 72
7
1 7 |
14 |
21
1 28 |
35 |
42
1 49 |
56
1 63
I 70
| 77
| 84
8
1 8 |
16 |
24
1 32 |
40 |
48
1 56 |
64
I 72
I 80
1 88
I 96
9
1 9 |
18 |
27
1 36 |
45 |
54
1 63 |
72
1 81
| 90
I 99
1108
10
1 Ю |
20 |
30
1 40 |
50 |
60
1 70 |
80
| 90
ЦОО
I 110
|120
11
1 11 1
22 |
33
1 44 |
55 |
66
1 77 |
88
I 99
1110
I 121
1132
12
1 12 |
24 |
36
1 48 |
60 |
72
1 84 |
96
I 108
Ц20
1132
Ц44
Press any key to continue . . .
Описание полученных результатов
Код выглядит довольно объемным, но большая его часть обеспечивает красивый вывод. С помощью показанных ниже операторов создается двумерный массив.
const int SIZE = 12;
array<int, 2>A products = gcnew array<int, 2>(SIZE,SIZE);
Первая строка определяет константное целочисленное значение, которое специфицирует количество элементов в каждом измерении массива. Вторая строка определяет массив с рангом 2, состоящий из 12 строк и 12 столбцов. В нем размещаются значения произведений таблицы умножения размером 12x12.
Значения элементов массива формируются во вложенном цикле:
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");
for (int i = 1 ; i <= SIZE ; i++)
Console::Write(L"{0,3} |", i) ;
Console::WriteLine ();// Вывести новую строку
В начале перед выводом заголовков столбцов отдельно выводится группа пробелов, потому что это — специальный случай без выходного значения; эти пробелы находятся над начальным столбцом заголовков строк. Каждая метка столбца выводится в цикле, затем печатается символ новой строки, подготавливая следующую строку вывода.
Строки значений выводятся с помощью вложенного цикла:
// Вывести остальные строки
for (int i = 0 ; KSIZE ; 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-й строке, разделяя их вертикальными линиями.
Оставшийся код выводит нижнюю линию таблицы.
Массив массивов
Элементы массива могут быть любого типа, поэтому вы можете создавать массивы, элементы которых представляют собой отслеживаемые дескрипторы, ссылающиеся на массивы. Это дает возможность создавать так называемые зубчатые массивы (jagged arrays), поскольку элементы-дескрипторы могут ссылаться на массивы с разным количеством элементов. Проще всего это понять, взглянув на пример. Предположим,
что требуется сохранить имена детей в классе в соответствии с уровнем их успеваемости, причем существует пять уровней классификации — А, В, С, D и Е. Сначала вы создаете массив из пяти элементов, где каждый элемент сохраняет массив имен. Вот оператор, которое это делает:
array< array< StringA >А >А 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< StringA >А >А grades = gcnew array< array< StringA >A >
gcnew
array<StringA:
>{"Louise", "Jack"},
H
Уровень
А
gcnew
array<StringA:
>{"Bill", "Mary", "Ben", "Joan"},
U
Уровень
В
gcnew
array<StringA:
>{"Jill", "Will", "Phil"},
H
Уровень
С
gcnew
array<StringA:
>{"Ned", "Fred", "Ted", "Jed", "Ed"},
П
Уровень
D
gcnew
array<StringA:
>{"Dan", "Ann"}
П
Уровень
Е
};
Начальные значения элементов заключены в фигурные скобки.
А теперь давайте соберем все это в работающий пример, который продемонстрирует обработку массива массивов.
практическое занятие Использование массива массивов
Создайте проект консольной программы CLR и модифицируйте ее следующим образом:
И Ех4_16.срр : main project file.
/ / Использование массива массивов
#include "stdafx.h"
using namespace System;
int main(array<System::String A> Aargs) {
array< array< String* >* >* grades = gcnew array< array< String* >* > (
gcnew array<String*>{"Louise", "Jack"},// Уровень A
gcnew array<String*>{ "Bill", "Mary", "Ben", "Joan"},// Уровень В
gcnew array<String*>{"Jill", "Will", "Phil"},// Уровень C
gcnew array<String*>{ "Ned", "Fred", "Ted", "Jed", "Ed”},// Уровень D gcnew array<String*>{"Dan", "Ann"}// Уровень E
};
wchar^t gradeLetter = 'A';
for each(array< String* >* grade in grades) {
Console: :WriteLine ("Студенты с уровнем успеваемости {0}: " , gradeLetter++);
for each( String* student in grade) Console: :Wri te("{0,12}", student) ; // Вывести текущее имя Console: :WriteLine() ; // Новая строка
) return 0;
}
Этот пример сгенерирует следующий вывод:
Студенты с уровнем успеваемости А:
LouiseJack
Студенты с уровнем успеваемости В:
BillMaryBen Joan
Студенты с уровнем успеваемости С: JillWillPhil
Студенты с уровнем успеваемости D: NedFredTedJedEd
Студенты с уровнем успеваемости Е: DanAnn
Press any key to continue . . .
Описание полученных результатов
Определение массива написано точно так, как было предложено в предыдущем разделе. После него определяется переменная gradeLetter типа wchar_t с начальным значением ’ А'. Она используется для представления классификации уровня успеваемости при выводе.
Студенты и их уровни перечисляются вложенными циклами. Внешний цикл проходит по элементам массива grades:
for each(array< String* >* grade in grades) {
H Обработать студентов текущего уровня... }
Переменная цикла grade имеет тип array< String* >*, поскольку такой тип у элементов массива grades. Переменная grade ссылается на каждый массив дескрипторов String* по очереди, поэтому на первой итерации цикла она ссылается на массив имен студентов уровня А, на второй — массив студентов уровня В и так далее — до тех пор, пока не дойдет до массива имен студентов уровня Е.
При каждой итерации внешнего цикла выполняется следующий код:
Console::WriteLine("Студенты с уровнем успеваемости {0}:", gradeLetter++);
for each ( StringA student in grade)
Console::Write("{0,12}",student);// Вывести текущее имя
Console::WriteLine();// Новая строка
Первый оператор выводит строку, которая включает текущее значение gradeLetter, которое начинается с ’А’. Оператор также выполняет инкремент этой переменной, так что она последовательно получает значения ’ В ’, ’ С ’, ’ D ’ и ’ Е ’ на каждой итерации внешнего цикла.
Далее следует внутренний цикл for each, который перебирает все имена из текущего массива grade по очереди. Оператор вывода вызывает функцию Console: : Write (), так что все имена текущего grade выводятся в одной строке. Имена представлены в выходном поле шириной 12 символов, выровненные вправо. После завершения внутреннего цикла WriteLineO просто выводит символ новой строки, потому имена студентов следующего уровня начинаются с новой строки.
Внутренний цикл также можно было бы записать так:
for (int i = 0 ; i < grade->Length ; i++)
Console: :Write("{0,12}" ,grade[i]) ; // Вывести текущее имя
Цикл ограничен значением свойства 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]->Leng th ; i++)
Console: : Write (" {0,12)", grades [j] [i]) ; // Вывести текущее имя Console: :WriteLineO ;
}
Теперь grades [ j ] ссылается на j-й массив имен, так что выражение grades [ j ] [ i] ссылается на i-e имя из j-ro массива имен.
Строки
Вы уже видели, что тип класса String, определенный в пространстве имен System, представляет строку в C++/CLI (фактически строки состоят их символов Unicode). Выражаясь точнее, он представляет строку, состоящую из последовательности символов типа System: :Char. Объекты класса String предоставляют в ваше распоряжение громадный объем мощной функциональности, что значительно облегчает обработку строк. Начнем с начала — с создания строки.
Объект String можно создать следующим образом:
System::StringA saying = L"MHoro рук выполняют мало работы.";
Переменная saying — отслеживаемый дескриптор, который ссылается на объект String, инициализированный строкой, находящейся справа от знака =. Вы всегда должны использовать отслеживаемые дескрипторы для сохранения ссылок на объекты String. Представленный здесь строковый литерал является строкой, состоящей из широких символов, поскольку снабжен префиксом L. Если вы пропустите префикс L, то получите строковый литерал, состоящий из 8-битовых символов, а так компилятор гарантирует, что он будет преобразован в строку широких символов.
Вы можете обращаться к индивидуальным символам в строке, используя нотацию массивов, и первый символ строки имеет индекс 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* патеЗ = namel + L" и " + name2;
После выполнения этих операторов патеЗ содержит строку "Пат и Паташон”. Обратите внимание на использование операции + для объединения объектов String со строковыми литералами. Также вы можете объединять объекты String с числовыми значениями, или значениями bool, при этом получая автоматическое их преобразование в строковый вид. Следующие операторы иллюстрируют сказанное.
String* str = Ь"3начение: ";
String*strl=str+2.5;//Врезультате— новаястрока:"Значение:2.5"
String*str2=str+ 25;// Врезультате— новаястрока:"Значение:25"
String*str3=str+ true;//Врезультате— новаястрока:"Значение:True"
Можно также соединять String с символом, но результат при этом зависит от типа символа:
char ch = 1 Z ’ ;
wchar_t wch = ’ Z1
String* str4 = str + ch;// В результате — новая строка "Значение: 90"
String* str5 = str + wch; // В результате — новая строка "Значение: Z"
В комментариях отражены результаты этих операций. Символ типа char трактуется как числовое значение, так что вы получаете код символа, присоединенный к строке. Символ wchar_t имеет тот же тип, что и символы в объекте String (тип Char), поэтому символ добавляется к строке.
Не забывайте, что объекты String не изменяемы; будучи однажды созданы, изменяться они не могут. Это значит, что все операции, которые предположительно изменяют объекты String, всегда в результате порождают новые объекты String.
В классе String также определена функция Join (), которую вы можете использовать, когда хотите объединить серию строк, сохраненных в массиве, в одну общую
строку с разделителями. Вот как можно объединить вместе имена, разделив их запятыми:
array<StringA>A names = { "Джил", "Тед", "Мери", "Ева", "Билл" };
StringA separator = ", " ;
StringA 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 formatStr 1 = "{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 = formatStr 1 * (maxLength+1) 4- formatStr2;
/ / Вывод значений
int numberPerLine = 3;
for (int i = 0 ; i< values->Length ; i++) {
Console::Write(format, values[i]);
if ((i-H) %numberPerLine — 0) Console: :WriteLine() ;
}
return 0;
}
Вывод этой программы выглядит следующим образом:
2
456
23
-46
34211
456
5609
112098
234
-76504
341
6788
-909121
99
10
Press any
key to continue
Описание полученных результатов
Назначение этой программы — создать форматную строку для выравнивания вывода целых чисел из массива в столбцах, имеющих ширину, достаточную для размещения строки максимальной длины, которая представляет целые числа. Изначально форматная строка создается из двух частей:
StringA formatStrl = "{О,"; // 1-я половина форматной строки
StringA formatStr2 = "}";// 2-я половина форматной строки
Эти две строки представляют собой начальную и конечную части требуемой форматной строки. Вам необходимо найти длину максимальной числовой строки, вложить ее между formatStrl и formatStr2, чтобы сформировать полную строку формата.
Эта длина находится с помощью следующего кода:
int maxLength = 0;Н Максимальная найденная длина
for each(int value in values) {
number = nn + value; // Создать строку из значения if(maxLength<number->Length)
maxLength = number->Length;
Внутри цикла каждое число из массива преобразуется в его String-представление посредством объединения его с пустой строкой. Свойство Length каждой строки сравнивается с maxLength и большее из них снова помещается в maxLength, формируя подобным образом максимальную длину.
Создание форматной строки очень просто:
StringA 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, вывод выполняется в столбцы, шириной на единицу больше, чем максимальная длина выводимого значе-
ния. Переменная 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(L1w1, L’o’, L'l*, L* 1));
Это даст тот же вывод, что и предыдущая версия оператора. Вы можете передавать столько аргументов типа wchar t, сколько хотите, но если символов слишком много, то массив будет более удачным решением.
Если вы хотите усечь только один конец строки, то для этого можете использовать функции TrimEnd () или TrimStart (). Они имеют такие же версии, что и функция Trim (), поэтому без аргументов усекают пробелы, с аргументом-массивом — отсекают символы, содержащиеся в массиве, а с явными аргументами wchar t удаляет переданные в аргументах символы.
Противоположность усечению строки — ее дополнение на любом конце пробелами или другими символами. Для выполнения этой операции предусмотрены функции PadLeft () и PadRight (), которые дополняют строку слева и справа соответственно. Главное назначение этих функций — форматирование вывода, когда нужно вы-
вести строки, выравнивая их вправо или влево в пределах фиксированной ширины. Простейшие версии функций PadLeft () и PadRight () принимают один аргумент, указывающий длину строки, которая должна получиться в результате этой операции. Например:
StringA value = L"3.142";
StringA leftPadded = value->PadLeft (10);H Результат: "3.142"
StringA rightPadded = value->PadRight(10);// Результат: "3.142"
Если длина, переданная в аргументе, меньше или равна длине исходной строки, обе функции возвращают новый объект String, идентичный оригиналу.
Чтобы дополнить строку символом, отличным от пробела, можно специфицировать дополняющий символ в качестве второго аргумента функций PadLeft () и PadRight (). Ниже показана пара примеров.
StringA value - L”3.142";
StringA leftPadded = value->PadLeft (10, L’*’);// Результат: "*****3.142"
StringA rightPadded = value->PadRight(10, L’#’); H Результат: "3.142#####"
Конечно, во всех этих случаях можно поместить результирующую строку обратно в дескриптор, ссылающийся на исходную строку, что приведет к потере исходной версии строки.
Класс String также включает в себя функции ToUpper () и ToLower (), предназначенные для преобразования всей строки в верхний и нижний регистр соответственно. Вот как это работает:
StringA proverb = Ь"Много рук выполняют мало работы.";
StringA upper = proverb->ToUpper();//Результат: "МНОГО РУК ВЫПОЛНЯЮТ МАЛО РАБОТЫ."
Функция ToUpper () возвращает новую строку, которая повторяет исходную, но преобразует все ее символы в заглавные.
Функция Insert () служит для вставки строки в заданную позицию существующей строки. Вот пример этой операции:
StringA proverb = Б"Много рук выполняют мало работы.";
StringA newProverb = proverb->Insert (6, 1"кривых ");
Функция вставляет строку, указанную во втором аргументе, в исходную, начиная с позиции, переданной первым аргументом. В результате получается новая строка следующего содержания:
Много кривых рук выполняют мало работы.
Вы можете также заменить все вхождения заданного символа или подстроки в строке на другой символ или подстроку. Следующий фрагмент кода иллюстрирует эту возможность.
StringA proverb = 1"Много рук выполняют мало работы.";
Console::WriteLine(proverb~>Replace(L1 1, L** 1);
Console::WriteLine(proverb~>Replace(Ь"Много рук", Ъ”Нажатия кнопок");
Выполнение этого фрагмента даст следующий вывод:
Много*рук* выполняют *мало*работы.
Нажатия кнопок выполняют мало работы.
Первый аргумент функции Replace () специфицирует символ или подстроку, которая должна быть заменена, а второй аргумент описывает замену.
Поиск строк
Возможно, простейшая операция поиска — это проверка того, начинается ли либо заканчивается заданная строка указанной подстрокой. Это делают функции StartsWith () и EndsWith (). Обеим функциям передается дескриптор подстроки, которую нужно найти, и они возвращают булевское значение, указывающее на то, присутствует подстрока в исходной строке или нет. Вот фрагмент, демонстрирующий применение функции StartsWith ():
StringA sentence = Ь"3акон суров, но это закон.";
if(sentence->StartsWith(Ь"3акон"))
Console::WriteLine("Фраза начинается с 'Закон'.");
Выполнение этого фрагмента даст такой вывод:
Фраза начинается с 'Закон'.
Конечно, вы можете также применить функцию EndsWith () к строке sentence:
Console::WriteLine("Фраза {0}заканчивается словом 'закон'.",
. sentence->EndsWith(1"закон") ? L"" : Ь"не ");
Результат выражения с условной операцией вставляется в выходную строку. Это будет пустая строка, если EndsWith () вернет true, и "не ", если она вернет false. В данном случае функция возвращает false (из-за точки в конце исходной строки).
Функция IndexOf () ищет в строке первое вхождение указанного символа или подстроки и возвращает индекс вхождения, если оно есть, либо -1 в противном случае. Искомый символ или подстрока передается в аргументе функции. Например:
StringA sentence = Ь"3акон суров, но это закон.";
int ePosition = sentence->IndexOf(L'о');// Возвращает 3
int thePosition = sentence->IndexOf(L"ho");// Возвращает 13
В первом случае ищется буква “о”, а во втором — слово “но”. Возвращаемые значения IndexOf () указаны в комментариях.
1ораздо чаще вам понадобится находить все вхождения заданного символа или подстроки. Для этого подойдет другая версия функции 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. В результате выполнения этого фрагмента получается такой вывод:
'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;// Индекс найденного символа
int count = 0;// Счетчик знаков препинания
while((index = sentence->IndexOfAny(punctuation, index)) >= 0) {
indicators [index] = L,A';/ / Установить маркер
++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) ) ;
Второй оператор создает в куче новый объект 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; он не может содержать адрес цело-
го объекта в куче 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}; interiorjptr<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-Sstrings[0] < strings->Length ; ++pstrings)
Console::WriteLine(*pstrings);
return 0;
}
Вывод этого примера будет таким:
Сумма элементов данных массива =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
До тех пор, пока pstrings содержит адрес корректного элемента массива, разница между адресом pstrings и адресом первого элемента массива остается меньше количества элементов в массиве, которое сообщает выражение strings->Length. То есть, когда эта разница становится равной длине массива, цикл завершается. Взглянув на вывод программы, вы можете убедиться, что все работает, как ожидалось.
Чаще всего внутренние указатели используются для обращения к объектам, являющимся частью объектов кучи CLR, и позже из нашей книги вы узнаете об этом подробнее.
Резюме
Вы ознакомились со всеми базовыми типами значений C++, с тем, как создавать и использовать массивы этих типов, и как создавать и использовать указатели. Вы также получили представление о концепции ссылки. Однако все эти темы раскрыты не полностью. Позднее в этой книге я еще вернусь к теме массивов, указателей и ссылок. Ниже приведен краткий перечень тем этой главы, касающихся программирования на “родном” C++.
Массив позволяет управлять множеством однотипных переменных с использованием одного общего имени. Каждое измерение массива задается между квадратными скобками, следующими за именем массива при его объявлении.
Каждое измерение массива индексируется, начиная с нуля. Таким образом, пятый элемент одномерного массива имеет значение индекса 4.
Массивы могут инициализироваться в объявлении списком значений, заключенным в фигурные скобки.
Указатель — это переменная, содержащая адрес другой переменной. Указатель объявляется как “указатель на тип" и может получать значения адресов переменных этого типа.
Указатель может указывать на константный объект. Такой указатель можно переустановить на другой объект. Но указатель также может быть объявлен как константный — в этом случае ему нельзя присвоить другой адрес.
Ссылка — это псевдоним другой переменной, который может использоваться в тех же местах, что и переменная, на которую она ссылается. Ссылка должна быть инициализирована в объявлении.
Ссылка не может быть переназначена на другую переменную.
Операция sizeof возвращает количество байт, занятых объектом, специфицированным в ее аргументе. Аргументом может служить переменная или имя типа в скобках.
Операция new динамически выделяет память в свободном хранилище приложений “родного” C++. Когда блок памяти выделен, операция возвращает адрес его начала. Если память не может быть выделена по какой-то причине, возбуждается исключение, которое по умолчанию прерывает выполнение программы.
Механизм указателя иногда выглядит несколько запутанным, потому что он работает на разных уровнях в пределах одной программы. Иногда он ведет себя как адрес, а в других случаях работает со значением, расположенным по этому адресу. Очень важно, чтобы вы научились правильно понимать способы применения указателей, поэтому если вам не все ясно, напишите несколько собственных примеров, использующих указатели, чтобы чувствовать себя уверенно в их применении.
Далее перечислены ключевые моменты, с которыми вы ознакомились в этой главе, касающиеся программирования CLR.
В программах CLR память выделяется операцией gcnew в управляемой куче, которую обслуживает сборщик мусора.
Объекты классов ссылок вообще и объекты String в частности всегда размещаются в куче CLR.
При работе со строками в программах CLR используются объекты String.
CLR имеет собственные типы массивов, предлагающие более богатую функциональность, чем родные типы массивов.
Отслеживаемый дескриптор — это разновидность указателя, служащего для обращения к переменным, размещенным в куче CLR. Отслеживаемые дескрипторы автоматически обновляются, когда объекты, на которые они указывают, перемещаются в куче сборщиком мусора.
Переменные, ссылающиеся на объекты и массивы в куче, всегда являются отслеживаемыми дескрипторами.
Отслеживаемая ссылка подобна обычной ссылке “родного” C++ во всем, за исключением того, что адрес, на который она ссылается, автоматически обновляется при перемещении объекта в куче сборщиком мусора.
Внутренний указатель — это тип указателя C++/CLI, к которому можно применять те же операции, что и к обычному указателю “родного” C++.
Адрес, содержащийся во внутренней ссылке, может модифицироваться арифметическими операциями и оставаться корректным, даже если он ссылается на нечто, размещенное в куче CLR.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Напишите программу на “родном” C++, которая позволит ввести неограниченное количество значений и сохранить их в массив, размещенный в свободном хранилище. Программа должна вывести введенные значения, по пять в строке, после чего вычислить и показать их среднюю величину. Начальный размер массива должен составлять пять элементов. При необходимости программа должна создавать массив с пятью добавочными элементами и копировать содержимое старого массива в новый.
Повторите предыдущий пример, применив нотацию указателей вместо нотации массивов.
Объявите символьный массив и инициализируйте его подходящей строкой. Используйте цикл для изменения регистра каждого символа на верхний. Подсказка: в наборе символов ASCII значения кодов символов верхнего регистра на 32 меньше соответствующих кодов символов нижнего регистра.
Напишите программу на C++/CLI, которая создает массив случайных чисел типа int. Массив должен иметь от 10 до 20 элементов. Присвойте элементам случайные значения от 100 до 1000. Выведите их на экран, по пять в строке, в порядке возрастания, без сортировки массива; например, найдите самый ма-
ленький элемент и выведите его, затем следующий самый маленький, исключая первый, и так далее.
Напишите программу на C++/CLI, которая генерирует случайное целое число больше 10 000. Выведите его на экран цифрами, а затем — словами, перечисляющими десятичные цифры этого числа. Например, если будет сгенерировано число 345 678, вывод должен быть таким:
Значение числа 345678
три четыре пять шесть семь восемь
Напишите программу на 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++ можно говорить достаточно много, поэтому во избежание несварения желудка, вы не должны пытаться проглотить все сразу. После того, как вы хорошенько разжуете и прочувствуете аромат маленького кусочка, то сможете двинуться дальше, к следующей главе, где получите более подробное представление об этой теме.
Что такое функции
Для начала рассмотрим общие принципы работы функций. Функция — это изолированный блок кода, имеющий определенное специфическое назначение. Функция обладает именем, которое как идентифицирует ее, так и служит для вызова на выполнение внутри программы. Имя функции глобально, но не обязательно уникально в C++, как вы увидите это в следующей главе; однако функции, которые выполняют различные действия, обычно должны иметь разные имена.
Имена функций подчиняются тем же правилам, что и имена переменных. То есть имя функции — это последовательность букв и цифр, начинающаяся с буквы, причем знак подчеркивания тоже считается буквой. Имя функции должно обычно отражать то, что она делает, поэтому, например, вы можете назвать функцию, которая подсчитывает бобы, count_beans ().
Вы передаете информацию функции с помощью аргументов, специфицированных при ее вызове. Эти аргументы должны соответствовать параметрам, появляющимся в определении функции. Когда функция выполняется, то указанные вами аргументы заменяют параметры, использованные в ее определении. Код функции выполняется так, как будто он был написан с применением значений ваших аргументов. На рис. 5.1 показаны отношения между аргументами при вызове функции и параметрами, специфицированными в ее определении.
Аргументы
cout << add_ints( 2,3);
Значение аргументов заменяют соответствующие параметры в определении функции
int add_ints( int i, int j)
Определение функцииОпределение функцииreturn i + j;return i + j;ПараметрыПараметры
Определение функции
Определение функции
return i + j;
return i + j;
Параметры
Параметры
Возвращается значение 5
Рис. 5.1. Отношения между аргументами и параметрами
В этом примере функция возвращает сумму двух переданных ей аргументов. В общем случае функция возвращает либо одно значение в точку программы, откуда она была вызвана, либо вообще ничего — в зависимости от того, как она определена. Можно подумать, что возврат единственного значения из функции ограничивает ее возможности, но это единственное значение может быть указателем, содержащим, например, адрес массива. Чуть позднее в этой главе вы узнаете подробнее о возврате данных из функции.
Зачем нужны функции?
Одно из главных преимуществ, предоставляемых функцией, состоит в том, что она может быть выполнена столько раз, сколько необходимо, в разных точках программы. Без такой возможности упаковывать блоки кода в функции, программы были бы намного больше, поскольку тогда пришлось бы повторять один и тот же код везде, где он может понадобиться. Но реальная необходимость в функциях вызвана тем, чтобы можно было разбивать программу на легко управляемые фрагменты для независимой разработки и тестирования.
Представьте себе действительно большую программу — скажем, в миллион строк кода. Программу' такого размера практически невозможно написать без функций. Функции позволяют сегментировать программу так, что ее можно писать по частям, и тестировать каждую часть независимо, прежде чем соединять ее с прочими частями. Это также дает возможность распределить работу между членами команды разработчиков, где каждый член команды отвечает за четко определенную часть программы, с хорошо определенным функциональным интерфейсом для остального кода.
Структура функции
Как вы уже видели, когда писали функции main (), функция состоит из заголовка функции, который идентифицирует ее, а за ним следует тело функции, заключенное в фигурные скобки и содержащее ее исполняемый код. Рассмотрим пример. Попробуем написать функцию, которая будет возводить значение в заданную степень, то есть вычислять результат умножения значения х на себя п раз, что в математике записывается как хп.
И Функция для вычисления х в степени п, где п больше или равно О double power(double х, int n) // Заголовок функции
{ // Тело функции начинается здесь...
double result = 1.0;// Здесь сохраняется результат
for (int i = 1; i <= n; i++) result *= x;
return result;
} // . . .и заканчивается здесь
Заголовок функции
Сначала рассмотрим в этом примере заголовок функции. Следующая строка — первая строка функции:
double power(double х, int n) // Заголовок функции
Она состоит из трех частей, которые описаны ниже.
Тип возвращаемого значения (в данном случае — double).
Имя функции (в данном случае — power).
Параметры функции, заключенные в скобки (в данном случае — х и п, типа double и int соответственно).
Возвращаемое значение возвращается вызывающей функции, поэтому, когда данная функция вызывается, то ее результат типа double подставляется в выражение, из которого она вызвана.
Наша функция имеет два параметра: х — значение типа double, которое нужно возвести в степень, и п — значение степени типа int. Эти переменные параметры участвуют в вычислениях, выполняемых функцией, вместе с другой переменной — result, объявленной в ее теле. Имена параметров и любые переменные, определенные в теле функции являются локальными по отношению к ней.
Обратите внимание, что в кон-це заголовка функции и после закрывающей фигурной скобки ее тела точка с запятой не требуется
Общая форма заголовка функции
Общая форма заголовка функции может быть записана следующим образом:
тип_возврата имя_функции (список_параметров)
тип_возврата может быть любым легальным типом. Если функция не возвращает значения, то тип возврата указывается ключевым словом void. Ключевое слово void также применяется для обозначения отсутствия параметров, поэтому функция, которая не имеет параметров и не возвращает значения, должна иметь следующую форму заголовка:
void my_function(void)
Пустой список параметров также означает, что функция не имеет аргументов, поэтому вы можете пропустить ключевое слово void между скобками:
void my_function()
Функция с типом возврата void не должна использоваться в составе выражений в вызывающей программе. Поскольку она не возвращает значения, то, по сути, не может быть значимой частью выражения, поэтому применение ее в таком виде заставит компилятор генерировать сообщение об ошибке.
Тело функции
Все необходимые вычисления функции выполняются операторами в ее теле, которое следует за заголовком. Тело функции из нашего последнего примера начинается с объявления переменной result, инициализированной значением 1.0. Переменная result локальна по отношению к функции, как и все автоматические переменные, объявленные в ее теле. Это значит, что переменная result прекращает существование после того, как функция завершит работу. Скорее всего, у вас немедленно возникнет вопрос: если переменная result прекращает существование по завершении работы функции, то как она может быть возвращена? Ответ в том, что при этом автоматически делается копия значения, подлежащего возврату, и эта копия возвращается в программу.
Вычисление производится в цикле for. Управляющая переменная цикла 1 объявлена в самом цикле, и предполагается, что она последовательно принимает значения от 1 до п. Переменная result умножается на х при каждой итерации цикла, поэтому это происходит п раз, чтобы сгенерировать необходимое значение. Если п равно 0,
то оператор цикла не будет выполнен ни разу, поскольку условное выражение сразу возвратит false, и result останется равным 1.0.
Как я сказал, параметры и все переменные, объявленные в теле функции, локальны по отношению к ней. Ничто не мешает вам использовать те же имена переменных в других функциях, для других целей. В самом деле, иначе было бы чрезвычайно трудно обеспечить уникальность имен переменных внутри программы, состоящей из множества функций, особенно, если эти функции разрабатывает не один человек.
Область видимости переменных, объявленных внутри функции, определяется таким же образом, как уже упоминалось. Переменная создается в точке ее объявления и прекращает свое существование в конце блока, в котором была объявлена. Однако существует разновидность переменных, которая составляет исключение из этого правила— переменные, объявленные как static. Позднее в этой главе мы поговорим о статических переменных.
Будьте осторожны с маскированием глобальных переменных одноименными локальными. Вы уже встречались с такой ситуацией в главе 2, где использовали операцию разрешения контекста :: для доступа к глобальным переменным.
Оператор return
Оператор return возвращает значение result в точку вызова функции. Общая форма оператора return такова:
return выражение;
где выражение должно вычисляться как значение типа, специфицированного в заголовке функции для возврата значения. Выражение может быть любым, какое хотите, до тех пор, пока оно в результате отдает значение требуемого типа. Выражение может включать вызовы функций — даже вызов той самой функции, в которой оно появляется, и вы увидите это дальше в настоящей главе.
Если тип возврата функции специфицирован как void, то за оператором return не должно следовать никакого выражения. Оно должна записываться очень просто:
return;
Использование функций
В точке, где в программе используется функция, компилятор должен знать кое-что о ней, чтобы скомпилировать ее вызов. Ему нужна достаточная информация, чтобы идентифицировать функцию, и убедиться, что вы применяете ее корректно. И если только функция, которую вы намерены использовать, не появилась где-то ранее в том же исходном файле, вы должны объявить функцию с помощью оператора, который называется прототипом функции.
Прототипы функций
Прототип функции предоставляет базовую информацию, которую компилятор должен проверить, чтобы убедиться, что функция используется корректно. Он специфицирует параметры, передаваемые функции, ее имя и тип возвращаемого значения. По сути, прототип содержит ту же информацию, что содержится в заголовке функции, с добавлением точки с запятой. Понятно, что количество параметров и их типы в прототипе функции должны быть такими же, как в ее заголовке.
Прототипы функций, которые вызываются из другой функции, должны появляться перед операторами, где эти функции вызываются, и потому обычно помещаются
в начале исходного файла программы. Заголовочные файлы, которые вы включаете для использования стандартных библиотечных функций, помимо прочего, включают в себя прототипы этих библиотечных функций.
Для примера функции 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)); // Применение функции в качестве аргумента
cout « endl// с автоматическим приведением 2-го параметра
cout « endl;
return 0;
}
// Функция для вычисления х в степени п, где п больше или равно О double power(double х, int n) / / Заголовок функции
{ // Тело функции начинается здесь...
double result = 1.0;И Здесь сохраняется результат
for (int i = 1; i <= n; i++)
result *= x;
return result;
} // ...и заканчивается здесь
В этой программе демонстрируются некоторые способы использования функции power () путем передачи ей аргументов различными способами. Если запустить этот пример, получится следующий вывод:
.О в кубе = 125
. О в кубе = 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. О и 3 — аргументы функции. В данном случае они являются константами, но вы можете использовать любые выражения в качестве аргументов, если
они дают результат требуемого типа. Аргументы функции 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.0, 2)));
Такое кодирование оператора позволяет избежать обоих предупреждений компилятора, которые вызывает исходная версия. Применение статического приведения не исключает возможности потери данных при преобразовании одного типа в другой. Но поскольку оно указано явно, то компилятору ясно, что это входит в ваши намерения.
х = power( х, power( 2.0,2.0 ));
начальное значение ф Преобразовано3.0KTMnyint
(§) результат сохранен▼
обратно в х @ power( 2.0,2)
.0 (type double)
Преобразовано /^к типу int
▼▼
© power( 3.0,4)
81.0 (типа double)
Рис. 5.2. Результат вызова функции power ()
Передача аргументов в функцию
Очень важно понимать, как аргументы передаются в функцию, потому что это влияет на то, как вы их пишете, и то, как они в конечном итоге работают. Здесь есть множество ловушек, которых следует избегать, поэтому рассмотрим этот механизм поближе.
Аргументы, которые вы специфицируете при вызове функции, обычно должны соответствовать по типу и последовательности параметрам, заданным в ее определении. Как вы видели из последнего примера, если тип аргумента, указанного при вызове функции, не соответствует типу параметра, указанному в ее определении, то там, где возможно, происходит преобразование к требуемому типу в соответствии с правилами приведения операндов, описанными в главе 2. Если такое приведение невозможно, вы получаете от компилятора сообщение об ошибке. Однако даже если приведение возможно и код компилируется, это может привести к потере данных (например, когда тип long приводится к short), а потому его следует избегать.
Существуют два механизма, обычно используемые C++ для передачи аргументов в функции. Первый применяется, когда параметры в определении функции специфицированы как обычные переменные (не ссылки). Это называется методом передачи по значению (pass-by-value) данных в функцию, и его мы рассмотрим первым.
Механизм передачи по значению
Когда применяется такой механизм, то переменные или константы, которые вы специфицируете в качестве аргументов, вообще не передаются в функцию. Вместо этого создаются копии аргументов, и эти копии используются в качестве передаваемых значений. На рис. 5.3 показана диаграмма применительно к функции power ().
indexindexvaluevaluedouble power ( double xdouble power ( double xint n)int n)int index = 2;double value = 10.0;double result = power(value, index);int index = 2;double value = 10.0;double result = power(value, index);
index
index
value
value
double power ( double x
double power ( double x
int n)
int n)
int index = 2;
double value = 10.0;
double result = power(value, index);
int index = 2;
double value = 10.0;
double result = power(value, index);
Исходные аргументы не доступныздесь, а только копии
Рис. 5.3. Передача no значению
Каждый раз, когда вызывается функция power (), компилятор создает копии переданных аргументов и размещает их во временном участке памяти. Во время выполнения функции все ссылки на ее параметры отображаются на эти временные копии аргументов.
практическое занятие пёредачапозначению
Одно из последствий механизма передачи по значению состоит в том, что функция не может напрямую модифицировать переданные ей аргументы. В этом можно убедиться на следующем примере:
// Ех5_02.срр
// Бесполезная попытка модификации аргументов вызывающего кода
#include <iostream>
using std::cout;
using std::endl;
int incr!0(int num); // Прототип функции
int main(void) {
int num = 3; cout « endl
« "incrlO(пищ) = " « incrlO(num) « endl
« "num = " « num;
cout « endl;
return 0;
}
// Функция для увеличения переменной на 10
int incrlO(int num) // Применение такого имени может помочь... {
num +=10;// Попытка инкрементировать аргумент вызова
return num;// Возврат измененного значения
}
Конечно, эта программа обречена на неудачу. Если вы запустите ее, то получите следующий вывод:
incrlO(num) = 13
num = 3
Описание полученных результатов
Вывод подтверждает, что исходное значение num остается незатронутым. Инкременту подвергается копия num, которая была создана и затем отброшена при выходе из функции.
Понятно, что механизм передачи по значению обеспечивает высокую степень защиты аргументов вызова от повреждения недобросовестной функцией, но все же может случиться, что вы действительно хотите, чтобы переданные аргументы были модифицированы функцией. Конечно, существует способ сделать это. Вспомните, насколько удобными могут оказаться указатели.
Указатели как аргументы функций
Когда вы используете указатели в качестве аргументов, механизм передачи по значению работает, как и прежде; однако указатель — это адрес другой переменной, и если вы возьмете копию этого адреса, то она будет по-прежнему указывать на ту же переменную. Таким образом, применение указателя в качестве параметра позволяет вашей функции получить сам аргумент, а не его копию.
Практическое занятие| ПврвДаЧВ ПО уКаЗаТвЛЮ
Для демонстрации эффекта можно изменить последний пример, чтобы он использовал указатель:
// Ех5_03.срр
/ / Успешная попытка модифицировать аргумент
#include <iostream>
using std::cout;
using std::endl;
int incrlO (int* num) ; // Прототип функции
int main (void)
{
int num = 3;
int* pnum = # // Указатель на num
cout « endl
« "Передан адрес = " « pnum;
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 выполняют разыменование указателя, чтобы обратиться к значению, хранящемуся в нем.
Передача массивов в функцию
В функцию можно передавать массивы, но в этом случае массив не копируется, даже несмотря на то, что при этом по-прежнему применяется передача аргумента по значению. Имя массива преобразуется в указатель, и копия указателя на начало массива передается в функцию по значению. Это достаточно выгодно, потому что копирование больших массивов требует значительных затрат времени. Однако, как вы можете обнаружить в этом случае, элементы массива могут быть изменены внутри функции, и потому массив — единственный тип, который не может быть передан по значению.
Практическое занятие | ПврвДаЧа МаССИВОВ
Плюсы и минусы этого можно проиллюстрировать на примере функции, вычисляющей среднюю величину значений, переданных в массиве.
И Ех5_04.срр
II Передача массива в функцию
#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 ().
Вывод программы подтверждает, что все работает, как надо.
р ктическое з нятие Использование нотации указателей припередаче массивов
Но этим мы не исчерпали все возможности. Как было сказано, имя массива передается в виде указателя (точнее говоря, в виде копии указателя), поэтому внутри функции вы вообще не обязаны работать с данными как с массивом. Можно так модифицировать функцию в примере, чтобы повсюду использовать нотацию указателей, несмотря на тот факт, что речь идет о массиве.
// Ех5_05.срр
II Сокрытие массива в функции указателем
#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;
}
11 Функция для вычисления среднего
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. Но на самом деле это правило вовсе не нарушается. Вспомните, что механизм передачи по значению создает копию исходного адреса массива и передает его, а потому в теле функции модифицируется лишь эта копия (исходный адрес массива остается неизменным). В результате всякий раз, когда вы передаете одномерный массив в функцию, то вольны трактовать переданное значение в любом смысле как указатель, а также произвольным образом изменять адрес, хранящийся в нем.
Передача в функцию многомерных массивов
Передача в функцию многомерного массива достаточно проста. Следующий оператор объявляет двумерный массив 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;
}
H Функция для вычисления всего объема урожая
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;
}
Вывод этого примера выглядит следующим образом:
Урожай =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) ;
cout « endl
« "пглп = " « пглп;
cout « endl
« "incrlO(value) = ” « incrlO(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 — ссылка.
Это наглядно демонстрирует разницу между ссылкой и указателем. Ссылка — псевдоним другой переменной, а потому может использоваться в качестве ее альтернативного имени. Это полностью эквивалентно применению ее исходного имени.
Вывод доказывает, что функция incrlO() непосредственно модифицирует переменную, переданную ей в аргументе.
Но если вы попытаетесь использовать числовое значение, такое как 20, в качестве аргумента incrlO (), то компилятор выдаст сообщение об ошибке. Дело в том, что компилятор обнаруживает, что параметр-ссылка может быть модифицирован внутри функции, но изменять значения констант нельзя. Это может внести в программы некоторое излишнее волнение, без которого имеет смысл обойтись.
Эта защита хороша, но если функция не собирается модифицировать переданное ей по ссылке значение, то вряд ли вы захотите, чтобы компилятор каждый раз выдавал сообщения об ошибках, когда вы передаете константу. Должен же быть какой-то способ добиться этого? Разумеется, способ есть.
Использование модификатора const
Чтобы сообщить компилятору, что вы не собираетесь никоим образом модифицировать параметр функции, вы можете применить модификатор const. Это заставит компилятор проверить код функции на предмет того, действительно ли он не изменяет значения аргумента, и если нет, то никаких сообщений об ошибках не выдается при использовании константного аргумента.
Практическое занятие | ПврвДаЧа ПЭрЭМеТрЭ Const
Чтобы увидеть, как модификатор const изменяет ситуацию, модифицируем предыдущую программу.
// Ех5_08.срр
И Использование константной ссылки
#include <iostream>
using std::cout;
using std::endl;
int incrlO (const int6 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;
/ / num +=10;/ / теперь этот оператор является незаконным
return num+10;/ / Возвратить увеличенное значение
}
Ниже показан вывод этой программы.
Получено значение = 3
incrlO(num) = 13
num = 3
Получено значение = 6
incrlO(value) = 16 value = 6
Описание полученных результатов
Вы объявляете переменную num в main () как const, чтобы показать, что когда параметр функции incrlO () объявлен как const, то компилятор более не выдает сообщений об ошибке при передаче константного объекта.
Необходимо также закомментировать оператор, который увеличивает значение num в функции incr10(). Если вы уберете комментарий с этой строки, программа перестанет компилироваться, потому что компилятор не допустит появления num в левой части присваивания. Когда вы специфицируете num как const в заголовке и в прототипе функции, это значит, что вы обязуетесь не модифицировать его, и компилятор проверяет, как вы держите слово.
Все работает как прежде, за исключением того, что переменные из main () более не изменяются в функции.
Используя аргументы-ссылки, вы берете лучшее из двух миров. С одной стороны, вы можете написать функцию, которая получает прямой доступ к исходному аргументу вызывающего кода, позволив избежать копирования, которое подразумевает механизм передачи по значению. С другой стороны, когда вы не намерены модифицировать аргумент, вы получаете полную защиту от непреднамеренной его модификации, используя модификатор const со ссылкой.
Аргументы main ()
Функцию main () можно объявлять без аргументов (или лучше со списком параметров void), либо же специфицировать список параметров, которые позволяют этой функции получать аргументы из командной строки. Значения, передаваемые в качестве аргументов main (), всегда интерпретируются как строки. Если вы хотите в main () получить данные из командной строки, то должны определить ее следующим образом:
int main(int argc, char* argv[])
{
// Код main () . . .
}
Первый параметр — счетчик строк, переданных программе в командной строке, включая имя самой программы, а второй параметр — массив указателей на эти строки плюс один дополнительный нулевой указатель. Таким образом, значение argc всегда не меньше 1, поскольку при запуске программы всегда, как минимум, указывается ее имя. Количество полученных аргументов зависит от того, что вы вводите в командной строке для запуска программы. Например, предположим, что вы выполняете программу DoThat с помощью следующей команды:
DoThat.ехе
Это просто имя исполняемого файла . ехе программы, поэтому 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) « ": " « argv[i] « 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
Описание полученных результатов
Во-первых, программа выводит значение 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 ; i<count ; i++)
sum += va_arg(arg_ptr, int); // Прибавить значение int из arg_ptr
va_end(arg_ptr);// Сбросить указатель в null
return sum;
}
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 . . .
Описание полученных результатов
Функция main () вызывает функцию sum () в двух операторах вывода, в первом случае с шестью аргументами, а во втором — с девятью.
Функция 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 1 = 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
Макрос 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;
}
Практическое занятие |Возврат ПЛОХОГО уКЭЗЭТ6ЛЯ
Для того чтобы посмотреть, что получится, можно написать небольшую тестовую программу (напомню, что функция treble не будет работать, как ожидается).
//Ех5_11.срр
#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 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, становится доступной для других целей, и здесь она как раз использована для чего-то другого.
Железное правило возврата адресов
Существует абсолютное железное правило относительно возвращаемых адресов:
Никогда не возвращать из функции адрес локальной автоматической переменной.
Очевидно, что нельзя применять функцию, которая не работает, но как же это исправить? Можно использовать ссылочный параметр и модифицировать в функции его исходное значение, но это не совсем то, что нужно. Вы пытаетесь вернуть указатель на нечто полезное, потому что в конечном итоге, вам может понадобиться возвращать нечто более сложное, чем отдельный элемент данных. Ответ заключается в динамическом выделении памяти (вы видели это в действии в предыдущей главе).
С помощью операции 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;
Возврат ссылки
Функция может возвращать ссылку. Это также чревато потенциальными ошибками, как и в случае возврата указателя, поэтому здесь вы также должны быть осторожны. Поскольку ссылка не является какой-то отдельной сущностью (это всегда псевдоним чего-то другого), вы должны быть уверены, что объект, на который она ссылается, все еще существует после завершения выполнения функции. Очень легко забыть об этом, используя в функции ссылки, поскольку они выглядят как обычные переменные.
Ссылки, как возвращаемые типы, особенно важны в контексте объектно-ориентированного программирования. Как вы увидите позднее в этой книге, они позволяют делать такие вещи, которые невозможно осуществить без их применения (в частности, это касается “перегрузки операций”, о которой речь пойдет в главе 9). Принципиальная характеристика возвращаемого значения типа ссылки в том, что оно является lvalue. Это значит, что вы можете использовать результат функции, которая возвращает ссылку, в левой части оператор присваивания.
Практическое занятие | Возврат ССЬ|ЛКИ
Теперь рассмотрим пример, иллюстрирующий использование возвращаемых типов ссылок, а также демонстрирующий, как функция может применяться в левой части операции присваивания, когда она возвращает lvalue. Этот пример предполагает, что у вас есть массив, который содержит смешанный набор значений. Всякий раз, когда вы хотите вставить новое значение в массив, вы заменяете элемент с наименьшим значением.
/ / Ех5_12.срр
// Возврат ссылки
#include <iostream>
#include <iomanip>
using std::cout;
using std::endl;
using std::setw;
doubles lowest(double values [], int length); H Прототип функции
// возвращающей ссылку 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 1 = 0; i < len; i++)
cout << setw(6) << array[i];
cout << endl;
return 0;
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];// Вернуть ссылку на наименьший элемент
Ниже показан вывод этого примера.
3101.5152.7234.5126.813.52.114
3106.9152.7234.5126.813.57.914
Описание полученных результатов
Посмотрим сначала, как реализована функция. Прототип функции lowest () использует doubles в качестве спецификации возвращаемого типа, который, таким образом, является “ссылкой на double”. Возвращаемое значение ссылочного типа пишется точно так же, как это вы видели в случае объявления ссылочных переменных — с добавлением s к имени типа. Функция принимает два параметра — одномерный массив типа double и параметр типа int, указывающий длину массива.
Тело функции содержит цикл for, в котором определяется, какой элемент переданного массива содержит минимальное значение. Индекс j найденного элемента с минимальным значением изначально равен 0, а затем модифицируется внутри цикла, если текущий элемент а [ i ] меньше а [ j ]. Таким образом, по завершении цикла j равно индексу элемента массива с минимальным значением. Оператор return выглядит следующим образом:
return a[j];// Вернуть ссылку на наименьший элемент
Несмотря на тот факт, что это выглядит точно так же, как оператор, возвращающий значение, поскольку тип возврата объявлен как ссылка, здесь возвращается не значение элемента а [ j ], а ссылка на него. Адрес а [ j ] используется для инициализации возвращаемой ссылки. Эта ссылка создается компилятором, потому что возвращаемый тип объявлен как ссылка.
Не путайте возврат Sa [ j ] с возвратом ссылки. Если вы укажете в качестве возвращаемого значения Sa [ j ], это будет означать адрес а [ j ], то есть указатель. Если вы сделаете это после спецификации типа возврата как ссылки, то получите от компилятора сообщение об ошибке. Если конкретно, вы получите:
error С2440: 'return* : cannot convert from 'double *w64 ' to 'double S' ошибка C2440: ’return* : не удается преобразовать ’double *w64 ’ в ’double &’
Функция main (), которая вызывает lowest (), очень проста. Здесь объявляется массив типа double и инициализируется 12-ю произвольными значениями, а переменная len инициализируется длиной массива. Начальные значения массива выводятся на экран для сравнения.
Опятытаки, использование в программе манипулятора потока setw() для выравнивания выводимых значений по ширине требует директивы ^include <iomanip>.
Функция main () использует функцию lowest () в левой части операции присваивания, чтобы изменить элемент, содержащий минимальное значение в массиве. Это делается дважды, чтобы продемонстрировать, что все это действительно работает, и написано не случайно. Затем содержимое массива снова выводится на дисплей, с той
же шириной полей, что и раньше, так что соответствующие значения оказываются друг под другом.
Как вы можете видеть из вывода перед первым вызовом lowest (), третий элемент массива, array [2], содержит минимальное значение, так что функция возвращает ссылку на него и его значение изменяется на 6.9. Аналогично, после второго вызова array [ 10 ] изменяется на 7.9. Это достаточно ясно демонстрирует, что возврат ссылки позволяет использовать функцию в левой части оператора присваивания.
Конечно, при желании вы можете использовать ее и в правой части присваивания или в составе любых других подходящих выражений. Если есть два массива — X и У — с количеством элементов, указанным соответственно в lenx и leny, вы можете присвоить минимальному элементу массива х удвоенное значение удвоенного минимального элемента у в следующем операторе:
lowest(х, lenx) = 2.0*lowest (у, leny);
Этот оператор должен вызвать функцию lowest () дважды — один раз с аргументами у и leny в выражении правой части присваивания и один — с аргументами х и lenx для получения адреса, куда должен быть сохранен результат правого выражения.
Еще одно железное правило: возврат ссылок
То же правило, которое применяется к возврату указателей из функций, также касается возврата ссылок.
Никогда не возвращайте из функции ссылку на локальную переменную.
Пока мы оставим тему возврата ссылок из функций, но пока не будем закрывать ее окончательно. Мы вернемся к ней опять в контексте определяемых пользователем типов и объектно-ориентированного программирования и узнаем еще о нескольких волшебных вещах, которые можно делать со ссылками.
Статические переменные в функциях
Есть несколько вещей, которые невозможно делать с автоматическими переменными внутри функций. Например, невозможно подсчитать, сколько раз функция вызывалась, поскольку нельзя накапливать значение счетчика от вызова к вызову. Однако существует не один способ обойти это при необходимости. Например, вы можете использовать параметр-ссылку для обновления счетчика в вызывающей программе, хотя это не поможет, если функция вызывается из множества разных мест программы. Вы можете применить глобальную переменную, значение которой увеличивать внутри функции, но глобальные объекты — рискованная вещь, поскольку доступны в любой точке программы, что открывает путь к их непреднамеренному изменению.
Глобальные переменные также опасно применять в многопоточных программах, когда несколько одновременно выполняющихся потоков управления имеют доступ к ним, и вы должны специально позаботиться об управлении доступом из разных потоков. Основная проблема, связанная с тем, что более одного потока имеют доступ к глобальной переменной, заключается в том, что один поток может изменить значение глобальной переменной в то время как другой работает с ней. Наилучшее решение в таких случаях состоит в том, чтобы вообще избегать применения глобальных переменных.
Чтобы создать переменную, чье значение сохраняется от одного вызова функции до другого, можно объявить ее внутри функции с ключевым словом static. При этом
используется точно такая же форма объявления 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)// просто для получения...
{
cout « "st";
break;
cout « "nd";
break;
cout « "rd";
break;
default: cout « "th"; // правильного окончания для...
} // 1st (1-й), 2nd (2-й), 3rd (3-й), 4st (4-й) и так далее.
cout « ” time I have been called";
return;
Функция record () служит только для фиксации того факта, что она была вызвана. В результате запуска этого примера получается следующий вывод:
This is the 1st time I have been called
This is the 2nd time I have been called
This is the 3rd time I have been called
This is the 4th time I have been called
This is the 5th time 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 равен произведению 1х2хЗх...х^ Этот пример очень часто приводится для демонстрации рекурсии в действии. Рекурсия также применяется для анализа программы во время процесса компиляции. Однако мы рассмотрим нечто более простое.
Практическое занятие
Рекурсивная функция
В начале этой главы (см. Ех5_01. срр) мы написали функцию возведения значения в целочисленную степень — то есть для вычисления хп. Это эквивалентно х, умноженному на себя п раз. Теперь в качестве элементарного примера применения рекурсии мы можем реализовать это в виде рекурсивной функции. Можно также при этом усовершенствовать реализацию этой функции, чтобы она могла работать с отрицательными значениями степени, когда х-n эквивалентно 1/хп.
// Ех5_14.срр (основан на Ех5_01.срр)
// Рекурсивная версия возведения х в степень п
#include <iostream>
using std::cout;
using std::endl;
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
Описание полученных результатов
Теперь наша функция поддерживает положительные и отрицательные степени х, поэтому первое, что необходимо сделать — проверить, не является ли степень п, в которую нужно возвести х, отрицательной:
if(n < 0) {
x = 1.0/x; n = -n;
Поддержка отрицательных степеней проста; здесь просто используется тот факт, что хп может быть вычислено как (1/х)п. Поэтому если п отрицательно, то х присваивается 1.0/х, а знак п меняется на положительный.
В следующем операторе if принимается решение о том, должна ли функция 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 (), и конечно, восстанавливать их значение при каждом возврате. При сравнительно небольшой глубине рекурсивных вызовов накладные расходы будут заметно больше, чем при использовании цикла.
Однако из этого не следует, что вы никогда не должны использовать рекурсию. Когда проблема для своего решения требует применения рекурсивных вызовов функций, это может оказаться весьма мощной техникой, значительно упрощающей исходный код. Пример такого случая будет показан в следующей главе.
Результат: х3Результат: х3power( х , 3 )power( х , 3 )х*х*хх*х*хdouble power( double х, int n ) {double power( double х, int n ) {return x*power( x , n - 1 );return x*power( x , n - 1 );xxxxdouble power( double x, int n )double power( double x, int n )return x*power( x , n -1 );return x*power( x , n -1 );xxAAdouble power( double x, int n )double power( double x, int n )return x*power( x , n -1 );return x*power( x , n -1 );double power( double x, int n )double power( double x, int n )return 1.0;return 1.0;
Результат: х3
Результат: х3
power( х , 3 )
power( х , 3 )
х*х*х
х*х*х
double power( double х, int n ) {
double power( double х, int n ) {
return x*power( x , n - 1 );
return x*power( x , n - 1 );
xx
xx
double power( double x, int n )
double power( double x, int n )
return x*power( x , n -1 );
return x*power( x , n -1 );
x
x
A
A
double power( double x, int n )
double power( double x, int n )
return x*power( x , n -1 );
return x*power( x , n -1 );
double power( double x, int n )
double power( double x, int n )
return 1.0;
return 1.0;
Puc. 5.4. Пример рекурсивной функции
Программирование на C++/CLI
В основном функции в программах C++/CLI работают точно так же, как и в программах “родного” C++. Конечно, здесь вам приходится иметь дело с дескрипторами и отслеживаемыми ссылками вместо родных указателей и ссылок, и это ведет к некоторым отличиям. Существуют несколько моментов, которые слегка отличаются, попробуем систематизировать их.
Параметры функций и возвращаемые значения в программе CLR могут быть типами классов значений, отслеживаемыми дескрипторами, отслеживаемыми ссылками и внутренними указателями.
Когда параметром является массив, нет необходимости в дополнительном параметре, передающем его длину, поскольку массивы C++/CLI хранят в себе информацию о собственной длине — в свойстве Length.
Нельзя применять адресную арифметику с параметрами-массивами в программах C++/CLI, как это делается в программах на родном C++, то есть всегда нужно использовать индексацию массивов.
Возврат дескриптора памяти, выделенной в куче 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;
for 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;
}'
Этот пример генерирует следующий вывод:
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 A> Aargs) {
Console::WriteLine(Ь"Принято {0} аргументов командной строки. ”, args->Length); Console::WriteLine(Ь"Приняты следующие аргументы командной строки:”); int i = 1;
for each(StringA str in args)
Console::WriteLine(Ь"Аргумент {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.
Резюме
В этой главе вы изучили основы структурной организации программ. Вы должны были получить хорошее представление о том, как определяются функции, как им передаются данные и как возвращаются результаты в вызывающую программу. Функции — фундаментальная концепция в программировании на C++, поэтому все, что вы будете делать, начиная с этого момента, будет включать использование множества функций в каждой программе. Ниже перечислены ключевые моменты, о которых вам следует помнить при написании своих собственных функций.
Функции должны быть компактными единицами кода, предназначенными для четко определенных целей. Типичная программа должна состоять из множества небольших функций, а не из малого количества крупных.
Для каждой функции, определенной в программе, всегда следует представлять прототип, располагая его перед ее вызовами.
□ Передача значений в функцию с применением ссылок позволяет избежать неявного копирования, которое выполняется при передаче аргументов по значению. Параметры, переданные по ссылке, которые не должны быть модифицированы в функции, следует специфицировать как const.
□ При возврате ссылки или указателя из функции родного C++ убедитесь, что возвращаемый объект имеет правильную область определения. Никогда не возвращайте указатель или ссылку на локальный объект функции родного C++.
□ В программах C++/CLI не возникает проблем с возвратом дескриптора памяти, которая была выделена динамически, потому что сборщик мусора позаботится о его удалении, когда он будет не нужен.
□ Когда вы передаете массив C++/CLI в функцию, нет необходимости в дополнительном параметре, указывающем его длину, поскольку количество элементов массива можно получить в теле функции через свойство Length массива.
Использование ссылок в качестве аргументов — очень важная концепция, поэтому убедитесь, что вы уверены в том, что делаете. Когда мы будем говорить об объектно- ориентированном программировании, вы узнаете намного больше о ссылках в качестве аргументов.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Факториал 4 (записывается, как 4!) равен 4*3*2*1=24, а 3! равен 3*2*1=6, отсюда следует, что 4!=4*3!, или, в более общем виде:
fact(n) = n*fact(n — 1)
Ограничивающим условием является случай, когда п=1, при этом 1! = 1. Напишите рекурсивную функцию для вычисления факториала и протестируйте ее.
Напишите функцию, которая обменивает значениями две целочисленных переменных, используя указатели. Напишите программу, вызывающую эту функцию, и протестируйте, чтобы убедиться, что она работает правильно.
Тригонометрические функции (sin(), cos () и tan ()) из стандартной математической библиотеки принимают аргументы в радианах. Напишите три эквивалентных функции sind (), cosd () и tand (), которые принимают аргументы в градусах. Все аргументы и возвращаемые значения должны быть типа double.
Напишите программу на родном C++, которая читает с клавиатуры число (целое) и имя (до 15 символов). Спроектируйте программу так, чтобы данные вводились в одной функции, а выводились в другой. Сохраняйте данные в главной программе. Программа должна завершаться, когда пользователь введет в качестве числа 0. Подумайте о том, как передавать данные между функциями — по значению, по указателю или по ссылке?
(Усложненное.) Напишите функцию, которая, получив строку, состоящую из слов, разделенных пробелами, возвращает первое слово; следующий ее вызов с аргументом NULL должен вернуть второе слово, и так далее до тех пор, пока вся строка не будет успешно обработана, когда должен быть возвращен NULL. Это должна быть упрощенная версия того, как работает функция стандартной библиотеки родного C++ strtok (). То есть, когда функция получает строку "один два три’, то при первом вызове должна вернуть ‘один’, при втором — ‘два’, и наконец — ‘три’. Передача новой строки должна прекратить обработку старой и отбросить ее прежде, чем начать обрабатывать новую.
6
Дополнительные сведенияо структурах программ
В предыдущей главе вы изучили основы определения функций и узнали о различных способах передачи информации в функции. Вы также видели, как функции возвращают результаты в вызвавшую их программу.
В этой главе мы раскроем дополнительные аспекты правильного применения функций, включая'перечисленные ниже вопросы.
Что такое указатель на функции.
Как определять и использовать указатели на функции.
Как определять и использовать массивы указателей на функции.
Что такое исключение, и как создавать обработчики исключений.
Как создавать множество функций с одинаковыми именами для автоматической обработки разнотипных данных.
Что такое шаблоны функций, как их определять и использовать.
Как написать реальный пример программы на родном C++, использующей несколько функций.
Что собой представляют обобщенные функции в C++/CLI.
Как написать реальный пример программы на C++/CLI, использующей несколько функций.
Указатели на функции
Указатель сохраняет значение адреса, и все рассмотренные нами до настоящего времени указатели хранили адреса других переменных с тем же базовым типом, что и конкретный указатель. Это обеспечивало относительную гибкость в смысле того, что
можно было обращаться в разное время к разным переменным через один и тот же указатель. Но указатель также может хранить адрес функции. Это дает возможность вызывать функцию через указатель, адрес которой был присвоен данному указателю.
Очевидно, что указатель на функцию должен содержать адрес памяти, где находится функция, которую необходимо вызвать. Чтобы работать правильно, однако, такой указатель должен поддерживать информацию о списке параметров соответствующей функции и типе ее возвращаемого значения. Таким образом, когда вы объявляете указатель на функцию, то в дополнение к имени указателя должны специфицировать типы параметров и тип возврата функции, на которую он может указывать. Ясно, что это необходимо для того, чтобы ограничить то, что можно присваивать каждому конкретному указателю функции. Если вы объявили указатель на функцию, принимающую один аргумент типа 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. Поскольку вы намерены объявить указатель, это совсем не то, что вам нужно в данный момент.
Общая форма объявления указателя на функцию выглядит так:
тип_возврата (*имя_указателя) (список_типов_параметров);
Указатель может указывать только на функции с тем же типом возврата и списком типов параметров, что заданы в его объявлении.
Это показывает, что объявление указателя на функцию состоит из трех перечисленных ниже компонентов.
Тип возврата функции, на которую он указывает.
Имя указателя, предваренное звездочкой, говорящей о том, что это — указатель.
Типы параметров функции, на которую он указывает.
Если вы попытаетесь присвоить адрес функции указателю, который не соответствует типам в его объявлении, компилятор выдаст сообщение об ошибке.
Вы можете инициализировать указатель на функцию именем функции непосредственно в операторе объявления этого указателя. Ниже приведены примеры этого.
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 b) ;// Прототип функции
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;
}
Эта программа генерирует следующий вывод:
3*5 = 15
3* (4 + 5) + 6 = 33
Описание полученных результатов
Это не особенно полезная программа, однако на очень простом примере она показывает, как объявить указатель на функцию, как ему присвоить значение и затем использовать для вызова функции.
После обычной преамбулы вы объявляете указатель на функцию pdo_it, который может указывать на любую из двух функций, которую вы определили — 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.
Указатель на функцию в качестве аргумента
Поскольку указатель на функцию — вполне законный тип, любая функция может иметь параметр типа указателя на функцию. Такая функция затем может вызывать функцию, указатель на которую ей передан в аргументе. Поскольку указатель может быть переустановлен на другую функцию в других обстоятельствах, это позволяет определять вызывающей программе определять, какая функция должна быть вызвана из данной. В этом случае вы можете передать функцию явно в виде аргумента.
pdojt (27,6)
эквивалентно
sum (27,6) — дает в результате —> 33
Рис. 6.1. Указатель на функцию
| практическое занятие | передача указателя на функцию
В сказанном можно убедиться на следующем примере. Предположим, что вам нужна функция, обрабатывающая массив чисел, в одних случаях вычисляя сумму квадратов этих чисел, а в других — сумму кубов этих чисел. Одним из способов обеспечить такую возможность является применение указателя функции в качестве аргумента.
//Ехб_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;
}
И Функция для возведения значения в квадрат
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
Описание полученных результатов
Первый оператор, который представляет интерес — это прототип функции sumarray (). Ее третий параметр — указатель на функцию, принимающую параметр типа 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);II Прототип функции
double product(double, double);II Прототип функции
double difference(double, double); // Прототип функции
double (*pfun[3])(double,double) =
{ sum, product, difference };// Массив указателей на функции
Каждый из элементов массива инициализируется соответствующим адресом функции, указанным в фигурных скобках. Чтобы вызвать функцию product (), используя второй элемент массива указателей на функции, вы должны записать так:
pfun[l] (2.5, 3.5) ;
Квадратные скобки, в которых указывается индекс элемента массива указателей на функции, появляется немедленно после имени массива и перед аргументами вызываемой функции. Конечно, вы можете поместить вызов функции через обращение к элементу массива указателей на функции в любое выражение, где можно легитимно применять имя исходной функции, а значением индекса, выбирающего конкретный указатель из массива, может служить любое выражение, порождающее корректное целочисленное значение индекса.
Инициализация параметров функций
Со всеми функциями, которые использовались до сих пор, вы должны были позаботиться о передаче аргументов, соответствующих каждому из параметров вызываемой функции. Было бы довольно удобно, если бы при вызове функции можно было пропускать один или более аргументов, имеющих значения по умолчанию, чтобы эти значения передавались автоматически. Этого можно добиться, инициализируя параметры функции в ее прототипе.
Например, представим, что вы пишете функцию для отображения сообщения, где отображаемое сообщение передается в аргументе. Вот ее определение:
void showit(const char message[]) { cout « endl
« message; return;
}
Вы можете инициализировать параметр этой функции, специфицируя начальное строковое значение в прототипе функции, как показано ниже:
void showit(const char messaged = "Что-то не так.");
Здесь параметр message инициализируется указанным строковым литералом. Если вы инициализируете параметр функции в прототипе, то в случае пропуска аргумента при ее вызове, используется это значение инициализации.
Практическое занятие ПрОПуСК арГуМСНТОВ фуНКЦИИ
Пропуск аргумента при вызове функции приводит к тому, что она выполняется с параметром по умолчанию. Если вы применяете аргумент, он заменяет значение по умолчанию. Вы можете использовать функцию showit () для вывода широкого разнообразия сообщений.
//Ехб_03.срр
Н Пропуск аргументов функции
#include <iostream>
using std::cout;
using std::endl;
void showit(const char message[] = "Что-то не так.");
int main(void)
const char mymess[] = "Близится конец рабочего дня.";
showitQ;// Отобразить базовое сообщение
showit("Что-то ужасно не так!");// Отобразить альтернативное сообщение showitQ;// Опять отобразить сообщение по умолчанию
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 возбуждает исключение, если запрошенная вами память не может быть выделена.
До сих пор вы обычно обрабатывали ошибочные условия в программах, используя оператор 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[]) {
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 рассматриваются компилятором как единое целое, поэтому оба они формируют блок — тело цикла for — и работа цикла продолжается после возбуждения исключения.
Деление выполняется в операторе вывода, следующем за оператором if, который проверяет делитель. Когда выполняется оператор throw, управление немедленно передается первому оператору блока catch, так что оператор, выполняющий деление, в случае возникновения исключения пропускается. После того, как выполнится оператор блока catch, цикл продолжается со следующей итерации, если проверочное условие цикла истинно.
Возбуждение исключений
Исключения могут быть возбуждены в любом месте блока try, и операнд оператора throw определяет тип исключения — исключение, которое возбуждается в этом примере, является строковым литералом, а потому имеет тип const char [ ]. Операнд, следующий за ключевым словом throw, может быть любым выражением, и тип результата вычисления этого выражения определяет тип возбужденного исключения.
Исключения также могут быть возбуждены из функций, вызываемых изнутри блока try, и перехвачены блоком catch, следующим за этим блоком try. Чтобы продемонстрировать это, к предыдущему примеру вы можете добавить функцию со следующим определением:
void testThrow(void)
throw ’’Нулевое количество — вычисление невозможно.”;
Затем вы помещаете вызов этой функции в предыдущий пример на место оператора 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 {
while (ch — ’у’| I ch ==’Y’) {
cout « "Введите высоту в дюймах: ";
cin » height; // Читать высоту, подлежащую преобразованию
tryН Определение блока try, в котором
{II могут быть возбуждены исключения
if (height > 100)
throw "Высота превышает максимум"; // Исключение возбуждено if (height < 9)
throw height;// Исключение возбуждено
cout « static_cast<double>(height)*inchesToMeters
« ” метров" « endl;
}
catch(const char aMessage[]) // начало блока catch, который
{/ / перехватывает исключения типа
cout « aMessage « endl; // const char[]
}
cout « "Хотите продолжить (у или n)?"; cin » ch;
I } 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, которые возбуждают исключения. Эти функции обычно возбуж-
дают исключения типов классов (вы узнаете о типах классов перед тем, как начнете использовать 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;
}
На моей машине этот пример выдал следующее:
Не удалось выделить память.
Информация из объекта исключения: 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;
Теперь вам нужно создать функцию, которая выдает максимальное значение из массива типа 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) 1 : 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);
Фактически, хотя этот вызов функции max () не имеет особого смысла, потому что возвращаемое значение игнорируется, все же он не является незаконным. Если бы тип возврата служил признаком различия перегруженных функций, то в данном случае компилятор не мог бы решить, какую версию функции следует вызвать — возвращающую значение long или же возвращающую double. По этой причине тип возврата не рассматривается как средство различия перегруженных функций.
Фактически о каждой функции (не только перегруженной) говорят, что она имеет сигнатуру, определяемую ее именем и списком параметров. Все функции в программе должны иметь уникальные сигнатуры, иначе программа не компилируется.
практическое занятие | использование перегруженных функций
Вы можете испытать средство перегрузки на примере описанной функции max (). Следующий пример включает три версии — для массивов типа int, long и double.
// Ех6_07.срр
// Использование перегруженных функций
#include <iostream>
using std::cout;
using std::endl;
int max(int array[], int len);H Прототип для
long max(long array[], int len); H набора перегруженных double max(double array[], int len); // функций
int main(void) {
int small[] = {1, 24, 34, 22};
longmedium[] = {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; }
H Максимум из массива 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; }
// Максимум из массива 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;
// Максимум из массива 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 () для предыдущего примера.
Использование шаблона функции
Шаблон для функции шах () можно определить следующим образом:
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;
}
Создание конкретного экземпляра функции называется реализацией (instantiation).
Всякий раз, когда вы используете функцию max () в своей программе, компилятор проверяет, не существует ли уже функция с такими типами аргументов, как вы использовали. Если требуемая функция не существует, компилятор создает ее, подставляя тип аргумента, использованный вами в ее вызове, на место параметра Т по всему исходному коду в соответствующем определении шаблона. Вы можете поупражняться в применении шаблона функции max () в той же функции main (), что была использована в предыдущем примере.
Практическое занятие | Использование ШабЛОНа фУНКЦИИ
Ниже приведена версия предыдущего примера, модифицированная с целью использования шаблона функции max ().
И Ех6_08.срр
/ / Использование шаблона функции
#include <iostream>
using std::cout;
using std::endl;
/ / Шаблон функции для поиска максимального элемента массива 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 () с одним из типов, использованных ранее, новая версия кода функции не генерируется.
Обратите внимание, что использование шаблона не уменьшило размера скомпилированной программы. Компилятор генерирует версию исходного кода для каждой функции, которую вы запросили. На самом деле применение шаблонов обычно ^вели-
чивает размер ваших программ, поскольку версии функций создаются автоматически, даже несмотря на то, что существующие версии могут быть успешно применены с соответствующим приведением типов аргументов. Вы можете принудительно форсировать создание определенных экземпляров шаблона, явно включив их объявления. Например, если вы хотите гарантировать создание экземпляра функции max (), соответствующего типу float, то можете поместить следующее объявление после определения шаблона:
float max(float, int);
Это форсирует создание такой версии шаблонной функции. Для нашего примера это не имеет особого значения, но может оказаться удобным, когда вы знаете, что некоторые версии шаблонной функции могут быть сгенерированы, но хотите форсировать генерацию подмножества, которое планируете использовать с приведением аргументов к соответствующему типу при необходимости.
Пример использования функций
До настоящего момента вы уже получили солидную базу знаний о C++, и только из одной этой главы узнали достаточно много о функциях. После преодоления вброд целого моря разнообразных языковых средств не всегда бывает легко увидеть, как они связаны друг с другом. Теперь настал подходящий момент, чтобы посмотреть, как некоторые из изученных нами средств можно использовать вместе, чтобы создать нечто более сложное, чем простенькая демонстрационная программа.
Давайте рассмотрим более реалистичный пример, чтобы увидеть, как одна проблема может быть разбита на несколько функций. Процесс включает формулирование проблемы, которую нужно решить, ее анализ, направленный на прояснение того, как это можно реализовать на C++, и, наконец, написание кода. Подход к следующему примеру направлен на то, чтобы проиллюстрировать, как различные функции могут работать совместно для достижения финального результата, а не представлять руководство по разработке программ.
Реализация калькулятора
Предположим, что вам нужна программа, которая работает подобно калькулятору — но только не одно их этих причудливых устройств с множеством кнопок и штучек, придуманных для тех, кого легко этим порадовать, а такое устройство, которое предназначено для людей, знающих, куда они идут, выражаясь арифметически. Вы сможете просто запустить его и инициировать вычисление с клавиатуры как единое арифметическое выражение, получая результат немедленно. Вот пример того, что можно будет ввести:
2*3.14159*12.6*12.6 / 2 + 25.2*25.2
Чтобы пока избежать ненужного усложнения, мы не будем разрешать скобки в выражениях, кроме того, все выражение должно быть введено в одной строке; однако, чтобы обеспечить пользователю ввод выражений, выглядящих более привлекательно, мы разрешим вставлять куда угодно пробелы. Введенное выражение может содержать операции умножения, деления, сложения и вычитания, представленные, соответственно, символами *, /, + и -. Выражения будут вычисляться согласно обычным правилам арифметики, поэтому умножение и деление будут иметь приоритет перед сложением и вычитанием.
Программа должна обеспечивать выполнение стольких последовательных вычислений, сколько потребуется, и должна прекращать свою работу при вводе пустой строки. Кроме того, она должна уметь выдавать полезные и дружественные сообщения об ошибках.
Анализ проблемы
Хорошим местом старта является ввод. Программа читает арифметическое выражение любой длины из одной строки, которое может представлять собой любую конструкцию в пределах описанных терминов. Поскольку никак элементы, составляющие выражение, не фиксируются, вы должны читать его как строку символов, а затем в программе разбирать и обрабатывать соответствующим образом. Можно решить произвольно, что строка выражения не превысит по длине 80 символов, поэтому вы сможете поместить ее в символьный массив, объявленный следующим образом:
const int MAX = 80; И Максимальная длина выражения, включая '\0* char buffer[МАХ]; И Область ввода вычисляемого выражения
Чтобы изменить максимальную длину обрабатываемой программой строки, вам придется изменить только начальное значение МАХ.
Вам нужно понять базовую структуру информации, поступающей из входной строки, поэтому давайте разберем ее шаг за шагом.
Для начала надо исключить из строки все лишнее, поэтому перед тем, как приступить к ее анализу, нужно удалить все пробелы. Назовем функцию, которая сделает это, 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Индекс] 0 1 2 3 4 5 6 75 * 3 \05 * 3 \03 \03 \0Индекс i 0 1 2 3 4 5Индекс i 0 1 2 3 4 5Индекс i не увеличивается в этой позиции, потому что обнаружен пробел. Пробелы перезаписываются следующим непробельным символом, найденным в буфереИндекс i не увеличивается в этой позиции, потому что обнаружен пробел. Пробелы перезаписываются следующим непробельным символом, найденным в буфере
Индекс] 0 1 2 3 4 5 6 7
Индекс] 0 1 2 3 4 5 6 7
5 * 3 \0
5 * 3 \0
3 \0
3 \0
Индекс i 0 1 2 3 4 5
Индекс i 0 1 2 3 4 5
Индекс i не увеличивается в этой позиции, потому что обнаружен пробел. Пробелы перезаписываются следующим непробельным символом, найденным в буфере
Индекс i не увеличивается в этой позиции, потому что обнаружен пробел. Пробелы перезаписываются следующим непробельным символом, найденным в буфере
Буферный массив после копирования его содержимого на себя
Рис. 6.2. Изъятие пробелов из введенной пользователем строки
Операции сложения и вычитания имеют наименьший приоритет, а потому выполняются последними. Можно рассматривать строку, как состоящую из одного или более элементов, соединенных между собой операциями, которые могут быть либо +, либо -. Будем называть эти операции addop. По этой терминологии можно представить общую форму входного выражения следующим образом:
выражение: элемент addop элемент . . . addop элемент
Выражение содержит как минимум один элемент и может иметь произвольное число следующих за ним комбинаций addop элемент. Фактически, если предположить, что удалены все пробелы, существует только три легальных возможности появления символов, следующих за элементом.
Следующий символ — ' \0', то есть, обнаружен конец строки.
Следующий символ — ' - '. В этом случае нужно вычесть следующий элемент из значения выражения, полученного до этой точки.
Следующий символ — ' + ’. В этом случае нужно прибавить значение следующего элемента к значению выражения, накопленного до этого момента.
Если за элементом следует что-то другое, значит, строка содержит нечто неожиданное, поэтому следует выдать сообщение об ошибке и завершить выполнение программы. На рис. 6.3 показана структура простого выражения.
Рис. 6.3. Структура простого выражения
Далее нам понадобится более детальное и точное определение элемента. Элемент — это просто последовательность чисел, соединенных операцией *, либо операцией /. Таким образом, элемент (в общем виде) выглядит так:
элемент: число multop число ... multop число
Здесь multop представляет либо операцию умножения, либо операцию деления. Можно определить функцию term (), которая будет возвращать значение элемента. Для этого потребуется сканировать строку до обнаружения первого числа, а затем найти mu 1 top, за которым следует другое число. Если будет обнаружен символ, не являющийся multop, то функция term() должна предположить, что это addop, и вернуть значение, которое было вычислено до этого момента.
Последнее, что нам нужно решить перед тем, как писать программу — это как распознать число. Чтобы упростить код, будем распознавать только беззнаковые числа; таким образом, число состоит из последовательности десятичных цифр, за которыми необязательно может следовать десятичная точка и еще несколько десятичных цифр. Чтобы определить значение числа, следует пройти по буферу в поисках десятичных цифр. Если обнаружится нечто, не являющееся десятичным числом, нужно проверить, не является ли это нечто десятичной точкой. Если это не точка, то с числом
далее делать нечего, поэтому должно быть возвращено то, что получено до этого момента. Если же обнаружится десятичная точка, потребуется продолжить поиск десятичных цифр. Как только будет найдено нечто, не являющееся десятичным числом, значит, следует завершить формирование числа и вернуть его.
Предположим, что мы назовем функцию, распознающую число и возвращающую его значение, как number (). На рис. 6.4 показан пример разбиения выражения на элементы и числа.
ВыражениеВыражениеЭлемент addopЭлемент addopЭлементЭлементЧислоЧислоЧисло multop ЧислоЧисло multop Числора Цифрара Цифраaddop Элементaddop ЭлементЧислоЧислора Цифрара ЦифраЗначение выражения, возвращаемое функцией ехрг()Значение каждого элемента, возвращаемое функцией term()Значение выражения, возвращаемое функцией ехрг()Значение каждого элемента, возвращаемое функцией term()(дифра Точка Цифра'(дифра Точка Цифра'Цифра ЦифраЦифра ЦифраЗначение каждого числа, возвращаемое функцией number()Значение каждого числа, возвращаемое функцией number()Рис. 6.4. Пример разбиения выражения на элементы и числаРис. 6.4. Пример разбиения выражения на элементы и числа
Выражение
Выражение
Элемент addop
Элемент addop
Элемент
Элемент
Число
Число
Число multop Число
Число multop Число
ра Цифра
ра Цифра
addop Элемент
addop Элемент
Число
Число
ра Цифра
ра Цифра
Значение выражения, возвращаемое функцией ехрг()
Значение каждого элемента, возвращаемое функцией term()
Значение выражения, возвращаемое функцией ехрг()
Значение каждого элемента, возвращаемое функцией term()
(дифра Точка Цифра'
(дифра Точка Цифра'
Цифра Цифра
Цифра Цифра
Значение каждого числа, возвращаемое функцией number()
Значение каждого числа, возвращаемое функцией number()
Рис. 6.4. Пример разбиения выражения на элементы и числа
Рис. 6.4. Пример разбиения выражения на элементы и числа
Теперь мы достигли достаточного понимания проблемы, чтобы написать некоторый код. Сначала разработаем все необходимые функции, а затем напишем функцию main (), в которой соберем их все вместе. Первая, и, возможно, самая простая функция, которую потребуется написать — это eatspaces (), которая исключит все пробелы из входной строки.
Удаление пробелов из строки
Прототип функции eatspaces () будет таким:
void eatspaces(char* str); И Функция для удаления пробелов
Эта функция не должна возвращать никаких значений, потому что пробелы можно удалить из строки ио месту, модифицируя исходную строку непосредственно, через указатель, переданный в качестве аргумента. Процесс удаления пробелов очень прост. Вы копируете строку в саму себя, перезаписывая все пробелы, как объяснялось выше.
Определение функции может выглядеть следующим образом:
II Функция для удаления пробелов из строки void eatspaces(char* str)
int i = 0;// Индекс места в строке, ’куда копировать’
int j = 0;// Индекс места в строке, ’откуда копировать’
while ((* (str + i) = *(str + j++)) != ’\0’) II Цикл, пока очередной // символ не ’ \0’
if(*(str + i) != ’ ’)// Инкремент i до тех пор,
i++;И пока символ — не пробел
return; }
Как работает функция
Все действие происходит в цикле while. Условие цикла копирует строку, перемещая символ из позиции j в позицию i, а затем увеличивая j до следующего символа. Если скопирован символ ’ \ 0 ', значит, достигнут конец строки и работа окончена.
Единственное действие в теле цикла — увеличение i до следующего символа, если последний скопированный символ не был пробелом. Если же этот символ — пробел, инкремент i не выполняется, и потому пробел будет перезаписан символом, скопированным на следующей итерации.
Не так уж трудно, не правда ли? Теперь попробуем написать функцию, которая вернет результат вычисления выражения.
Вычисление выражения
Функция ехрг() возвращает значение выражения, специфицированного в строке, переданной в качестве аргумента, поэтому вы можете написать ее прототип следующим образом:
double ехрг(char* str); И Функция, вычисляющая выражение
Объявленная здесь функция принимает строку как аргумент и возвращает результат типа double. На основе структуры выражения, которую мы исследовали ранее, можно нарисовать логическую диаграмму процесса вычисления выражения, как показано на рис. 6.5.
Используя это базовое описание логики, можем написать функцию:
// Функция для вычисления арифметического выражения
double ехрг(char* str)
{
double value = 0.0;H Здесь сохранять результат
int index = 0;// Текущая позиция символа
value = term(str, index); И Получить первый элемент
for(;;)// Бесконечный цикл, выход - внутри
{
switch(*(str + index+t)) И Выбрать действие на основе текущего символа {
case *\0’:II найден конец строки,
return value; // поэтому возвращаем то, что имеем
case ’ + ’ :// найден знак плюс, значит - сложение
value += term(str, index); И следующий элемент break;
case ’-’:// найден знак минус, значит - вычитание
value -= term(str, index); II следующий элемент break;
default:// Если то, что найдено в строке -
cout « endl / / мусор
« "Эй, повнимательней можно?! Здесь обнаружена ошибка”
« endl;
exit (1) ;
}
}
}
Рис. 6.5. Логическая диаграмма процесса вычисления выражения
Как работает функция
Учитывая, что эта функция выполняет анализ переданного ей арифметического выражения (при условии, что в нем используются только описанные операции), в ней не так много кода. Мы определили переменную index типа int для отслеживания текущей позиции в строке, с которой мы работаем, и инициализировали ее значением 0, что соответствует позиции первого символа строки. Также определена переменная value типа double, в которой накапливается значение выражения, переданного в функцию в символьном (типа char) массиве str.
Поскольку выражение должно содержать как минимум один элемент, первое действие этой функции — получение значения первого элемента вызовом функции term (), которую еще предстоит написать. Это позволяет сразу сформулировать три требования к функции term ().
Она должна принимать в качестве параметров указатель char* и переменную int, причем второй параметр — это индекс первого символа элемента в строке.
Она должна обновлять переданное ей значение индекса, устанавливая его в позицию, следующую за последним символом найденного элемента.
Она должна возвращать значение элемента типа double.
Остальная часть программы — это бесконечный цикл for. Внутри цикла текущее действие определяется оператором switch, который управляется текущим символом строки. Если этот символ — ' +', вызывается функция term (), чтобы получить значение следующего элемента выражения и прибавить его к переменной value. Если же этот символ — ’ - 1, то значение, возвращенное 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;II Здесь накапливается значение результата
value = number(str, index); // Получить первое число элемента
// Выполнять цикл, пока имеем допустимую операцию while((*(str + index) ==***) | | (*(str + index) == * / ') ) {
if(*(str + index) ==***)// Если это знак умножения,
value *= number(str, ++index); II умножить на следующее число
if(*(str + index) ==*/*)// Если знак деления,
value /= number(str, ++index);// разделить на следующее число
}
return value;// Готово, возвращаем то, что получилось
}
Как работает функция
Сначала мы объявляем локальную переменную value типа double, в которой будем накапливать значение текущего элемента. Поскольку элемент должен содержать, как минимум, одно число, первое действие этой функции — получение значения первого числа элемента вызовом функции number () и сохранением результата в value. Предполагается, что number () в качестве аргументов принимает строку и индекс позиции в строке, а возвращает значение найденного числа. Поскольку number () может обновить индекс позиции в строке после того, как прочтет в ней число, при определении этой функции мы опять-таки специфицируем второй параметр как ссылку.
Остальная часть функции term () — это цикл while, который продолжается до тех пор, пока следующий символ строки будет знаком умножения — 1 * 1, либо деления — 1 / 1. Внутри цикла, если в текущей позиции найден ’ *1, значение index увеличивается до позиции начала следующего числа, вызывается функция number (), чтобы получить значение следующего числа, после чего текущее значение value умножается на возвращенное значение. Аналогично, если текущий символ — ' / ', значение index также увеличивается, а значение value делится на число, возвращенное number (). Поскольку функция number () автоматически изменяет значение переменной index, так что оно устанавливается на символ, следующий за найденным числом, index оказывается установленным на следующий символ строки, и все готово к следующей итерации.
Цикл прерывается, когда обнаруживается символ, отличный от знаков умножения и деления, и накопленное текущее значение value возвращается вызывающей программе.
Последняя аналитическая функция, которая нам понадобится — это number (), определяющая очередное содержащееся в строке числовое значение.
Анализ числа
Основываясь на способе использования функции number () внутри функции term (), объявим ее прототип:
double number(char* str, int& index); И Функция, распознающая число
Спецификация второго параметра как ссылки позволяет функции обновлять значение переданного ей аргумента непосредственно, что и требовалось.
Здесь можно использовать функцию из стандартной библиотеки C++. Заголовочный файл <cctype> предоставляет определение диапазона функций для тестирования отдельных символов. Эти функции возвращают значения типа int, причем положительные значения соответствуют true, а ноль — false. Четыре из этих функций перечислены в табл. 6.1.
Таблица 6.1. Некоторые функции из <cctype>
Функция из <cctype>
Результат тестирования отдельных символов
int isalpha(int с)
Возвращает true, если аргумент — буква, иначе — false.
int isupper(int с)
Возвращает true, если аргумент — буква верхнего регистра, иначе — false.
int islower(int c)
Возвращает true, если аргумент — буква нижнего регистра, иначе — false.
int isdigit(int c)
Возвращает true, если аргумент — десятичная цифра, иначе — false.
Существует множество других функций, представленных <cctype>, но пока я не хотел бы погружаться в детали. Если интересуетесь, загляните в справочную систему MSDN Library Help. Поиск по строке “is routines” должен найти их.
Итак, нам осталось написать последнюю функцию для программы. Запомните, что isdigit () проверяет символ, например, такой как ’ 91 (ASCII-код 57 в десятичной нотации), а не число 9, поскольку на входе появляется строка.
Определение number () может выглядеть следующим образом:
II Функция для распознавания числа в строке double number(char* str, int& index) {
double value = 0.0;// Хранит результирующее значение
while(isdigit(*(str + index)))// Цикл накапливает ведущие цифры
value = 10*value + (*(str + index++) - * O');
// Если не цифра,
if(*(str + index) !=’.’)// то проверяем на десятичную точку,
return value;// и если это не точка, возвращаем значение
double factor =1.0;И Множитель для десятичных разрядов
while(isdigit(*(str + (++index)))) II Выполнять цикл, пока идут цифры {
factor *=0.1;// Уменьшить factor в 10 раз
value = value + (*(str + index) - ' 0*)*factor; / / Добавить десятичную
}return value;}return value;// позицию
}
return value;
}
return value;
/ / По выходе из цикла возвращаем значение
}
Как работает функция
Объявляем локальную переменную value типа double, которая будет хранить значение найденного числа. Инициализируем ее 0.0, поскольку значение будет накапливаться по мере чтения цифр.
Поскольку число представлено в строке как последовательность символов ASCII, функция проходит по строке, накапливая значение числа разряд за разрядом. Это происходит в две фазы — в первой накапливаются цифры до десятичной точки, а после обнаружения десятичной точки начинается вторая фаза — накопление разрядов после нее.
Первый шаг в цикле while продолжается до тех пор, пока текущий выбранный символ строки является десятичной цифрой. Значение цифры извлекается и прибавляется к значению переменной value в теле цикла:
value = 10*value + (*(str + index++) - ' O');
Эта конструкция требует более пристального рассмотрения. Символы десятичных цифр имеют ASCII-коды между 48, что соответствует ' 0 ’, и 57, соответствующий 1 91. Таким образом, если вычесть ASCII-код символа 1 0 1 из кода цифры, это преобразует ее в эквивалентное число от 0 до 9. Подвыражение * (str + index++) - * 0 ’ взято в скобки; это не существенно, но скобки несколько проясняют то, что здесь происходит. Значение переменной value умножается на 10, чтобы сдвинуть его на один десятичный разряд влево перед тем, как добавить значение цифры, поскольку они выбираются слева направо — то есть, самая значащая цифра идет первой. Описанный процесс показан на рис. 6.6.
цифры в числе
Л
Третья value = 10*value + (51 - 48)
Цифра=10*51.0 + 3
= 513.0
Рис. 6.6. Получение целой части десятичного числа из строки
Как только встречается что-то, отличное от десятичной цифры, это будет либо десятичная точка, либо нечто другое. Если это не десятичная точка, обработка завершается, и в вызывающую программу возвращается значение переменной value. Если же очередной символ — десятичная точка, то в следующем цикле начинается накопление цифр, составляющих дробную часть числа. В этом цикле используется переменная factor, имеющая начальное значение 1.0, чтобы устанавливать текущую десятичную позицию, и эта переменная последовательно умножается на 0.1 при каждой найденной цифре после точки. То есть, первая цифра после точки умножается на 0.1, вторая — на 0.01, третья — на 0.001 и так далее. Этот процесс показан рис. 6.7.
десятичные цифры целой части числадесятичные цифры целой части числадесятичные цифрыдробной части числадесятичные цифрыдробной части числа
десятичные цифры целой части числа
десятичные цифры целой части числа
десятичные цифрыдробной части числа
десятичные цифрыдробной части числа
13
ASCII-коды как десятичные значенияASCII-коды как десятичные значения53 49 51 46 54 49 5653 49 51 46 54 49 56
ASCII-коды как десятичные значения
ASCII-коды как десятичные значения
53 49 51 46 54 49 56
53 49 51 46 54 49 56
До десятичной точки value = 513.0 factor = 1.0До десятичной точки value = 513.0 factor = 1.0Первая цифраПервая цифраfactor = 0.1*factorfactor = 0.1*factorvalue = value + factor*(54 - 48)= 513.0 + 0.1*6value = value + factor*(54 - 48)= 513.0 + 0.1*6= 513.6= 513.6Вторая цифраВторая цифраfactor = 0.1‘factor ▼value = value + factor*(49 - 48)factor = 0.1‘factor ▼value = value + factor*(49 - 48)= 513.6 + 0.01*0= 513.60= 513.6 + 0.01*0= 513.60Третья цифраТретья цифраfactor = 0.1‘factor vvalue = value + factor* (56 - 48)factor = 0.1‘factor vvalue = value + factor* (56 - 48)= 513.60 + 0.001*8= 513.608= 513.60 + 0.001*8= 513.608Puc. 6.7. Получение дробной части десятичного числа из строкиPuc. 6.7. Получение дробной части десятичного числа из строкиКак только обнаруживается символ, не являющийся десятичным числом, обработка завершается, поэтому после второго цикла значение переменной value возвращается в вызывающую программу. Теперь почти все готово. Осталось написать лишь функцию main (), чтобы прочесть ввод и запустить процесс.Как только обнаруживается символ, не являющийся десятичным числом, обработка завершается, поэтому после второго цикла значение переменной value возвращается в вызывающую программу. Теперь почти все готово. Осталось написать лишь функцию main (), чтобы прочесть ввод и запустить процесс.Собираем программу вместеСобираем программу вместеМы можем собрать вместе все операторы #include и прототипы всех функций в начале программы:// Ех6_09.сррII Программа, реализующая калькуляторМы можем собрать вместе все операторы #include и прототипы всех функций в начале программы:// Ех6_09.сррII Программа, реализующая калькулятор#include <iostream>#include <cstdlib>#include <cctype>#include <iostream>#include <cstdlib>#include <cctype>Для потокового ввода-выводаДля функции exit ()Для функции isdigitOДля потокового ввода-выводаДля функции exit ()Для функции isdigitO
До десятичной точки value = 513.0 factor = 1.0
До десятичной точки value = 513.0 factor = 1.0
Первая цифра
Первая цифра
factor = 0.1*factor
factor = 0.1*factor
value = value + factor*(54 - 48)= 513.0 + 0.1*6
value = value + factor*(54 - 48)= 513.0 + 0.1*6
= 513.6
= 513.6
Вторая цифра
Вторая цифра
factor = 0.1‘factor ▼
value = value + factor*(49 - 48)
factor = 0.1‘factor ▼
value = value + factor*(49 - 48)
= 513.6 + 0.01*0
= 513.60
= 513.6 + 0.01*0
= 513.60
Третья цифра
Третья цифра
factor = 0.1‘factor v
value = value + factor* (56 - 48)
factor = 0.1‘factor v
value = value + factor* (56 - 48)
= 513.60 + 0.001*8
= 513.608
= 513.60 + 0.001*8
= 513.608
Puc. 6.7. Получение дробной части десятичного числа из строки
Puc. 6.7. Получение дробной части десятичного числа из строки
Как только обнаруживается символ, не являющийся десятичным числом, обработка завершается, поэтому после второго цикла значение переменной value возвращается в вызывающую программу. Теперь почти все готово. Осталось написать лишь функцию main (), чтобы прочесть ввод и запустить процесс.
Как только обнаруживается символ, не являющийся десятичным числом, обработка завершается, поэтому после второго цикла значение переменной value возвращается в вызывающую программу. Теперь почти все готово. Осталось написать лишь функцию main (), чтобы прочесть ввод и запустить процесс.
Собираем программу вместе
Собираем программу вместе
Мы можем собрать вместе все операторы #include и прототипы всех функций в начале программы:
// Ех6_09.срр
II Программа, реализующая калькулятор
Мы можем собрать вместе все операторы #include и прототипы всех функций в начале программы:
// Ех6_09.срр
II Программа, реализующая калькулятор
#include <iostream>
#include <cstdlib>
#include <cctype>
#include <iostream>
#include <cstdlib>
#include <cctype>
Для потокового ввода-вывода
Для функции exit ()
Для функции isdigitO
Для потокового ввода-вывода
Для функции exit ()
Для функции isdigitO
using std::cin;
using std::cout;
using std::endl;
void eatspaces(char* str);H Функция удаления пробелов
double ехрг(char* str);// Функция вычисления выражения
double term(char* str, int& index); // Функция анализа элемента double number(char* str, int& index); И Функция распознавания числа
const int MAX = 80; H Максимальная длина выражения, включая '\0’
Мы также определили глобальную переменную МАХ, хранящую максимальное количество символов в выражении, обрабатываемом программой (включая ограничивающий символ ' \ 0 ’).
Теперь мы можем добавить определение функции main (), и программа будет готова. Функция main () должна читать строку и завершаться, если эта строка пуста, в противном случае вызывать ехрг () для вычисления ввода и отображения результата. Этот процесс должен повторяться бесконечно. Звучит несколько сложно, поэтому давайте попытаемся реализовать сказанное.
int main () {
char buffer[MAX] = {0}; И Область хранения вычисляемого входного выражения cout « endl
« "Добро пожаловать в дружественный калькулятор!"
« endl
« "Введите выражение или пустую строку для завершения."
« endl;
for(;;)
{
cin.getline(buffer, sizeof buffer); H Читать входную строку eatspaces(buffer); И Удалить пробелы из строки
if(!buffer[0])И Пустая строка — признак завершения работы
return 0;
cout « "\t= " « expr(buffer)H Вывести значение выражения
« endl « endl;
}
}
Как работает функция
В main () объявляется символьный массив buffer, который будет принимать выражения до 80 символов длиной (включая символ — ограничитель строки). Выражение читается внутри бесконечного цикла for посредством функции ввода getline (), и после получения ввода с помощью вызова eatspaces () из строки удаляются пробелы.
Все остальное, что делается в функции main (), происходит внутри цикла. Здесь проверяется пустая строка, которая состоит из нулевого символа ’ \ 0 ’ — в этом случае программа завершается, и здесь же выводится значение вычисленного функцией ехрг () выражения.
После того, как в программу будет введен код всех функций и программа скомпилирована, то запустив ее, мы получим:
2 * 35
= 70
2/3 + 3/4 + 4/5 + 5/6 + 6/7
= 3.90714
+ 2.5 + 2.5*2.5 + 2.5*2.5*2.5
= 25.375
Вы можете выполнять столько вычислений, сколько захотите, и когда надоест — просто нажмите <Enter> для завершения программы.
Расширение программы
Теперь, когда мы имеем работающий калькулятор, можно подумать о его усовершенствовании. Не правда ли, было бы неплохо, если бы он понимал скобки? Это же должно быть не сложно? Давайте попробуем.
Подумаем об отношении между тем, что находится в скобках, и способом анализа выражений, использованном до сих пор. Возьмем пример выражения, которое хотелось бы обрабатывать:
2*(3 + 4) / 6 - (5+6) / (7+8)
Обратите внимание, что выражение в скобках всегда формирует часть, которую можно воспринимать как элемент в нашей исходной терминологии. Какого бы рода вычисления ни пришлось выполнять, это всегда верно. Фактически, если мы сможем подставлять значение выражений в скобках обратно в исходную строку, то получим нечто такое, с чем мы уже имели дело. Это указывает на возможный подход к обработке скобок. Выражение в скобках можно трактовать как еще одно число, и модифицировать функцию number () таким образом, чтобы она упорядочивала любое значение, появляющееся в скобках.
Это звучит, как неплохая идея, но об “упорядочивании” выражения в скобках нужно немного задуматься: ключ к успеху лежит в используемой здесь терминологии. Выражение, появляющееся в скобках — исключительно удачный пример полноценного выражения, а у нас уже есть функция ехрг (), которая возвращает значения выражений. Если мы научим функцию number () обрабатывать содержимое скобок и извлекать его из строки, то можно будет передавать эту подстроку функции ехрг (), то есть рекурсия должна действительно упростить проблему. Что еще лучше — не придется заботиться о вложенных скобках. Поскольку любая пара скобок содержит то, что определяется как выражение, их обработка выполнится автоматически. Опять побеждает рекурсия.
Попробуем переписать функцию number () так, чтобы она распознавала выражения в скобках.
II Функция для распознавания выражения
/ / в скобках или числа в строке
double number(char* str, int& index) {
double value =0.0;// Хранит результирующее значение
if(*(str + index) ==’(')// Открытие скобки
char* psubstr = 0;// Указатель на подстроку
psubstr = extract(str, ++index); II Извлечь подстроку в скобках value = ехрг(psubstr); II Получить значение выражения-подстроки delete[]psubstr;// Освободить память в свободном хранилище
return value;// Вернуть значение выражения-подстроки
}
while(isdigit(*(str + index))) // Накапливать в цикле ведущие цифры value = 10*value + (*(str + index++) - 48);
/ / Обнаружена не цифра
if(*(str + index)!='.') I/ Проверка на равенство десятичной точке return value;// Если не точка, вернуть значение
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); H Функция для извлечения подстроки
Обнаружение ’)' при значении счетчика ’(’
Отмечает начало подстрокиравным 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 (numL == 0) {
buffer[index - bufindex] = *\0*; // Заменить ’)’ на *\0* ++index;
pstr = new char [index - bufindex]; if(Ipstr) (
cout « ’’Выделение памяти не удалось,” « " программа прервана.";
exit(1); }
strcpy_s(pstr, index-bufindex, buffer);//Копировать подстроку //в новую память
return pstr;Н Вернуть подстроку в новой памяти } else
numL—;// Уменьшить значение счетчика 1 (’
break; case * (*: numL++;// Увеличить значение счетчика * (*
break;
}
} while(*(str + index++) != ’\0’); И Цикл до конца строки
cout « "Выход за пределы выражения, возможно, плохой ввод."
« endl;
exit(1) ;
return pstr;
}
Как работает функция
Здесь объявлен массив char для временного хранения подстроки. Мы не знаем, насколько длинной будет подстрока, но она не может быть длиннее, чем МАХ символов. Мы не можем вернуть адрес buffer вызывающей функции, потому что это локальная переменная, которая уничтожается при выходе из функции, поэтому нужно выделить некоторое пространство памяти из свободного хранилища, когда становится ясно, какова длина строки. Это делается объявлением переменной pstr типа “указатель на char”, который будет возвращен по значению, когда подстрока будет скопирована в память из свободного хранилища.
Мы объявляем счетчик numb, который отслеживает левые скобки в подстроке (как я объяснил выше). Начальное значение index (когда функция начинает работу) сохраняется в переменной buf index. Она используется в комбинации с увеличенным значением index для индексации массива buffer.
Исполняемую часть функции составляет один большой цикл do-while. Подстрока копируется из str в buf fer по одному символу за каждую итерацию цикла, с проверкой появления левых и правых скобок на каждом шаге. Если обнаружена левая скобка, значение numb увеличивается, а если правая, и при этом numb больше нуля, то ее значение уменьшается. Если же при обнаружении правой скобки значение numb равно О, то это значит, что достигнут конец подстроки. Затем содержимое buffer копируется в область памяти, полученную операцией new, посредством функции strcpy_s (), объявленной в заголовочном файле <cstring>; это безопасная версия функции strcpy (), объявленной в том же заголовочном файле. Эта функция копирует строку, специфицированную третьим аргументом — 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
Эй, повнимательней можно?! Здесь обнаружена ошибка
Дружественное и информативное сообщение об ошибке в последней строке вывода вызвано применением запятой вместо десятичной точки в последнем введенном выражении, где должно было быть 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:
try
{
throw Ь"Поймай меня, если сможешь.";
}
catch(StringA ex) И Исключение здесь не будет перехвачено
{
Console::WriteLine(L"StringA: {0}”,ex);
}
Блок catch не может здесь перехватить возбужденный объект, потому что оператор throw генерирует исключение типа const wchar_t *, а не String*. Чтобы перехватить такое исключение, блок catch должен быть таким:
try
throw Ь"Поймай меня, если сможешь.";
}
catch (const wchar__t* ex) //OK. Возбуждено исключение этого типа
{
String* exc = gcnew String (ex) ;
Console::WriteLine (L"wchar—t: {0}", exc) ;
}
Этот блок catch перехватывает исключение, поскольку теперь имеет правильный тип.
Чтобы исключение было перехвачено исходным блоком catch, следовало бы изменить код блока try:
try { throw gcnew String (Ь"Поймай меня, если сможешь.");
}
catch(StringA ex) Н ОК. Возбуждено исключение этого типа {
Console::WriteLine(L"StringA: {0}", ex);
}
Теперь исключение представляет собой объект String, и брошено, как тип St ringА, — дескриптор, ссылающийся на строку.
Вы можете использовать шаблоны функций в программах C++/CLI, но в вашем распоряжении также имеется дополнительная возможность, называемая обобщенными функциями (generic functions), которые выглядят похоже, однако представляют собой нечто отличающееся.
Что такое обобщенные функции
Хотя обобщенные функции делают то же самое, что и шаблоны функций, а потому на первый взгляд выглядят излишними, все же, по сути, они работают иначе, чем шаблоны функций, и это отличие делает их полезным дополнением к программам CLR. Когда вы применяете шаблон функции, компилятор генерирует исходный код конкретных требуемых экземпляров функций; затем этот генерированный код компилируется вместе с остальным кодом вашей программы. В некоторых случаях это приводит к генерации большого числа функций и существенно увеличивает размер исполняемого кода. С другой стороны, код обобщенной функции сам по себе компилируется, и когда вызывается функция, отвечающая спецификации обобщенной функции, действительные параметры типа подставляются во время выполнения. Во время выполнения уе генерируется никакого дополнительного кода, отсюда не происходит такого “разбухания” кода, как в случае применения шаблонов функций.
Некоторые аспекты определения обобщенных функций требуют дополнительных знаний, которые будут изложены в последующих главах, и в конечном итоге вам все станет ясно. Пока что я приведу минимум объяснений тех вещей, которые покажутся новыми, а детали вы изучите позднее.
Определение обобщенных функций
Обобщенные функции определяются с параметрами типов, которые заменяются действительными типами при вызове функций. Ниже показан пример определения обобщенной функции.
generic<typename Т> where Т:IComparable
Т MaxElement(аггау<Т>А х) {
Т шах = х [ 0 ];
for(int i = 1; i < x->Length; i++)
if(max->CompareTo(x[i]) < 0) max = x [ i ] ; return max;
}
Эта обобщенная функция делает то же самое, что и шаблон функции родного C++, который вы видели ранее в этой главе. Ключевое слово generic в первой строке идентифицирует последующую обобщенную спецификацию, и первая строка определяет параметр типа функции как Т; ключевое слово typename в угловых скобках гово-
рит о том, что Т — имя параметра типа обобщенной функции, и этот параметр типа заменяется действительным типом при использовании данной функции. Для обобщенной функции с несколькими параметрами типа имена параметров заключаются в угловые скобки, снабжаются ключевым словом typename и отделяются друг от друга запятыми.
Ключевое слово where, следующее за закрывающей угловой скобкой, представляет ограничение (constraint), накладываемое на действительный тип, подставляемый вместо Т при вызове функции. Это конкретное ограничение говорит о том, что любой тип, который заменяет Т при вызове обобщенной функции, должен реализовывать интерфейс I Comp arable. Об интерфейсах вы узнаете позднее в этой книге, а пока я скажу, что он предполагает, что подставляемый тип должен определять функцию CompareTo (), которая позволяет сравнивать два объекта данного типа. Без этого ограничения компилятор не может знать, какие операции можно использовать с типом, заменяющим Т, потому что до того, как функция будет вызвана, это остается совершенно неизвестным. С этим ограничением вы можете использовать функцию CompareTo () для сравнения max со значениями элементов массива. Функция CompareTo () возвращает целочисленное значение, которое меньше нуля, если значение объекта, для которого она вызвана (в данном случае — max), меньше значения аргумента, ноль, если значение объекта равно значению аргумента, и больше ноля, если значение объекта больше значения аргумента.
Вторая строка специфицирует имя обобщенной функции MaxElement, которая возвращает тип Т, и список ее параметров. Это выглядит почти как заголовок обычной функции, с тем отличием, что здесь участвует обобщенный параметр типа Т. Тип возврата обобщенной функции и тип элемента массива являются частью спецификации параметра типа, и оба имеют тип Т, поэтому оба эти типа определяются в момент вызова обобщенной функции.
Использование обобщенных функций
Простейший способ вызова обобщенной функции — просто использовать ее, как любую другую обычную функцию. Например, обобщенную функцию MaxElement () из предыдущего раздела можно вызвать следующим образом:
array<double>A data = {1.5, 3.5, 6.7, 4.2, 2.1};
double maxData = MaxElement(data);
Компилятор может определить, что в данном случае аргумент обобщенной функции имеет тип double, и генерирует соответствующий код для ее вызова. Функция выполняется со всеми экземплярами Т, замененными на double. Как я сказал раньше, это не то же самое, что шаблон функции; здесь не происходит создания экземпляра функции во время компиляции. Скомпилированная обобщенная функция в состоянии обработать подстановку типов аргументов при вызове.
Обратите внимание, что если вы передадите строковый литерал в качестве аргумента обобщенной функции, компилятор определит его тип как StringA, независимо от того, будет ли это “узкая” строковая константа вроде "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;
// Обобщенная функция для вывода списка элементов массива generic<typename Т> where Т: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(Е"\пМассив содержит:");
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(Е"\пМассив содержит:");
ListElements(strings);
Console::WriteLine(Е"\пМаксимальный элемент = {0}\n", MaxElement(strings));
Console::WriteLine(L"\n После удаления максимума массив содержит:");
ListElements(RemoveElement(MaxElement(strings), strings));
return 0;
}
Вывод этого примера будет таким:
Массив содержит:
3.56.74.22.1
Максимальный элемент =6.7
После удаления максимума, массив содержит:
3.54.22.1
Массив содержит:
312701011
Максимальный элемент =12
После удаления максимума, массив содержит:
3701011
Массив содержит:
Manyhandsmakelightwork
Максимальный элемент = work
После удаления максимума, массив содержит:
Many hands make light
Press any key to continue . . .
Описание полученных результатов
Первая обобщенная функция, определенная в этом примере — MaxElement (), находящая максимальный элемент массива — идентична той, что вы видели в предыдущем разделе, поэтому обсуждать ее снова нет особой необходимости.
Следующая обобщенная функция — RemoveElements () — удаляет элемент, переданный ей в первом аргументе, из массива, указанного вторым аргументом, и возвращает дескриптор нового массива, получившегося в результате этой операции. Из первых двух строк определения функции вы можете видеть, что и тип параметра, и тип возврата определены через параметр типа Т.
generic<typename Т> where Т:IComparable
array<T>A RemoveElement(T element, array<T>A data)
На параметр типа T здесь накладывается то же ограничение, что и в первой обобщенной функции, и это ограничение предполагает, что тип, используемый в качестве аргумента, должен реализовывать функцию 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;
} else { if(index == newData->Length) {
Console::WriteLine(Ь"Элемент для удаления не найден"); return data;
} newData[index++] = item;
} }
Копируются все элементы, кроме того, что идентифицирован первым аргументом функции. Переменная index служит для указания следующего элемента массива newData, которому присваивается значение следующего элемента массива data. Каждый элемент data копируется, только если он не равен element, в этом случае found устанавливается в true, и оператор continue пропускает следующую итерацию. Вполне может быть, что массив может содержать более одного элемента, равного первому аргументу, и переменная found предотвращает пропуск при копировании последующих элементов, равных element.
Необходимо также проверить переменную index на предмет вхождения в разрешенные пределы индексации элементов newData. Это может понадобиться в случае, если значение, переданное в первом аргументе функции, не будет найдено в массиве 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 и StringA. Таким образом, вы можете видеть, как все три обобщенных функции работают с типами значений и дескрипторами. Во втором и третьем примерах обобщенные функции используются в комбинации, в одном операторе. Например, взглянем на следующий оператор в третьем примере применения функций:
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); H Функция распознавания числа StringA extract(StringA str, intA index);// Функция извлечения подстроки
Все параметры теперь — дескрипторы; строковые параметры имеют тип StringA, а параметры индекса, хранящие текущую позицию строки — также дескрипторы типа intA. Конечно же, строки возвращаются как StringA.
Ниже показана реализация функции main().
int main(array<System::String A> Aargs) {
StringA buffer; // Область хранения вычисляемого входного выражения Console::WriteLine(Ь"Добро пожаловать в дружественный калькулятор!"); Console::WriteLine(Ь"Введите выражение или пустую строку для завершения."); for(;;) {
buffer = eatspaces(Console::ReadLine()); // Читать входную строку if(String::IsNullOrEmpty(buffer))// Пустая строка —признак
И завершения работы return 0;
Console::WriteLine(L" = {0}\n\n”,ехрг(buffer)); //Вывести значение выражения } return 0;
}
Как видите, функция получилась гораздо короче и проще для чтения. Внутри бесконечного цикла for вызывается функция класса String — IsNullOrEmpty (). Функция возвращает true, если переданная строка равна null или имеет нулевую длину, что и требуется здесь.
Удаление пробелов из входной строки
Функция для удаления пробелов также получается проще и короче:
// Функция для удаления пробелов из строки
StringA eatspaces(StringA str) {
// Массив для хранения строки без пробелов array<wchar_t>A chars = gcnew array<wchar_t>(str->Length); int length = 0; H Количество пробелов в строке
// Копировать непробельные символы в массив char for each (wchar_t ch in str) if(ch != * ’)
chars[length++] = ch;
H Возвратить символьный массив в виде строки return gcnew String(chars, 0, length);
}
Здесь сначала создается массив, куда будут помещены символы строки без пробелов. Это массив элементов типа wchar_t, поскольку строки в C++/CLI состоят из символов Unicode. Процесс удаления пробелов очень прост — копируются все символы, не являющиеся пробелами, из строки str в массив chars, и количество скопированных символов запоминается в переменной length. В конце создается новый объект String посредством конструктора класса String, который создает объект из элементов массива. Первый аргумент конструктора — массив, являющийся источником символов строки, второй — позиция индекса первого элемента массива, который формирует строку, а третий — общее количество символов массива, которые должны быть использованы. Класс String определяет ряд конструкторов для создания строк разными способами.
Вычисление арифметического выражения
Функция вычисления выражения может быть реализована следующим образом:
/ / Функция для вычисления арифметического выражения
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(Ь"Эй,case ' +' :++(*index);value += term(str, index);break;case ’-’:++(*index);value -= term(str, index);break;default:Console::WriteLine(Ь"Эй,{
case ' +' :
++(*index);
value += term(str, index);
break;
case ’-’:
++(*index);
value -= term(str, index);
break;
default:
Console::WriteLine(Ь"Эй,
case ' +' :
++(*index);
value += term(str, index);
break;
case ’-’:
++(*index);
value -= term(str, index);
break;
default:
Console::WriteLine(Ь"Эй,
// найден +
И увеличить индекс и прибавить
/ / следующий элемент
// найден -
// увеличить индекс и
И вычесть следующий элемент
// Неверный формат строки выражения повнимательней можно?!
Здесь обнаружена ошибка.\п");
exit(1);
}
}
return value;
Переменная index объявлена как дескриптор, потому что нам нужно передать ее функции term () и позволить ей модифицировать оригинальное значение index. Если объявить index просто как int, то функция term() примет копию значения и не сможет обратиться к оригинальной переменной, чтобы изменить ее.
Объявление index приводит к появлению предупреждающих сообщений компилятора, поскольку оператор полагается на автоматическую упаковку значения 0 для производства объекта класса значения Int32, на который ссылается дескриптор, и предупреждение нужно потому, что люди часто пишут такое объявление, имея в виду инициализацию дескриптора значением null. Конечно, чтобы правильно сделать это, необходимо в качестве значения инициализации использовать nullpt г вместо 0. Чтобы избежать предупреждающего сообщения компилятора, этот оператор потребуется переписать так:
intA index = gcnew int(0);
Этот оператор явно использует конструктор, чтобы создать объект и инициализировать его значением 0, поэтому никакого предупреждения компилятор не выдает.
После обработки начального элемента вызовом функции term () цикл while проходит по строке в поисках операций 4- или -, следующих за другим элементом. Оператор switch идентифицирует и обрабатывает операции. Если вы долгое время работаете с родным C++, у вас может возникнуть соблазн написать операторы case внутри switch несколько иначе. Например, так:
// Некорректный код!! Не работает!!
case ’+’: // найден +
value += term (str, ++(*index));//увеличить index и прибавить следующий элемент break;
Конечно, обычно подобное пишут без этого начального комментария. Код неправильный, но почему? Функция term () ожидает дескриптора типа intА в качестве второго аргумента, и именно это применяется, хотя, может быть, это не то, чего можно ожидать. Компилятор вычисляет выражение ++ (*index) и его результат помещается во временную область памяти. Выражение на самом деле обновляет значение, на которое ссылается index, но дескриптор, передаваемый функции term () — это дескриптор временного объекта, содержащего результат вычисления, а не дескриптор index. Этот дескриптор порождается в результате автоматической упаковки, когда значение сохраняется во временной области памяти. Когда функция term () обновляет его, то обновляется значение временного объекта, а не значение index, поэтому все обновления index, выполненные внутри term (), будут утеряны. Если вам нужно обновить переменную из вызывающей программы, вы не должны использовать выражение в качестве аргумента функции — всегда нужно применять имя дескриптора переменной.
Получение значения элемента
Как и в версии на родном C++, функция 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;}// Выполнять цикл, 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 и// разделить на следующее число/ / Выйти из цикла// Умножение// Увеличить index и// умножить на следующее число// Деление// Увеличить 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;
}
// Выполнять цикл, 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 и
// разделить на следующее число
/ / Выйти из цикла
// Умножение
// Увеличить index и
// умножить на следующее число
// Деление
// Увеличить index и
// разделить на следующее число
/ / Выйти из цикла
// Готово, возвращаем то, что получилось return value;
После вызова функции number () для получения значения первого числа или выражения в скобках в элементе функция проходит по строке циклом while. Цикл продолжается до тех пор, пока остаются во входной строке есть символы операций * и /, за которыми следует другое число или выражение в скобках.
Извлечение числа
Функция number () извлекает и оценивает выражение в скобках, если оно есть; в противном случае она просто определяет и возвращает очередное числовое значение из входной строки:
// Функция для распознавания числа
double number(StringА str, intA index) {
double value =0.0;// Сохраняет значение результата
/ / Проверить выражение в скобках if(str[*index] == L* (* )// Открытие скобок
{
++(*index);
StringA substr = extract(str, index); // Извлечь подстроку в скобках return expr (substr);H Вернуть значение подстроки
}
// Цикл, аккумулирующий ведущие цифры
while((*index < str->Length) && Char::IsDigit (str, *index)) {
value = 10.0*value + Char::GetNumericValue(str[(*index)]); ++(*index);
}
// Обнаружена не цифра if((*index == str->Length) || str[*index] !=’.') // Проверить на // десятичную точку return value;// Если нет, вернуть value
double factor = 1.0;/ / Множитель для десятичных разрядов
++(*index);И Перейти к цифре
// Выполнять цикл, пока идут цифры
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 (), возвращающую подстроку в скобках из входной строки, можно реализовать так:
// Функция для извлечения подстроки в скобках
StringA extract(StringA str, intA index) {
// Временное место для подстроки
array<wchar_t>A buffer = gcnew array<wchar_t>(str->Length);
StringA substr;H Возвращаемая подстрока
int numL = 0;H Счетчик левых открывающих скобок
int bufindex = *index; // Сохранить начальное значение index
while(*index < str->Length) { buffer[*index - bufindex] = str[*index]; switch(str[*index]) { case *) *:
if(numL == 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
numL—;// Уменьшить значение счетчика * (’
break;
case 1(’:
numL++;// Увеличить значение счетчика 1(’
break;
}
++(*index);
}
Console: :WriteLine(Ь"Выход за конец выражения. Возможно, некорректный ввод"); exit(1) ;
return substr;
}
Опять-таки стратегия та же, что и в версии родного C++, отличия — лишь в деталях. Чтобы найти соответствующую правую скобку, функция отслеживает, сколько найдено новых левых скобок, в переменной numL. Подстрока извлекается, когда найдена правая скобка при значении счетчика левых скобок numL равным нулю. Подстрока копируется в массив substrChars, используя функцию СоруТо () с объектом str типа String. Функция копирует символы, начиная с позиции индекса, специфицированной первым аргументом, в массив, специфицированный вторым аргументом; третий аргумент определяет начальный элемент массива, который принимает символы, а четвертый аргумент — количество символов, которые должны быть скопированы. Возвращаемая строка создается конструктором класса String, который строит объект на основе всех элементов массива substrChars, переданных в аргументе.
Если вы соберете все функции в один консольный проект CLR, то получите реализацию калькулятора на C++/CLI, выполняемую под управлением CLR. Вывод программы должен быть таким же, как и в версии на родном C++.
Резюме
Теперь вы имеете достаточно знаний, чтобы разрабатывать и использовать функции. В этой главе вы научились применять указатель на функцию в практическом контексте для обработки условия переполнения памяти свободного хранилища, а также ознакомились с перегрузкой, используемой для реализации набора функций, представляющих одну и ту же операцию с параметрами разного типа. В следующих главах вы узнаете больше о перегрузке функций.
Ниже перечислены важнейшие моменты, которые вы изучили в этой главе.
Указатель на функцию хранит ее адрес, плюс информацию о количестве и типах аргументов, а также типе возвращаемого функцией значения.
Указатель на функцию можно использовать для хранения адреса любой функции с соответствующим типом возврата и количеством и типами параметров.
Указатель функции можно применять для вызова функции, адрес которой он содержит. Указатель на функцию можно передавать в виде параметра функции.
Исключение — это способ сигнализации об ошибке в программе, так что код, обрабатывающий ошибку, может быть отделен от кода нормальных операций.
Чтобы возбудить исключение, следует применить оператор с ключевым словом throw.
Код, который может возбуждать исключения, должен быть помещен в блок try, а код, обрабатывающий исключения определенного рода — в блок catch, следующий непосредственно за блоком try. Может существовать несколько блоков catch, следующих за блоком try — каждый перехватывает определенный тип исключений.
Перегруженные функции — это функции с одинаковыми именами, но разными списками параметров.
Когда вызывается перегруженная функция, конкретная версия подлежащей вызову функции выбирается компилятором на основе количества и типов указанных аргументов.
Шаблон функции — это рецепт для автоматической генерации перегруженных функций.
Шаблон функции имеет один или более аргументов, представляющих переменные типа. Экземпляр шаблона функции, то есть, определение функции, создается компилятором для каждого вызова функции, который соответствует уникальному набору типов — аргументов шаблона.
Вы можете заставить компилятор создать определенный экземпляр шаблона функции, указав объявление прототипа требуемой функции.
Вы приобрели также некоторый опыт в использовании нескольких функций в одной программе на примере программы калькулятора. Однако имейте в виду, что все случаи применения функций, рассмотренные до сих пор, находятся в контексте традиционного процедурного подхода к программированию. Когда мы перейдем к рассмотрению объектно-ориентированного программирования, мы по-прежнему будем интенсивно пользоваться функциями, но с очень отличающимся подходом к структуре программ и дизайну решений проблем.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Пусть имеется следующая функция:
int ascVal(size_t i, const char* p) {
// печатать ASCII-значение символа
if (!p || i > strlen(p))
return -1;
else
return p[i];
}
Напишите программу, которая вызовет эту функцию через указатель и протестирует ее работу. Вам понадобится включить директиву #include для заголовочного файла <cstring>, чтобы использовать библиотечную функцию strlen ().
Напишите семейство перегруженных функций по имени equal (), которые принимают два аргумента одного типа, возвращают 1, если аргументы эквивалентны, и 0 — в противном случае. Предложите версии для аргументов типов char, int, double и char*. (Для проверки эквивалентности строк применяйте функцию strcmp () из библиотеки времени выполнения. Если вы не знаете, как использовать st гетр (), поищите в справочной системе. Вам также понадобится директива #include с заголовочным файлом <cstring>.) Напишите тестовый код для проверки того, что вызвана корректная версия.
Сейчас, когда калькулятор встречает неправильный входной символ, он печатает сообщение об ошибке, но не показывает, в каком месте входной строки она обнаружена. Напишите процедуру обработки ошибок, которая печатает входную строку, помещая символ А под местом обнаружения ошибки, например: 12 + 4,2*3
Добавьте в программу калькулятора операцию возведения в степень А, в дополнение к * и /. Каковы возможные ограничения ее реализации, и как их преодолеть?
(Усложненное.) Усовершенствуйте калькулятор так, чтобы он мог обрабатывать тригонометрические и прочие математические функции, позволяя вводить выражения вроде:
2 * sin(0.6)
Все математические библиотечные функции работают с радианами; предложите версии тригонометрических функций, использующих градусы:
2 * sin(30)
7
Определение собственныхтипов данных
Настоящая глава посвящена созданию собственных типов данных, предназначенных для решения конкретных проблем. Здесь также будет рассказано о создании объектов — строительных блоков объектно-ориентированного программирования. Объект может показаться чем-то мистическим для непосвященных, но, как вы увидите в этой главе, объект может быть всего лишь экземпляром одного из собственных типов данных.
В этой главе вы изучите перечисленные ниже вопросы.
Структуры и их использование.
Классы и их использование.
Базовые компоненты класса и определение типов классов.
Создание и использование объектов класса.
Управление доступом к членам класса.
Конструкторы и их создание.
Конструктор по умолчанию.
Ссылки в контексте классов.
Конструктор копирования и его реализация.
Чем отличается класс C++/CLI от классов “родного” C++.
Свойства классов C++/CLI, их определение и использование.
Литеральные поля, их определение и использование.
Поля in it only, их определение и использование.
□ Что такое статический конструктор.
Структуры в 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. Может показаться, что это се-
рьезное ограничение, но отметьте, что вы можете включить в структуру указатель на переменную типа 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 следующим образом:
// Начальное значение Title// Начальное значение Author// Начальное значение Publisher// Начальное значение Year// Начальное значение Title// Начальное значение Author// Начальное значение Publisher// Начальное значение YearBOOK Novel = (
// Начальное значение Title
// Начальное значение Author
// Начальное значение Publisher
// Начальное значение Year
// Начальное значение Title
// Начальное значение Author
// Начальное значение Publisher
// Начальное значение Year
"Paneless Programming",
"I.C. Fingers",
"Gutter Press", 1981
Инициализирующие значения появляются между скобками и разделены запятыми — почти так же, как определяются начальные значения членов массива. Как и в случае массивов, последовательность инициализирующих значений очевидно должна совпадать с последовательностью членов структуры в ее определении. Каждый член структуры Novel имеет соответствующее значение, присвоенное ему, на что указывают комментарии.
Доступ к членам структуры
Чтобы обратиться к индивидуальным членам структуры, вы можете использовать операцию выбора члена, которая обозначается точкой; иногда ее называют операцией доступа к члену. Чтобы сослаться на определенный член структуры, вы пишете имя переменной-структуры, за которым следует точка, а за ней — имя члена, к которому вы хотите обратиться. Для изменения члена Year структуры Novel вы можете записать так:
Novel.Year = 1988;
Это установить значение члена Year равным 1988. Вы можете использовать член структуры точно таким же образом, как любую другую переменную того же типа, что и этот член. Чтобы увеличить Year на 2, например, можно написать:
Novel.Year += 2;
Это увеличит значение члена Year структуры, как увеличило бы значение любой другой переменной.
Практическое занятие | ИСПОЛЬЗОВаНИв СТРУКТУР
Теперь воспользуемся примером консольного приложения, чтобы разобраться, как работает обращение к членам структур. Предположим, что вы хотите написать программу, которая будет иметь дело с некоторыми вещами, находящимися у вас во дворе — как это проиллюстрировано на плане, приведенном на рис. 7.1.
Я произвольно установил координаты 0, 0 для левого верхнего угла двора. Правый нижний угол имеет координаты 100, 120. Аналогичным образом первое значение координаты измеряется в горизонтальном направлении относительно левого верхнего угла, и ее значение увеличивается слева направо, а вторая координата измеряется по вертикали, начиная от той же точки, и ее значения возрастают сверху вниз.
Позиция 100,120
Рис. 7.1. План двора
На рис. 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; Hut1.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;
}
// Функция вычисления площади прямоугольника
long Area(RECTANGLE& aRect) {
return (aRect.Right - aRect.Left)*(aRect.Bottom - aRect.Top);
}
// Функция перемещения прямоугольника
void MoveRect(RECTANGLE& aRect, int x, int y) (
int length = aRect.Right - aRect.Left;// Получить длину прямоугольника
int width = aRect.Bottom - aRect.Top;
aRect.Left = x;
aRect. Top = y;
aRect.Right = x + length;
H Получить ширину прямоугольника // Установить верхнюю левую точку / / в новую позицию
// Получить правую нижнюю точку как
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, соответствующая второму сараю — в нижней левой части двора, сначала устанавливается точно такой же, как Hutl с помощью такого оператора присваивания:
Hut2 = Hutl; // Определение Hut2 таким же, как Hutl
После выполнения этого оператора значения членов Hutl копируются в соответствующие члены Hut2. Вы можете присвоить структуру данного типа только другой
структуре того же типа. Невозможно непосредственно выполнить инкремент структуры или использовать ее в арифметической операции.
Чтобы изменить позицию Hat 2 для размещения его в нижней левой части двора, вызывается функция Move Re с t () с координатами нужной позиции, переданными в аргументах. Этот окружной путь получения координат Hut 2 совершенно не обязателен, и служит только для того, чтобы показать, как можно использовать структуру в качестве аргумента функции.
Поддержка средства Intellisense при работе со структурами
Вероятно, вы заметили, что редактор в Visual C++ 2005 довольно-таки умный — так, например, он знает типы переменных. Если в окне редактора вы наводите курсор мыши на имя переменной, всплывает маленькая рамка, в которой показано его определение. Редактор также может здорово помочь при работе со структурами (и, как вы увидите позже, с классами), потому что он не только знает типы обычных переменных, но также и члены, входящие в переменные типа определенной структуры. Если ваш компьютер достаточно быстр, то по мере ввода операции выбора члена, вслед за именем структурной переменной редактор показывает окно со списком всех членов данной структуры. Если вы щелкнете на одном из них, будет показан комментарий, указанный в исходном определении структуры, так что вы сразу видите, что это такое. Это видно на рис. 7.2, где используется фрагмент предыдущего примера.
55^ I }
59^ L
Н Function to Move a Rectangle
61LJ raid Моте Sect (RECTJUTGY.E6 aHect, int x, int61LJ raid Моте Sect (RECTJUTGY.E6 aHect, int x, int6 ; T63;64!65|ее;67 i 6si 69^ -3!71;7^ 73^ 74^6 ; T63;64!65|ее;67 i 6si 69^ -3!71;7^ 73^ 74^int leu,th - aRect.Right - aRect.*eft; int width " aRect Bcttom - aRect.Торгint leu,th - aRect.Right - aRect.*eft; int width " aRect Bcttom - aRect.Торгa^.ect ,L^rt = к;aRect.Top “ Y? aRect.Right = x lengt. ; a^er:r .|Во1мГ\looordinatr риг It Left.Film rx^Ol.Q.t,♦ pkfit ♦ Opa^.ect ,L^rt = к;aRect.Top “ Y? aRect.Right = x lengt. ; a^er:r .|Во1мГ\looordinatr риг It Left.Film rx^Ol.Q.t,♦ pkfit ♦ OpF)if Get length o.’ rectangle// Get width of rectangle// Set top left pointH tn new position// Get bottom right point азF)if Get length o.’ rectangle// Get width of rectangle// Set top left pointH tn new position// Get bottom right point аз
61LJ raid Моте Sect (RECTJUTGY.E6 aHect, int x, int
61LJ raid Моте Sect (RECTJUTGY.E6 aHect, int x, int
6 ; T
63;
64!
65|
ее;
67 i 6si 69^ -3!
71;
7^ 73^ 74^
6 ; T
63;
64!
65|
ее;
67 i 6si 69^ -3!
71;
7^ 73^ 74^
int leu,th - aRect.Right - aRect.*eft; int width " aRect Bcttom - aRect.Торг
int leu,th - aRect.Right - aRect.*eft; int width " aRect Bcttom - aRect.Торг
a^.ect ,L^rt = к;
aRect.Top “ Y? aRect.Right = x lengt. ; a^er:r .|
Во1мГ\looordinatr риг I
t Left.Film rx^Ol.Q.t,
♦ pkfit ♦ Op
a^.ect ,L^rt = к;
aRect.Top “ Y? aRect.Right = x lengt. ; a^er:r .|
Во1мГ\looordinatr риг I
t Left.Film rx^Ol.Q.t,
♦ pkfit ♦ Op
F)
if Get length o.’ rectangle
// Get width of rectangle
// Set top left point
H tn new position
// Get bottom right point аз
F)
if Get length o.’ rectangle
// Get width of rectangle
// Set top left point
H tn new position
// Get bottom right point аз
Puc. 7.2. Работа средства Intellisense
Теперь есть реальный стимул добавлять комментарии и писать их кратко и выразительно. Если выполняется двойной щелчок на члене в списке либо нажимается клавиша <Enter> при выделенном элементе списка, он автоматически вставляется после операции выбора члена (точки), избавляя вас от необходимости вручную вводить его с клавиатуры. Здорово, не правда ли?
При желании вы можете отключить любое или все средства Intellisense через меню Tools ^ Options (Сервис ^ Параметры), но я предполагаю, что для их отключения может быть только одна причина — если ваша машина слишком медленна, что делает эти средства неудобными. Вы можете отключить средства завершения операторов на странице редактора C/C++, который выбираете в правой панели опций. Если вы их отключите, то всегда можете вызвать их обратно — либо через меню Edit (Правка), либо через клавиатуру. Нажатие <Ctrl+J>, например, вызывает отображение членов объекта, на котором находится курсор. Редактор также показывает список параметров для функции, когда вы вводите код для ее вызова — он выводит всплывающее
поле, как только нажата клавиша с левой скобкой перед списком аргументов. Это, в частности, удобно с библиотечными функциями, поскольку напоминает списки параметров каждой из них. Конечно, чтобы это средство работало, директива #include для заголовочного файла уже должна присутствовать в исходном коде. Без этого редактор не имеет понятия о библиотечных функциях. При изучении классов вы увидите и другие полезные возможности редактора, благодаря которым облегчается ввод кода.
После этого небольшого интересного отступления давайте вернемся к структурам.
Структура RECT
Прямоугольники в программах Windows используются очень широко. По этой причине существует предопределенная структура RECT в заголовочном файле windows. h. Ее определение, по сути, совпадает со структурой, которую вы видели в предыдущем примере:
struct RECT
<
int left;//Пара координат
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
Как вы видели, когда была представлена идея структуры, struct не может содержать член того же типа, как она сама, однако она может содержать указатель на struct того же типа. Например, вы можете определить структуру примерно так:
struct ListElement
{
RECT aRect;// Член структуры RECT
ListElement* pNext; // Указатель на элемент списка
};
Первый элемент структуры 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 = SaRect;
Первый оператор объявляет и определяет объект aRect типа RECT с первой парой членов инициализированных (0, 0) и второй парой (100, 100). Второй оператор объявляет pRect как указатель на тип RECT и инициализирует его адресом aRect.
Теперь вы можете обращаться к членам 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;
};
Имея такое определение нового типа данных по имени 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 по умолчанию. Как вы увидите чуть позже в настоящей главе, на доступность членов класса можно накладывать ограничения.
Вы можете объявить переменную, скажем, bigBox, которая представляет экземпляр класса СВох, следующим образом:
СВох bigBox;
Это выглядит в точности так же, как объявление переменной типа структуры или любого другого типа. После того, как вы определили класс СВох, объявления переменных этого типа достаточно стандартны.
Первый класс
Понятие класса было придумано англичанами, чтобы большая часть населения чувствовала себя счастливой. Оно происходит из теории, что люди, которые знают свое место и функцию в обществе, будут намного более защищенными и успешными в жизни, чем те, которые его не знают. Знаменитый датчанин, Бьерн Страуструп, который изобрел C++, без сомнения почерпнул глубокие знания о концепции классов во время учебы в Кембриджском университете в Англии, и очень успешно применил эту идею в своем новом языке.
Класс C++ подобен этой английской концепции — в том смысле, что каждый класс обычно имеет очень точно определенную роль и допустимый набор действий. Однако он и отличается от английской идеи, поскольку класс в C++ имеет некоторую “социалистическую окраску”, сосредоточивая внимание на рабочих классах. В самом деле, в определенном смысле это противоположность английскому идеалу, потому что, как вы увидите, рабочие классы в C++ часто живут за счет классов, которые не делают ничего.
Операции с классами
В C++ вы можете создавать новые типы данных как классы, чтобы представлять объекты любого вида, который вам нужен. Как вы вскоре увидите, классы (и структуры) не ограничены просто хранением данных; вы можете определять функции-члены или даже операции, которые работают с объектами ваших классов, с использованием стандартных операций C++. Вы, например, можете определить класс СВох, так что будут работать следующие операторы и иметь тот смысл, который вы в них вложите:
СВох boxl;
СВох Ьох2;
if(boxl > box2) // Наполнить больший ящик boxl. fill () ;
else
box2.fill () ;
Вы также можете реализовать операции как часть класса СВох для сложения, вычитания и даже умножения ящиков — фактически, почти любая операция, которой вы можете приписать осмысленное значение в контексте ящиков.
Говоря о невероятной мощи этого волшебства, следует отметить, что оно требует коренного изменения подхода к программированию. Вместо разбиения проблемы на части в терминах привязки к машинно-ориентированным типам данных (целым числам, числам с плавающей точкой и так далее) с последующим написанием программы, вы переходите к программированию в терминах проблемно-ориентированных типов данных — другими словами, в терминах классов. Эти классы могут именоваться CEmployee, или CCowboy, или CCheese, или CChutney, каждый из которых определен специально для типа проблем, которые вы хотите решить, полон функций и операций, необходимых для манипулирования экземплярами этих новых типов.
Дизайн программы теперь начинается с решения о том, какие новые специфичные для приложения типы данных понадобятся для решения проблемы, и написании программы в терминах операций, специфичных для рассматриваемой проблемы, будь то CCof f ins или CCowpokes.
Терминология
Для начала определим основную терминологию, которую будем использовать, обсуждая классы в C++.
Класс — определенный пользователем тип данных.
Объектно-ориентированное программирование (ООП) — стиль программирования, основанный на идее определения собственных типов данных как классов.
Объявление объекта типа класса иногда называется созданием экземпляра (реализацией) класса.
Экземпляры класса известны как объекты.
Идея объекта, содержащего данные, указанные в его определении, вместе с функциями, оперирующими этими данными, называется инкапсуляцией.
Когда я обращаюсь к деталям объектно-ориентированного программирования, это может показаться несколько усложненным, но возврат к основам того, что вы делаете, часто может помочь прояснить ситуацию, поэтому всегда имейте в виду, для чего предназначены объекты. Они предназначены для написания программ в терминах объектов, специфичных для проблемной области. Все средства, присущие классам C++, направлены на это и призваны сделать это как можно более всесторонним и гибким образом. Давайте займемся осмыслением классов.
Что такое класс?
Класс — это спецификация типа данных, определенного вами. Он может содержать элементы данных, которые могут быть как переменными базовых типов C++, так и других определенных пользователем типов. Элементы данных класса могут быть отдельными элементами данных, массивами, указателями, массивами указателей почти любого рода, объектами других классов, так что в вашем распоряжении практически неограниченная гибкость в отношении того, что можно включать в ваши типы данных. Класс также может содержать функции, которые оперируют объектами класса, обращаясь к элементам данных, которые они включают в себя. Таким образом, класс комбинирует определение элементарных данных, из которых состоит объект, и средства манипулирования данными, относящимися к индивидуальным объектам класса.
Данные и функции внутри класса называются членами класса. Довольно забавно, что члены класса, которые являются элементами данных, называются данными-чле- нами, а функции, принадлежащие классу, называются функциями-членами или членами-функциями. Функции-члены класса также иногда называются методами; я не стану использовать этот термин в настоящей книге, но имейте в виду, что вы можете встретить этот термин в других источниках.
Данные-члены также называются полями, и эта терминология используется в C++/CLI, поэтому я тоже буду применять ее время от времени.
Когда вы определяете класс, то тем самым создаете “проект” типа данных. Это в действительности не определяет никаких данных, но определяет смысл для имени
класса, то есть, что будет содержать объект этого класса и какие операции могут быть выполнены над таким объектом. Это почти то же самое, что подготовить описание базового типа double. Это не будет переменной типа double, но определением того, как она создается и как с ней обращаться. Чтобы создать переменную базового типа, вы должны использовать операторы объявления. Точно так же и с классами, и вы в этом вскоре убедитесь.
Определение класса
Давайте взглянем еще раз на пример класса, упомянутый ранее — класс ящиков. Вы определили тип данных СВох, используя ключевое слово class, следующим образом:
class СВох
{
public:
double m_Length; // Длина ящика в дюймах double m_Width; // Ширина ящика в дюймах double m_Height; // Высота ящика в дюймах
};
Имя класса следует за ключевым словом class, и между фигурными скобками определены три данных-члена. Данные-члены определены в классе с использованием операторов объявления, которые вы уже знаете и любите, а все определение класса завершается точкой с запятой. Имена всех членов класса локальны по отношению к нему. Поэтому вы можете использовать те же имена еще где-то в программе, и это не вызовет никаких проблем.
Управление доступом в классе
Ключевое слово public выглядит как метка, но на самом деле это нечто большее. Оно определяет атрибут доступа к членам класса, которые следуют за ним. Спецификация данных-членов как public означает, что эти члены объекта класса могут быть доступны в любой точке внутри области видимости объекта класса, к которому они относятся. Вы также можете специфицировать члены класса как private или protected. Фактически, если вы вообще пропустите спецификатор доступа, то члены будут иметь атрибут по умолчанию — private. (В этом заключается единственное отличие между классом и структурой в C++ — у структур спецификатором доступа по умолчанию является public.) Чуть позже вы увидите, в чем состоит эффект от этих ключевых слов в определении класса.
Запомните, что все, что вы определили до сих пор — это класс, который является типом данных. Вы не объявляли никаких объектов типа класса. Когда я говорю о доступе к членам класса, скажем, m_Height, то я говорю о доступе к членам данных конкретного объекта, и этот объект должен быть где-то определен.
Определение объектов класса
Вы объявляете объекты класса точно таким же образом, как и объекты базовых типов, поэтому вы можете объявлять объекты типа класса СВох с помощью следующих операторов:
СВох boxl; // Объявление boxl типа СВох
СВох box2; I/ Объявление Ьох2 типа СВох
Оба объекта — boxl и Ьох2 — конечно же, будут иметь свои собственные данные- члены. Это проиллюстрировано на рис. 7.4.
Ьох1
m_Length
m_Width
m_Height
Збайт
Збайт
Збайт
Ьох2
m_Length
m_Width
m_Height
Збайт
Збайт
Збайт
Рис. 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 CBox // Определение класса в глобальном контексте
{
public:
double m_Length;
double m_Width;
double m_Height;
/ / Длина ящика в дюймах
/ / Ширина ящика в дюймах
// Высота ящика в дюймах
};
int main ()
{
CBox boxl;
CBox box2;
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
// Вычислить объем 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 H Отобразить размер ящика в памяти
« "Объект СВох занимает "
« sizeof boxl << " байт."; cout <<endl;
return 0;
}
При вводе кода функции main () вы должны наблюдать подсказки редактора со списком имен членов при каждом вводе операции выбора члена вслед за именем объекта класса. Затем вы можете выбирать нужный член в списке двойным щелчком мыши. Наведение курсора мыши на любую переменную в вашем коде приведет к отображению ее типа.
Описание полученных результатов
Поскольку все примеры пока оформлены в виде консольных приложений, проект должен быть определен соответствующим образом. Все здесь работает так, как и можно ожидать, основываясь на опыте применения структур. Определение класса находится вне функции main (), и потому относится к глобальному контексту. Это позволяет вам объявлять объекты в любой функции программы и отображает класс на вкладке Class View (Представление классов), как только программа будет скомпилирована.
Вы объявили два объекта типа СВох внутри функции main () —boxl и box2. Конечно, как это было бы и с переменными базовых типов, объекты boxl и box2 локальны по отношению к main (). Объекты типов классов подчиняются тем же правилам видимости объявленных переменных, что и переменные базовых типов (вроде переменной boxVolume, использованной в этом примере).
Первых три оператора присваивания устанавливают значения данных-членов boxl. Вы определяете значения данных-членов Ьох2 в терминах данных-членов boxl в следующих трех операторах присваивания.
Затем идет оператор, который вычисляет объем boxl как произведение его трех данных-членов, после чего полученное значение выводится на экран. Далее выводится сумма данных-членов Ьох2, для чего применяется выражение, суммирующее дан- ные-члены непосредственно в операторе вывода. Финальное действие программы — вывод количества байт памяти, занятых boxl, которое возвращает операция sizeof.
Если запустить эту программу, вы должны получить такой вывод:
Объем boxl = 33696
Ьох2 имеет сумму сторон 66.5 дюймов.
Объект СВох занимает 24 байт.
Последняя строка показывает, что объект boxl занимает 24 байта памяти, что объясняется наличием 3 членов данных по 9 байт каждый. Оператор, выдающий последнюю строку, с тем же успехом может быть записан так:
cout « endl / / Отобразить размер ящика в памяти
« "Объект СВох занимает "
« sizeof (СВох) << " байт.";
Здесь я использовал в качестве операнда операции 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() { CBox boxl;/ / Объявление boxl типа СВох
СВох box2;/ / Объявление box2 типа СВох
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 () ;
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, определяющей количество байт памяти, занятых объектом.
Имена данных-членов класса в функции-члене автоматически ссылаются на дан- ные-члены конкретного объекта, используемого для вызова функции, и функция может быть вызвана только с определенным объектом класса. В данном случае это делается через операцию прямого обращения к члену, следующего за именем объекта.
Если вы попробуете вызвать функциючлен, не указывая имени объекта, программа компилироваться не будет.
Расположение определения функции-члена
Определение функции-члена должно быть помещено внутрь определения класса. Если вы хотите разместить его вне определения класса, то внутри класса потребуется указать ее прототип. Если переписать предыдущий класс с определением функции вне класса, он будет выглядеть следующим образом:
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
в начале заголовка функции. Таким образом, для этой функции определение должно выглядеть следующим образом:
// Функция для вычисления объема ящика
inline double СВох::Volume()
{
return m_Length*m_Width*m_Height;
}
При таком определении функции программа будет точно такой же, как и раньше. Но так можно поместить определение функции-члена за пределами определения класса, если вы этого хотите, и, тем не менее, воспользоваться преимуществами в скорости выполнения, которые дает встраивание.
Вы можете применить ключевое слово inline к обычным функциям вашей программы, которые не имеют отношения к классам, и получить тот же эффект. Помните, однако, что лучше всего это работает с короткими простыми функциями.
Теперь разберемся с тем, что происходит, когда вы объявляете объект класса.
Функция в классе объявленакак встроеннаяФункция в классе объявленакак встроеннаяinline void function()inline void function(){тело}{тело}Компилятор заменяет вызовы встроенной функцииКомпилятор заменяет вызовы встроенной функциителом ее кода, соответствующимтелом ее кода, соответствующимint main(void) {int main(void) {образом исправленным, чтобыизбежать проблем с именамиобразом исправленным, чтобыизбежать проблем с именамипеременных или областью видимостипеременных или областью видимости
Функция в классе объявленакак встроенная
Функция в классе объявленакак встроенная
inline void function()
inline void function()
{тело}
{тело}
Компилятор заменяет вызовы встроенной функции
Компилятор заменяет вызовы встроенной функции
телом ее кода, соответствующим
телом ее кода, соответствующим
int main(void) {
int main(void) {
образом исправленным, чтобыизбежать проблем с именами
образом исправленным, чтобыизбежать проблем с именами
переменных или областью видимости
переменных или областью видимости
}
Рис. 7.5. Механизм встроенных функций
Конструкторы классов
В предыдущем примере программы вы объявили объекты СВох, boxl и Ьох2, а затем старательно прошли по всем данным-членам каждого объекта, чтобы присвоить им начальные значения. Это неудовлетворительно с нескольких точек зрения. Прежде всего, очень легко пропустить инициализацию каких-то данных-членов, в частности в классах, у которых их намного больше, чем в нашем классе СВох. Инициализация данных-членов некоторых объектов сложных классов может потребовать многих страниц кода, состоящих из операторов присваивания. Конечное ограничение такого подхода проявляется, когда определяются данные-члены класса, которые не имеют атрибута public — у вас нет доступа к ним извне класса. Должен существовать другой, лучший способ, и конечно же, он есть — это конструктор.
Что такое конструктор?
Конструктор класса — это специальная функция класса, которая вызывается при создании нового объекта этого класса. Таким образом, она предоставляет возмож-
ность инициализировать объекты во время их создания и гарантировать, что все дан- ные-члены будут иметь корректные значения. Класс может иметь несколько конструкторов, позволяя создавать объекты различными способами.
У вас нет выбора в именовании конструкторов класса — они всегда называются по имени класса, в котором определены. Функция СВох (), например — конструктор нашего класса СВох. К тому же конструктор не имеет типа возврата. Нельзя специфицировать тип возврата конструктора и даже не нужно его объявлять как 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;
}
Описание полученных результатов
Конструктор СВох () принимает три параметра типа 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: ’СВох1: no appropriate default constructor available
ошибка C2512: ’СВох*: нет доступных конструкторов по умолчанию
Это значит, что компилятор ищет конструктор по умолчанию для box2 (также называемый конструктором без аргументов, поскольку при вызове никаких аргументов не требует), поскольку вы не указали никаких инициализирующих значений для членов данных. Конструктор по умолчанию — это конструктор, не требующий аргументов, и он может либо не иметь параметров в своей спецификации, либо быть та-
ким, у которого аргументы необязательны. Но этот оператор отлично удовлетворял компилятор в Ех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;II Установка значений
m_Width = bv;// данных-членов
m_Height = hv;
}
/ / Определение конструктора по умолчанию
СВох () { cout « endl « "Вызван конструктор по умолчанию.";
>
// Функция вычисления объема ящика double Volume() {
return m_Length*m_Width*m_Height;
}
int main ()
CBox boxl (78.0,24.0,18.0); // Объявление и инициализация boxl
СВох Ьох2;/ / Объявление Ьох2 — никаких инициализирующих значении
CBox cigarBox(8.О, 5.0, 1.0);// Объявление и инициализация cigarBox double boxvolume =0.0;// Сохранить объем ящика
boxVolume = boxl.Volume();// Вычислить объем boxl
cout « endl
« ’’Объем boxl = ” « boxVolume;
box2 .mJHeight = boxl .mJHeight -10;/ / Определение
box2 .m_Length = boxl.m_Length /2.0;/ / членов box2
box2.m__Width = 0.25*boxl.m__Length; // в терминах boxl cout « endl
« ’’Объем box2 = ”
« box2.Volume() ;
cout « endl
« ’’Объем cigarBox = ”
« cigarBox.Volume() ;
cout « endl;
return 0;
}
Описание полученных результатов
Теперь, когда вы включили собственную версию конструктора по умолчанию, нет никаких сообщений об ошибках от компилятора, и все работает. Программа выдает следующий вывод:
Вызван конструктор.
Вызван конструктор по умолчанию.
Вызван конструктор.
Объем boxl = 33696
Объем Ьох2 = 6084
Объем cigarBox = 40
Все, что делает конструктор по умолчанию — это отображает сообщение. Очевидно, что он вызывается в точке объявления объекта Ьох2. Вы также получаете корректное значение объема всех трех объектов СВох, так что остальная часть программы работает должным образом.
Один аспект этого примера, который вы, возможно, не заметили, заключается в том, что конструкторы можно перегружать, как вы перегружали функции в главе 6. Последний пример включает два конструктора, отличающихся списками параметров. Один имеет три параметра типа double, а другой вообще не имеет параметров.
Присваивание параметрам в классе значений по умолчанию
Когда мы говорили о функциях, вы видели, как можно специфицировать значения параметров по умолчанию в прототипе функции. То же самое можно делать с функциями-членами классов, включая конструкторы. Если вы поместите определение функции-члена внутрь определения класса, то можете указать значения ее параметров по умолчанию прямо в заголовке функции. Если же в определение класса включен только прототип функции, то в этом прототипе должны присутствовать значения параметров по умолчанию.
Если вы решили, что значение размера объекта СВох по умолчанию должно быть равно одиночному ящику кубической формы со всеми сторонами длиной 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 = Iv; // Установка значений 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.
практическое занятие |Использование значений по умолчанию
для аргументов конструктора
Сказанное можно продемонстрировать в следующем простом примере.
// Ех7_06.срр
// Использование значений по умолчанию для аргументов конструктора
#include <iostream>
using std::cout;
using std::endl;
class CBox // Определение класса в глобальном контексте
{
public:
double m_Length; // Длина ящика в дюймах
double m_Width; // Ширина ящика в дюймах double m_Height; // Высота ящика в дюймах
// Определение конструктора
CBox (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()
CBox box2;// Объявление box2 — никаких начальных значений
cout << endl
« "Объем box2 = "
« box2.Volume();
cout « endl; return 0;
}
Описание полученных результатов
Вы объявили одну неинициализированную переменную СВох по имени Ьох2, потому что для целей демонстрации этого достаточно. Эта версия программы выдает следующий вывод:
Вызван конструктор.
Объем Ьох2 = 1
Это доказывает, что конструктор со значениями параметров по умолчанию выполняет свою работу, устанавливая значения объектов, для которых не заданы инициализирующие значения.
Вы не должны предполагать на основании этого, что это единственный или даже рекомендуемый способ реализации конструктора по умолчанию. Бывает много случаев, когда не нужно присваивать значения по умолчанию подобным способом, и тогда лучше написать отдельный конструктор по умолчанию. Бывают даже ситуации, когда вообще не нужен конструктор по умолчанию, даже несмотря на то, что вы определяете другой конструктор. Это должно гарантировать, что все объявленные объекты класса будут иметь инициализирующие значения, явно специфицированные в их объявлении.
Использование списка инициализации в конструкторе
Ранее вы инициализировали члены объекта в конструкторе класса, используя явные аргументы. Однако существует и другая техника, которая основана на применении списка инициализации. Ниже она демонстрируется в альтернативной версии конструктора класса СВох.
// Определение конструктора с использованием списка инициализации
СВох (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 CBox // Определение класса в глобальном контексте {
public:
// Определение конструктора с использованием списка инициализации
CBox (double lv = 1.0, double bv = 1.0, double hv = 1.0) :
m_Length(lv) , m_Width(bv), m_Height(hv)
{
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 поместить конструктор, вы не сможете объявить ни одного объекта типа этого класса.
Предыдущий пример генерирует следующий вывод:
Вызван конструктор.
Вызван конструктор.
Объем match = 1.21
Объем Ьох2 = 1
Это демонстрирует, что класс по-прежнему работает удовлетворительно, хотя его данные-члены определены с атрибутом доступа private. Основное отличие в том, что теперь они полностью защищены от неавторизованного доступа и модификации.
Если вы не укажете иначе, атрибутом доступа по умолчанию, применяемым ко всем членам класса, будет pri va te. Таким образом, вы можете поместить все приватные члены в начало определения класса и позволить им по умолчанию быть приватными, пропустив ключевое слово доступа. Однако лучше потрудиться и явно определить состояние атрибута доступа в каждом случае, дабы не возникало никаких сомнений относительно ваших намерений.
Конечно, вы не обязаны объявлять все данные-члены как private. Если приложение, использующее ваш класс, того требует, можно иметь некоторые члены определенные как private, а другие — public. Все зависит от того, что вы хотите сделать. Если нет веских причин делать члены класс public, то лучше объявить их private, поскольку это сделает класс более безопасным. Обычные функции не будут иметь доступа ни к одному из членов private вашего класса.
Доступ к приватным членам класса
Если подумать, объявление членов данных класса как private довольно-таки экстремально. Это очень хорошо защищает их от неавторизованных модификаций, но нет причин всегда держать их значение в секрете. Что вам нужно, так это “Закон о свободе информации” для приватных членов.
Вам не придется писать письмо депутатам — этот закон уже к вашим услугам. Все, что потребуется сделать — это написать функцию-член, которая будет возвращать значение члена данных. Взгляните на следующую функцию-член класса СВох:
inline double СВох::GetLength()
{
return m_Length;
}
Только для того, чтобы показать, как это будет выглядеть, определение этой функции вынесено за пределы определения класса. Я специфицировал ее как inline, поскольку это дает преимущество в скорости без слишком большого увеличения размера кода. Предполагая, что объявление этой функции находится в разделе public класса, вы можете применять ее в операторах вроде следующего:
int len = box2 .GetLength () ;// Получить член данных, описывающий длину
Все, что необходимо сделать — написать подобную функцию для каждого члена данных, который вы хотите сделать доступным для чтения внешнему миру, и их значения будут доступны без нарушения безопасности класса. Конечно, если вы поместите определения этих функций внутрь определения класса, они будут встроенными по умолчанию.
Дружественные функции класса
Бывают случаи, когда по той или иной причине вы хотите некоторым функциям, не являющимся членами класса, разрешить доступ ко всем членам класса, то есть определить разновидность элитной группы со специальными привилегиями. Такие функции называются дружественными функциями класса и определяются с помощью ключевого слова 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 (СВох аВох)
{
return 2. О* (aBox.m__Length*aBox.m_Width + аВох.m_Length*aBox.m_Height -I* aBox.m_Height*aBox.m_Width);
}
int main() {
CBox match(2.2r 1.1, 0.5); // Объявление ящика match
СВох box2; // Объявление box2 — без инициализирующих значений cout « endl
« "Объем match = "
« match.Volume ();
cout « endl
« "Площадь поверхности match = " « BoxSurface (match) ;
cout « endl
« "Объем box2 = "
« box2 .Volume () ; cout « endl
« "Площадь поверхности box2 = " « BoxSurface(box2);
cout « endl;
return 0;
}
Описание полученных результатов
Вы объявляете функцию BoxSurface () как друга класса СВох, написав сс прототип с ключевым словом friend в начале. Поскольку сама функция BoxSurface () является глобальной, неважно, куда именно вы поместите объявление friend внутри объявления класса, но будет хорошей идеей придерживаться определенного стиля в размещении этого объявления. Как видите, я предпочел поместить се после всех public- и private-членов класса. Помните, что дружественная функция не является членом класса, поэтому атрибуты доступа к ней не относятся.
Определение функции следует за классом. Обратите внимание, что вы специфицируете доступ к данным-членам объекта внутри определения BoxSurface (), используя объект СВох, переданный функции в качестве параметра. Поскольку дружественная функция не является членом класса, она нс может ссылаться на данные-члены класса просто по именам; они должны быть квалифицированы именем объекта — точно так же, как в обычной функции, за исключением того, конечно, что обычная функция не может получить доступ к приватным членам класса. Дружественная функция — это то же самое, что и обычная функция, но с тем отличием, что ей разрешен доступ ко всем членам класса или классов, другом которых она является, безо всяких ограничений.
Этот пример дает следующий вывод:
Вызван конструктор.
Вызван конструктор.
Объем match = 1.21
Площадь поверхности match = 8.14
Объем Ьох2 = 1
Площадь поверхности Ьох2 = 6
Это именно то, чего следовало ожидать. Дружественная функция вычисляет площадь поверхности объектов СВох по значениям приватных членов.
Размещение определения дружественной функции внутри класса
Можно комбинировать определение функции с ее объявлением как друга класса СВох внутри определения класса, и код будет выполняться, как и ранее. Определение функции в классе должно выглядеть следующим образом:
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; // Ширина ящика в дюймах
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, что представляет собой полностью специфицированную ссылку на член используе-
мого объекта. Компилятор берет на себя заботу о добавлении необходимого имени указателя this к именам членов в функции.
При необходимости вы можете использовать указатель this явно внутри функции-члена. Так, например, вы можете вернуть указатель на текущий объект.
Практическое занятие | ЯВНОв ИСПОЛЬЗОВаНИе 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 Сопраге (СВох хВох) { return this->Volume() > хВох. Volume () ;
} private: double m_Length;/ /Длина ящика в дюймах
double m_Width;//Ширина ящика в дюймах
double m_Height;/ /Высота ящика в дюймах
};
int main () { СВох match(2.2, 1.1, 0.5);// Объявление ящика match
СВох cigar(8.0, 5.0,1.0);// Объявление ящика cigar
if (cigar. Софаге (match)) cout « endl « "match меньше, чем cigar";
else cout « endl « "match равен или больше cigar" ; cout << endl; return 0;
}
Описание полученных результатов
Функция-член 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";
Это выглядит немного лучше и легче читается, чем оригинальная версия; однако существует намного лучший способ сделать то же самое, и о нем вы узнаете в следующей главе.
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; // данных-членов
m_Height = hv; }
II Функция для вычисления объема ящика 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;
Это предполагает, что все функции-члены определены отдельно, включая конструктор. Оба члена — 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 « "Вызван конструктор.”;
Массивы объектов класса
Массив объектов класса объявляется точно так же, как обычный массив, элементы которого относятся к одному из встроенных типов. Каждый элемент в массиве объектов класса требует вызова для него конструктора по умолчанию.
Практическое занятие МаССИВЫ ОбЪвКТОВ КЛЭССЭ
Мы можем воспользоваться определением класса СВох из последнего примера, изменив его добавлением специфичного конструктора по умолчанию:
// Ех7_11.срр
/ / Использование массива объектов класса
#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; II данных-членов m__Height = hv;
}
СВох () / / Конструктор по умолчанию
{
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.
Статические члены класса
И данные-члены, и функции-члены класса могут быть объявлены как static (статические). Поскольку их контекст — определение класса, эффект от этого будет несколько большим, чем эффект от ключевого слова static вне класса, так что давайте рассмотрим для начала статические данные-члены.
Статические данные-члены класса
Когда вы объявляете данные-члены класса как static, это дает тот эффект, что они создаются в единственном экземпляре и разделяются всеми объектами класса. Каждый объект получает собственные копии обычных данных-членов класса, но для всего класса имеется только по одному экземпляру каждого статического члена, независимо от того, сколько объектов этого класса было определено. Сказанное иллюстрируется на рис. 7.7.
Определение класса
class СВох
{
public:
static int objectCount;
private:
double m_Length;
double m_Width;
double m_Height;
object 1object 1objectsobjectsОдна копия каждого статического члена данных разделяется между всеми объектами типа классаОдна копия каждого статического члена данных разделяется между всеми объектами типа класса
object 1
object 1
objects
objects
Одна копия каждого статического члена данных разделяется между всеми объектами типа класса
Одна копия каждого статического члена данных разделяется между всеми объектами типа класса
objectCount
Рис. 7.7. Статические данные-члены класса
Одним из применений статических данных-членов является подсчет количества существующих объектов данного класса. Вы можете поместить статические данные- члены в общедоступный (public) раздел класса СВох, добавив следующий оператор к предыдущему определению класса:
static int objectcount; // Счетчик существующих объектов
Теперь возникает проблема: как инициализировать статический член данных?
Вы не можете инициализировать его в определении класса — ведь это только “проект” объекта, и инициализирующие значения в нем не разрешены. Вы не должны инициализировать его в конструкторе, поскольку желаете увеличивать его значение при каждом вызове конструктора, чтобы подсчитывать количество созданных объектов. Вы не можете инициализировать его в другой функции-члене, поскольку она ассоциирована с объектом, а инициализация нужна до создания любого объекта. Ответ заключается в том, чтобы инициализировать статический член данных вне определения класса с помощью следующего оператора:
int СВох::objectcount = 0; И Инициализация статического члена объекта СВох
Обратите внимание, что ключевое слово static здесь отсутствует; однако вы должны квалифицировать имя члена с использованием имени класса и операции разрешения контекста, чтобы компилятор понял, что вы ссылаетесь на статический член класса. Иначе вы бы просто создали глобальную переменную, не имеютцую отношения к классу.
Практическое занятие | Г1ОДСчётЭКзёмПЛЯрОВ
Давайте добавим к последнему примеру статический член данных и возможность подсчета объектов.
И Ех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;H данных-членов
m_Height = hv;
objectCount++;
}
CBox()// Конструктор по умолчанию
{ cout « endl « "Вызван конструктор по умолчанию.";
m_Length = m_Width = m_Height = 1.0; objectCount++;
}
II Фудкорта. для вычисления объема ящика double Volume () const {
return m_Length*m_Width*m_Height ;
}
private:
double m_Length;/ / Длина ящика в дюймах
double m_Width;// Ширина ящика в дюймах
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. Интересно отметить, что статические члены класса существуют, даже если никаких членов класса нет. И это очевидно, поскольку вы инициализируете статический член Objectcounter до того, как будет объявлен любой объект класса.
Статические данные-члены автоматически создаются при запуске программы и инициализируются 0, если только вы не инициализируете их каким-то другим значением. Поэтому вы должны инициализировать статические данные-члены класса только в том случае, если хотите, чтобы они имели начальное значение, отличное от 0.
Статические функции-члены класса
Объявляя функцию-член класса как static, вы делаете ее независимой от любого конкретного объекта данного класса. Обращение к членам класса из статической функции должно выполняться с использованием квалифицированных имен (как вы
должны это делать с обычными глобальными функциями, обращающимися к общедоступным данным-членам). Статическая функция-член обладает тем преимуществом, что она существует и может быть вызвана даже тогда, когда еще не существует ни одного объекта данного класса.
В этом случае в ней могут использоваться только статические данные-члены, поскольку на этот момент только они и существуют. Таким образом, вы можете вызвать статическую функцию-член для проверки статических данных-членов, даже если не знаете, существуют ли объекты данного класса, и если существуют — то сколько.
Конечно, после того, как объекты определены, статическая функция-член может обращаться как к приватным (private), так и к общедоступным (public) членам объектов класса. Статическая функция может иметь следующий прототип:
static void Afunction(int n);
Статическая функция может быть вызвана с конкретным объектом в операторе вроде следующего:
аВох.Afunction(10) ;
где аВох — объект класса. Та же функция также может быть вызвана без ссылки на объект. В этом случае оператор примет следующую форму:
СВох::Afunction(10) ;
где СВох — имя класса. Использование имени класса и операции разрешения контекста необходимо, чтобы сообщить компилятору, к какому классу относится функция Afunction().
Указатели и ссылки на объекты классов
Использование указателей, и в частности, ссылок на объекты классов — очень важно в объектно-ориентированном программировании, в частности, для спецификации параметров функций. Объекты классов могут включать в себя значительные объемы данных, поэтому использование механизма передачи по значению параметров-объектов в функции может оказаться очень дорогостоящим по затратам времени и неэффективным, поскольку при этом должно выполняться копирование каждого объекта-аргумента. Существует также техника, предусматривающая применение ссылок, которая незаменима при некоторых операциях с классами. Как вы увидите, невозможно написать конструктор копирования без использования параметра-ссылки.
Указатели на объекты класса
Вы объявляете указатель на объект класса точно так же, как и любой другой указатель. Например, указатель на объект класса СВох объявляется следующим образом:
СВох* рВох = 0;// Объявление указателя на СВох
Теперь вы можете использовать его для хранения адреса объекта СВох путем присваивания, применяя операцию получения адреса:
рВох = &cigar; // Сохранить адрес объекта СВох cigar в рВох
Как видите, при использовании указателя this в определении функции-члена Compare () вы можете вызвать функцию, применяя указатель на объект. Например, функцию Volume () для указателя рВох можно вызвать с помощью такого оператора:
cout « pBox->Volume(); //Отобразить объем объекта, на который указывает рВох
Опять-таки, здесь применяется операция непрямого обращения к члену. Это — типичная нотация, используемая большинством программистов для операций подобного типа, поэтому с настоящего момента я буду применять ее повсеместно.
Практическое занятие | указатели на клаССЫ
Попробуем испытать операцию непрямого обращения к члену. Для этого обратимся к примеру Ех7_10. срр, но немного изменим его.
// Ех7_13.срр
// Испытание операции непрямого доступа к члену класса
#include <iostream>
using std::cout;
using std::endl;
class CBox // Определение класса в глобальном контексте {
public:
// Определение конструктора
CBox (double lv = 1.0, double bv = 1.0, double hv = 1.0) {
cout « endl « "Вызван конструктор.";
m_Length = lv;H Установить значения
m_Width = bv;// данных-членов
m_Height = hv;
}
/ / Функция для вычисления объема ящика
double Volume() const {
return m_Length*m_Width*m_Height;
}
// Функция сравнения объемов двух ящиков; возвращает true (1), // если первый больше второго, и false (0) - в противном случае int Compare (CBox* рВох) const {
return this->Volume() > pBox->Volume (); }
private:
double m_Length;// Длина ящика в даймах
double m_Width;/ / Ширина ящика в даймах
double m_Height;/ / Высота ящика в даймах
};
int main ()
{
CBox boxes [5];И Объявление массива объектов СВох
CBox match (2.2, 1.1, 0.5); Н Объявление ящика match
CBox cigar(8.0, 5.0, 1.0); И Объявление ящика cigar
CBox* рВ1 = &cigar;/ / Инициализация указателя адресом объекта cigar
CBox* рВ2 = 0;И Указатель на СВох, инициализированный null
cout « endl
« "Адрес cigar равен " « рВ1 И Отображение адреса
« endl
« "Объем cigar равен "
« pBl->Volume() ; И Объем объекта, на который установлен указатель рВ2 = Smatch;
if (pB2->Conipare (pBl)) H Сравнение через указатель cout « endl
« "match больше, чем cigar"; else
cout « endl « "match меньше или равен cigar" ;
pBl = boxes;// Установка адреса массива
boxes[2] = match; // Установка 3-го элемента равным 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.
В выводе программы можно видеть, что выполняется семь вызовов конструктора объектов СВох: пять для элементов массива boxes и по одному для каждого из объектов cigar и match.
В целом почти нет разницы между использованием указателей на объекты класса и использованием указателей на базовый тип, такой как double.
Ссылки на объекты класса
Практическая польза ссылок особенно проявляется, когда они используются с классами. Как и в случае указателей, почти нет разницы между способом объявления и использования ссылок на объекты класса и способом объявления и использования ссылок на переменные базовых типов. Чтобы объявить ссылку на объект cigar, например, вы должны написать следующее:
СВох& rcigar = cigar; // Определение ссылки на объект cigar
Чтобы применить ссылку для вычисления объема объекта cigar, вы должны просто использовать имя ссылки вместо имени объекта:
cout « rcigar.Volume();// Вывод объема cigar через ссылку
Как вы должны помнить, ссылка действует в качестве псевдонима объекта, на который она ссылается, поэтому ее применение в точности совпадает с применением исходного имени объекта.
Реализация конструктора копирования
Важность ссылок проявляется в контексте аргументов и возвращаемых значений функций — в частности, функций-членов класса. Давайте вернемся к вопросу о конструкторе копирования. Пока оставим в стороне вопрос о том, когда следует писать собственный конструктор копирования и сосредоточимся на вопросе о том, как вы можете его написать. Я использую класс СВох просто для того, чтобы сделать решение более конкретным.
Конструктор копирования — это конструктор, создающий объект за счет инициализации его существующим объектом того же класса. Поэтому он должен принимать объект класса в качестве аргумента. Вы можете рассмотреть написание прототипа примерно так:
СВох(СВох initB);
Теперь посмотрим, что произойдет, когда конструктор будет вызван. Если вы напишете следующее объявление:
СВох myBox = cigar;
то компилятор сгенерирует вызов конструктора копирования:
СВох::СВох(cigar) ;
Выглядит так, словно никаких проблем нет, пока вы не поймете, что аргумент передается по значению. Поэтому перед тем, как объект cigar может быть передан, компилятор должен будет создать его копию. Таким образом, он вызовет конструктор копирования, чтобы создать аргумент для вызова конструктора копирования. К сожалению, поскольку аргумент передается по значению, этот вызов также потребует копирования аргумента, так что конструктор копирования будет вызываться снова и снова. То есть вы получите бесконечный цикл вызовов конструктора копирования.
Решение, которого, как я полагаю, вы ожидаете, заключается в использовании константного параметра-ссылки. Значит, прототип конструктора копирования можно написать следующим образом:
СВох(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). Одна существенная разница между классами значений (и структурами значений), с одной стороны, и классами ссылочными (и ссылочными структурами), с другой, состоит в том, что переменные типов значений содержат свои собственные данные, тогда как переменные типов ссылок должны быть дескрипторами, а потому они содержат адрес.
Обратите внимание, что функции-члены классов C++/СЫ не могут быть объявлены как const. Другое отличие от родного C++ заключается в том, что указатель this в нестатических функциях-членах класса значений типа Т является внутренним указателем типа interior_ptr<T>, в то время как указатель this в ссылочном классе Т — это дескриптор типа ТА. Следует иметь это в виду при возврате указателя this из функций C++/CLI или при сохранении его в локальной переменной. Существуют три других ограничения, которые касаются и классов значений, и ссылочных классов.
Класс значений или ссылочный класс не может содержать поля, являющиеся массивами “родного” C++ или типами классов “родного” C++.
Дружественные функции не разрешены.
Класс значений или ссылочный класс не может иметь членов, представляющих собой битовые поля.
Вы уже знаете из главы 4, что имена фундаментальных типов, такие как int или double, являются сокращениями для типов классов значений в программах CLR. Когда вы объявляете элемент данных типа класса значений, память для него выделяется в стеке, однако вы можете создавать объекты классов значений и в куче, применяя операцию gcnew — в этом случае переменная, используемая для обращения к объекту класса значений, должна быть дескриптором. Например:
double pi = 3.142;// pi сохраняется в стеке
intA lucky = gcnew int (7); // lucky - дескриптор, и 7 сохраняется в куче doubleA two = 2.0;// two — дескриптор, и 2.0 сохраняется в куче
Вы можете использовать любую из этих переменных в арифметических выражениях, но следует применять операцию * для разыменования дескрипторов, чтобы обратиться к значению, например:
Console: .-WriteLine (L"2pi = {0}", *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;
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); II Рост 7 футов 8 дюймов
Этот оператор создает переменную tall, которая содержит объект Height, представляющий 7 футов и 8 дюймов; этот объект создается вызовом конструктора с двумя параметрами.
Height baseHeight;
Этот оператор создает переменную baseHeight, которая будет автоматически инициализирована нулевым ростом. Класс Height не имеет конструктора без аргументов, и поскольку это класс значений, вы не можете определить его самостоятельно в определении класса. Поэтому в класс значений будет автоматически включен конструктор без аргументов, который инициализирует все значения полей эквивалентом нуля, а поля-дескрипторы — nullpt г, и вы не можете заменить этот неявный конструктор собственной версией. Именно этот конструктор по умолчанию и будет применяться для создания значения baseHeight.
Существует еще пара других ограничений относительно того, что может содержать в себе класс значений.
Вы не должны включать конструктор копирования в определение класса значений.
Вы не можете переопределить операцию присваивания в классе значений (переопределение операций в классе мы обсудим в главе 8).
Объекты классов значений всегда копируются простым копированием полей, и присваивание одного объекта такого класса другому выполняется так же. Классы значений предназначены для представления простых объектов, содержащих в себе небольшой объем данных, поэтому для представления объектов, не удовлетворяющих таким характеристикам, или для которых ограничения классов значений создают проблемы, вы должны использовать типы ref class.
Возьмем на пробу класс Height.
практическое занятие | Определение и использование типа класса значений
Ниже приведен код для испытания класса значения 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 (L" Твой рост ест {0)", yourHeight) ;
Console: :WriteLine(L”Его рост есть {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 дюймов, создается
в куче 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;
И Создает рост в футах и дюймах
Height(int ft, int ins) : feet(ft), inches(ins) {}
// Создает строку, представляющую объект virtual StringA ToStringO override {
return feet + L” футов " + inches + L" дюймов”;
}
Комбинация ключевого слова virtual перед типом возврата ToString () и ключевого слова override, за которым следует список параметров функции, указывает, что эта версия функции ToString () переопределяет версию, представленную в классе по умолчанию. Подробнее об этом вы узнаете в главе 8. Наша новая версия функции ToStringO теперь выводит строку, выражающую рост в футах и дюймах. Если вы добавите эту функцию в определение класса из предыдущего примера, то получите следующий вывод после компиляции и выполнения программы:
Мой рост 6 футов 3 дюймов
Твой рост 5 футов 10 дюймов
Его рост 5 футов 10 дюймов
Press any key to continue . . .
Это больше похоже на то, что вы ожидали получить. Вы можете заметить в этом выводе, что функция WriteLineO вполне успешно справляется с объектом из кучи CLR, на который ссылается дескриптор yourHeight, так же, как и с объектами myHeight и hisHeight, которые созданы в стеке.
Литеральные поля
Множитель 12, использованный для преобразования из футов в дюймы и наоборот, немного беспокоит. Это пример так называемого “магического числа” — когда человек, читающий код, должен сам догадываться о его назначении и происхождении. В данном случае достаточно очевидно, что такое 12, но в большинстве случаев смысл числовых констант, участвующих в вычислениях, не столь ясен. В C++/CLI имеется специальное средство — литеральные поля, предназначенные для представления числовых констант в классе, — которое позволяет решить эту проблему. Ниже показано, как можно исключить “магические числа” из кода в конструкторе с одним аргументом класса Height.
value class Height {
private:
// Записывает рост в футах и дюймах int feet;
int inches;
literal int inchesPerFoot = 12;
public:
// Создает рост в дюймах
Height(int ins) {
feet = ins / inchesPerFoot; inches = ins % inchesPerFoot;
}
/ / Создает рост в футах и дюймах
Height(int ft, int ins) : feet(ft), inches(ins){}
И Создает строку, представляющую объект
virtual StringA ToStringO override {
return feet + L" футов ”+ inches + L" дюймов";
}
};
Теперь конструктор использует имя inches Per Foot вместо 12, так что не возникает никаких сомнений относительно того, что происходит.
Вы можете определить значение литерального поля через другие литеральные поля, если только имена полей, через которые выражается данное поле, определены раньше. Например:
value class Height
{
II Прочий код...
literal int inchesPerFoot = 12;
literal double millimetersPerlnch = 25.4;
literal double millimetersPerFoot = inchesPerFoot*millimetersPer!nch;
// Прочий код...
}
Здесь литеральное поле millimetersPerFoot определено как произведение двух других литеральных полей. Если вы переместите определение поля millimetersPerFoot, так что оно будет предшествовать одному или обоим другим полям, код компилироваться не будет.
Определение типов ссылочных классов
Ссылочный класс по своим возможностям совместим с классом “родного” C++ и не имеет ограничений, которые присущи классу значений. В отличие от класса “родного” C++, однако, ссылочный класс не имеет конструктора копирования по умолчанию или операции присваивания по умолчанию. Если ваш класс должен поддерживать любую из этих операций, вы должны явно добавить соответствующую функцию для ее реализации. Как это сделать — будет показано в следующей главе.
Ссылочный класс определяется с использованием ключевого слова ref class — слова в этом сочетании, разд елейные одним или более пробелами, представляют единое ключевое поле. Рассмотрим класс СВох из примера Ех7_07, переопределенный как ссылочный класс.
ref class Box
{
public:
11 Конструктор без аргументов, устанавливающий значения полей по умолчанию Вох(): Length(1.0), Width(1.0), Height (1.0) {
Console::WriteLine(Ь”Вызван конструктор без аргументов.");
}
// Определение конструктора, использующего список инициализации
Box(double lv, double bv, double hv) :
Length(Iv), Width(bv), Height(hv) {
Console::WriteLine(Ь"Вызван конструктор.");
}
// Функция для вычисления объема ящика double Volume()
{
return Length*Width*Height;
}
private:
double Length; // Длина ящика в дюймах
double Width;// Ширина ящика в дюймах
double Height;// Высота ящика в дюймах
Прежде всего, обратите внимание, что я исключил префикс С из имени класса, и префикс ш_ из имен полей, поскольку эта нотация не рекомендована для классов C++/CLI. Вы не можете специфицировать значения по умолчанию для параметров функций и конструктора в классах 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(l.O), Height(l.O) {
Console: -.WriteLine(L"Вызван конструктор без аргументов.") ; } // Определение конструктора, использующего список инициализации Box (double lv, double bv, double hv) : Length (Iv), Width (bv) , Height (hv)
{
Console:: WriteLine (Ь"Вызван конструктор. ") ; } / / Функция для вычисления объема ящика double Volume () {
return Length*Width*Height;
}
private:
double Length;// Длина ящика в дюймах
double Width;// Ширина ящика в дюймах
double Height;// Высота ящика в дюймах
};
int main(array<System::String A> Aargs) {
BoxA aBox;// Дескриптор типа BoxA
BoxA newBox = gcnew Box (10, 15, 20) ;
аВох = gcnew Box;// Инициализировать Box по умолчанию
Console: :WriteLine(Ь”Объем ящика по умолчанию: {0}”, aBox->Volume()) ; Console: : WriteLi ne (L" Объем нового ящика: {0}н, newBox->Volume()) ;
return 0;
}
Вывод этого примера будет таким:
Вызван конструктор.
Вызван конструктор без аргументов.
Объем ящика по умолчанию: 1
Объем нового ящика: 3000
Press any key to continue . . .
Описание полученных результатов
Первый оператор в main () создает дескриптор объекта 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 () для свойства, а когда вы используете имя свойства в левой части оператора присваивания, то вызывается функция set (). Если свойство представляет только определение функции get (), оно называется свойством только для чтения, потому что функция set () не-
доступна для установки значения свойства. Свойство может иметь только функцию 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:
И Записывает рост в футах и дюймах 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
{
// Возвращает значение свойства double get ()
{ return inchesToMeters*(fееt*inchesPerFoot+inches);
}
// Здесь вы можете определить функцию set() ...
}
// Создает строковое представление объекта virtual StringA 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 get(); // Возвращает значение свойства // Здесь вы можете определить функцию set()... }
// Код прежний. . .
};
Функция get () свойства meters теперь объявлена, но не определена в классе Height, поэтому определение должно быть представлено вне определения класса. В определении функции get () в файле Height. срр имя функции должно быть квалифицировано именем класса и именем свойства, так что определение выглядит, как показано ниже.
Height::meters::get()
(
return inchesToMeters*(feet*inchesPerFoot+inches);
}
Квалификатор Height указывает, что определение функции принадлежит классу Height, а квалификатор meters говорит о том, что функция относится к свойству meters этого класса.
Конечно, вы можете определить свойства и для ссылочного класса. Вот пример:
ref class Weight
{
private:
int lbs;
int oz;
public:
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; } }
};
Здесь свойства 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 () и set () — в этом случае оно называется тривиальным скалярным свойством. Чтобы специфицировать тривиальное скалярное свойство, вы просто опускаете фигурные скобки, содержащие определения функций get () и set(),и завершаете объявление свойства точкой с запятой. Рассмотрим пример класса значения с тривиальными скалярными свойствами:
value class Point
{
public:
property int x; // Тривиальное свойство
property int у; // Тривиальное свойство
virtual String* ToStringO override {
return L" (” + x + L”," + у + L")"; / / Возвращает " (х,у)"
}
};
Определения функций get () и set () применяются автоматически к каждому тривиальному скалярному свойству и возвращают значение свойства, а также устанавли-
вают его по аргументу типа, специфицированного для свойства. Приватное пространство выделяется для размещения значения свойства “за сценой”.
Давайте посмотрим, как работают скалярные свойства.
практическое занятие Использование скалярных свойств
В примере используются три класса — два класса значений и один ссылочный.
// Ех7_16.срр : main project file.
// Использование скалярных свойств
#include "stdafx.h”
using namespace System;
H Класс, определяющий рост человека 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 H Скалярное свойство {
И Возвращает значение свойства double get() {
return inchesToMeters*(feet*inchesPerFoot+inches); }
// Здесь можно определить функцию set() свойства... }
// Создает строковое представление объекта virtual StringA ToStringO override {
return feet + L" футов ”+ inches + L" дюймов”; }
};
H Определение класса веса человека value class Weight {
private: int lbs; int oz;
literal int ouncesPerPound = 16;
literal double IbsToKg = 1.0/2.2;
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 StringA ToStringO override
{ return lbs + L" фунтов " + oz + L" унций"; } };
// Класс, определяющий персону ref class Person
{
private:
Height ht;
Weight wt;
public:
property StringA Name; H Тривиальное скалярное свойство Person(StringA name, Height h, Weight w) : ht(h), wt(w) {
Name = name;
}
Height getHeight(){ return ht; } Weight getWeightO{ return wt; } };
int main(array<System::String A> Aargs) {
Weight hisWeight = Weight(185, 7) ;
Height hisHeight = Height(6, 3);
PersonA him = gcnew Person (L"Fred", hisHeight, hisWeight);
Weight herWeight = Weight(105, 3) ;
Height herHeight = Height(5, 2) ;
PersonA 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;
Этот пример генерирует следующий вывод:
Ее зовут 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) ;
Person^ 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} килограмм.11, her->getWeight().kilograms);
Console::WriteLine(L"Ee рост {0} что равно {1:F2} метра.", her->getHeight(),her->getHeight().meters);
В первом операторе выполняется обращение к свойству Name объекта, на который ссылается дескриптор her, в выражении her->Name; результат его выполнения — дескриптор строки, которую возвращает функция get () этого свойства, так что его тип —StringA.
Во втором операторе происходит обращение к свойству kilograms поля wt объекта, на который ссылается дескриптор her, в выражении her->getWeight () . kilograms. Часть her->getWeight () этого выражения возвращает копию поля wt, и она используется для доступа к свойству kilograms; таким образом, значение, возвращенное функцией get () свойства kilograms, становится значением второго аргумента функции WriteLine().
В третьем операторе вывода второй аргумент — это результат выражения 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<StringA>A Names; // Сохраняет имена, как элементы массива public:
Name(...array<StringA>A names) Names(names) {}
// Индексируемое свойство, возвращающее любое имя property StringA default[int] {
// Извлечь значение индексируемого свойства StringA get(int index) { if(index >= Names->Length)
throw gcnew Exception(Ь"Индекс вне диапазона"); return Names[index];
} } };
Идея класса Name состоит в хранении имен персоны в виде массива индивидуальных имен. Конструктор принимает произвольное количество аргументов типа StringA, которые затем сохраняются в поле Names, так что объект Name может накапливать любое количество имен.
Индексированное свойство здесь является индексируемым свойством по умолчанию, потому что его имя специфицировано ключевым словом default. Если вы применяете обычное имя в этой позиции спецификации свойства, то такое свойство будет именованным индексируемым свойством. Квадратные скобки, следующие за ключевым словом default, указывают на то, что это действительно индексируемое свойство, и тип, который они в себе заключают (в данном случае int), является ти-
пом значений индекса, который должен применяться для извлечения значений свойства. Тип индекса не обязательно должен быть числовым типом, и вы можете иметь более одного параметра индекса для доступа к значениями индексируемого свойства.
Для индексируемого свойства, доступ к которому осуществляется по единственному индексу, функция get () должна иметь параметр, специфицирующий индекс того же типа, который указан между квадратными скобками, следующими за именем свойства. Функция set () для такого индексируемого свойства должна иметь два параметра: первый параметр — индекс, а второй — новое значение, которое должно быть установлено для свойства, соответствующее первому параметру.
Практическое занятиеПрактическое занятиеПосмотрим, как работают индексируемые свойства на практике.
Практическое занятие
Практическое занятие
Использование индексируемого свойства по умолчанию
Ниже приведен пример, использующий слегка расширенную версию класса Name.
// Ех7_17.срр : главный файл проекта.
// Определение и использование индексируемых свойств по умолчанию
Hnclude "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", L’’Isaiah’’, L’’Ezra’’, L”Inigo”,
L’’Whelkwhistle’’) ;
H Список имен
for (int i = 0 ; i < myName->NameCount ; i++)
Console::WriteLine(Ъ”Имя {0} : {1}”, i+1, myName[i]);
return 0;
Этот пример выдаст на экран следующее:
Имя 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, myName[i]);
Свойство NameCount применяется для управления циклом for. Без этого свойства вы не можете знать, сколько имен должны быть выведены. Внутри цикла последний аргумент функции WriteLine () обращается к i-тому индексируемому свойству. Как видите, обращение к индексируемому свойству по умолчанию предполагает просто применение значения индекса в квадратных скобках после имени переменной myName. Вывод демонстрирует, что индексируемое свойство работает, как ожидалось.
Индексируемое свойство доступно только по чтению, потому что класс Name включает только функцию get () для этого свойства. Чтобы позволить изменять свойства, вы должны добавить определение функции set () к индексируемому свойству по умолчанию, как показано ниже:
ref class Name {
// Прежний код...
// Индексируемое свойство, возвращающее имена property StringA default[int] {
StringA get(int index) { if(index >= Names->Length) throw gcnew Exception(Ь"Индекс вне диапазона"); return Names[index];
}
void set(int index, StringA name)
<
if (index >= Names->Length)
throw gcnew Exception(L"Индекс вне диапазона”) ;
Names [index] = name;
Теперь вы можете использовать способность устанавливать индексируемое свойство, добавив оператор в main (), который установит значение последнего индексируемого свойства:
NameA myName = gcnew Name(L"Ebenezer", I/’Isaiah”, L"Ezra”, L"Inigo", L"Whelkwhistle") ;
myName[myName->NameCount - 1] = L”Oberwurst"; H Изменить последнее
II индексируемое свойство
// Список имен
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, и вывод показывает, что они работают так, как и ожидалось.
Более сложные индексируемые свойства
Как упоминалось ранее, индексируемые свойства могут быть определены так, что для доступа к значению понадобится более одного индекса, к тому же индексы не обязательно должны быть числовыми. Рассмотрим пример класса с таким индексируемым свойством.
enum class Day{Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday};
II Класс, определяющий магазин
ref class Shop {
public:
property StringA Opening[Day, StringA] II Время открытия {
StringA get(Day day, StringA AmOrPm) {
switch(day) {
case Day::Saturday:II Время открытия в субботу:
if(AmOrPm == L”am”)
return L”9:00";/ / 9 утра
else
return L”14:30";// 2:30 дня
break;
case Day::Sunday:// Время открытия в воскресенье:
return L"closedH; // Весь день закрыт 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”. Если сможете придумать, зачем это может понадобиться, то также можете определить в классе индексируемые свойства с тремя и более индексами.
Статические свойства
Статические свойства подобны статическим членам класса в том, что они определены для класса в целом, и существуют в единственном экземпляре для всех объектов этого типа класса. Вы определяете статическое свойство, добавляя ключевое слово static к определению свойства. Вот как можно определить статическое свойство в классе Length, который вы видели раньше:
value class Length {
// Прежний код...
public:
static property StringA Units {
StringA get() { return Е"футы и дюймы"; }
}
};
Это простое свойство возвращает единицы измерения, принятые в классе, в виде строки. Вы обращаетесь к статическому свойству, квалифицируя его имя именем класса — точно так же, как любой другой статический член класса:
Console::WriteLine(Ь"Единицы измерения класса: {0}.", Length::Units);
Статические свойства класса существуют независимо от наличия объектов типа этого класса. Конечно, если у вас определен объект класса, вы можете обратиться к статическому свойству с использованием имени переменной. Например, если вы определили объект типа Length по имени 1еп, то можете вывести значение статического свойства Units с помощью следующего оператора:
Console::WriteLine(L" Единицы измерения класса: {0}.", len.Units);
Для доступа к статическому свойству в ссылочном классе через дескриптор объекта этого типа следует использовать операцию ->.
Зарезервированные имена свойств
Хотя свойства отличаются от полей, значения свойств также должны где-то сохраняться, и место их хранения как-то должно быть идентифицировано. Внутренне свойства имеют имена, созданные для мест хранения, которые им необходимы, и эти имена зарезервированы в классе, к которым принадлежат свойства, так что вы не должны использовать их для других целей.
Если вы определяете в классе скалярное или именованное индексируемое свойство по имени NAME, то имена get NAME и set NAME резервируются в классе, так что вы не должны использовать их для других целей. Оба имени зарезервированы независимо от того, определены ли функции get () и set () для свойства. Когда вы определите в классе индексированное свойство по умолчанию, то резервируются имена get ltem и set_Item. Вероятность создания резервированных имен с символами подчеркивания — достаточно веская причина избегать применения этого символа в ваших собственных именах в программе на C++/CLI.
Поля initonly
Литеральные поля — удобный способ представления констант в классе, но они имеют ограничение: их значения должны быть определены на момент компиляции программы. C++/CLI также предлагает в классах поля initonly, представляющие со-
бой константы, которые можно инициализировать в конструкторе. Рассмотрим пример поля initonly в скелетной версии класса Length:
value class Length
{
private:
int feet;
int inches;
public:
initonly int inchesPerFoot;// поле initonly
II Конструктор
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), II Инициализировать поля
{
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
// Конструктор
Length(int ft, int ins) : feet(ft), inches(ins)II Инициализация полей
{}
};
Теперь поле inchesPerFoot статическое, и его значение специфицировано в объявлении, а не в списке инициализации конструктора. На самом деле, вам не позволено устанавливать значения статических полей любого вида в конструкторе. Если хорошо подумать, то смысл этого очевиден, поскольку статические поля разделяются между всеми объектами класса, и потому установка значений этих полей при каждом вызове конструктора привела бы к конфликту с их нотацией.
Похоже, мы вернулись к ситуации, когда поля initonly могут быть инициализированы только во время компиляции, где в любом случае это лучше сделать литеральными полями; однако существует и другой способ инициализации статических полей initonly во время выполнения, а именно — в статическом конструкторе.
Статические конструкторы
Статический конструктор — это конструктор, который вы объявляете с использованием ключевого поля static и применяете для инициализации статических полей — обычных и initonly. Статический конструктор не принимает параметров и не может иметь списка инициализации. Он всегда private, независимо от того, поместите ли вы его в раздел public определения класса. Вы можете определить статический конструктор для классов значений и для ссылочных классов. Вы не можете вызвать статический конструктор напрямую — он будет вызван автоматически до первого выполнения нормального конструктора. Любые статические поля, которые имеют начальные значения, специфицированные в их объявлениях, будут инициализированы до выполнения статического конструктора. Вот как вы можете инициализировать поле initonly в классе Length с помощью статического конструктора:
value class Length {
private:
int feet; int inches;
II Статический конструктор
static Length () { inchesPerFoot = 12; }
public:
initonly static int inchesPerFoot = 12; // Статическое поле initonly
// Конструктор
Length(int ft, int ins) : feet(ft), inches(ins)// Инициализация полей
{}
};
Этот пример использования статического конструктора не имеет определенных преимуществ перед явной инициализацией inchesPerFoot, но имейте в виду, что большая разница состоит в том, что инициализация теперь происходит во время выполнения, и значение может быть получено из внешнего источника.
Резюме
Теперь вы понимаете базовые идеи, положенные в основу классов C++. В остальной части книги вы увидите все новые и новые примеры применения классов. Ниже перечислены ключевые моменты, которые вы должны усвоить после прочтения настоящей главы.
Класс представляет собой средство определения собственных типов данных. Классы могут отражать любые типы объектов, необходимых для решения ваших конкретных проблем.
Класс может содержать данные-члены и функции-члены. Функции-члены класса всегда имеют свободный доступ к данным-членам того же класса. Данные- члены в классах C++/CLI называются полями.
Объекты класса создаются и инициализируются специальными функциями — конструкторами. Они вызываются автоматически в точке объявления объектов. Конструкторы могут быть перегруженными, чтобы представлять разные способы инициализации объекта.
Классы в программах на C++/CLI могут быть классами значений или ссылочными классами.
Переменные типов классов значений хранят данные непосредственно, в то время как переменные, ссылающие на объекты ссылочных классов, всегда являются дескрипторами.
Классы C++/CLI могут иметь статические конструкторы для инициализации статических членов класса.
Члены класса могут быть специфицированы как public; в этом случае они свободно доступны для любой функции программы. Альтернативно их можно специфицировать как private — тогда они могут быть доступны только функциям- членам или дружественным функциям класса.
Члены класса могут быть определены как static. Существует только один экземпляр каждого статического члена класса, и он разделен между всеми экземплярами класса, независимо от того, сколько объектов этого класса создано.
Каждый нестатический объект класса содержит указатель this, указывающий на текущий объект, с которым вызвана функция.
В нестатических функциях-членах классов типа значений this является внутренним указателем, в то время как в ссылочных классах это дескриптор.
Функция-член, объявленная как const, имеет константный указатель this, и потому не может модифицировать данные-члены объекта класса, с которым она вызвана. Она также не может вызывать другие функции-члены, не объявленные как const.
Вы можете вызывать только константные функции-члены для объектов класса, объявленных как const.
Функции-члены классов значений и ссылочных классов не могут быть объявлены как const.
Использование ссылок на объекты класса в качестве аргументов при вызове функции позволяет избежать существенных накладных расходов, связанных с передачей сложных объектов в функцию.
□ Конструктор копирования, который представляет собой конструктор объекта, инициализируемого другим существующим объектом того же класса, должен принимать параметр, специфицированный как константная ссылка.
□ Вы не можете определить конструктор копирования для класса значений, потому что копирование объектов классов значений всегда выполняется копированием членов, одного за другим.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Определите структуру Sample, содержащую два целочисленных элемента данных. Напишите программу, которая объявит два объекта типа Sample с именами а и Ь. Установите значения для элементов данных, относящихся к а, и затем убедитесь, что вы можете копировать значения b простым присваиванием.
Добавьте в структуру Sample из предыдущего упражнения член типа char* по имени sPtr. Когда будете заполнять данными а, динамически создайте буфер символов, инициализированный строкой “Hello World!”, и установите на него указатель а. sptr. Скопируйте а в Ь. Что случится, когда вы измените содержимое символьного буфера, на который указывает а. sptr, а затем выведете содержимое строки, на которую указывает b. sptr? Объясните, что произошло? Как это исправить?
Создайте функцию, принимающую в качестве аргумента указатель на объект типа Sample и выводящую значения всех членов переданного ей объекта. Протестируйте эту функцию, расширив программу, написанную в предыдущем упражнении.
Определите класс CRecord с двумя приватными данными-членами, которые хранят имя до 14 символов длиной и целочисленный элемент. Определите функцию-член getRecord () класса CRecord, которая установит значения дан- ных-членов, читая ввод с клавиатуры, и функцию-член putRecord (), выводящую значения данных-членов. Реализуйте функцию getRecord () так, чтобы вызывающая ее программа могла обнаружить, когда вводится нулевой числовой элемент. Протестируйте класс CRecord с помощью функции main (), которая читает и выводит объекты CRecord до тех пор, пока не будет введен нулевой числовой элемент.
Напишите класс по имени СТгасе, который можно будет использовать для отображения во время выполнения входа в блок кода и выхода из него, представляя вывод вроде следующего: function * fl * entry
* if ’ block entry
* if * block exit
function * fl* exit
Подумайте о способе управления автоматическим отступом в предыдущем примере, чтобы вывод выглядел так: function ’fl* entry
* if * block entry
* if * block exit
function * fl* exit
Определите класс, представляющий стек магазинного типа (push-down stack) целых чисел. Стек — это список элементов, который позволяет добавление (“заталкивание”) или удаление (“выталкивание”) элементов только из одного конца и работает по принципу “последний вошел — первый вышел”. Например, если стек содержит элементы [10 4 16 20], то функция pop () должна вернуть 10, после чего стек будет содержать [4 16 20]; последующий вызов push (13) должен привести к следующему содержимому стека: [13 4 16 20]. Вы не должны получить элемент, не находящийся в вершине стека, предварительно не достав все, что находятся после него. Ваш класс должен реализовать функции push () и pop () плюс функцию print (), чтобы можно было проверить содержимое стека. Организуйте для начала внутреннее хранение элементов в виде массива. Напишите тестовую программу для проверки корректности всех операций вашего класса.
Что случится с вашим решением из предыдущего упражнения, если вы попытаетесь вытолкнуть функцией pop () больше элементов, чем было помещено в него вызовом push () ? Что произойдет, если попытаться сохранить больше элементов, чем отведено места? Можете ли вы предложить устойчивый способ перехвата таких ситуаций? Иногда может понадобиться увидеть число, находящееся в вершине стека, не извлекая его оттуда; реализуйте функцию реек (), чтобы делать это.
Повторите пример Ех7_04, но в виде консольной программы CLR, использующей ссылочные классы.
8
Дополнительныесведения о классах
В этой главе вы расширите свои знания о классах, узнав о том, как можно заставить объекты классов работать подобно базовым типам C++. Мы поговорим о перечисленных ниже моментах.
Что такое деструкторы классов и когда они необходимы.
Как реализовать деструктор класса.
Как разместить члены данных класса “родного” C++ в области свободного хранилища и как удалить, когда необходимость в них отпадает.
Когда необходимо писать конструктор копирования класса.
Что такое объединения и как они могут использоваться.
Как заставить объекты ваших классов работать с операциями C++, такими как + или *.
Что такое шаблоны классов, как их определять и каким образом использовать.
Как перегружать операции в классах C++/CLI.
Деструкторы классов
Хотя заголовок этого раздела ссылается на деструкторы, он в такой же мере посвящен и динамическому распределению памяти. Когда выделяется память в свободном хранилище для членов класса, вы обязаны использовать деструктор в дополнение к конструктору и, как увидите позднее в этой главе, применение динамического распределения членов класса также требует написания собственного конструктора копирования.
Что такое деструктор?
Деструктор (destructor) — это функция, которая уничтожает объект, когда необходимость в нем отпадает или когда он выходит из области видимости. Разрушение объекта включает освобождение памяти, занятой данными-членами объекта (за исключением статических членов, которые продолжают существовать, даже когда нс остается ни одного экземпляра класса). Деструктор класса — это функция-член с именем, совпадающим с именем класса, но предваренным символом “тильда” (~). Деструктор класса не возвращает значения и не принимает параметров. Для класса СВох прототип деструктора класса выглядит следующим образом:
~СВох(); // Прототип деструктора класса
Поскольку деструктор не имеет параметров, в классе может существовать лишь один деструктор.
Специфицировать возвращаемое значение или параметры деструктора — ошибка!
Деструктор по умолчанию
Все объекты, которые вы использовали до настоящего момента, уничтожались автоматически с помощью деструктора по умолчанию. Деструктор класса по умолчанию всегда генерируется компилятором автоматически, если только вы не определяете собственного деструктора для этого класса. Деструктор по умолчанию не удаляет объекты или члены объекта, которые были размещены в свободном хранилище операцией new. Если место для членов класса было выделено в конструкторе динамически, вы должны определить собственный деструктор, который явно воспользуется операцией delete, чтобы освободить память, выделенную в конструкторе операцией new — так же, как вы это делаете с обычными переменными. Для правильного написания деструкторов нужна практика, так что давайте попробуем.
Практическое занятие | Простой деСТруКТОр
Чтобы получить представление о том, когда вызывается деструктор класса, можно включить деструктор в класс СВох. Ниже приведен пример, включающий класс СВох с деструктором.
// Ех8_01.срр
// Класс с явным деструктором
#include <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 « "Вызван конструктор.";
}
// Функция для вычисления объема ящика
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.Q, 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 4- 2)->Volume () ; // Теперь обратиться через указатель cout « endl;
return 0;
}
Описание полученных результатов
Единственное, что делает деструктор класса СВох — отображает сообщение о том, что он вызван.
Вывод этой программы будет таким:
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
Объем cigar равен 40
Объем boxes[2] равен 1.21
Вызван деструктор.
Вызван деструктор.
Вызван деструктор.
Вызван деструктор.
Вызван деструктор.
Вызван деструктор.
Вызван деструктор.
Вы получили по одному вызову деструктора в конце программы для каждого из созданных объектов. Каждому вызову конструктора соответствует свой вызов деструктора. Вы не должны вызывать деструктор явно. Когда объект класса выходит из области видимости (контекста), компилятор автоматически помещает в эту точку вызов деструктора. В нашем примере вызов деструктора случается после того, как main () завершает выполнение, поэтому вполне возможно, что ошибка в деструкторе вызовет крах программы после благополучного завершения main ().
Деструкторы и динамическое распределение памяти
Нередко возникает необходимость в динамическом выделении памяти для членов класса. Вы можете воспользоваться операцией new в конструкторе, чтобы выделить место в памяти для члена объекта. В таком случае ответственность за освобождение этого места в памяти, когда объект уже не нужен, должна быть возложена на подходящий деструктор. Давайте для начала определим простой класс, в котором сможем это сделать.
Предположим, что нужно определить класс, в котором каждый объект содержит некоторое сообщение, например, текстовую строку. Класс должен использовать память наиболее эффективным образом, поэтому вместо определения члена как массива char, достаточно большого, чтобы уместилась строка максимальной длины, какая может потребоваться, мы будем распределять память для сообщения в области свободного хранилища вЪ время создания объекта. Ниже показано определение класса.
{{ Листинг 08_01
class CMessage
{
private:
char* pmessage; // Указатель на объект — текстовую строку public:
И Функция для вывода сообщения
void Showlt() const
{
cout « endl « pmessage;
}
H Определение конструктора
CMessage(const char* text = "Сообщение по умолчанию") {
pmessage = new char[strlen(text) + 1]; // Выделить место для текста strcpy(pmessage, text); H Копировать text в новую память
}
-CMessage(); // Прототип деструктора
};
Этот класс имеет только один член данных — pmessage, который является указателем на текстовую строку. Он определен в разделе private класса, так что не может быть доступен извне класса.
В разделе public находится функция Showlt (), которая выводит объект CMessage на экран. Есть также определение конструктора и прототип деструктора класса, -CMessage (), к которому вы вскоре вернемся.
Конструктор класса принимает строку в качестве аргумента, но если не передано ничего, использует строку по умолчанию, специфицированную для этого параметра. Конструктор получает длину строки, переданной в аргументе, исключая завершающий NULL, используя для этого библиотечную функцию strlen (). Чтобы конструктор мог вызывать библиотечную функцию, в файле должен присутствовать оператор
#include с заголовочным файлом <cstring>. Конструктор определяет количество байт памяти, необходимых для размещения строки в области свободного хранилища, добавляя 1 байт к значению, возвращенному функцией strlen ().
Конечно, если выделение памяти не удастся, будет возбуждено исключение, которое прервет выполнение программы. Если вы хотите обработать такой сбой и обеспечить более элегантное завершение программы, то должны будете перехватить исключение внутри кода конструктора (информацию об обработке условия нехватки памяти ищите в главе 6).
Получив память для строки, выделенную операцией new, вы используете библиотечную функцию strcpy (), которая также объявлена в заголовочном файле <cstring>, чтобы скопировать переданную в аргументе конструктора строку в область памяти, выделенную для нее. Функция strcpy () копирует строку, специфицированную вторым аргументом-указателем, по адресу, содержащемуся в первом аргументе-указателе.
Теперь вы должны написать деструктор класса, чтобы он освобождал память, выделенную для текста сообщения. Если вы не представите деструктора класса, то не будет никакой возможности освободить память, выделенную объекту. Если вы используете этот класс в таком виде, как он есть в программе, в которой создается большое количество объектов CMessage, то память свободного хранилища будет постепенно “съедаться” — до тех пор, пока программа не завершится аварийно. И это легко может произойти в ситуациях, когда совсем не очевидно, что подобное возможно. Например, если вы создаете временный объект CMessage в функции, вызываемой в программе многократно, то можете ожидать, что объект уничтожается при возврате из функции. Конечно, вы правы — сами указатели уничтожаются, но память из свободного хранилища, на которую они указывали, остается занятой. Поэтому при каждом вызове функции будет занято все больше и больше памяти свободного хранилища отброшенными объектами CMessage.
Код деструктора класса CMessage выглядит следующим образом:
U Листинг 08—02
И Деструктор для освобождения памяти, выделенной new CMessage::~CMessage() {
cout « "Вызван деструктор." // Просто чтобы видеть, что происходит
<< endl;
delete [] pmessage; // Освобождением памяти, на которую установлен указатель }
Поскольку деструктор определен вне определения класса, вы должны квалифицировать имя деструктора именем класса, CMessage. Все, что делает деструктор — отображает сообщение, чтобы было видно, что происходит, и вызывает операцию delete для освобождения памяти, на которую указывает член класса pmessage. Обратите внимание на добавление квадратных скобок рядом с delete, поскольку удаляется массив (типа char).
Практическое занятие | Использование ^03 CMeSSage
Вы можете испытать класс CMessage в небольшом примере.
// Ех8_О2.срр
// Использование деструктора для освобождения памяти
#include <iostream> И Для потокового ввода-вывода
#include <cstring> // Для strlen() и strcpy()
using std::cout; using std::endl;
// Сюда поместить определение класса CMessage (Листинг 08_01)
// Сюда поместить определение деструктора (Листинг 08_02) int main()
{
// Объявление объекта
CMessage motto("Большое видится на расстоянии.");
// Динамический объект
CMessage* рМ = new CMessage("Кот, который смотрел на королеву.");
motto.Showlt();// отобразить 1-е сообщение
pM->Show!t() ;// отобразить 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
также вызывает ваш деструктор, чтобы обеспечить освобождение память, занятую членами объекта. Поэтому когда вы используете delete для объекта, динамически созданного с помощью new, объекта перед освобождением памяти, занятой объектом, всегда вызывается деструктор.
Реализация конструктора копирования
Когда вы динамически распределяете пространство для членов класса, за этим присматривают демоны свободного хранилища. Для класса CMessage конструктор копирования вопиюще неадекватен. Предположим, что вы напишете следующие операторы:
CMessage mottol("Практика программирования на Visual C++.");
CMessage motto2(mottol); // Вызывается конструктор копирования по умолчанию
Эффект от конструктора копирования по умолчанию состоит в том, что копируется адрес, хранящийся в указателе-члене класса, из mottol в motto2, поскольку процесс копирования, реализованный конструктором копирования по умолчанию, включает простое копирование значений, хранящихся в данных-членах исходного объекта, в новый объект. Следовательно, одна текстовая строка будет совместно использоваться двумя объектами, как показано на рис. 8.1.
CMessage mottol (“Практика программирования на Visual C++”);
mottol
CMessage motto2 (mottol); // Вызов конструктора копирования по умолчанию
mottolmotto2
Рас. 8.1. Совместное использование текстовой строки двумя объектами
Если строка изменяется в любом из двух объектов, она одновременно изменяется и в другом, потому что оба объекта разделяют одну и ту же строку. Если mottol уничтожается, то указатель в motto2 указывает на область памяти, которая была освобождена, и может быть использована где-то еще, так что хаос гарантирован. Конечно, та же проблема возникает, если удаляется motto2; mottol будет в этом случае содержать член, указывающий на несуществующую строку.
Решение заключается в создании конструктора копирования, который заменит версию по умолчанию. Вы можете реализовать его в разделе класса public, как показано ниже:
CMessage(const CMessage& initM) // Определение конструктора копирования
{
// Выделить место для текста
pmessage = new char[ strlen(initM.pmessage) + 1 ];
// Копировать текст в новую память strcpy(pmessage, initM.pmessage);
}
Вспомним, что было сказано в предыдущей главе: для того, чтобы избежать бесконечного цикла вызовов конструкторов копирования, параметр должен быть специфицирован как const-ссылка. Конструктор копирования сначала выделяет достаточную память, чтобы уместилась строка из объекта initM, сохраняя адрес в члене данных нового объекта, а затем копирует текстовую строку из инициализирующего объекта. Теперь новый объект идентичен, но достаточно независим от старого.
Но не думайте, что вы в безопасности и не должны беспокоиться о конструкторе копирования только потому, что не инициализируете один объект CMessage другим. В свободном хранилище таится еще один монстр, который может нанести удар, когда вы менее всего этого ожидаете. Рассмотрим следующие операторы:
CMessage thought("Шила в мешке не утаишь.");
DisplayMessage(thought); II Вызов функции для вывода сообщения
Функция DisplayMessage () определена следующим образом:
void DisplayMessage(CMessage localMsg) (
cout « endl « "Сообщение: "
« localMsg.Showlt();
return;
}
Выглядит просто, не правда ли? Что здесь может быть не так? Катастрофическая ошибка — вот что! В действительности то, что делает эта функция, совершенно неуместно. Проблема заключена в параметре. Параметр — объект CMessage, поэтому при вызове аргумент передается по значению. При конструкторе копирования по умолчанию последовательность событий перечислена ниже.
Объект thought создается с местом для сообщения "Шила в мешке не утаишь. ", выделенным в свободном хранилище.
Функция DisplayMessage () вызывается и, поскольку аргумент передан по значению, копия localMsg создается конструктором копирования по умолчанию. Теперь указатель в копии указывает на ту же строку в свободном хранилище, что и в оригинальном объекте.
В конце функции локальный объект выходит из области видимости, поэтому вызывается деструктор класса CMessage. Он удаляет локальный объект (копию), освобождая память, на которую указывает ртеssage.
При возврате из функции DisplayMessage (), указатель на исходный объект, thought, все еще указывает на область памяти, которая уже была освобождена. В следующий раз, когда вы попытаетесь использовать оригинальный объект (или даже если вы этого не сделаете, все равно рано или поздно его придется удалить), программа начнет вести себя мистическим образом.
Любой вызов функции, которая принимает по значению объект класса, имеющего динамически определенный член, приведет к проблемам. Поэтому, чтобы избежать этого, вы должны на 100% придерживаться следующего золотого правила.
Если имеется динамическое выделение памяти для члена класса “родного” C++, всегда реализуйте конструктор копирования.
Разделение памяти между переменными
В качестве реликта с тех времен, когда 64 Кбайт считались достаточно большим объемом памяти, в C++ сохранилась возможность хранения более чем одной переменной в одной и той же памяти (но очевидно, что не в одно и то же время). Это называется union (объединение), и существует четыре основных способа его исполь- зования.
Можно сделать так, чтобы переменная А занимала в одной точке программы блок памяти, который позднее отдавался другой переменной В другого типа, поскольку необходимость в А отпала. Я не советую так поступать. Не стоит рисковать ошибками, к которым приведет такая организация. Того же эффекта можно достичь динамическим выделением памяти.
Можно столкнуться с ситуацией в программе, когда требуется большой массив данных, но вы заранее не знаете, какого типа данные придется хранить — это будет зависеть от вводимых данных. Я также рекомендую не использовать объединения в этом случае, поскольку того же результата можно достичь, используя несколько указателей разного типа и, опять-таки, выделяя память динамически.
Третье возможное применение объединения — когда вы хотите интерпретировать одни и те же данные двумя или более разными способами. Это может случиться, когда есть переменная типа long, а вы хотите трактовать ее как две переменных типа short. Windows иногда упаковывает два значения short в единственный параметр типа long с целью передачи в функцию. Другой случай — когда вы хотите трактовать блок памяти, содержащий числовые данные, как строку байт, или наоборот.
Вы можете использовать объединение в качестве средства передачи объекта или значения данных, когда заранее не известно, какого типа эти данные будут. Тогда можно применить объединение, которое способно хранить любое значение из диапазона возможных типов.
Определение объединений
Объединение определяется ключевым словом 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.
< Збайт ►
dval
Рис. 8.2. Объединение shareDLF
Вы можете инициализировать первый член объединения при объявлении экземпляра.
Анонимные объединения
Можно объявить объединение без имени типа — в этом случае экземпляр объединения объявляется автоматически. Например, предположим, что определено следующее объединение:
union
{
char* pval;
double dval; long Ival;
};
Этот оператор определяет неименованное объединение и экземпляр этого объединения. Впоследствии вы можете обращаться к переменным, которые оно содержит, просто по именам, под которыми они указаны в определении объединения: pval, dval и Ival. Это может оказаться более удобным, чем обычное объединение с именем типа, но следует быть осторожным, чтобы не спутать члены объединения с обычными переменными. Члены объединения все равно разделяют одну и ту же память. В качестве иллюстрации работы анонимного объединения, для использования члена double вы можете написать следующий оператор:
dval =99.5; И Использование члена анонимного объединения
Как видите, ничто не указывает на то, что переменная dval является членом объединения. Если вам нужно использовать анонимные объединения, вы можете применить соглашение об именовании, чтобы сделать более очевидной принадлежность переменных объединению, и таким образом предохранить ваш код от неправильного понимания.
Объединения в классах и структурах
Вы можете включить экземпляр объединения в класс или структуру. Если вы намерены сохранять разные типы значений в разное время, это обычно влечет за собой необходимость поддержки члена данных класса для индикации типа значения,
хранимого в объединении на данный момент. Поэтому применение объединений как членов класса или структуры обычно не дает особых преимуществ.
Перегрузка операций
Перегрузка операций — очень важное средство, поскольку позволяет заставить стандартные операции C++, такие как +, ~, * и так далее, работать с объектами ваших собственных типов данных. Это позволяет написать функцию, которая переопределяет некоторую операцию, чтобы она выполняла определенное действие, будучи использованной с объектами класса. Например, вы можете переопределить операцию >, чтобы, будучи примененной к объектам класса СВох, который вы видели выше, она возвращала true, если первый аргумент СВох имеет больший объем, чем второй.
Перегрузка операций не позволяет вводить новые операции, как не позволяет изменять приоритеты операций, так что ваша перегруженная версия операции будет иметь тот же приоритет в последовательности вычисления выражений, что и оригинальная базовая операция. Таблицу приоритетов операций можно найти в главе 2 этой книги, а также в библиотеке MSDN.
Хотя вы не можете перегрузить все операции, ограничения не слишком суровы. Ниже перечислены операции, которые не могут быть перегружены:
Операция разрешения контекста::
Условная операция?:
Операция прямого выбора члена
Операция размера объектаsizеоf
Разыменующий указатель на член класса . *
Во всем остальном ваши руки развязаны. Понятно, что хорошей идеей будет обеспечить разумную согласованность ваших собственных версий стандартных операций с их обычным применением, или, по крайней мере, интуитивно обоснованную их функциональность. Было бы не слишком разумно написать перегруженную операцию + для класса, выполняющую нечто, эквивалентное умножению объектов этого класса. Лучший способ понимания работы перегруженных операций состоит в разборе примера, поэтому давайте реализуем то, о чем я упомянул выше — операцию > — для класса СВох.
Реализация перегруженной операции
Чтобы реализовать перегруженную операцию для класса, необходимо написать специальную функцию. Предполагая, что она будет членом класса СВох, объявление функции для перегрузки операции > внутри определения класса выглядит следующим образом:
class СВох
{
public:
bool operator>(СВох& аВох) const; // перегруженная операция 'больше чем* // остальная часть определения класса.. .
};
Слово operator здесь является ключевым. Комбинированное с символом операции или ее именем, в данном случае, >, оно определяет функцию операции. Таким образом, в данном случае именем функции будет operator> (). Вы можете написать
функцию операции с пробелом или без, между ключевым словом operator и символом самой операции, до тех пор, пока нет неоднозначности. Неоднозначность возникает с операциями, обладающими именами вместо символов, такими как new или delete. Если вы напишете operatornew или operatordelete без пробела, это будут легальные имена обычных функций, поэтому в таких случаях необходимо оставлять пробелы между словом operator и самим именем операции. Обратите внимание, что вы объявляете функцию operator> () как const, потому что она не модифицирует данные-члены класса.
С функцией operator> () правый операнд перегружаемой операции определяется как ее параметр. Левый операнд определяется неявно указатели this. Поэтому, если у вас есть следующий оператор if:
if(boxl > Ьох2)
cout « endl « "boxl больше, чем box2";
выражение между скобками в i f вызовет функцию операции и эквивалентно следующему:
boxl.operator>(box2);
Соответствие между объектами СВох в выражении и параметрами функции операции проиллюстрировано на рис. 8.3.
if( boxl > Ьох2 )
Аргумент функции
bool CBox::operator>(const СВох& аВох) const {
Объект, на который
указывает this|
return (this->volume()) > (aBox.Volume());
}
Рис. 8.3. Соответствие между объектами СВох в выражениии параметрами функции операции
Посмотрим, как устроен код функции operator> ():
// Функция операции 'больше чем*, сравнивающая // объемы объектов СВох.
bool СВох::operator>(const СВох& аВох) const {
return this->Volume() > аВох.Volume О;
}
Здесь используется параметр-ссылка, дабы избежать излишнего копирования при вызове функции. Поскольку функция не меняет объекта, для которого она вызывается, можно объявить ее как const. Если этого не сделать, вы вообще не сможете использовать операцию для сравнения константных объектов СВох.
В выражении return используется функция-член Volume () для вычисления объема объекта СВох, на который указывает this, и производится сравнение результата с объемом объекта СВох с помощью базовой операции >. Базовая операция > воз-
424 Глава 8
вращает значение типа int (не bool) и потому возвращается 1, если объект СВох, на который указывает this, имеет больший объем, чем объект аВох, переданный в аргументе-ссылке, и 0 — в противном случае. Значение, полученное в результате сравнения, будет автоматически преобразовано в тип возврата функции операции, то есть в bool.
Практическое занятие | ПврвГруЗКв ОПвраЦИИ
Чтобы испытать функцию opereator> (), рассмотрим пример.
// Ех8_03.срр
// Испытание перегруженной операции 'больше чем*
#include <iostream> // Для потокового ввода-вывода
using std::cout;
using std::endl;
class СВох// Определение класса в глобальном контексте
{
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; // Высота ящика в дюймах 1;
// Функция операции 'больше чем*, // сравнивающая объемы объектов СВох.
bool СВох: :operator>(const СВох& аВох) const {
return this->Volume() > аВох.Volume () ;
}
int main ()
{
СВох smallBox(4.0, 2.0, 1.0);
СВох mediumBox (10.0, 4.0, 2.0);
СВох bigBox(30.0, 20.0, 40.0);
if (mediumBox > smallBox) cout « endl
« "mediumBox больше, чем smallBox";
if (mediumBox > bigBox) cout « endl
« "mediumBox больше, чем bigBox"; else
cout « endl
« "mediumBox не больше, чем bigBox";
cout « endl;
return 0;
}
Описание полученных результатов
Прототип operator> () функции операции появляется в разделе public объявления класса. Поскольку определение функции находится вне определения класса, она не является по умолчанию inline. Это довольно-таки произвольно. Точно так же вы могли бы поместить ее определение вместо прототипа внутри определения класса. Тогда вам не пришлось бы квалифицировать имя функции префиксом СВох::. Как вы помните, это необходимо, когда функция-член определена вне определения класса, чтобы сообщить компилятору, что данная функция является членом класса СВох.
Функция main () включает два оператора if, использующих операцию > с экземплярами класса. Это автоматически вызывает перегруженную операцию. Если вы хотите получить подтверждение этому, можете добавить оператор вывода в функцию операции. Вывод этого примера должен быть таким:
Вызван конструктор.
Вызван конструктор.
Вызван конструктор.
mediumBox больше, чем smallBox
mediumBox не больше, чем bigBox
Вызван деструктор.
Вызван деструктор.
Вызван деструктор.
Этот вывод демонстрирует, что оператор if работает правильно с нашей функцией операции, а потому выражает решение проблем СВох непосредственно в терминах объектов СВох, что весьма реалистично.
Реализация полной поддержки операции
Однако при такой реализации функции operator> () остается множество вещей, которых вы делать не можете. Спецификация решения проблемы в терминах объектов СВох может включать операторы вроде следующего:
if (аВох > 20.0)
// Что-то делать.. .
Наша функция с этим не справится. Если вы попытаетесь использовать выражение сравнения объекта СВох с числовым значением, то получите сообщение об ошибке. Чтобы поддерживать такую возможность, понадобится написать другую версию функции operator> () как перегруженную функцию.
Запрограммировать поддержку выражения, приведенного выше, достаточно легко. Объявление функции-члена внутри класса должно быть таким:
// Сравнение объекта СВох с константой
bool operatcr>(const doubles value) const;
Это может появиться в определении класса, и правый операнд операции > будет соответствовать параметру функции. Объект СВох, являющийся левым операндом, передается в виде неявного указателя this.
Реализация этой перегруженной операции также проста. Тело функции включает только один оператор:
// Функция для сравнения объекта СВох с константой
bool СВох::operator>(const doubles value) const {
return this->Volume() > value;
}
Что может быть проще, не так ли? Однако у вас по-прежнему остается проблема с применением операции > с объектом СВох. Вы можете пожелать написать оператор вроде следующего:
if (20.0 > аВох) // что-то делать...
Можно возразить, что это можно сделать, реализуя функцию operator< (), которая принимает правый аргумент типа double, и переписать последний оператор, чтобы в нем использовалась именно эта функция, что совершенно верно. В самом деле, реализация операции <, вероятно, в любом случае будет необходимой для сравнения объектов СВох, но реализация поддержки типа объекта не должна ограничивать способы использования объектов в выражении. Применение объектов должно быть насколько возможно естественным. Проблема в том, как это сделать.
Функция-член операции всегда представляет левый аргумент как указатель this. Поскольку в данном случае левый аргумент имеет тип double, вы не можете реализовать это как функцию-член. Это оставляет вам лишь два выбора: обычная функция или дружественная функция (friend). Поскольку вам не нужен доступ к private- членам класса, то и нет необходимости в дружественной функции, поэтому вы можете реализовать перегруженную операцию > с левым операндом типа double в виде обычной функции. Прототип, помещенный вне определения класса, конечно, не будучи членом, должен выглядеть так:
bool operator>(const doubles value, const CBoxS aBox);
А вот и реализация:
H Функция сравнения константы с объектом СВох
bool operator>(const doubles value, const CBoxS aBox) {
return value > aBox.Volume();
}
Как вы уже видели, обычная функция (да и дружественная функция) обращается к членам объекта, используя операцию обращения к члену и имя объекта. Конечно, обычная функция имеет доступ только к общедоступным членам. Функция-член Volume () является public, поэтому с ее использованием здесь проблем нет.
Если бы класс не имел общедоступной функции Volume (), вы могли бы либо воспользоваться дружественной функцией, обращающейся к приватным (private) членам класса непосредственно, либо написать набор функций-членов, которые возвращают значения приватных данных-членов и применить их в обычной функции, чтобы реализовать сравнение.
практическое занятие | псхлна?Гпё^^
А теперь соберем все в один пример, чтобы продемонстрировать, как это работает.
// Ех8_04.срр
// Реализация полностью перегруженной операции "больше чем"
#include <iostream> // Для потокового ввода-вывода using std::cout;
using std::endl;
class СВох// Определение класса в глобальном контексте
{
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
I
return m_Length*m_Width*m_Height; }
// Функция операции ’больше чем’, // сравнивающая объемы объектов СВох.
bool operator> (const CBoxS аВох) const {
return this->Volume () > aBox. Volume () ;
}
// Функция сравнения объекта СВох с константой bool operator>(const doubles value) const
{ return this->Volume() > value;
}
II Определение деструктора
-СВох ()
{ 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";
if(10.О > smallBox) cout « endl
« "Вместимость smallBox меньше 10";
else
cout « endl
« "Вместимость 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); H Прототип функции
Незавершенное объявление идентифицирует СВох для компилятора как имя класса, и этого достаточно, чтобы компилятор смог правильно обработать прототип функции, потому что он знает, что СВох — пользовательский тип, который будет специфицирован позднее.
Этот механизм также важен в тех случаях, когда у вас есть два класса, каждый из которых имеет указатель на объект другого класса в качестве члена. Каждый из них требует предварительного объявления второго. Эту тупиковую ситуацию можно разрешить, используя незавершенные объявления класса.
Ниже показан вывод этого примера.
Вызван конструктор.
Вызван конструктор.
mediumBox больше, чем smallBox
Вместимость mediumBox больше 50
Вместимость smallBox меньше 10
Вызван деструктор.
Вызван деструктор.
После сообщений конструктора об объявлении объектов smallBox и mediumBox имеем три строки вывода от операторов if, каждый из которых работает так, как и можно было ожидать. Первый из них вызывает функцию операции, которая является членом класса и работает с двумя объектами СВох. Второй вызывает функцию-член, имеющую параметр типа double. Выражение в третьем операторе if вызывает функцию операции, реализованную в виде обычной функции.
В данном случае обе функции операций, которые являются членами класса, можно было бы также сделать обычными функциями, поскольку им необходим доступ только к функции-члену Volume (), которая объявлена как public.
Любая операция сравнения может быть реализована почти так, как было показано здесь. Они могут отличаться лишь в незначительных деталях, однако общий подход к реализации будет точно таким же.
Перегрузка операции присваивания
Если вы не представляете в своем классе перегруженной функции операции присваивания, компилятор использует ее вариант по умолчанию. Версия по умолчанию просто выполняет процесс копирования член за членом, подобно тому, как это делает конструктор копирования; однако не путайте конструктор копирования по умолчанию с операцией присваивания по умолчанию. Конструктор копирования по умолчанию вызывается при объявлении объекта класса, инициализируемого существующим объектом того же класса, или при передаче такого объекта в функцию по значению. С другой стороны, операция присваивания по умолчанию вызывается, когда левая и правая части операции присваивания являются объектами одного и того же типа класса.
Для класса СВох операция присваивания по умолчанию работает без проблем, но для любого класса, в котором память для членов выделяется динамически, необходимо тщательно исследовать требования, которые к нему предъявляются. Существует потенциальная угроза породить хаос в программе, если вы оставите без внимания операцию присваивания в таком классе.
Вернемся на минутку к классу CMessage, который мы использовали, когда говорили о конструкторах копирования. Вспомните, что в нем был член ртеssage — указатель на строку. Теперь представьте эффект, который может иметь операция присваивания по умолчанию. Предположим, что имеются два экземпляра этого класса— mottol и motto2. Можно попытаться установить значения членов motto2 равными членам mottol, используя для этого операцию присваивания по умолчанию, как показано ниже:
motto2 = mottol; // Использовать операцию присваивания по умолчанию
Эффект от применения операции присваивания по умолчанию для этого класса будет точно таким же, как от использования конструктора копирования: случится беда! Поскольку каждый объект имеет в себе указатель на одну и ту же строку, то строка, изменяемая в одном объекте, меняется в обоих. Кроме того, существует проблема, связанная с тем, что когда один из экземпляров класса разрушается, его деструктор освобождает память, использованную для строки, и другой объект остается с указателем на память, которая уже может быть задействована для чего-то другого.
Поэтому необходимо, чтобы операция присваивания копировала текст в область памяти, которой владеет целевой объект.
Решение проблемы
Вы можете исправить это, применив собственную функцию операции присваивания, которая может быть определена в рамках определения класса:
// Перегруженная операция присваивания для объектов CMessage
CMessage& operator=(const CMessage& aMess) {
// Освободить память 1-го операнда delete[] pmessage;
pmessage = new char[ strlen(aMess.pmessage) + 1] ;
// Скопировать строку 2-го операнда в 1-й strcpy(this->pmessage, aMess.pmessage);
// Возвратить ссылку на 1-й операнд return *this;
}
Присваивание может выглядеть очень простым, однако есть пара тонкостей, которые требуют дополнительного исследования. Обратите внимание, что вы возвращаете ссылку из функции операции присваивания. Не сразу можно понять, почему так сделано (в конце концов, функция выполняет полную операцию присваивания, и объект справа копируется в объект слева). На первый взгляд может показаться, что вы не должны ничего возвращать, однако следует тщательно подумать о возможных случаях применения этой операции.
Существует вероятность, что вам понадобится использовать результат операции присваивания в правой части выражения. Рассмотрим следующий оператор:
mottol = motto2 = motto3;
Поскольку операция присваивания имеет ассоциативность справа налево, сначала выполняется присваивание motto3 объекту motto2, что транслируется в такой оператор:
mottol = (motto2.operator=(motto3));
Результат этого вызова функции операции расположен справа от знака равенства, поэтому данный оператор в конце принимает следующий вид:
mottol.operator=(motto2.operator=(motto3));
Чтобы это работало, вы, безусловно, должны что-то возвращать. Вызов функции operator= () между скобками должен возвращать объект, который может быть использован в качестве аргумента для вызова функции operator= (). В этом случае должен подойти тип возврата либо CMessage, либо CMessages, поэтому ссылка в такой ситуации не обязательна, но, по крайней мере, вы должны вернуть объект CMessage.
Рассмотрим следующий пример:
(mottol = motto2) = motto3;
Это совершенно легитимный код (скобки служат для того, чтобы обеспечить выполнение в первую очередь левого присваивания). Пример транслируется в такой оператор:
(mottol.operator=(motto2)) = motto3;
Если выразить оставшуюся операцию присваивания как явный вызов перегруженной функции, в конечном итоге получаем:
(mottol.operator=(motto2)).operator=(motto3);
Теперь у нас ситуация, когда возвращенный из функции operator= () объект используется для вызова функции operator= (). Если возвращаемый тип — просто CMessage, это не допустимо, поскольку в действительности возвращается временная копия оригинального объекта, и компилятор не разрешает вызов функции-члена с временным объектом. Другими словами, возвращаемое значение, когда его тип — CMessage, не является lvalue. Единственный способ обеспечить возможность компиляции и корректной работы вещей подобного рода — возвращать ссылку, которая является lvalue, так что единственно возможным типом возврата для полноценной реализации операции присваивания будет CMessages.
Обратите внимание, что “родной” 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 char[ strlen(aMess.pmessage) +1];
// Копировать 2-й операнд в 1-й strcpy(this->pmessage, aMess.pmessage);
// Вернуть ссылку на 1-й операнд return *this;
}
Этот код предполагает, что определение функции находится внутри объявления класса.
практическое занятие I перегрузка операции присваивания
Соберем все это в работающий пример. Одновременно добавим в класс функцию под названием Reset (). Она будет просто сбрасывать сообщение в строку звездочек.
// Ех8_05.срр
/ / Окончательный вариант перегруженной операции присваивания
#include <iostream>
#include <cstring>
using std::cout;
using std::endl;
class CMessage
{
private:
char* pmessage; // Указатель на объект — текстовую строку public:
// Функция для отображения сообщений
void Showlt() const {
cout « endl « pmessage;
}
/ / Функция для сброса сообщений в *
void Reset () {
char* temp = pmessage;
while(*temp)
*(temp++) = ***;
}
// Перегруженная операция присваивания для объектов CMessage CMessageS operator®(const CMessage& aMess)
{
if(this = &aMess) // Проверить адреса, если совпадают, return *this;// вернуть 1-й операнд
// Освободить память для 1-го операнда delete[] pmessage;
pmessage = new char[ strlen(aMess.pmessage) +1];
// Копировать 2-й операнд в 1-й strcpy(this->pmessage, aMess.pmessage);
/ / Вернуть ссылку на 1-й операнд return *this;
}
// Определение конструктора
CMessage(const char* text = "Сообщение по умолчанию") {
pmessage = new char[ strlen(text) +1 ]; // Выделить место для текста strcpy(pmessage, text); // Скопировать текст в новую память
}
// Деструктор для освобождения памяти, выделенной new ~CMessage() {
cout « "Вызван деструктор." // Просто, чтобы видеть, что происходит « endl;
delete [] pmessage;// Освободить память, присвоенную указателю
}
};
int main ()
{
CMessage mottol ("Дьявол заботится о себе сам") ;
CMessage motto2;
cout « "motto2 содержит - " ;
motto2.Showlt() ;
cout « endl;
motto2 = 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 двух объектов, так что результирующий объект СВох может содержать два других объекта СВох. Это не обязательно самое оптимальное решение, но для наших целей его
вполне достаточно. Изменив конструктор, мы также определим, что член m Length объекта СВох всегда больше или равен члену m_Width.
Нашу версию операции сложения ящиков легче объяснить графически, и это показано на рис. 8.4.
Рис. 8.4. Графическая трактовка операции сложения для класса СВох
Поскольку необходимо обращаться к членам объекта непосредственно, сделаем operator+ () функцией-членом. Объявление функции-члена внутри определения класса будет выглядеть так:
СВох operator+(const СВох& аВох) const; // Функция сложения двух объектов СВох
Мы определяем параметр как ссылку, дабы избежать ненужного копирования правого аргумента при вызове функции, и делаем его константной ссылкой, поскольку функция не должна модифицировать аргумент. Если не определить параметр как константную ссылку, то компилятор не позволит передавать в функцию const-объект, так что невозможно будет правому операнду операции + быть объектом const СВох. Кроме того, сама функция определена как const, так как она не изменяет объект, для которого вызывается. Если так не сделать, левый операнд операции + не сможет быть объектом const СВох.
Определение функции operator* () теперь принимает следующий вид:
// Функция сложения двух объектов СВох
СВох СВох::operator*(const СВох& аВох) const {
// Новый объект имеет максимальную длину и ширину, а также сумму высот return СВох (m__Length > аВох.m_Length ? m_Length:aBox.m_Length, m_Width > aBox.m_Width ? m_Width:aBox.m_Width, m_Height + aBox.m_Height);
}
Локальный объект СВох конструируется из текущего объекта (*this) и объекта, переданного в качестве аргумента — аВох. Напомним, что процесс возврата создает временную копию локального объекта и передает обратно вызывающей функции его, а не локальный объект, который исчезает при возврате из функции.
практическое занятие | испытание новой операции сложения
Работу перегруженной операции сложения для класса СВох можно испытать в приведенном ниже примере.
// Ех8__06.срр
// Сложение объектов СВох
#include <iostream> // Для потокового ввода-вывода using std::cout; using std::endl;
class СВох// Определение класса в глобальном контексте
{
public:
// Определение конструктора
СВох (double lv = 1.0, double wv = 1.0, double hv = 1.0) : mjieight(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 СВох& аВох) const { return this->Volume() > aBox.Volume();
}
/ / Функция сравнения объекта СВох с константой int operator>(const doubles value) const {
return Volume() > value;
}
/ / Функция сложения двух объектов СВох
СВох operator*(const CBoxS аВох) 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) ;
}
// Функция для отображения размеров ящика
void ShowBox() const {
cout « m_Length « ” "
« inWidth « ” ”
« m Height « endl;
}
private:
double m_Length; // Длина ящика в дюймах
double m_Width; H Ширина ящика в дюймах double m_Height; // Высота ящика в дюймах
};
int operator>(const doubles value, const CBoxS аВох); II Прототип функции
int main() {
CBox smallBox(4.0, 2.0, 1.0);
CBox mediumBox(10.0, 4.0, 2.0);
СВох aBox;
CBox bBox;
aBox = smallBox + mediumBox;
cout « "Размеры aBox: ";
aBox.ShowBox();
bBox = aBox * smallBox * mediumBox;
cout « "Размеры bBox: ";
bBox.ShowBox();
return 0;
}
11 Функция сравнения константы с объектом СВох
int operator>(const doubles value, const CBoxS aBox)
{
return value > aBox.Volume();
}
Ниже в настоящей главе мы еще обратимся к определению класса СВох.
Описание полученных результатов
В этом примере я немного изменил члены класса СВох. Деструктор исключен, поскольку он в этом классе не нужен, а конструктор модифицирован, чтобы гарантировать, что член m_Length не будет меньше, чем m_Width. Уверенность в том, что длина ящика всегда не меньше ширины, несколько облегчает операцию сложения. Я также добавил функцию ShowBox () для вывода размеров объекта СВох. Благодаря
ей, мы сможем проверить, что наша перегруженная операция сложения работает, как ожидалось.
Ниже показан вывод этой программы.
Размеры аВох: 10 4 3
Размеры ЬВох: 10 4 6
Это выглядит согласованно с принятой нами идеей сложения объектов СВох и, как видите, функция также работает в примере с множественными операциями сложения. Для вычисления ЬВох перегруженная операция сложения вызывается дважды.
Вы также можете реализовать сложение для этого класса в виде дружественной функции. Вот ее прототип:
friend СВох operator+(const CBoxS аВох, const CBoxS ЬВох);
Процесс получения результата был бы почти таким же, за исключением необходимости использовать операцию прямого обращения к членам для получения членов обоих аргументов функции. Это будет работать почти так же хорошо, как и первая версия функции операции.
Перегрузка операций инкремента и декремента
Я кратко представлю механизм перегрузки операций инкремента и декремента в классе, поскольку они обладают некоторыми особыми характеристиками, которые отличают их от других унарных операций. Нужно каким-то образом учесть тот факт, что операции ++ и — могут иметь префиксную и постфиксную формы, и эффект от них отличается. В “родном” C++ перегруженные операции для префиксной и постфиксной форм инкремента и декремента отличаются. Вот, например, как они могут быть определены в классе Length:
class Length
{
private:
double len;// Значение длины для класса
public:
Lengths operator++();// Префиксная операция инкремента
const Length operator++(int); // Постфиксная операция инкремента Lengths operator—();// Префиксная операция декремента
const Length operator—(int); // Постфиксная операция инкремента // остальная часть класса...
}
Этот простой класс предполагает, что длина хранится как значение типа double. В реальности вы можете сделать этот класс несколько сложнее, однако он служит только для иллюстрации перегрузки операций инкремента и декремента.
Первое, чем отличаются перегруженные формы операций — это списками параметров; префиксная форма не принимает параметров, а постфиксная принимает параметр типа int. Параметр в функции постфиксной операции служит только для того, чтобы отличать ее от префиксной формы, и никак не используется в реализации функции.
Операции префиксного инкремента и декремента обрабатывают операнд перед тем, как его значение будет использовано в выражении, так что вы просто возвращаете ссылку на текущий объект после того, как он будет увеличен или уменьшен. С префиксной формой операнд увеличивается (или уменьшается) после того, как его текущее значение используется в выражении. Это достигается созданием копии те-
кущего объекта перед выполнением самой операции и возвратом этой копии после модификации объекта.
Шаблоны классов
Как было сказано в главе 6, вы можете определять шаблоны функций, которые автоматически генерируют различные вариации функций в зависимости от типа принятого аргумента или типа возвращаемого значения. В C++ включен подобный механизм и для классов. Шаблон класса сам по себе не является классом. Это некоторая разновидность “рецепта” для компилятора, по которому он генерирует код класса. Как видно на рис. 8.5, он подобен шаблону функции — вы определяете класс, который хотите генерировать, специфицируя выбор типа параметра (в данном случае Т), который указывается между угловыми скобками в объявлении шаблона. Это заставляет компилятор генерировать код определенного класса, который мы называем экземпляром шаблона класса. Процесс создания класса из шаблона называется реализацией шаблона.
Соответствующее определение класса генерируется, когда вы создаете экземпляр объекта шаблона класса для конкретного параметра типа, так что вы можете генерировать любое количество классов из одного-единственного шаблона. Чтобы получить представление о том, как это работает на практике, рассмотрим соответствующий пример.
Экземпляры классаЭкземпляры классаРис. 8.5. Реализация шаблона классаРис. 8.5. Реализация шаблона класса
Экземпляры класса
Экземпляры класса
Рис. 8.5. Реализация шаблона класса
Рис. 8.5. Реализация шаблона класса
Определение шаблона класса
Для иллюстрации определения и использования шаблона класса рассмотрим простой пример. Мы не станем чересчур усложнять его, слишком заботясь об ошибках, которые могут возникнуть. Предположим, что вы хотите определять классы, которые могут хранить количество выборок данных некоторого вида, и каждый класс предоставляет функцию Мах () для определения максимальной выборки. Эта функция подобна той, что вы видели при обсуждении о шаблонах из главы 6. Вы можете определить шаблон класса, который генерирует класс CSample для хранения выборок любого типа, который хотите.
template <class Т>
class CSamples
{
public:
// Определение конструктора — принимает массив выборок
CSamples(const Т values[], int count)
I
m_Free = count < 100? count:100;// He превышать размер массива
for (int i = 0; i < m_Free; i++)
m_Values[i] = values[i];// Сохранить счетчик выборок
}
H Конструктор, принимающий единственную выборку
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++) II Проверить все выборки
if(m_Values[i] > theMax)
theMax = m_Values[i); // Сохранять любое большее значение return theMax;
}
private:
T m_Values[100];// Массив для хранения выборок
int m_Free;// Индекс свободного места в m_Values
Чтобы указать, что вы определяете шаблон, а не обычный класс, вставляется ключевое слово template и параметр типа Т между угловыми скобками непосредственно перед ключевым словом class и именем класса CSamples. По сути, это тот же синтаксис, который применялся для определения шаблона функции в главе 6. Параметр Т — это переменная типа, которая заменяется специфическим типом, когда вы объявляете объект класса. Всякий раз, когда параметр Т появляется в определении класса, он заменяется типом, который вы специфицируете в объявлении объекта; это создает определение класса, соответствующего этому типу. Вы можете задавать любой тип (базовый или тип класса), но он, конечно, должен иметь смысл в контексте шаблона класса. Любой тип класса, используемый для реализации класса из шаблона, должен иметь определенными все операции, которые применяются функциями-членами этого шаблона с объектами этого типа. Например, если ваш класс не имеет реализованной функции operator> (), он не работает с шаблоном класса CSample. Вообще вы можете специфицировать множество параметров в шаблоне класса, если это необходимо. Я вернусь к этой возможности немного позже.
Возвращаясь к примеру, обратим внимание на то, что тип массива, в котором сохраняются выборки данных, указан как Т. Поэтому этот массив будет массивом любого типа, который вы зададите вместо Т при объявлении объекта CSample. Как видим, тип Т также используется в двух конструкторах класса, а также в функциях Add () и Мах (). В каждом из этих мест Т заменяется реальным типом при создании объекта класса с использованием шаблона.
Конструктор поддерживает создание пустого объекта, объекта с единственной выборкой и объекта, инициализированного массивом выборок. Функция Add () всегда позволяет добавлять к объекту новые выборки по одной. Вы также можете перегрузить эту функцию для добавления сразу массива выборок. Шаблон класса включает некоторые элементарные меры предосторожности, чтобы предотвратить переполнение массива m Values в функции Add () и конструкторе, принимающем массив выборок.
Как уже упоминалось, теоретически вы можете создавать объекты классов CSample, которые обрабатывают данные любых типов: типа int, типа double и любого типа класса, определенного вами. На практике это не значит, что они будут компилироваться и работать так, как вы ожидаете. Все зависит от того, что делает определение шаблона, и обычно шаблон работает только для определенного диапазона типов. Например, функция Мах () неявно предполагает, что для любого обрабатываемого типа доступна операция >. Если это не так, программа не скомпилируется. Понятно, что вы всегда будете определять шаблон, который работает только с некоторыми типами, а с другими — не работает, однако нет способа ограничить типы, применимые с конкретным шаблоном.
Шаблонная функция-член
Может возникнуть необходимость в размещении определения шаблонной функции-члена класса за пределами определения шаблона класса. Синтаксис, применяемый для этого, не столь очевиден, так что давайте посмотрим, как это сделать. Вы помещаете объявление функции в определение шаблона класса обычным способом, например:
template <class Т> class CSamples
{
// Остальная часть определения шаблона...
Т Мах() const; // Функция для получения максимальной выборки
// Остальная часть определения шаблона...
Здесь объявляется функция Мах () как член шаблона класса, однако без своего определения. Теперь вы должны создать отдельный шаблон функции для определения такой функции-члена. Для этого вы должны использовать имя шаблона класса плюс параметры в угловых скобках, чтобы идентифицировать шаблон класса, к которому относится шаблон функции:
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];H Сохранить максимальную выборку
return theMax;
Вы видели синтаксис шаблона функции в главе 6. Поскольку этот шаблон функции предназначен для члена шаблона класса с параметром Т, определение шаблона функции должно иметь те же параметры, что и определение шаблона класса. В данном случае он один — Т, но вообще их может быть несколько. Если шаблон класса принимает два или более параметров, точно так же должна быть определена каждая из его функций-членов.
Обратите внимание на то, как должно указываться имя параметра Т наряду с именем класса перед операцией разрешения контекста. Это необходимо — параметры важны для идентификации класса, к которому относится функция, произведенная от шаблона. Типом будет Scamples<T> с любым типом, назначенным вместо Т, когда вы создаете экземпляр шаблона класса. Ваш тип включается в шаблон класса для генерации определения класса и в шаблон функции — для генерации определения функции Мах () для класса. Каждый класс, полученный от шаблона класса, должен иметь собственное определение функции Мах ().
Определение конструктора или деструктора вне шаблона класса выполняется аналогично. Вы можете написать определение конструктора, который принимает массив выборок, следующим образом:
templatecclass Т>
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>). Параметр с шаблоном класса вы используете только перед операцией разрешения контекста.
Создание объектов из шаблона класса
Когда вы применяете функцию, определенную шаблоном функции, компилятор способен сгенерировать функцию на основании используемых типов аргументов. Параметр типа для шаблона функции неявно определен специфическим применением этой функции. Шаблоны классов в этом отношении немного отличаются. Чтобы
создать объект на основе шаблона класса, вы всегда должны специфицировать в объявлении параметр типа следом за именем класса.
Например, чтобы объявить объект С Sample so для работы с выборками типа double, потребуется написать объявление так:
CSamples<double> myData(10.0);
Это определяет объект типа CSamples<double>, который может хранить выборки типа double, и объект создается с одной выборкой, сохраненной в нем, со значением 10.0.
| Практическое занятие | Шаблонь| классов
Вы можете создать объект из шаблона CSampleso, который будет хранить объекты СВох. Это работает, потому что в классе СВох реализована функция operator> () для перегрузки операции “больше чем”. Вы можете испытать шаблон класса с функцией main () в следующем примере.
II Ех8_07.срр
// Использование шаблона класса
#include <iostream>
using std::cout;
using std::endl;
II Сюда поместить определение класса СВох из Ех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)
{
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];II Сохранять любое большее значение
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. срр, приведенного ранее в этой главе. Вам не нужно беспокоиться о функции operator> (), поддерживающей сравнение объекта СВох со значением типа double, поскольку этот пример в нем не нуждается. За исключением конструктора по умолчанию, все функции-члены шаблона определены отдельными шаблонами функций, только чтобы показать вам полный пример того, как это делается.
В main () вы создаете массив из трех объектов СВох и затем используете его для инициализации объекта CSamples, который может хранить объекты СВох. Объявление объекта CSamples в основном такое же, как оно могло быть для обычного класса, но с добавлением параметра типа в угловых скобках, следующего за именем шаблона класса.
Эта программа генерирует следующий вывод:
Объем самого большого ящика: 120
Обратите внимание, что когда вы создаете экземпляр шаблона класса, из этого не следует, что экземпляры шаблонов функций для функций-членов также будут созданы. Компилятор создает экземпляры шаблонов только для функций-членов, которые действительно вызываются в программе. Фактически ваши шаблоны функций могут даже содержать ошибки в коде, и до тех пор, пока вы не вызываете функцию-член, которую генерирует шаблон, компилятор не станет жаловаться. Вы можете протестировать это на примере. Попробуйте внести несколько ошибок в шаблон функции-чле-
на Add (). Программа по-прежнему будет компилироваться и запускаться, потому что она не вызывает функцию Add ().
Вы можете попробовать модифицировать пример и возможно, увидите, что случится, когда вы реализуете классы, используя шаблон с другими типами.
Наверное, вас удивит то, что произойдет, если добавить несколько выходных параметров в конструкторы класса. Выяснится, что конструктор СВох будет вызван 103 раза! Посмотрите, что происходит в функции ma in (). Сначала вы создаете массив из 3 объектов СВох — отсюда 3 вызова. Затем вы создаете объект CSamples для их хранения, но объект CS amp les содержит массив из 100 переменных типа СВох, значит, вы вызываете конструктор по умолчанию 100 раз— по одному для каждого элемента массива. Конечно, объект тахВох будет создан конструктором копирования по умолчанию, созданным компилятором.
Шаблоны классов с множественными параметрами
Применение множественных параметров типа в шаблоне класса — естественное продолжение представленного выше примера с единственным параметром. Можно использовать каждый из параметров типа всякий раз, когда они понадобятся в определении шаблона. Например, вы можете определить шаблон класса с двумя параметрами типа:
template<class Tl, class Т2> class CExampleClass
{
// Данные-члены шаблона private:
Tl 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) {
m_Free = count < Size ? count:Size;// 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; // Ничего не сохранено, поэтому свободен первый )
/ / Функция для добавления выборки
int Add(const Т& value)
I
int OK = m^Free < Size; // Признак наличия свободного места
if(OK)
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++) H Проверить все выборки
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) > МуТуреО; Н ОК
Скобки гарантируют, что выражение для второго аргумента шаблона не будет спутано с угловой скобкой.
Использование классов
Я рассказал о большинстве базовых аспектов определения классов в “родном” C++, поэтому теперь имеет смысл разобрать пример использования класса для решения проблемы. Придется ограничиться простой проблемой, дабы сохранить размер настоящей книги в разумных пределах, потому рассмотрим проблемы, для решения которых можно использовать расширенную версию класса СВох.
Понятие интерфейса класса
Реализация расширенного класса СВох должна включать понятие интерфейса класса. Сейчас мы попробуем разработать набор инструментов для любого, кто пожелает работать с объектами СВох — нам понадобится собрать множество функций, предоставляющих интерфейс к миру ящиков. Поскольку интерфейс — это лишь способ работы с объектами СВох, он должен быть определен так, чтобы адекватно покрывать все, что может понадобиться делать с объектом СВох, и быть реализованным так, чтобы насколько возможно, предотвращать неправильное понимание и непреднамеренные ошибки.
Первый вопрос, который вы должны рассмотреть при проектировании класса — это природа проблемы, которую вы намерены решить, и отсюда определить вид функциональности, которую вы хотите представить в интерфейсе класса.
Определение проблемы
Принципиальное назначение ящика — содержать объекты того или иного типа, что можно обозначить одним словом: упаковка. Попытаемся представить класс, который вообще избавляет от проблем упаковки, и посмотрим, как он может использоваться. Предположим, что всегда можно упаковать объекты СВох в другой объект СВох, потому что, если вы хотите упаковать конфеты в коробку, то всегда можно представить каждую конфету как идеализированный объект СВох. Основные операции, которые вы можете пожелать представить в классе СВох, перечислены ниже.
Вычисление объема СВох. Это фундаментальная характеристика объекта СВох, и ее реализация у вас уже имеется.
Сравнение объемов двух объектов СВох для определения того, который из них больше. Вероятно, вы должны поддерживать полный набор операций сравнения для объектов СВох. Версия операции > у вас уже есть.
Сравнение объема объекта СВох с указанным значением и наоборот. Реализация этой операции > также уже имеется, но функции, поддерживающие другие операции сравнения, придется реализовать дополнительно.
Сложение двух объектов СВох для создания нового объекта СВох, который будет содержать оба исходных. Таким образом, результат будет иметь, как минимум, сумму их объемов, но может быть и больше. У вас уже есть версия этого, которая перегружает операцию +.
□ Умножение объекта СВох на целое (и наоборот) для создания объекта СВох, который будет содержать указанное количество исходных объектов. Это позволит эффективно моделировать коробку, в которую пакуется группа ящиков.
□ Определение, сколько объектов СВох заданного размера можно упаковать в другой объект СВох заданного размера. Это, по сути, деление, поэтому вы могли бы реализовать его, перегрузив операцию /.
□ Определение объема свободного пространства, остающегося в объекте СВох после помещения в него максимального количества объектов СВох заданного размера.
На этом я лучше остановлюсь! Несомненно, могут существовать и другие функции — очень удобные, но для экономии места мы договоримся, что приведенного перечня достаточно, помимо таких служебных средств, как, например, доступ к от: дельным размерам (длине, ширине, высоте).
Реализация класса СВох
Вам следует подумать о степени защиты от ошибок, которую вы хотите встроить в класс СВох. Базовый класс, который мы определили для иллюстрации различных аспектов классов, является начальной точкой, но вы должны также рассмотреть некоторые моменты более глубоко. В предложенном дизайне конструктор — слабое место, поскольку он не гарантирует правильности размерностей СВох, поэтому, возможно, первое, что вам потребуется сделать — это гарантировать, что вы всегда получите корректные объекты. Чтобы это сделать, можно переопределить базовый класс, как показано ниже.
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;Hразмеры
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;
}
H Функция, представляющая длину ящика
double GetLengthO const { return m_Length; }
H Функция, представляющая ширину ящика
double GetWidthO const { return m_Width; }
H Функция, представляющая высоту ящика
double GetHeightO const { return m_Height; } private:
double m_Length;И Длина ящика в дюймах
double m_Width;// Ширина ящика в дюймах
double m_Height;// Высота ящика в дюймах
Теперь конструктор безопасен, потому что любая размерность, которую пользователь класса попытается указать, как отрицательное число или ноль, автоматически в конструкторе заменяется единицей. Вы можете также выдать в этом случае сообщение, поскольку это, очевидно, ошибочная ситуация, и произвольная и молчаливая установка размера в 1 может оказаться не лучшим решением.
Конструктор копирования по умолчанию удовлетворителен для нашего класса, поскольку здесь нет никакого динамического выделения памяти для членов данных, и операция присваивания также будет работать, как надо. Деструктор по умолчанию также в этом случае работает отлично, и нет необходимости переопределять его. Возможно, теперь следует рассмотреть, что понадобится для поддержки сравнения объектов нашего класса.
Сравнение объектов свох
Вы должны включить поддержку операций >, >=. ==, < и <=, чтобы они работали с обоими операндами объектов СВох, а также между объектом СВох и значением типа double. Это можно реализовать в виде обычных глобальных функций, поскольку не обязательно, чтобы они были функциями-членами. Вы можете написать функции сравнения двух объектов СВ ох в терминах функций, сравнивающих объем объекта СВох со значением типа double, поэтому начнем с последних. Повторим функцию operator> (), которую уже вы видели ранее:
Н Функция проверки того, больше ли константа объекта СВох
int operator> (const doubles value, const CBoxS aBox) {
return value > aBox.Volume();
Теперь вы можете написать функцию operator< () аналогичным образом:
// Функция проверки того, меньше ли константа объекта СВох
int operator<(const doubles value, const CBoxS aBox)
{
return value < aBox.Volume();
}
Вы можете закодировать реализацию тех же операций с аргументами в противоположном порядке в терминах двух предыдущих функций:
Н Функция проверки того, больше ли объект СВох, чем константа
int operator>(const CBoxS aBox, const doubles value)
{ return value < aBox; }
H Функция проверки того, меньше ли объект СВох, чем константа
int operator<(const CBoxS aBox, const doubles value)
{ return value > aBox; }
Вы просто используете соответствующие перегруженные функции операций, который написали ранее, но с перестановкой аргументов.
Функции, реализующие операции >= и <= — такие же, как первые две, но с заменой операции < на <=, а > — на >=; на этой стадии их реализовать совсем не сложно. Функция operator== () также очень похожа:
/ / Функция для проверки равенства константы объему объекта СВох
int operator== (const doubles value, const CBoxS aBox)
{
return value == aBox.Volume();
/ / Функция проверки равенства объема объекта СВох константе 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);// n нечетное
else
return CBox(m_Length, 2.0*m_Width, (n/2)*m_Height);// n четное
Умножение СВох: п нечетное : 3*аВохУмножение СВох: п нечетное : 3*аВохРис. 8.6. Графическая трактовка операции умножения для класса СВохРис. 8.6. Графическая трактовка операции умножения для класса СВох
Умножение СВох: п нечетное : 3*аВох
Умножение СВох: п нечетное : 3*аВох
Рис. 8.6. Графическая трактовка операции умножения для класса СВох
Рис. 8.6. Графическая трактовка операции умножения для класса СВох
Умножение СВох: п четное : 6*аВох
Здесь вы используете операцию % для определения того, является ли п нечетным. Если п нечетное, то значение п%2 равно 1 и оператор if дает true. Если же п четное, то п%2 равно 0 и весь оператор дает false.
Теперь вы можете использовать только что написанную функцию в реализации версии с левым операндом как целым. Это можно оформить в виде обычно функции — не члена:
/ / Операция умножения СВох - п*аВох
СВох operator*(int n, const CBox& aBox)
{ return aBox*n;
}
Данная версия операции умножения просто обращает порядок операндов, чтобы непосредственно использовать предыдущую версию функции. Это завершает набор арифметических операций для объектов СВох, которые вы определили. И, наконец, рассмотрим две функции аналитических операций — operator/ () и operator% ().
Анализ объектов СВох
Как я уже говорил, операция деления определяет, сколько объектов СВох, идентичных тому, который указан в правом операнде, могут быть помещены в объект СВох, специфицированный в левом операнде. Чтобы не слишком усложнять, предположим, что все объекты СВох упаковываются вверх — то есть, по вертикальному измерению. Также предположим, что они все они выравниваются по длине. Без этих предположений все было бы несколько сложнее.
Проблема состоит в том, чтобы определить, сколько объектов правого операнда могут быть помещены в один слой, и затем решить, сколько слоев мы можем иметь внутри СВох левого операнда.
Вы можете закодировать это в виде функции-члена примерно так:
int operator/(const СВох& аВох) {
int tel = 0;// Временное значение числа в горизонтальном плане / / в одном направлении
int tc2 = 0;// Временное значение для числа в другом направлении
tel = static_cast<int>((m_Length / аВох.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() - ((аВох/ЬВох)*ЬВох.Volume());
}
8/3 = 2
Н = 1
W = 2
В этой конфигурации может быть сохранено 12
Рис. 8.7. Графическая трактовка операции деления для класса СВох
Это вычисление реализуется очень легко с помощью существующих функций класса. Результат — объем большего ящика, аВох, минус объем всех ЬВох, которые могут быть помещены в него. Количество объектов ЬВох, упакованных в аВох, дает выражение аВох/ЬВох, которое использует ранее перегруженную операцию деления. Вы умножаете его на объем объектов ЬВох, чтобы получить объем, который нужно вычесть из общего объема большего ящика аВох.
Практическое занятиеПрактическое занятиеНа этом интерфейс класса завершен. Ясно, что для реального использования класса может понадобиться намного больше функций, однако в качестве интересной работающей модели, демонстрирующей проектирование класса, предназначенного для решения определенного рода проблем, этого вполне достаточно. Теперь можно двинуться дальше и попытаться применить ее для решения реальной проблемы.
Практическое занятие
Практическое занятие
Многофайловый проект, использующий
класс СВох
Прежде чем вы сможете действительно начать написание кода для использования класса СВох и его перегруженных операций, сначала понадобится собрать определение класса в единое целое. Мы применим несколько другой подход, отличающийся
от того, что вы видели ранее — составим проект из нескольких файлов. Начнем с использования средств, предоставляемых Visual Studio C++ 2005 для создания и сопровождения кода наших классов. Это значит, что у вас будет меньше работы, но это также значит, что местоположение кода слегка изменится.
Начнем с создания нового проекта консольного приложения Win32 по имени Ех8_08, отметив опцию Empty project (Пустой проект). После выбора вкладки Class View (Представление классов) вы увидите окно, показанное на рис. 8.8.
Рис. 8.8. Вкладка Class View (Представление классов) проекта Ех8^08Рис. 8.8. Вкладка Class View (Представление классов) проекта Ех8^08
Рис. 8.8. Вкладка Class View (Представление классов) проекта Ех8^08
Рис. 8.8. Вкладка Class View (Представление классов) проекта Ех8^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, а реализация каждого класса — в собственный файл . срр.
Рис. 8.9. Мастер Generic C++ Class Wizard (Мастер создания обобщенных классов C++)
Когда вы щелкаете на кнопке Finish (Готово) в диалоговом окне, происходят два события.
Создается файл Box.h, в котором содержится скелет определения класса СВох. Он включает конструктор без аргументов и деструктор.
Создается файл Box. ерр, в котором содержится скелет определения реализации функций класса, включая тела конструктора и деструктора — разумеется, пустые.
Панель редактора, отображающая код, должна выглядеть так, как показано на рис. 8.10. Если она еще не отображена, щелкните правой кнопкой на СВох в Class View, и она появится.
Рис. 8.10. Панель редактора с кодом класса СВох
Как видите, над панелью, содержащей листинг кода класса, расположены два элемента управления. Левый отображает текущее имя класса — СВох, а щелчок на кнопке
справа от имени класса отобразит список всех классов проекта. В общем случае, вы можете использовать этот элемент управления для переключения на другой класс, выбирая его из списка, но в данной ситуации определен только один класс. Элемент управления справа имеет отношение к членам текущего класса, определенным в файле . срр, и щелчок на его кнопке отобразит члены класса. Выбор члена из списка делает видимым его код в панели, находящейся ниже.
Начнем разработку класса СВох на основе того, что нам автоматически предоставляет 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, записанную так:
#if ! defined BOX_H
#define BOX_H
H Код, который не должен включаться более одного раза -
И такой, как определение класса СВох
#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 затем изменяется, и будет выглядеть так:
#pragma once
class СВох {
public:
СВох(void);
public:
~CBox(void);
private:
/ / Длина ящика в дюймах
double m__Length;
И Ширина ящика в дюймах double m_Width;
/ / Высота ящика в дюймах
double m_Height;
Конечно, при желании вы можете вводить объявления этих членов вручную, непосредственно в коде. Но всегда есть выбор — использование автоматизации, представленной IDE-средой.
Вы также можете вручную удалить любую часть, сгенерированную автоматически, но не забудьте при этом внести соответствующие изменения в файлы .Ни. срр.
Хорошей идеей будет сохранение всех файлов при каждом ручном изменении, поскольку это вызовет обновление информации в Class View.
Если вы заглянете в файл Box. срр, то увидите, что мастер также добавил список инициализации в определение конструктора для данных-членов, которые вы добавили, причем каждая переменная инициализируется нулем. Далее вы можете модифицировать конструктор, чтобы он делал то, что необходимо.
Определение конструктора
Вам нужно изменить объявление конструктора без аргументов, сгенерированного в определении класса, чтобы он принимал аргументы со значениями по умолчанию, поэтому сделайте так:
СВох (double Iv = 1.0, double wv = 1.0, double hv = 1.0);
Теперь вы готовы к реализации. Откройте файл Box. срр, если он еще не открыт, и модифицируйте определение конструктора следующим образом:
СВох::СВох(double Iv, double wv, double hv)
Iv = Iv <= 0.0 ? wv = wv <= 0.0 ? hv = hv <= 0.0 ? m_Length = lv>wvIv = Iv <= 0.0 ? wv = wv <= 0.0 ? hv = hv <= 0.0 ? m_Length = lv>wv1.0 : Iv;1.0 : wv;1.0 : hv;? Iv : wv1.0 : Iv;1.0 : wv;1.0 : hv;? Iv : wvm_Width = wv<lv ? wv : Iv; m_Height = hv;m_Width = wv<lv ? wv : Iv; m_Height = hv;/ / Обеспечить положительные// размеры// объекта// Обеспечить, чтобы// length >= width/ / Обеспечить положительные// размеры// объекта// Обеспечить, чтобы// length >= width
Iv = Iv <= 0.0 ? wv = wv <= 0.0 ? hv = hv <= 0.0 ? m_Length = lv>wv
Iv = Iv <= 0.0 ? wv = wv <= 0.0 ? hv = hv <= 0.0 ? m_Length = lv>wv
1.0 : Iv;
1.0 : wv;
1.0 : hv;
? Iv : wv
1.0 : Iv;
1.0 : wv;
1.0 : hv;
? Iv : wv
m_Width = wv<lv ? wv : Iv; m_Height = hv;
m_Width = wv<lv ? wv : Iv; m_Height = hv;
/ / Обеспечить положительные
// размеры
// объекта
// Обеспечить, чтобы
// length >= width
/ / Обеспечить положительные
// размеры
// объекта
// Обеспечить, чтобы
// length >= width
Вспомните, что инициализаторы параметров функций-членов должны появляться только в объявлении члена, находящемся в определении класса, а не в определении самой функции. Если вы поместите их в определение функции, ваш код не будет компилироваться. Вы уже видели этот код, поэтому я не стану на нем останавливаться. Будет хорошей идеей сохранить файл в этой точке, щелкнув на кнопке Save (Сохранить) в панели инструментов. Возьмите за правило сохранять редактируемый файл перед тем, как переключаетесь на что-то другое. Если вам опять понадобится редактировать конструктор, вы сможете легко это сделать, выполнив двойной щелчок на нем в нижней панели на вкладке Class View или выбрав его из правого выпадающего меню над панелью с кодом.
Также вы можете напрямую обратиться к определению функции-члена в файле . срр или к ее объявлению в файле . h непосредственно правым щелчком на его имени в панели Class View и выбором соответствующей позиции в появившемся контекстном меню.
Добавление функций-членов
Теперь вам нужно добавить все функции, которые вы видели ранее в классе СВох. Ранее вы уже определяли несколько функций-членов внутри определения класса, так что эти функции по умолчанию были встроенными (inline). Можно достичь одного и того же результата, вводя код этих функций в определение класса вручную или же с помощью мастера Add Member Function Wizard (Мастер добавления функций-членов).
Вы можете подумать, что можно определить каждую встроенную функцию в файле . срр с добавлением к ее определению ключевого слова inline, однако проблема в том, что встроенные функции на самом деле не являются “настоящими” функциями. Поскольку код тела каждой функции должен вставляться непосредственно в точку их вызова, определение функции должно быть доступно, когда компилируется файл, содержащий эти вызовы. Если этого не будет, вы получите ошибки компоновки и ваша программа не запустится. Если вы хотите, чтобы функции-члены были встроенными, то вы должны включить определения функций в заголовочный файл . h класса. Они могут быть определены либо внутри определения класса, либо немедленно вслед за ним, в файле . h. Вы должны поместить все глобальные встроенные функции, которые вам нужны, в файл . h и включить с помощью #include этот файл в каждый файл . срр, который использует их.
Чтобы добавить функцию GetHeight () как встроенную, щелкните правой кнопкой мыши на СВох во вкладке Class View и выберите Add^Add Function (Добавить1^ Добавить функцию) из контекстного меню. Затем вы можете ввести данные, определяющие функцию в диалоге, как показано на рис. 8.11.
Рис. 8.11. Создание в мастере Add Member Function Wizard (Мастер добавления функций-членов) встроенной функции
Вы можете указать в качестве типа возврата double, выбирая его в выпадающем списке, но точно также можете ввести его вручную. Очевидно, что тип, которого нет в списке, придется вводить с клавиатуры. Отметка флажка Inline (Встроенная) обеспечивает создание GetHeight () в виде встроенной функции. Обратите внимание, что среди других опций для объявления функции имеются флажки Static (Статическая), Virtual (Виртуальная) и Pure (Чистая). Как вы знаете, статическая (static) функция-член существует независимо от объектов класса. Что касается виртуальных (virtual) и чистых (pure) функций, то с ними мы познакомимся в главе 9. Функция GetHeight () не имеет параметров, так что ничего более добавлять не нужно. Щелчок
на кнопке ОК добавит определение функции к определению класса в 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:
II Длина ящика в дюймах
double m_Length;
/ / Ширина ящика в дюймах
double m_Width;
/ / Высота ящика в дюймах
double m_Height;
public:
double GetHeight(void)
{
return 0;
}
double GetWidth(void)
{
return 0;
}
double CBox::GetLength(void)
{
return 0;
1
// Вычисление объема ящика
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, что более удобно. Как и ранее, щелкните пра-
вой кнопкой на СВ ох во вкладке Class View и выберите пункт Add^Add Function (Добавить1^ Добавить функцию) из контекстного меню. Затем в появившемся диалоговом окне мастера вы можете ввести детали первой функции, которую хотите добавить (рис. 8.12).
Рис. 8.12. Создание в мастере Add Member Function Wizard (Мастер добавления функций-членов) обычной функции
Здесь я определил функцию operator* () как public с типом возврата СВох. Тип параметра и его имя также должны быть введены в соответствующих полях. Потребуется щелкнуть на кнопке Add (добавить) для регистрации параметра, а только затем щелкать на кнопке Finish (Готово). Это также обновит сигнатуру функции, показанную в нижней части диалогового окна Add Member Function Wizard. Затем можете ввести детали другого параметра, если их больше одного, и снова щелкнуть на кнопке Add, чтобы добавить его. Введенные в этом диалоговом окне комментарии мастер поместит в файлы Box. h и Box. срр. Когда вы щелкаете на кнопке Finish, объявление функции добавляется к определению класса в Box. h, а скелет определения функции — в файл Box. срр. Функция должна быть объявлена как const, поэтому вы должны добавить это ключевое поле к объявлению функции operator* () внутри определения класса и к определению функции в Box. срр. Вы должны также добавить код в тело функции, примерно так:
СВох СВох::operator 4(const СВох& аВох) const {
// Новый объект имеет максимальную длину и ширину из двух, И и сумму двух высот
return СВох (m_Length > aBox.m_Length ? m_Length : aBox.m_Length, m__Width > aBox.mJWidth ? m_Width : aBox. m_Width, mJHeight * aBox.m_Height);
}
Вы должны повторить этот процесс для функций operator* () и operator / (), которые видели ранее. По завершении определение класса в 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;
// Высота ящика в дюймах
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:
// Перегруженная операция сложения
СВох operator*(const CBox& аВох) const;
public:
И Умножение ящика на целое число
СВох operator*(int n) const;
public:
// Деление одного ящика на другой int operator/(const СВох& аВох) const;
};
Вы можете редактировать или упорядочивать код любым способом по своему желанию (конечно, при условии его корректности). Я добавил несколько пустых строк, чтобы сделать код чуть более читаемым.
Содержимое файла Box. срр в конечном итоге будет выглядеть примерно так:
#include ".\box.h"
СВох::СВох(double lv, double wv, double hv)
{
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 ? Iv : wv; // Гарантировать, что m_Width = wv<lv ? wv : Iv;// length >= width
m_Height = hv;
}
CBox::~CBox(void)
{ }
// Перегруженная операция сложения
СВох СВох: : opera tor + (const CBox& 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);
}
/ / Умножение ящика на целое число
СВох СВох: :operator*(int n) const
{
if(n%2)
return CBox(m_Length, m_Width, n*m_Height) ;// n нечетное
else
return CBox(m_Length, 2.0*mJWidth, (n/2)*m_Height) ;// n четное
}
/ / Деление одного ящика на другой
int СВох::operator/(const СВох& aBox) const
{
// Временное значение числа в горизонтальном плане в одном направлении 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.mJWidth)) * static_cast<int>((m_Width / aBox.m_Length)) ;// и этак
//Вернуть наилучшее наполнение
return static_cast<int>( (m_Height/aBox.m_Height) * (tcl>tc2 ? tel : tc2));
}
Полужирным выделены строки, модифицированные или добавленные вручную.
Очень короткие функции, в частности те, что просто возвращают значение члена данных, содержат свои определения внутри определения класса, поэтому они являются встроенными. Если заглянуть во вкладку Class View, щелкнув на ней и затем на + рядом с именем класса СВох, вы увидите, что все члены класса показаны в нижней панели.
На этом класс СВох завершен, но еще потребуется определить глобальные функции, реализующие операции сравнения объема объекта СВох с числовым значением.
Добавление глобальных функций
Вам нужно создать файл . срр, который будет содержать определения глобальных функций, поддерживающих операции объектов СВох. Этот файл также должен быть частью проекта. Щелкните на вкладке Solution Explorer (Проводник решений) для его отображения (у вас в данный момент открыта вкладка Class View) и щелкните правой кнопкой на папке Source Files (Исходные файлы). Выберите пункт Add^New
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(); }
/ / Функция проверки объекта СВох на предмет > константы bool operator>(const CBoxS aBox, const doubles value) { return value < aBox; }
// Функция проверки объекта СВох на предмет < константы bool operator<( const CBoxS aBox, const doubles value) { return value > aBox; }
// Функция проверки константы на предмет >= объекту СВох bool operator>=(const doubles value, const CBoxS aBox) { return value >= aBox.Volume(); }
// Функция проверки константы на предмет <= объекту СВох bool operator<=(const doubles value, const CBoxS aBox) { return value <= aBox.Volume(); }
/ / Функция проверки объекта СВох на предмет >= константе bool operator>=( const CBoxS aBox, const doubles value) { return value <= aBox; }
// Функция проверки объекта СВох на предмет <= константе bool operator<=( const CBoxS aBox, const doubles value) { return value >= aBox; }
// Функция проверки константы на предмет равенства объекту СВох bool operator==(const doubles value, const CBoxS aBox)
{ return value == aBox.Volume(); }
// Функция проверки объекта СВох на предмет равенства константе bool operator==(const CBoxS aBox, const doubles value)
{ return value == aBox; }
// Операция умножения СВох - n*aBox
СВох operator* (int n, const CBoxS aBox) { return aBox * n; )
// Операция для возврата свободного объема в упакованном СВох double operator%( const CBoxS aBox, const CBoxS bBox)
{ return aBox.Volume() - (aBox / bBox) * bBox.Volume(); }
Здесь нужна директива #include для Box.h, поскольку функции ссылаются на класс СВох. Сохраните файл. Когда вы покончите с этим, можете перейти на вкладку Class View. Теперь она будет содержать папку Global Functions and Variables (Глобальные функции и переменные), в которой находятся все только что добавленные функции.
Определения всех этих функций вы уже видели ранее в настоящей главе, поэтому мы не будем еще раз обсуждать их реализацию. Если вы захотите использовать любую из этих функций в другом файле . срр, нужно будет обеспечить объявление всех этих функций, которые предполагаете применять, чтобы компилятор узнал их. Этого можно достичь, поместив набор объявлений в заголовочный файл. Переключитесь еще раз на панель 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 operatorc(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 ЬВох);
Директива #pragma once обеспечивает однократное включение содержимого файла в процессе сборки. Вы только должны добавить директиву #include для BoxOperators .h в каждый исходный файл, где используется любая из этих функций.
Теперь вы готовы к тому, чтобы начать применение этих функций наряду с классом СВох для решения специфических проблем из мира ящиков.
Использование класса СВох
Предположим, что вы пакуете конфеты. Конфеты имеют большой размер, — настоящие зуболомы: в обертке имеют 1,5 дюйма в длину, 1 дюйм в ширину и 1 дюйм в высоту. У вас есть стандартная коробка для конфет размером 4,5 дюйма на 7 дюймов и на 2 дюйма, и вы хотите знать, сколько конфет уместится в эту коробку, чтобы установить на нее цену. У вас есть также стандартный картонный ящик 2 фута и 6 дюймов в длину, 18 дюймов в ширину и 18 дюймов в глубину, и вы хотите знать, сколько коробок конфет может в него поместиться и сколько пустого места останется, когда вы его наполните.
И если стандартная коробка конфет окажется не лучшим решением, вам также нужно будет знать, какого размера специальные коробки подойдут в этом случае. Вы знаете, что можно установить хорошую цену на коробки длиной от 3 до 7 дюймов, шириной от 3 до 5 дюймов и высотой от 1 до 2,5 дюймов, причем каждый размер может изменяться с шагом в полдюйма. Вы также знаете, что вам нужно иметь как минимум 30 конфет в коробке, потому что это минимальное количество, потребляемое вашим крупнейшим покупателем за один присест. К тому же коробка конфет не должна иметь пустого пространства, поскольку иначе могут последовать жалобы от покупателей, которые подумают, что их обманули. Более того, в идеале вы хотели бы наполнить стандартный ящик полностью, чтобы в нем ничего не тарахтело при тра-
спортировке. Вы не хотите быть слишком строги, иначе упаковка будет сильно затруднена, поэтому будем считать, что вы не тратите пространство впустую, если упаковываемый ящик имеет свободного места меньше, чем объем одной коробки конфет.
При наличии класса СВох проблема становится почти тривиальной и ее решение представлено в следующей функции 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);// Определение коробки конфет
СВох carton(30.0, 18.0, 18.0); И Определение картонного ящика
II Вычислить количество конфет в коробке
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.GetHeightO « endl
« "Пробная коробка содержит " « tryBox / candy « " конфет" «"ив ящик поместится " « carton / tryBox
« " таких коробок.";
} cout « endl; return 0;
}
Посмотрим, как структурирована программа. Вы разделили ее на множество файлов, что общепринято при написании программ на C++. Вы можете увидеть их все, если взглянете на вкладку Solution Explorer, которая выглядит, как показано на рис. 8.13.
Solution Explorer - Solution 'Ex8_D8' (1 project) ▼ 4 X I
и Solution 'Ex8_08' (1 project) i- ^ ЕхЯ_ОЯ
-i -r Header Files
^ Box.h
^ BoxOperators.h
Л Resource Files
В . Source Files
<У Box.cpp
cj BoxOperators.cpp
^1 Ex8_C8.cpp
^Solution Explorer (Sass View ^Resource View
Pac. 8.13. Вкладка Solution Explorer проекта Ex8_08
Файл Ex8_08 . cpp содержит функцию main () и директиву #include для файла BoxOperators. h, содержащего прототипы функций из BoxOperators. срр (не являющихся членами класса). Он также содержит директиву #include для определения класса СВох в Box. h. Консольная программа C++ обычно делится на множество файлов, каждый из которых попадает в одну из трех перечисленных ниже категорий.
Файлы .Н, содержащие команды #include библиотек, глобальные константы и переменные, определения класса и прототипы функций — другими словами, все, за исключением исполняемого кода. Они также содержат определения встроенных функций. Когда программа имеет несколько определений классов, обычно они помещаются в отдельные файлы . h.
Файлы . срр, содержащие исполняемый код программы плюс команды #include для всех определений, необходимых исполняемому коду.
Еще один файл . срр, содержащий функцию main ().
Код нашей функции main () не требует особых пояснений — он почти напрямую выражает определение проблемы в ее терминах, поскольку операции интерфейса класса выполняют проблемно-ориентированные действия над объектами СВох.
Решение об использовании стандартных коробок, принятое в операторах объявления, также вычисляет ответы, которые нам нужны, как значения инициализации. Затем эти значения выводятся с некоторыми поясняющими комментариями.
Вторая часть проблемы решается использованием трех вложенных циклов for, выполняющих итерации в пределах допустимых диапазонов m_Length, m Width и m_Height, так что оцениваются все возможные комбинации. Вы можете вывести их все, но поскольку из 200 комбинаций вас могут интересовать лишь несколько, у вас
есть оператор 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.
Время от времени вам может понадобиться использовать код из существующих файлов в новом проекте. В этом случае вы должны только добавить к проекту файлы . срр, что можно сделать, используя пункт меню Project^Add Existing Item (Проект^ Добавить существующий элемент) или щелкнув правой кнопкой мыши либо на папке Source Files (Исходные файлы), либо на папке Header Files (Заголовочные файлы) во вкладке Solution Explorer и выбрав из контекстного меню пункт Add1^ Existing Item (Добавить^Существующий элемент). Вам не обязательно до-
бавлять к проекту файлы .h, хотя вы можете сделать это, если хотите, чтобы они появились в панели Solution Explorer немедленно. Код из файлов . h добавляется в начало файлов . срр, которые требуют их через специфицированные вами директивы #include. Вам необходимы директивы #include для заголовков файлов с прототипами функций стандартной библиотеки и других стандартных определений, а также для ваших собственных заголовочных файлов. Visual C++ 2005 автоматически отслеживает все эти файлы и позволяет видеть их на вкладке Solution Explorer. Как показано в последнем примере, во вкладке Class View вы также можете видеть определения классов и глобальные константы и переменные.
В программе для Windows есть и другие виды определений для спецификации таких вещей, как меню и кнопки панели инструментов. Они сохраняются в файлах с расширениями вроде .геи. ico. Так же, как и файлы . h, их не обязательно явно добавлять к проекту, поскольку они создаются и отслеживаются автоматически Visual C++ 2005, когда в этом возникает необходимость.
Исходные файлы с расширением .сррИсходные файлы с расширением .сррОпределение классаОпределение классаОпределение классаОпределение классаИсходные файлы с расширением .hИсходные файлы с расширением .h
Исходные файлы с расширением .срр
Исходные файлы с расширением .срр
Определение класса
Определение класса
Определение класса
Определение класса
Исходные файлы с расширением .h
Исходные файлы с расширением .h
Рис. 8.14. Исходные файлы программы на C++
Именование программных файлов
Как я уже говорил, для классов любой сложности обычно принято хранить определения в файле . h с именем, основанным на имени класса, а реализацию его функций- членов, определенных вне определения класса — в файлах . срр с теми же именами. На основании этого определение класса СВох размещено в файле Box. h, а его реализация — в Box. срр. Мы не следовали этому соглашению в ранних примерах настоящей главы, поскольку те примеры были очень короткими, и было проще производить имя примера от номера главы и следующего за ним номера примера в главе. Но в программах любого размера лучше структурировать код упомянутым в начале образом, поэтому отныне будет хорошей идеей взять за привычку создавать отдельные файлы .Ни. срр для размещения кода программ.
Сегментирование программ C++ в файлах .Ни. срр — очень удобный подход, поскольку он облегчает нахождение определения или реализации любого класса, в частности, если вы имеете дело со средой разработки, которая не имеет набора инструментов, предлагаемого Visual C++. До тех пор, пока вы знаете имя класса, вы можете непосредственно обратиться к нужному файлу. Однако это не жесткое правило. Иногда удобно группировать определения набора тесно связанных классов вместе в одном файле и также собирать вместе их реализации; однако, как бы вы не решили структурировать ваши файлы, панель Class View (Представление классов) все равно будет отображать индивидуальные классы, а также всех членов каждого класса, как показано на рис. 8.15.
* Class View
<Search>
Б ^ Ех8_О8
Global Functions and Variables г 2 Macros and Constants
■ CBOX;
■■■■ ~CBox(void)
CBo\rdouble Iv = 1.000000, double wv = 1.000000, double hv = 1.000000)
GetH°ight(void)
GetLength(void)
GetWidth(void)
Volume(void) const
=^ operator *(int n) const
=§ operator /(const CBox SaBox) const
r ^ operator +{ const CBox &aBox) const
I m_Height
v ^ m_Length
„ ♦ m_Width
Solution Explorer^ Class View |l^If-source View
Puc. 8.15. Панель Class View (Представлениеклассов) для проекта Ех8__08
Я изменил размер панели Class View, чтобы были видны все элементы проекта. Здесь вы можете видеть детали классов и глобальные сущности из последнего примера. Как уже упоминалось, двойной щелчок на любом элементе в дереве переносит непосредственно в соответствующую точку исходного кода.
Программирование на 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 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);
}
);
Константа inches Per Foot статическая, поэтому она непосредственно доступна для статических и нестатических функций-членов класса. Объявление 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++/ СИ не допускаются, а внешние функции не должны иметь доступа к приватным членам класса, поэтому у вас не остается других возможностей выбора при реализации операции сложения.
Поскольку мы не работаем с площадями, умножение объектов 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 StringA ToString() override
{ return feet+L" футов ” + inches + L" дюймов"; }
// Операция сложения
Length operatort(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"(1) умножить на {0} равно {2}", factor, len2, len2*factor);
Обе строки вывода из этого фрагмента кода должны отображать один и тот же результат умножения (19 футов 2 дюйма). Аргумент выражения factor*1еп2 эквивалентен следующему:
Length::operator*(factor, 1еп2).ToString()
Результатом вызова статической функции operator* () является новый объект Length, и функция ToString () вызывается с ним для формирования аргумента для функции WriteLine (). Выражение 1еп2*factor аналогично, но вызывает функцию operator* () с параметрами в противоположном порядке. Хотя функция operator* ()
и была написана, чтобы иметь дело с множителем типа 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 ToStringO 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);
}
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”{l} умножить на {0} равно {2}”, factor, len2, len2*factor); Console::WriteLine(L”CyMMa {0} и {1} деленная на {2} равна {3}и, lenl, 1еп2, factor, (lenl+len2)/factor);
return 0;
}
Ниже показано, как выглядит вывод этого примера.
футов 9 дюймов плюс 7 футов 8 дюймов равно 14 футов 5 дюймов
.5 умножить на 7 футов 8 дюймов равно 19 футов 2 дюймов
футов 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, возвращенный функцией operator* (), а второй аргумент — переменная 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 +Length lenl = Length(2,6);Length 1еп2 = Length(3,5);Length len3 = Length(14,6);Length total = 12*(lenl + len2 +//2 фута 6 дюймов/ / 3 фута 5 дюймов// 14 футов 6 дюймов1епЗ) + (Ien3/Length(1,7))*1еп2;//2 фута 6 дюймов/ / 3 фута 5 дюймов// 14 футов 6 дюймов1епЗ) + (Ien3/Length(1,7))*1еп2;
Length lenl = Length(2,6);
Length 1еп2 = Length(3,5);
Length len3 = Length(14,6);
Length total = 12*(lenl + len2 +
Length lenl = Length(2,6);
Length 1еп2 = Length(3,5);
Length len3 = Length(14,6);
Length total = 12*(lenl + len2 +
//2 фута 6 дюймов
/ / 3 фута 5 дюймов
// 14 футов 6 дюймов
1епЗ) + (Ien3/Length(1,7))*1еп2;
//2 фута 6 дюймов
/ / 3 фута 5 дюймов
// 14 футов 6 дюймов
1епЗ) + (Ien3/Length(1,7))*1еп2;
Значение total будет составлять 275 футов и 9 дюймов. Последний оператор использует операцию присваивания, которая определена для каждого класса значения — так же, как функции operator* (), operator+ () и operator/ () в классе Length. Перегрузка операций — не только мощное средство, но и очень простое, не правда ли?
Перегрузка операций инкремента и декремента
Перегрузка инкремента и декремента в C++/CLI проще, чем в “родном” C++. Пока вы реализуете функцию операции как статический член класса, одна и та же функция будет служить и префиксной, и постфиксной формами операций. Вот как вы можете реализовать операцию инкремента в классе Length:
class Length
{ public:
// Ранее представленный код...
// Перегруженная функция операции инкремента — инкремент на 1 дюйм static Length opera tor++(Length len)
<
++len.inches;
len.feet += len.inches/len.inchesPerFoot;
len.inches %= len.inchesPerFoot; return len;
}
}
Эта реализация функции operator++ () увеличивает длину на 1 дюйм. С помощью приведенного ниже кода функция тестируется.
Length len = Length (1, 11); ft 1 фут 11 дюймов
Console::WriteLine(len++) ;
Console::WriteLine(++len);
Выполнение показанного фрагмента кода выдаст следующее:
фут 11 дюймов
фут 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){ }
I / Длина в виде строки virtual StringA ToStringO override { return feet+L" feet ” + inches + L” inches"; }
/ / Перегруженная операция сложения LengthA operator+(LengthA len) { int inchTotal = inches+len->inches+inchesPerFoot*(feet+len->feet); return gcnew Length(inchTotal/inchesPerFoot, inchTotal%inchesPerFoot); }
/ / Перегруженная операция деления — правый операнд типа double static LengthA operator/(LengthA 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/(LengthA lenl, LengthA len2) {
return (lenl->feet*inchesPerFoot + lenl->inches)/ (len2->feet*inchesPerFoot + len2->inches);
}
/ / Перегруженная операция остатка от деления
static LengthA operator%(LengthA lenl, LengthA len2) {
int ins = (lenl->feet*inchesPerFoot + lenl->inches)% (len2->feet*inchesPerFoot + len2->inches);
return gcnew Length(ins/inchesPerFoot, ins%inchesPerFoot); }
static LengthA operator* (double x, LengthA len); // Умножение - справа double static LengthA operator*(LengthA len, double x); // Умножение - слева double
/ / Префиксная и постфиксная операции инкремента static LengthA operator++(LengthA len) {
++len->inches;
len->feet += len->inches/len->inchesPerFoot;
len->inches %= len->inchesPerFoot;
return len;
} };
// Реализация операции умножения — правый операнд double LengthA Length::operator*(double x, LengthA len) {
int ins = safe_cast<int>(x*len->inches +x*len->feet*inchesPerFoot); return gcnew Length(ins/inchesPerFoot, ins%inchesPerFoot);
}
// Реализация операции умножения — левый операнд double LengthA Length::operator*(LengthА len, double x) { return operator*(x, len); }
int main(array<System::String A> Aargs) {
LengthAlenl=gcnewLength(2,6);II2 фута 6дюймов
LengthAlen2=gcnewLength(3,5);//3 фута 5дюймов
LengthAlen3=gcnewLength(14,6);II14 футов6 дюймов
// Использование операций +, * и /
LengthA 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);
LengthA 1еп4 = gcnew Length(l, 11); // 1 foot 11 inches
// Использование префиксной и постфиксной операций
Console: :WriteLine (1еп4++); // Использование постфиксной операции инкремента Console::WriteLine(++1еп4); // Использование префиксной операции инкремента return 0;
}
Ниже показан вывод этого примера.
275 футов 9 дюймов
14 футов 6 дюймов можно разделить на 5 частей 2 футов 6 дюймов длиной с остатком 2 футов 0 дюймов.
футов 0 дюймов
футов 1 дюймов
Press any key to continue . . .
Описание полученных результатов
Основные отличия от варианта с классом значений заключаются в типе параметров и возвращаемых значений функций перегруженных операций, как следствие — использовании операции ->, а также в том, что объекты типа Length теперь создаются в куче CLR с применением ключевого слова gcnew. Помимо этих изменений, код в основном тот же самый, и функции операций работают так же эффективно, как и в предыдущем примере.
Резюме
В этой главе вы изучили основы определения классов, создания и использования объектов классов. Вы также узнали о том, как перегруженные операции классов могут быть применены к объектам классов.
Ниже перечислены ключевые моменты, о которых вы узнали в этой главе и которые следует иметь в виду.
Объекты создаются с помощью функций, называемых конструкторами. Основное назначение конструкторов — устанавливать значения данных-членов (полей) в объекте класса.
Классы C++/CLI могут также иметь статический конструктор, который инициализирует статические поля класса.
Объекты уничтожаются с помощью функций, называемых деструкторами. В “родном” C++ важно определять деструктор для уничтожения объектов, которые содержат члены, распределяемые в куче, поскольку деструктор по умолчанию этого не делает.
Если вы не определяете в классе “родного” C++ конструктор копирования, то компилятор просто применяет конструктор копирования по умолчанию. Этот конструктор копирования по умолчанию не обрабатывает корректно объекты классов, в которых есть данные-члены, распределенные в свободном хранилище.
Когда вы определяете собственный конструктор копирования в классе “родного” C++, то должны использовать параметр-ссылку.
Вы не должны определять конструктор копирования в классах значений; копии объектов классов значений всегда создаются путем копирования полей.
Для ссылочных классов не создается конструкторов копирования по умолчанию, хотя при необходимости вы можете определить собственные.
Если вы не определяете операцию присваивания для своего класса “родного” C++, то компилятор применяет версию по умолчанию. Как и с конструктором копирования, операция присваивания по умолчанию не работает корректно с классами, в которых есть данные-члены, распределенные в свободном хранилище.
Вы не должны определять операцию присваивания в классе значений. Присваивание объектов класса значений всегда выполняется путем копирования полей.
Операция присваивания по умолчанию не предоставляется в ссылочных классах, но в случае необходимости вы можете определить собственную функцию операции присваивания.
Важно, чтобы вы всегда представляли деструктор, конструктор копирования и операцию присваивания для классов “родного” C++, который включают члены, распределенные с помощью new.
Объединение — это механизм, позволяющий двум или более переменным занимать одно и то же место в памяти.
Классы C++/CLI могут содержать литеральные поля, которые определяют константы внутри класса. Они также могут содержать поля initonly (только инициализируемые поля), которые не могут быть модифицированы после инициализации.
Большинство базовых операций могут быть перегружены, чтобы представить действия, специфичные для объектов класса. Вы должны реализовывать в своих классах функции операций, которые согласованы с нормальной интерпретацией базовых операций.
Шаблон класса — это образец, который вы можете применять для создания классов с одинаковой структурой, но поддерживающих разные типы данных.
Вы можете определять шаблоны классов, которые имеют множество параметров, включая параметры, которые могут быть константными значениями, а не типами.
Вы должны помещать определения для своих программ в файлы . h, а исполняемый код — определения функций — в файлы . срр. Вы можете затем встраивать файлы .h в свои файлы . срр с помощью директив #include.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Определите класс “родного” C++, представляющий приблизительные целые, такие как “около 40”. Это целые, значения которых могут быть рассмотрены как целое или приблизительное, поэтому класс должен иметь в качестве данных- членов значение и флаг “приблизительности”. Состояние этого флага влияет на арифметические операции, так что “2 * около 40” должно давать “около 80”. Состояние переменных должно быть переключаемым между “приблизительно” и “точно”.
Представьте один или более конструкторов для такого класса. Перегрузите операцию +, чтобы такие числа можно было использовать в арифметических
операциях. Хотите ли вы сделать операцию + глобальной функцией или функцией-членом? Нужна ли операция присваивания? Предложите функцию-член Print (), чтобы можно было распечатывать такие числа, используя ведущий символ “Е” для указания на то, что установлен флаг “приблизительности”. Напишите программу для тестирования операций вашего класса, особо обращая внимание на правильность операций с флагом приблизительности.
Реализуйте простой строковый класс в “родном” C++, который содержит char* и целочисленную длину как приватные члены. Представьте конструктор, принимающий аргумент типа const char*, и реализуйте конструктор копирования, операцию присваивания, а также функцию-деструктор. Убедитесь, что класс работает. Легче всего для этого воспользоваться строковыми функциями из заголовочного файла <cstring>.
Какие еще другие конструкторы можно применить в вашем строковом классе? Составьте список и закодируйте их.
(Усложненное.) Правильно ли ваш класс обрабатывает подобные ситуации? string si;
si = s2;
Если нет, как его можно модифицировать?
(Усложненное.) Перегрузите операции + и += вашего класса для выполнения конкатенации строк.
Измените пример стека из упражнения 7 предыдущей главы, чтобы можно было размещать стек заданного размера, который передается в конструкторе. Что еще потребуется добавить? Протектируйте работу полученного нового класса.
Определите ссылочный класс Box с той же функциональностью, что и класс СВох в примере Ех8_08. срр, и реализуйте пример в виде программы CLR.
9
Наследование классови виртуальные функции
В этой главе мы обратимся к теме, лежащей в сердце объектно-ориентированного программирования — наследованию классов. Если говорить просто, то наследование представляет собой средство, с помощью которого вы можете определить новый класс в терминах уже существующего. Это — фундаментальная концепция в программировании на C++, поэтому важно, чтобы вы поняли, как работает наследование.
В этой главе вы изучите следующие вопросы.
Как наследование укладывается в идею объектно-ориентированного программирования.
Определение нового класса в терминах существующего.
Использование ключевого слова protected для определения новой спецификации доступа к членам классов.
Как один класс может быть дружественным для другого класса.
Виртуальные функции и их использование.
Чистые виртуальные функции.
Абстрактные классы.
Виртуальные деструкторы и их использование.
Базовые идеи объектно-ориентированного программирования
Как вы уже видели, класс — это тип данных, которые вы определяете для удовлетворения требований вашего приложения. Классы в объектно-ориентированном про-
граммировании (ООП) также определяют объекты, с которыми имеет дело ваша программа. Вы программируете решение проблемы в терминах объектов, специфичных для этой проблемы, используя операции для непосредственной работы с этими объектами. Вы можете определить класс для представления некоторой абстракции, такой как комплексное число, представляющее собой математическую концепцию, или грузовик, который, несомненно, является физической сущностью (особенно если вы едете на одном из них по дороге). Поэтому, будучи типом данных, класс также может быть определением набора объектов реального мира определенного рода — по крайней мере, в той степени, которая необходима для решения данной проблемы.
Вы можете думать о классе как об определении характеристик конкретной группы вещей, которые специфицированы общим набором параметров и разделяют общий набор операций, выполняемых над ними. Операции, которые вы применяете к объектам данного класса, определяются его интерфейсом, который соответствует функциям, находящимся в разделе public определения класса. Класс СВох, который мы использовали в предыдущей главе — это хороший пример; он определяет ящик в терминах размеров плюс набор общедоступных функций, которые можно применять к объектам СВох для решения проблемы.
Конечно, в реальном мире существует великое разнообразие всяческих ящиков: картонные коробки, упаковки для мониторов, конфетные коробки, коробки для крупы и многие-многие другие, с которыми вам, определенно, приходилось сталкиваться. Вы можете различать ящики по содержимому, которое может в них храниться, материалу, из которого они сделаны, и по множеству других параметров. Но даже несмотря на то, что существует много разнообразных видов ящиков, все они разделяют некоторый общий набор характеристик — некую сущность “ягцичности”, если можно так выразиться. Поэтому вы можете визуализировать все виды ящиков в их отношениях друг к другу, невзирая на то, что они обладают многими отличительными особенностями. Вы можете определить некоторый конкретный вид ящиков как обладающий общими характеристиками всех ящиков — возможно, длиной, шириной и высотой. Затем вы можете добавить некоторые дополнительные характеристики к базовому типу ящика, чтобы отличать его от остальных. Вы можете также обнаружить, что существуют некоторые вещи, которые вы можете совершать со своим специфическим видом ящиков, но которые нельзя сделать ни с какими другими ящиками.
Также возможно, что некоторые объекты могут быть результатом комбинации определенных видов ящиков с другими типами объектов: например, коробка конфет или ящик пива. Чтобы описать все это, вы можете определить один вид ящиков как общий, обладающий базовыми характеристиками, присущими абсолютно всем ящикам, а затем специфицировать другую разновидность ящиков как уточняющую специализацию первой. На рис. 9.1 показан пример такого рода отношений, которые могут быть определены между разными видами ящиков.
Ящики становятся все более специализированными по мерс движения вниз по диаграмме, а стрелки на ней направлены от данного типа ящика к другому, на котором он основан. На рис. 9.1 определены три разных типа ящиков, основанных на общем для всех базовом типе СВох. На нем также определен ящик пива (CBeerCrate) как специальная разновидность ящиков, предназначенных для хранения бутылок.
Это хороший способ приближения к описанию реального мира с использованием классов C++, благодаря способности языка определять классы, которые связаны между собой. Коробка конфет может рассматриваться как ящик, обладающий всеми характеристиками базового ящика, плюс несколько собственных характеристик, которые. идентифицируют то, что делает ее специальной. Давайте посмотрим, как все это работает на практике.
специализированные
Рис. 9.1. Отношение между ящиками
Наследование в классах
Когда вы определяете один класс на базе другого, то такой класс называется производным. Производный класс автоматически получает данные-члены того класса, который использован в качестве базового при его определении, и, с некоторыми ограничениями, также функции-члены этого базового класса. Говорят, что класс наследует данные-члены и функции-члены класса, на котором он базируется.
Единственными членами базового класса, которые нс наследуются производным классом, является деструктор, конструкторы и любые функции-члены, перегружающие операцию присваивания. Все прочие функции-члены, вместе со всеми данными-члена- ми базового класса, наследуются производным классом. Конечно, причина того, что некоторые базовые члены не наследуются, состоит в том, что производный класс всегда имеет свои собственные конструкторы и деструктор. Если у базового класса есть операция присваивания, производный класс представляет ее собственную версию.
Когда я говорю, что эти функции не наследуются, то имею в виду, что они нс существуют как члены объекта производного класса. Однако они по-прежнему существуют как часть объекта, относящаяся к базовому классу, и вскоре вы в этом убедитесь.
Что такое базовый класс?
Базовый класс — это любой класс, который вы используете в качестве основы для определения другого класса. Например, если вы определяете класс В непосредственно в терминах класса А, то об А говорят, что это прямой базовый класс класса В. На рис. 9.1 класс CCrate — прямой базовый класс для CBeerCrate. Когда такой класс, как CBeerCrate, определяется в терминах другого класса CCrate, то о CBeerCrate говорят, что он унаследован от CCrate. Поскольку CCrate сам определен в терминах
класса СВох, то о СВох можно сказать, что для 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 Iv = 1.0, double wv = 1.0, double hv = 1.0):
m_Length(Iv), m_Width(wv), m_Height(hv){} };
Создайте новое пустое консольное приложение WIN32 по имени Ех9_01 и сохраните этот код в новом заголовочном файле Box.h. Директива #pragma once гарантирует, что определение класса СВох появится в сборке только один раз. В классе есть конструктор, так что вы можете инициализировать объекты при их объявлении. Предположим, что вам понадобится другой класс объектов — CCandyBox, который во всем похож на СВох, но также имеет еще один член данных — указатель на текстовую строку, идентифицирующую содержимое коробки.
Вы можете определить CCandyBox как производный класс, имеющий СВох в качестве базового, как показано ниже:
// Заголовочный файл CandyBox.h в проекте Ех9_01
#pragma once
#include "Box.h” class CCandyBox: CBox {
public:
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; };
};
Добавьте этот заголовочный файл в проект Ех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” // Для СВох и CCandyBox
using std::cout;
using std::endl;
int main() {
CBox myBox(4.0, 3.0, 2.0);// Создать объект СВох
CCandyBox myCandyBox;
CCandyBox myMintBox("Wafer Thin Mints"); // Создать объект CCandyBox
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; II удалите комментарий — получите ошибку
то программа перестанет компилироваться. Компилятор выдаст следующее сообщение:
error С2247: *СВох::m_Length* not accessible because ’CCandyBox* uses ’private* to inherit from ’CBox’
ошибка C2241: 'CBox::m_Length* недоступен, поскольку ’CCandyBox’ использует ’private' для наследования от ’СВох’
Это ясно указывает на то, что член m_Length из базового класса недоступен, потому что m_Length в производном классе стал private. Это потому, что для базового класса по умолчанию устанавливается спецификатор доступа private, когда вы определяете производный класс. Это все равно, как если бы первая строка определения класса-наследника выглядела бы следующим образом:
class CCandyBox: private СВох
Всегда необходимо указывать спецификатор доступа к базовому классу, который определяет состояние унаследованных членов в производном классе. Если вы опустите спецификатор доступа к базовому классу, то компилятор подразумевает private. Если изменить определение класса CCandyBox в CandyBox. h следующим образом:
class CCandyBox: public СВох {
public:
char* m_Contents;
CCandyBox(char* str = "Candy") II Конструктор { m_Contents = new char [ 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;
double m_Width;
double m_Height;
};
class CCandyBox: public CBox
I
public:
char* m_Contents;
// Функция вычисления объема объекта CCandyBox
double Volume() const// Ошибка — член не доступен
{ return m_Length*m_Width*m_Height; }
CCandyBox(char* str = "Candy") II Конструктор {
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:
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; };
А вот файл Ex 9_0 2. cpp в этом проекте:
// Ex9_02.cpp
// Использование функции, унаследованной от базового класса
#include <iostream> // Для потокового ввода-вывода
#include <cstring> // Для strlen() и strcpyO
#include "CandyBox.h" // Для СВох и CCandyBox using std::cout;
using std::endl; int main ()
{
CBox myBox(4.0,3.0,2.0);// Создать объект СВох
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.
Работа конструктора в производном классе
Хотя я говорил, что конструкторы базового класса не наследуются в производном классе, все же они существуют в базовом классе и используются для создания той части объекта производного класса, которая относится к базовому классу. Это объясняется тем, что создание этой части объекта производного класса — действительно задача конструктора базового класса, а не конструктора производного класса. В конце концов, вы видели, что приватные члены базового класса недоступны в объекте производного класса, даже несмотря на то, что они унаследованы, поэтому ответственность за них лежит на конструкторах базового класса.
В последнем примере конструктор базового класса по умолчанию вызывается автоматически, чтобы создать часть объекта производного класса, которая относиться к базовому классу. Вы можете вызвать определенный конструктор базового класса из конструктора производного класса. Это позволит инициализировать данные-члены базового класса конструктором, отличным от конструктора по умолчанию, или в самом деле выбрать определенный конструктор базового класса, в зависимости от данных, переданных конструктору производного класса.
Практическое занятие I
р I Вызов конструкторов
Вы можете увидеть все это в действии в модифицированной версии предыдущего примера. Чтобы сделать этот класс удобным в применении, вы должны предоставить конструктор для производного класса, который позволит специфицировать размеры объекта. Для этого вы можете добавить дополнительный конструктор в производный класс и вызывать в нем явно конструктор базового класса, чтобы установить значения данных-членов, которые унаследованы от базового класса.
Ниже показано содержимое файла Box .h из проекта Ех9_03.
И Box.h в Ех9_03
#pragma once
#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; }
private:
double m_Length;
double m_Width;
double m_Height;
};
А вот содержимое заголовочного файлы CandyBox. h.
// CandyBox.h в Ex9_03
#pragma once
#include <iostreaxn>
#include "Box.h"
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(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()// Деструктор
{ delete[] m_Contents; }
};
Директива #include для заголовка <iostream> и два объявления using здесь не являются абсолютно необходимыми, поскольку Box. h содержит тот же код, тем не менее, не вредно его повторить еще раз. Помещение этих операторов здесь также означает, что если вы удалите этот код из Box. h, поскольку он больше не нужен, то CandyBox. h все равно будет компилироваться.
Ниже показано содержимое файла Ех9_03. срр.
// Ех9_03.срр
/ / Вызов конструктора базового класса из конструктора производного класса
#include <iostream> // Для потокового ввода-вывода
#include <cstring> // Для strlenQ и strcpyO
#include "CandyBox.h" // Для СВох и 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;
Описание полученных результатов
Вместе с добавлением дополнительного конструктора к производному классу, мы добавили операторы вывода в каждый конструктор, чтобы наблюдать, когда каждый из них вызывается. Явный вызов конструктора класса СВох появляется после двоеточия в заголовке функции конструктора производного класса. Вы должны заметить, что нотация та же самая, как и использованная ранее для инициализации членов в конструкторе:
// Вызов конструктора базового класса
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.
Обратите внимание, что конструктор базового класса всегда вызывается перед конструктором производного класса.
Следующая строка получается от вызова конструктора производного класса, вызванного для объекта 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:
/ / Конструктор базового класса
СВох (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 m_Length;
double m_Width;
double m_Height;
Практическое занятиеПрактическое занятиеТеперь данные-члены по-прежнему являются приватными, и доступ к ним закрыт для обычных глобальных функций, но при этом они доступны функциям-членам производных классов.
Практическое занятие
Практическое занятие
Использование защищенных членов
Вы можете попробовать поработать с данными-членами protected, применив показанную ниже версию класса СВох в качестве базовой для новой версии класса С Candy Box, которая обращается к членам базового класса через его собственную функцию Volume ().
// CandyBox.h в Ех9_04
#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_Heigh t; }
И Конструктор, устанавливающий размеры и содержимое, //с явным вызовом конструктора СВох 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 . cpp показан ниже.
// Ex9_04.cpp
// Использование спецификатора доступа protected
#include <iostream> // Для потокового ввода-вывода
#include <cstring> // Для strlen() и strcpyO
#include "CandyBox.h” И Для СВох и CCandyBox
using std::cout;
using std::endl;
int main ()
{
CCandyBox myCandyBox;
CCandyBox myToffeeBox(2, 3, 4, "Stickjaw Toffee"); cout « endl
« "объем myCandyBox равен " « myCandyBox.Volume()
« endl
« "объем myToffeeBox равен " « myToffeeBox.Volume();
//cout « 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, объявленному в классе ’СВох1
Отсюда ясно, что член m_Length недоступен.
Уровень доступа унаследованных членов класса
Вы знаете, что если не указан спецификатор доступа базового класса в определении производного класса, то по умолчанию принимается private. Эффект от этого заключается в том, что унаследованные public- и protected-члены базового класса становятся в производном классе private, и потому не доступны функциям-членам производного класса. Фактически они остаются private по отношению к базовому классу, независимо от того, как специфицирован базовый класс в определении производного класса.
Вы также использовали public в качестве спецификатора для базового класса. Это оставляет в силе спецификации доступа членов базового класса в производном классе, так что public-члены остаются public, a protected — остаются protected.
Последняя возможность — объявить базовый класс как protected. Это меняет доступ унаследованных public-членов базового класса на protected в производном классе. Унаследованные члены с уровнем доступа protected (и private) сохраняют свою спецификацию доступа в производном классе. Все это резюмировано на рис. 9.3.
class CABox:public СВох {
наследуется, как► public:
наследуется, как► protected:
class СВох{public: -class СВох{public: -protected:protected:private: —}private: —}class СВВох: protected СВох наследуется, как► protected:- наследуется, как► protected:}class СВВох: protected СВох наследуется, как► protected:- наследуется, как► protected:}
class СВох
{
public: -
class СВох
{
public: -
protected:
protected:
private: —
}
private: —
}
class СВВох: protected СВох наследуется, как► protected:
- наследуется, как► protected:
}
class СВВох: protected СВох наследуется, как► protected:
- наследуется, как► protected:
}
Нет доступа — вообщеНет доступа — вообще
Нет доступа — вообще
Нет доступа — вообще
class ССВох: private СВох {
наследуется, как► private:
- наследуется, как► private:
Рис. 93. Наследование уровней доступа
Это может показаться несколько сложным, но вы можете свести все, что касается унаследованных членов, к следующим трем утверждениям.
Члены базового класса, которые объявлены как private, никогда не доступны в производном классе.
Определение базового класса как public не изменяет уровней доступа его членов в производном классе.
□ Определение базового класса как protected изменяет его public-члены на protected в производном классе.
Возможность изменять уровень доступа унаследованных членов в производном классе предлагает вам определенную степень гибкости, но не забывайте, что вы не можете ослабить уровень, специфицированный в базовом классе; вы можете только сделать доступ более ограниченным. Это предполагает, что ваш базовый класс должен объявлять как public те члены, доступ к которым вы хотите варьировать в производных классах. Это может показаться противоречащим идее инкапсуляции данных в классе для предотвращения неавторизованного доступа, но как вы вскоре убедитесь, часто приходится объявлять в подобной манере базовые классы, единственное назначение которых — служить базой для других классов, и которые не предназначены для того, чтобы создавать объекты их собственного типа.
Конструктор копирования в производном классе
Вспомните, что конструктор копирования вызывается автоматически, когда вы объявляете объект, инициализированный объектом того же самого класса. Взгляните на следующие операторы:
СВох шуВох(2.О, 3.0, 4.0); II Вызывает конструктор
СВох соруВох(шуВох);И Вызывает конструктор копирования
Первый оператор вызывает конструктор, принимающий три аргумента типа double, а второй вызывает конструктор копирования. Если вы не предусмотрите своего собственного конструктора копирования, то компилятор создаст его самостоятельно, и этот конструктор будет копировать инициализирующий объект в новый — член за членом. Поэтому, дабы видеть, что происходит во время выполнения, вы можете добавить свою собственную версию конструктора копирования в класс СВох. Затем вы можете использовать этот класс в качестве базового для определения класса CCandyBox.
// Box.h в Ех9_05
#pragma once
#include <iostream>
using std::cout; using std::endl;
class СВох // Определение базового класса {
public:
// Конструктор базового класса
СВох (double lv = 1.0, double wv = 1.0, double hv = 1.0) : m_Length(Iv), m_Width(wv), m_Height(hv) { cout « endl « "Вызван конструктор СВох"; }
// Конструктор копирования
СВох (const СВох& initB)
{
cout « endl « "Вызван конструктор копирования СВох";
m^Length = initB.m_Length;
m^Width = initB.m_Width;
m_Height = initB.m_Height;
}
И Деструктор СВох — только чтобы отследить вызов
-СВох ()
{ cout « "Вызван деструктор СВох" « 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) II Конструктор {
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;
};
Сюда пока не добавлен конструктор копирования, поэтому будем полагаться на версию, сгенерированную компилятором.
Практическое занятиеПрактическое занятиеI
Практическое занятие
Практическое занятие
Конструктор копирования в производных классах
Вы можете испытать только что определенный конструктор копирования в следующем примере.
// Ех9_05.срр
// Использование конструктора копирования производного класса
#include <iostream> // Для потокового ввода-вывода
#include <cstring> // Для strlen() и strcpyO
#include ’’CandyBox.h” // Для СВох и CCandyBox
using std::cout;
using std::endl;
int main()
{
CCandyBox chocBox (2.0, 3.0, 4.0, ’’Chockies’’) ; / / Объявление и инициализация CCandyBox chocolateBox(chocBox); // Использование конструктора копирования cout « end!
« ’’Объем chocBox равен ” « chocBox. Volume О
« endl
« ’’Объем chocolateBox равен ” « chocolateBox. Volume ()
« endl;
return 0;
}
Описание полученных результатов (или почему это не работает)
Если вы запустите отладочную версию этого примера, то в дополнение к ожидаемому выводу увидите диалоговое окно, показанное на рис. 9.4.
Щелкните на кнопке Abort (Прервать) для того, чтобы закрыть диалоговое окно и увидеть окно консоли вывода. Вывод покажет, что генерированный компилятором конструктор копирования для производного класса автоматически вызывает конструктор копирования базового класса.
crosoft Visual 0+ Debug Library
Debug Assertion Failed!
Drcgram: ...visual c++.netVodeYhapter D9\ex9_05\ex9_05\debug^x9_05.exe
File: dbgdel.cpp
Line: 52
Ехрн ession: _BLOCK_"nPE_IS_VALID(pHead- >nElodUse)
For information on how your program can cause an assertion failure, see ttie Visual C++ documentation on asserts.
{Press Refry to debug the application)
AbortAbortRetryRetryIgnoreIgnore
Abort
Abort
Retry
Retry
Ignore
Ignore
Puc. 9.4. Диалоговое окно утверждения отладки
Однако, как вы, возможно, заметили, все не так, как должно быть. В этом конкретном случае сгенерированный компилятором конструктор копирования вызывает проблемы, потому что область памяти, на которую указывает член m Content производного класса второго объекта — та же самая, что и у первого объекта. Когда один из объектов уничтожается (при выходе из области видимости в конце функции main ()), он освобождает память, занятую текстом. Потом, когда уничтожается второй объект, деструктор пытается освободить память, которая уже была освобождена вызовом деструктора предыдущего объекта — и это является причиной сообщения об ошибке в диалоговом окне.
Чтобы исправить это, необходимо предусмотреть конструктор копирования для производного класса, который будет распределять дополнительную память для нового объекта.
практическое занятие Решение проблемы конструкторакопирования
Вы можете сделать это, добавив в Ех9_05 следующий код конструктора копирования в раздел public производного класса CCandyBox:
// Конструктор копирования производного класса
CCandyBox(const CCandyBox& 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);
}
Теперь можете запустить эту новую версию последнего примера с той же функцией main () и убедиться, что новый конструктор копирования работает правильно.
Описание полученных результатов
Теперь после запуска пример ведет себя лучше и выдает следующий вывод:
Вызван конструктор СВох
Вызван конструктор2 CCandyBox
Вызван конструктор СВох
Вызван конструктор копирования CCandyBox
Объем chocBox равен 24
Объем chocolateBox равен 1
Вызван деструктор CCandyBox
Вызван деструктор СВох
Вызван деструктор CCandyBox
Вызван деструктор СВох
Однако все еще есть нечто такое, что идет не так. Третья строка вывода показывает, что для части СВох объекта chocolateBox вызывается конструктор по умолчанию вместо конструктора копирования. В результате этого объект получает размер по умолчанию вместо размера инициализирующего объекта, поэтому объем его выдается неправильно. Причина в том, что когда вы пишете конструктор для объекта производного класса, то отвечаете за правильную инициализацию членов этого объекта. Это касается и унаследованных членов.
Чтобы исправить это, потребуется вызвать конструктор копирования для базовой части класса в списке инициализации конструктора копирования класса CCandyBox. Тогда конструктор копирования будет таким:
// Конструктор копирования производного класса
CCandyBox (const CCandyBoxS 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;
private:
double m_Height;// Высота бутылки
double m_Diameter;H Диаметр бутылки
};
Теперь вам нужен класс, представляющий упаковку для дюжины бутылок, который автоматически принимает требуемые размеры, чтобы принять бутылки определенного типа. Это можно определить следующим образом:
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;// ...по три бутылки
}
private:
double m_Length;//Длина коробки
double m_Width;/ /Ширина коробки
double m_Height;//Высота коробки
};
Здесь конструктор устанавливает высоту коробки по высоте бутылки, а длину и ширину — на основе диаметра бутылок, чтобы в коробку поместилось 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.
Дружественные классы
Вы также можете разрешить всем функциям-членам одного класса иметь доступ ко всем членам другого класса, объявив его дружественным классом. Вы можете объявить класс CCarton другом класса CBottle, добавив объявление friend в определение класса CBottle:
friend CCarton;
При наличии такого объявления в классе CBottle все функции-члены класса CCarton теперь имеют свободный доступ ко всем членам класса CBottle.
Ограничения отношения дружественности классов
Отношение дружественности классов не является двухсторонним. Объявление класса CCarton другом класса CBottle не означает, что CBottle также становится другом CCarton. Если вы хотите, чтобы это было так, то должны добавить объявление friend для класса CBottle в класс CCarton.
Отношение дружественности классов не наследуется. Если вы определите другой класс как производный от CBottle, то члены класса CCarton не будут иметь доступа к его данным-членам, даже к тем, что унаследованы от CBottle.
Виртуальные функции
Давайте присмотримся к поведению унаследованных функций-членов и их отношениям с функциями-членами производного класса. Вы можете добавить функцию к классу СВох, чтобы вывести объем объекта СВох. Упрощенная версия класса станет такой:
// Box.h в Ех9_06
#pragma once
#include <iostream>
using std::cout;
using std::endl;
class СВох // Базовый класс
{
public:
// Функция для отображения объема объекта void ShowVolume () const
{ cout « endl
« ’’Полезный объем СВох равен ” « Volume () ;
}
// Функция вычисления объема объекта СВох double Volume () const
{ return m_Length*m_Width*m_Height; }
// Конструктор
СВох (double lv = 1.0, double wv = 1.0, double hv = 1.0)
:m_Length(Iv), m_Width(wv), m_Height(hv) {} protected:
double m_Length;
double m_Width;
double m_Height;
Теперь вы можете вывести полезный объем объекта СВох, просто вызвав функцию ShowVolume () с любым объектом, который вам нужен. Конструктор устанавливает значения данных-членов в списке инициализации, так что никакие операторы в теле функции не нужны. Данные-члены остаются прежними и специфицируются как protected, поэтому они доступны функциям-членам любого производного класса.
Предположим, что вы хотите создать производный класс для моделирования ящиков другого вида под названием CGlassBox, чтобы хранить стеклянную посуду. Содержимое хрупко, и поскольку для его предохранения добавляется упаковочный материал, емкость такого ящика будет меньше, чем базового объекта СВох. Поэтому вам понадобится другая функция Volume (), чтобы учесть это обстоятельство, и вы добавляете ее к производному классу:
// GlassBox.h in Ех9_06
#pragma once
#include "Box.h"
class CGlassBox: public СВох // Производный класс {
public:
// Функция для вычисления объема CGlassBox,
U резервирующая 15% на упаковку double Volume() const
{ return 0.85*m_Length*m_Width*m_Height; }
// Конструктор
CGlassBox(double Iv, double wv, double hv) : CBox(lv, wv, hv) {} };
Практическое занятиеПрактическое занятиеВероятно, в производном классе будут и другие дополнительные члены, но для простоты мы не будем их добавлять, а сосредоточимся на том, как работают унаследованные функции. Конструктор для объектов производного класса просто вызывает конструктор базового класса в своем списке инициализации, чтобы установить значения данных-членов. Никаких дополнительных операторов в его теле не требуется. Вы включили новую версию функции Volume () взамен версии из базового класса. Идея состоит в том, чтобы заставить унаследованную функцию ShowVolume () обращаться к версии функции вычисления объема Volume () производного класса, когда вы запускаете ее с объектом производного класса CGlassBox.
Практическое занятие
Практическое занятие
Использование унаследованной функции
Теперь посмотрим, как производный класс работает на практике. Вы можете проверить это очень просто — создав объект базового класса и объект производного класса одного и того же размера и затем сравнив правильность вычисления объемов в первом и втором. Функция main (), в которой это делается, будет выглядеть так:
// Ех9_06.срр
// Поведение унаследованных функций в производном классе
#include <iostream>
#include "GlassBox.h" // для СВох и 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);// Объявление производного ящика
// того же размера
туВох. 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 () в этих двух классах.
Можете попробовать это в новом проекте Ех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 Iv = 1.0, double wv = 1.0, double hv = 1.0)
:m_Length(Iv), 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; }
II Конструктор
CGlassBox(double Iv, double wv, double hv) : CBox(lv, wv, hv) {} };
Версия функции main () в файле Ex9_07 . cpp будет точно такой же, как и в предыдущем примере:
// Ех9_06.срр (то же самое, что и в Ех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);// Объявление производного ящика
// того же размера
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 ите ().
Использование указателей на объекты классов
Применение указателей с объектами базового класса или производного класса — важная техника. Указателю на объект базового класса можно присвоить адрес объекта производного класса, равно как и объекта базового класса. Поэтому вы можете использовать указатель типа “указатель на объект базового класса”, чтобы получить разное поведение с виртуальными функциями, в зависимости от того, на объект ка-
Практическое занятиеПрактическое занятиекого типа установлен указатель. Чтобы лучше понять, как это работает, рассмотрим пример.
Практическое занятие
Практическое занятие
Указатели на базовый и производный классы
Мы применим те же классы, что и в предыдущем примере, но внесем небольшое изменение в функцию main (), так что она будет использовать указатель на объект базового класса. Создайте проект Ех9_08 с заголовочными файлами Box. h и GlassBox. h — такими же, как в предыдущем примере. Вы можете скопировать их из проекта Ех9_07 в папку нового проекта. Добавить существующий файл к проекту достаточно легко; вы щелкаете правой кнопкой мыши на Ех9_08 во вкладке Solution Explorer (Проводник решений), выбираете Add^New Item (Добавить^Новый элемент) из всплывающего меню, после чего выбираете заголовочный файл, который нужно добавить в проект. Когда заголовки будут добавлены, модифицируйте Ех9_08. срр следующим образом:
// Ех9_08.срр
// Использование указателя базового класса для вызова виртуальной функции #include <iostream>
#include "GlassBox.h”// Для СВох и 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() ; / / Отобразить объем базового ящика рВох = fimyGlassBox; / / Установить указатель на адрес объекта
II производного класса pBox->ShowVolume (); / / Отобразить объем производного ящика
cout « endl;
return 0;
}
Описание полученных результатов
Здесь используются те же самые классы, что и в примере Ех9_07. срр, но функция main () изменена, чтобы использовать указатель для вызова ShowVolume (). Поскольку применяется указатель, с ним нужно использовать операцию непрямого выбора члена, ->, для вызова функции. Функция ShowVolume () вызывается дважды, и оба вызова используют один и тот же указатель на объект базового класса — рВох. В первом случае указатель содержит адрес объекта базового класса myBox, а при втором — адрес объекта производного класса, то есть myGlassBox.
Вывод этой программы будет таким:
Полезный объем СВох равен 24
Полезный объем СВох равен 20.4
Это в точности то же самое, что выдавал и предыдущий пример, где использовались явные обращения к объектам для вызова функции.
На основании этого примера вы можете сделать вывод, что механизм виртуальных функций работает одинаково хорошо и через указатель на базовый класс; при этом выбирается специфическая функция на основе действительного типа объекта, на который он указывает. Это проиллюстрировано на рис. 9.5.
Это значит, что даже когда вы точно не знаете тип объекта, на который установлен указатель базового класса в программе (например, когда указатель передается в функцию в качестве аргумента), механизм виртуальных функций гарантирует вызов правильной функции. Это чрезвычайно мощное средство, поэтому убедитесь, что вы хорошо понимаете его. Полиморфизм — фундаментальный механизм в C++, который вы будете использовать снова и снова.
pBox->ShowVolume()
Указатель thisустановлен на рВох
classCBoxclassCBoxvoid ShowVolume() constvoid ShowVolume() constvirtual double Volume () const
classCBox
classCBox
void ShowVolume() const
void ShowVolume() const
cout« endl
«"Полезный объем СВох равен" рВох, указывающий на объект СВох
« VolumeQ;
рВох, указывающийна объект CGIassBox
virtual double Volume () const {...}
Рис. 9.5. Механизм виртуальных функций
Использование ссылок с виртуальными функциями
Если вы определяете виртуальную функцию со ссылкой на базовый класс в качестве параметра, то можете передать в нее объект производного класса в виде аргумента. Когда ваша функция выполняется, соответствующая виртуальная функция для переданного объекта выбирается автоматически. Мы можем увидеть, что происходит, модифицировав main () из последнего примера, чтобы вызывалась функция, принимающая ссылку как параметр.
Использование ссылок с виртуальными функциями
Давайте перенесем вызов ShowVolume () в отдельную функцию и вызовем ее из main ():
// Ех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(туВох);// Вывод объема объекта базового класса
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 СВох;
Этот оператор просто указывает, что имя СВох ссылается на класс, который еще не определен в этой точке, но этого достаточно, чтобы компилятор знал, что СВох —
имя класса, и это позволяет ему обработать прототип функции 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;
II Функция для отображения объема virtual void ShowVolume() const {
cout « endl
« "Объем равен ” « Volume (); }
Оператор для виртуальной функции Volume () определяет ее, как не имеющую содержимого, путем размещения знака равно и нуля в заголовке. Это называется чистой (pure) виртуальной функцией. Любой класс, унаследованный от данного, должен либо определять функции Volume (), либо переопределять ее повторно как чистую виртуальную. Поскольку вы объявили Volume () как const, ее реализация в любом производном классе также должна быть const.
Вспомните, что вариации const и не-const функции с одинаковыми именами и списками параметров являются разными функциями. Другими словами, функцию можно перегрузить, добавив const.
Класс также включает функцию ShowVolume (), которая отображает объем объектов производных классов. Поскольку она объявлена как virtual, то может быть заменена в производном классе, но если нет, будет вызвана ее версия из базового класса, которую вы видите здесь.
Абстрактные классы
Класс, содержащий чистую виртуальную функцию, называется абстрактным классом. Он называется абстрактным, потому что определить объекты класса, включающего чистую виртуальную функцию, невозможно. Такой класс существует только для того, чтобы можно было определять классы, производные от него. Если класс, производный от абстрактного класса, также определяет чистую виртуальную функцию, он также является абстрактным.
Из предыдущего примера класса CContainer вы не должны заключить, что абстрактный класс не может иметь данных-членов. Наличие чистой виртуальной функции — это единственное условие, которое делает класс абстрактным. Иногда абстрактный класс может иметь более одной чистой виртуальной функции. Тогда производный от него класс должен реализовать каждую из этих функций, иначе он тоже будет абстрактным. Если вы забудете объявить версию функции Volume () в производном классе как const, то производный класс останется абстрактным, потому что он будет включать чистую виртуальную функцию-член Volume (), которая является const, наряду с не-cons t-функцией Volume (), которую вы определите.
Практическое занятие | ХбСТраКТНЫЙКЛЭСС
Вы можете реализовать класс ССап, представляющий банки пива или колы, вместе с оригинальным классом СВох, как наследников класса CContainer, определенного в предыдущем разделе. Определение класса СВох как подкласса CContainer будет таким:
Н Box.h для Ех9_10
#pragma once
#include "Container.h" // Для определения CContainer
#include <iostream>
using std::cout;
using std::endl;
class CBox: public CContainer // Производный класс {
public:
/ / Функция для отображения объема объекта virtual void ShowVolume() const { cout << endl
« "Полезный объем СВох равен " « Volume (); }
/ / Функция для вычисления объема объекта СВох virtual double Volume() const
{ return m_Length*m_Width*m_Height; }
// Конструктор
СВох (double Iv = 1.0, double wv = 1.0, double hv = 1.0)
:m_Length(Iv), m_Width(wv), m_Height(hv){} protected:
double m_Length;
double m_Width;
double m_Height;
Не выделенные полужирным строки — такие же, как и в предыдущей версии класса СВох. Класс СВох, по сути, тот же, что и в предыдущем примере, за исключением того, что теперь он объявлен как производный от класса CContainer. Функция Volume () полностью определена внутри класса (как и должно быть, если этот класс будет служить для создания объектов). Единственный альтернативный вариант заключается в спецификации ее как чистой виртуальной функции, поскольку она является таковой в базовом классе, но тогда мы не смогли бы создавать объекты СВох.
Класс ССап вы можете определить в заголовочном файле Can. h следующим образом:
// Can.h для Ех9_10
#pragma once
#include "Container.h"// Для определения CContainer
extern const double PI;// PI определено где-то
class CCan: public CContainer
{
public:
// Функция вычисления объема банки
virtual double Volume() const
{ return 0.25*PI*m_Diameter*m_Diameter*m_Height; }
// Конструктор
CCan(double hv = 4.0, double dv = 2.0): m_Height(hv), m_Diameter(dv){} protected:
double m_Height;
double m_Diameter;
};
В классе CCan также определена функция Volume () на основании формулы Ля/2, где h — высота банки, аг— радиус ее поперечного сечения. Объем вычисляется как высота, умноженная на площадь сечения. Выражение в определении функции предполагает, что определена глобальная константа PI, поэтому мы имеем внешний оператор, указывающий, что PI — глобальная переменная типа const double, которая определена где-то в другом месте — в данной программе она определена в файле Ех9_10. срр. Также обратите внимание, что мы переопределили функцию ShowVolume () в классе СВох, но не в классе ССап. Вы увидите эффект от этого, когда мы доберемся до вывода программы.
Испытать эти классы можно в следующей программе, включающей функцию main():
// Ех9_10.срр// Использование абстрактного класса// Ех9_10.срр// Использование абстрактного класса#include "Box.h"//#include "Can.h"//#include <iostream>//using std::cout; using std::endl;const double PI= 3.14159265; // int main(void)#include "Box.h"//#include "Can.h"//#include <iostream>//using std::cout; using std::endl;const double PI= 3.14159265; // int main(void)Для СВох и CContainerДля ССап (и CContainer)Для потокового ввода-выводаДля СВох и CContainerДля ССап (и CContainer)Для потокового ввода-выводаГлобальное определение PIГлобальное определение PI
// Ех9_10.срр
// Использование абстрактного класса
// Ех9_10.срр
// Использование абстрактного класса
#include "Box.h"//
#include "Can.h"//
#include <iostream>//
using std::cout; using std::endl;
const double PI= 3.14159265; // int main(void)
#include "Box.h"//
#include "Can.h"//
#include <iostream>//
using std::cout; using std::endl;
const double PI= 3.14159265; // int main(void)
Для СВох и CContainer
Для ССап (и CContainer)
Для потокового ввода-вывода
Для СВох и CContainer
Для ССап (и CContainer)
Для потокового ввода-вывода
Глобальное определение PI
Глобальное определение PI
// Указатель на абстрактный базовый класс, // инициализированный адресом объекта СВох CContainer* рС1 = new СВох (2.0, 3.0, 4.0);
// Указатель на абстрактный базовый класс, // инициализированный адресом объекта ССап CContainer* рС2 = new ССап (6.5, 3.0);
pCl->ShowVolume(); pC2->ShowVolume(); cout « endl;delete pCl;delete pC2;return 0;pCl->ShowVolume(); pC2->ShowVolume(); cout « endl;delete pCl;delete pC2;return 0;// Вывод объемов двух объектов
pCl->ShowVolume(); pC2->ShowVolume(); cout « endl;
delete pCl;
delete pC2;
return 0;
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 () объекта СВох). Указатель на базовый класс может содержать адрес объекта любого производного класса, даже когда несколько различных классов унаследованы от одного и того же базового класса, и потому вы можете иметь автоматический выбор правильной виртуальной функции для всего диапазона производных классов. Впечатляет, не правда ли?
Непрямые базовые классы
В начале этой главы упоминалось, что базовый класс подкласса, в свою очередь, может быть унаследован от другого, “еще более базового” класса. Маленькое расширение последнего примера предлагает иллюстрацию этого, а также демонстрацию использования виртуальной функции на втором уровне наследования.
практическое занятие | Более одного уровня наследования
Все, что потребуется сделать — это добавить класс CGlassBox к классам, перечисленным в предыдущем примере. Отношения между классами показаны на рис. 9.6.
Рис. 9.6. Отношения между классами CContainer, ССап» СВох и CGlassBox
Класс CGlassBox, как и ранее, унаследован от СВох, но мы опустили версию ShowVolume () производного класса, чтобы продемонстрировать, что версия базового класса по-прежнему распространяется на производные классы. В показанной здесь иерархии классов класс CContainer является непрямым базовым классом для CGlassBox и прямым базовым — для СВох и ССап.
Заголовочный файл GlassBox.h для этого примера выглядит следующим образом.
И GlassBox.h для Ех9_11
#pragma once
#include "Box.h"// Для СВох
class CGlassBox: public СВох// Производный класс
{
public:
II Функция для вычисления объема CGlassBox
//с учетом 15% на упаковку virtual double Volume() const { return 0.85*m_Length*m_Width*m_Height; }
// Конструктор
CGlassBox(double Iv, double wv, double hv) : CBox(lv, wv, hv) {} };
Заголовочные файлы Container. h, Can. h и Box. h содержат тот же код, что и в предыдущем примере Ех9_10.
Исходный файл для нового примера с обновленной функцией 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(); II Вывод объема ССап
рС1 = &myGlassBox; // Присвоить указателю адрес myGlassBox pCl->ShowVolume(); II Вывод объема CGlassBox cout « endl;
return 0;
}
Описание полученных результатов
Здесь мы имеем трехуровневую иерархию классов, показанную на рис. 9.6, с CContainer в качестве абстрактного базового класса, поскольку он содержит чистую виртуальную функцию Volume (). Функция main () теперь вызывает функцию ShowVolume () три раза, используя один и тот же указатель базового класса, но при этом указатель каждый раз содержит адреса объектов разных классов. Поскольку ShowVolume () не определена ни в одном из производных классов, каждый раз вызывается ее версия из базового класса. Отдельная ветвь от базы CContainer определяет производный класс ССап.
Пример выдает следующий вывод:
Полезный объем СВох равен 24
Объем равен 45.9458
Полезный объем СВох равен 20.4
Вывод показывает, что в соответствии с типом объекта, каждый раз для выполнения выбирается одна из трех разные версий Volume ().
Обратите внимание, что вы должны удалять объект СВох из свободного хранилища перед тем, как присваивать указателю другое значение адреса. Если этого не делать, вы не сможете очистить память, выделенную в свободном хранилище, потому что адрес оригинального объекта не сохранится. Это ошибка, которую легко допустить при переназначении указателей и использовании свободного хранилища.
Виртуальные деструкторы
При работе с объектами производных классов через указатель базового класса возникает одна проблема — в этом случае может не быть вызвана правильная версия деструктора. Такой эффект можно наблюдать, немного модифицировав последний пример.
Практическое занятие| ВЫЗОВ НбПраВИЛЬНОГО ДвСТруКТОрЭ
Чтобы отследить, какие деструкторы вызываются при уничтожении объектов, достаточно добавить деструктор, выдающий соответствующее сообщение, в каждый класс последнего примера. Файл Container .h для этого примера будет таким:
// Container.h для Ех9_12
#pragma once
#include <iostream>
using std::cout;
using std::endl;
class CContainer // Общий базовый класс для специфических контейнеров {
public:
// Деструктор
-CContainer()
{ cout « "Вызван деструктор CContainer” « endl; }
И Функция для вычисления объема — без содержимого.
// Это определение чистой (’pure1) виртуальной функции, на что указывает ’= О* virtual double Volume() const = 0;
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:
/ / Деструктор
-CBox()
{ cout « "Вызван деструктор CBox " « endl; }
// Функция для отображения объема объекта virtual void ShowVolume() const { cout « endl
« "Полезный объем CBox равен " « Volume (); }
// Функция для вычисления объема объекта СВох virtual double Volume() const
{ return m_Length*m_Width*m_Height; }
H Конструктор
CBox (double Iv = 1.0, double wv = 1.0, double hv = 1.0)
:m_Length(Iv), 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 Iv, double wv, double hv) : CBox(lv, wv, hv) {} };
И, наконец, исходный файл Ех9_12 . срр для этой программы должен выглядеть следующим образом:
U Ех9_12.срр
// Вызовы деструкторов с производными классами, / / используя объекты через указатель на базовый класс
#include
"Box.h"
// Для СВох и CContainer
#include
"Can.h"
// Для ССап (и CContainer)
#include
"GlassBox.h"
И Для CGlassBox (а также СВох и 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);
ССап myCan(6.5, 3.0);// Определение объекта ССап
CGlassBox myGlassBox(2.0, 3.0, 4.0); // Определение объекта CGlassBox
pCl->ShowVolume();pCl->ShowVolume();// Вывод объема объекта СВох
pCl->ShowVolume();
pCl->ShowVolume();
cout « endl « "Удаление СВох" « endl;
delete pCl;delete pCl;//Очистка памяти в свободном хранилище
delete pCl;
delete pCl;
pCl = new CGlassBox(4.0, 5.0, 6.0); // Динамически создать CGlassBox pCl->ShowVolume();// .. .вывести его объем. . .
cout « endl « "Удаление CGlassBox" « endl; delete pCl;// .. .и удалить его
pCl = &myCan;pCl->ShowVolume();pCl = &myGlassBox;pCl->ShowVolume(); cout « endl;return 0;pCl = &myCan;pCl->ShowVolume();pCl = &myGlassBox;pCl->ShowVolume(); cout « endl;return 0;// Получить в указатель адрес myCan
pCl = &myCan;
pCl->ShowVolume();
pCl = &myGlassBox;
pCl->ShowVolume(); cout « endl;
return 0;
pCl = &myCan;
pCl->ShowVolume();
pCl = &myGlassBox;
pCl->ShowVolume(); cout « endl;
return 0;
// Вывести объем ССап
// Получить в указатель адрес myGlassBox
// Вывести объем CGlassBox
Описание полученных результатов
Помимо добавления в каждый класс деструкторов, выводящих сообщения о своем вызове, единственное изменение состоит в появлении нескольких новых строк в функции main (). Эти дополнительные операторы динамически создают объект CGlassBox, выводят его объем и удаляют его. Добавился также вывод сообщения, указывающего момент удаления динамически созданного объекта СВох. Вывод, сгенерированный этим примером, показан ниже.
Полезный объем СВох равен 24
Удаление СВох
Вызван деструктор CContainer
Полезный объем СВох равен 102
Удаление CGlassBox
Вызван деструктор CContainer
Объем равен 45.9458
Полезный объем СВох равен 20.4
Вызван деструктор CGlassBox
Вызван деструктор СВох
Вызван деструктор CContainer
Вызван деструктор ССап
Вызван деструктор CContainer
Отсюда видно, что когда вы удаляете объект СВох, на который указывает рС1, вызывается деструктор базового класса CContainer, но нет записи о вызове деструктора СВох. Аналогично, при удалении дополнительного объекта CGlassBox, опять-таки вызывается деструктор базового класса CContainer, но не видно вызовов деструкторов CGlassBox и СВох. Для прочих объектов вызовы корректного деструктора происходит после того, как сначала вызван конструктор производного класса, за которым следовал вызов конструктора базового класса. Для первого объекта класса CGlassBox, созданного в объявлении, вызываются три деструктора: сначала деструктор производного класса, за которым идет деструктор непосредственного базового класса, и, наконец, деструктор непрямого базового класса.
Все проблемы связаны с объектами, созданными в свободном хранилище. В обоих случаях вызываются неправильный деструктор. Причина в том, что компоновка деструкторов разрешается статически, во время компиляции. Для автоматических объектов проблем нет — компилятор знает, что они собой представляют, и может установить вызов правильного деструктора. С объектами, созданными динамически и доступными через указатель, все иначе. Единственная информация, которую имеет компилятор при выполнении операции delete — это то, что указатель имеет тип базового класса. Действительный тип объекта, на который установлен указатель, компилятору неизвестен, потому что определяется во время выполнения программы. Поэтому компилятор просто решает, что операция delete должна вызвать деструктор базового класса. В реальном приложении это может вызвать множество проблем, оставляя рассыпанные куски объектов в свободном хранилище, а может, и более серьезные проблемы — в зависимости от природы вовлеченных объектов.
Решение просто. Нужно добиться того, чтобы вызовы разрешались динамически во время выполнения программы. Вы можете организовать это, предусмотрев в своих классах виртуальные деструкторы. Как уже говорилось при первом упоминании виртуальных функций, достаточно объявить функцию базового класса виртуальной, чтобы гарантировать, что все одноименные функции производных классов с тем же списком параметров и типом возврата, также были виртуальными. Это касается и деструкторов, как и обычных функций-членов. Вам нужно добавить ключевое слово virtual к определению деструктора класса CContainer в файле CContainer. h, чтобы определение класса стало таким:
class CContainer // Общий базовый класс для специфических контейнеров {
public:
// Деструктор
virtual -CContainer()
{ cout « "Вызван деструктор CContainer" « endl; } // Остальной код класса — тот же, что и ранее
};
Теперь деструкторы во всех производных классах автоматически станут виртуальными, даже несмотря на отсутствие в них явной спецификации, как таковых. Конечно, вы можете специфицировать их как виртуальные, если хотите, чтобы код был абсолютно прозрачен.
Если теперь запустить пример с этими модификациями, он выдаст следующее:
Полезный объем СВох равен 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);
СВох* рВох = dynamic_cast<CBox*>( pContainer);
CGlassBox* pGlassBox = dynamic_cast<CGlassB'ox*>( pContainer);
Первый оператор сохраняет адрес объекта CGlassBox, созданного в куче, в указателе на базовый класс типа CContainer*. Второй оператор приводит pContainer вверх по иерархии классов к типу СВох*. Третий оператор приводит адрес pContainer к его реальному типу CGlassBox*.
Вы можете применить также операцию dynamic cast к ссылкам, как и к указателям. Разница между dynamic_cast и static cast состоит в том, что операция dynamic cast проверяет корректность приведения во время выполнения, в то время как static_cast этого не делает. Если операция dynamic cast некорректна, резуль-
татом будет null. В отношении корректности операции static_cast компилятор полагается на программиста, поэтому вы всегда должны использовать dynamic_cast для приведения вверх и вниз по иерархии классов и проверять результат на равенство null, если хотите избежать аварийного прерывания вашей программы в результате использования нулевого указателя.
Вложенные классы
Определение одного класса можно поместить внутрь определения другого — в этом случае вы определяете вложенный класс. Вложенный класс ведет себя как статический член класса, включающего его, и является субъектом спецификаторов обращения к членам — как и любой другой член класса. Если вы поместите определение вложенного класса в раздел private класса, то к такому классу можно будет обращаться только из функций-членов включающего класса. Если же вы специфицируете определение вложенного класса как public, то такой класс будет доступен извне включающего класса, но в этом случае имя вложенного класса должно быть квалифицировано именем внешнего класса.
Вложенный класс имеет свободный доступ ко всем статическим членам включающего класса. Все члены экземпляров могут быть доступными через объект типа включающего класса, указатель или ссылку на такой объект. Включающий класс может иметь доступ только к общедоступным членам вложенного класса, но во вложенном классе, который объявлен как private во включающем классе, члены часто объявляются как public, чтобы обеспечить свободный доступ ко всему вложенному классу в пределах включающего класса.
Вложенный класс может оказаться полезным, когда вы хотите определить тип, который должен использоваться только внутри другого типа — тогда вложенный класс может быть объявлен как private. Ниже показан пример.
И Стек для хранения объектов Box class CStack {
private:
// Определение элементов, хранящихся в стеке struct CItem {
СВох* рВох;// Указатель на объект в данном узле
CItem* pNext;// Указатель на следующий элемент в стеке или null
// Конструктор
CItem(СВох* рВ, CItem* pN) : рВох(рВ), pNext(pN){} };
CItem* pTop;II Указатель на элемент, находящийся на верхушке
public:
/ / Затолкнуть объект Box в стек void Push(СВох* рВох) {
pTop = new CItem(рВох, рТор);//Создать элемент и поместить его на верхушку }
// Вытолкнуть объект из стека
СВох* Pop() {
if (рТор == 0)// Если стек пуст,
return 0;// вернуть null
СВох* рВох = рТор->рВох; // Получить ящик из элемента
CItem* pTemp = рТор;// Сохранить адрес верхнего элемента
рТор = 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
#include "GlassBox.h" // Для CGlassBox (а также СВох и CContainer)
#include "Stack.h"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(); H Вывод объема ящика cout « endl « endl
« "Теперь затолкаем ящики в стек..." « endl;
CStack* pStack = new CStack; II Создать стек
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 [] = {СВох* pBoxes [] = {Мы создали массив указателей на объекты СВох, так что каждый элемент массива может хранить адрес объекта СВох или адрес объекта любого типа, унаследованного от СВох. Массив инициализируется адресами четырех объектов, созданных в стеке:
СВох* pBoxes [] = {
СВох* pBoxes [] = {
4.0) ,3.0, 4.0),6.0), 5.0, 6.0)4.0) ,3.0, 4.0),6.0), 5.0, 6.0)new СВох (2.0, 3.0, new CGlassBox(2.0, new СВох (4.0, 5.0, new CGlassBox(4.0,
4.0) ,
3.0, 4.0),
6.0), 5.0, 6.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 в самом деле реализует стек, используя вложенную структуру для определения элементов, хранимых в стеке.
Программирование на 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, наследуемые всеми классами значений
ФункцияОписание
stringA ToString ()Возвращает строковое (string) представление объекта и реализована
в классе System::Object как возвращающая имя класса в виде строки. Обычно вы должны переопределять эту функцию в своих собственных классах, чтобы она возвращала строковое представление значения объекта.
bool Equals (ObjectA obj) Сравнивает текущий объект с obj и возвращает true, если они эквивалентны, либо false — в противном случае. Эквивалентность в данном случае означает ссылочную эквивалентность — то есть признак того, что объекты одинаковы. Обычно вы будете переопределять эту функцию в своих собственных классах, чтобы она возвращала true, когда текущий объект имеет то же значение, что и аргумент; другими словами, когда поля объектов эквивалентны.
int GetHashCode ()Возвращает целое число, представляющее хеш-код текущего объекта.
Хеш-коды используются в качестве ключей для сохранения объектов в коллекции, хранящей пары “ключ-объект”. Объекты последовательно извлекаются из такой коллекции по ключам, которые указывались при их сохранении.
Конечно, поскольку класс System::Object также является базовым и для ссылочных классов, вы можете переопределять эти функции и в ссылочных классах тоже.
Вы можете унаследовать ссылочный класс от существующего ссылочного класса точно так же, как определяете производный класс на “родном” C++. Давайте повторно реализуем Ех9_12 как программу на C++/CLI, чтобы заодно продемонстрировать вложенные классы в программе CLR. Начнем с определения класса Container.
U Container.h для Ех9_14
#pragma once
using namespaсе System;
// Абстрактный базовый класс для специфических контейнеров
ref class Container abstract {
public:
// Функция для вычисления объема - без содержимого,
// определена как 'абстрактная* виртуальная функция, // что отмечено ключевым словом ’abstract*
virtual double Volume () abstract;
// Функция для отображения объема virtual void ShowVolume()
{
Console; -.WriteLine (1”0бъем равен {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:
II Функция для показа объема объекта
virtual void ShowVolume() override (
Console::WriteLine(1”Полезный объем Box равен {0}", Volume());
}
II Функция для вычисления объема объекта virtual double Volume() override { 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) {} protected:
double m_Length;
double m_Width;
double m_Height;
};
Базовый класс для ссылочного класса всегда public, и ключевое слово public подразумевается по умолчанию. Вы можете специфицировать базовый класс явно как public, но это делать не обязательно. Базовый класс для ссылочного класса не может быть специфицирован иначе, чем public. Поскольку вы не можете применить значения по умолчанию для параметров, как в версии этого класса на “родном” C++, приходится определять конструктор без аргументов, чтобы он инициализировал три поля значениями 1.0. Класс Box определяет функцию Volume (), как переопределенную для унаследованной версии базового класса. Вы всегда должны специфицировать ключевое слово override, когда намереваетесь переопределить функцию базового класса. Если класс Box не реализует функцию Volume (), он будет абстрактным, и вы должны будете специфицировать это, чтобы компиляция класса выполнилась успешно.
Вот как выглядит определение класса GlassBox:
// GlassBox.h для Ех9_14
#pragma once
#include "Box.h"// Для Box
ref class GlassBox : Box H Производный класс {
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:
II Определение элементов для хранения в стеке ref struct Item {
ObjectA Ob j; II Дескриптор объекта в данном элементе
ItemA Next; // Дескриптор следующего элемента в стеке или nullptr
// Конструктор
Item(ObjectA obj, ItemA next): Obj(obj), Next(next){} );
ItemA Top; // Дескриптор элемента, находящегося на верхушке стека
public:
// Затолкнуть объект в стек void Push(ObjectA obj) {
Top = gcnew Item(obj. Top); // Создать новый элемент и поместить Н его на верхушку
}
/ Вытолкнуть объект из стека
ObjectA Pop() {
if (Тор == nullptr)М Если стек пуст
return nullptr;// вернуть nullptr
ObjectA obj = Top->Obj;// Получить объект из элемента
Тор = Top->Next;И Поместить следующий элемент на верхушку
return obj;
Первое отличие, которое следует отметить, заключается в том, что параметры функций и поля теперь являются дескрипторами, потому что мы имеем дело с объектами ссылочного класса. Вложенная структура Item теперь хранит дескриптор типа ObjectA, который позволяет объектам любого типа класса CLR сохраняться в стеке; это значит, что и классы значений и ссылочные классы могут быть приспособлены для этого, что представляет собой существенное усовершенствование по сравнению с классом CStack на “родном” C++. Вам не нужно беспокоиться об удалении объектов Item при вызове функции Pop (), потому что об этом позаботится сборщик мусора.
Подведем итоги всех отличий от “родного” C++, которые были продемонстрированы этими классами.
Только ссылочные классы могут быть унаследованными типами классов.
Базовый класс для производного ссылочного класса всегда public.
Функция, которая не имеет определения в ссылочном классе, является абстрактной и должна быть объявлена таковой с применением ключевого слова
abstract.
Класс, который содержит одну или более абстрактных функций, должен быть явно специфицирован как абстрактный с помощью ключевого слова abstract, следующего за именем класса.
Класс, не содержащий абстрактных функций, также может быть специфицирован как абстрактный; в этом случае экземпляры этого класса не могут быть
созданы.
Вы должны явно использовать ключевое слово override, определяя функцию, которая переопределяет функцию, унаследованную от базового класса.
Практическое занятиеПрактическое занятиеЧтобы попробовать все эти классы в действии, потребуется создать консольный проект CLR с определением main (), поэтому давайте это и сделаем.
Практическое занятие
Практическое занятие
Использование производных ссылочных классов
Создайте консольную программу CLR по имени Ех9_14 и добавьте классы из предыдущего раздела в проект; затем добавьте следующее содержимое в Ех9_14. срр:
// Ех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(L"\nTenepb затолкнем ящики в стек. . . ");
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 равен
120
Полезный объем
Box равен
20.4
Полезный объем
Box равен
24
Теперь затолкнем целые числа в стек:
2 4 6 8 10 12
Выталкивание целых чисел из стека порождает: 12 10 8 6 4 2
Press any key to continue . . .
Описание полученных результатов
Сначала в следующих строках создается массив дескрипторов:
4.0),3.0, 4.0),6.0),5.0, 6.0)4.0),3.0, 4.0),6.0),5.0, 6.0)array<BoxA>A boxes = { gcnew Box(2.0, 3.0,
4.0),
3.0, 4.0),
6.0),
5.0, 6.0)
4.0),
3.0, 4.0),
6.0),
5.0, 6.0)
gcnew GlassBox (2.0, gcnew Box(4.0, 5.0, gcnew GlassBox (4.0,
Поскольку Box и GlassBox — ссылочные классы, объекты создаются в куче CLR с применением gcnew. Полученные адреса объектов инициализируют элементы массива boxes.
Затем вы можете создать объект Stack и затолкнуть в него ящики:
StackA stack = gcnew Stack; // Создать стек for each (ВохЛ box in boxes) stack->Push(box);
Параметр функции Push () имеет тип Object24, поэтому функция принимает любой тип класса в качестве аргумента. Цикл 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А. Переменная item имеет тип ObjectA, а поскольку класс Object не определяет функцию ShowVolume (), вы не можете вызвать ее, применяя дескриптор этого типа. Чтобы вызвать функцию полиморфно, вы должны использовать дескриптор типа базового класса, который определяет эту функцию как виртуальный член. Приведя дескриптор к типу ContainerА, вы можете вызвать функцию ShowVolume () полиморфно, поэтому при выполнении выбирается версия этой функции, принадлежащая классу, к которому в действительности относится объект, на который ссылается дескриптор. В данном случае вы можете достичь того же результата, приведя item к типу ВохЛ. Здесь применяется safe_cast, поскольку приведение осуществляется вверх по иерархии классов, а в таком случае лучше применить ограниченную операцию приведения. Операция safe_cast проверяет правильность приведения и, если преобразование не удается, генерирует исключение типа System:: InvalidCastException. Вы можете использовать и dynamic_cast, но в программах CLR лучше применять safe_cast.
Интерфейсные классы
Определение интерфейсного класса внешне довольно похоже на определение ссылочного класса, но представляет довольно-таки отличающуюся концепцию. Интерфейс — это класс, определяющий набор функций, которые должны быть реализованы другими классами, чтобы обеспечить стандартизованный способ предоставления некоторой специфической функциональности. Как классы значений, так и ссылочные классы могут реализовывать интерфейсы. Интерфейс не определяет никакие из своих функций-членов — их определяет каждый класс, который реализует этот интерфейс.
Вы уже встречали интерфейс System: : I Comp а г able в контексте обобщенных функций, где специфицировали интерфейс IComparable в качестве ограничения. Интерфейс IComparable определяет функцию CompareTo () для сравнения объектов, так что все классы, которые реализуют этот интерфейс, обладают одинаковым механизмом сравнения объектов. Вы специфицируете интерфейс, реализуемый классом, точно так же, как базовый класс. Например, вот как можно было бы заставить класс Box из предыдущего примера реализовывать интерфейс System: : IComparable:
ref class Box : Container, IComparable II Производный класс {
public:
// Функция, специфицированная интерфейсом IComparable virtual int CompareTo(ObjectA 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 () имеет тип ObjectA, вы должны привести его к типу ВохА, прежде чем обращаться к членам объекта Box, на который он ссылается.
Определение интерфейсных классов
Интерфейсный класс определяется с применением ключевых слов interface class или interface struct. Независимо оттого, использовано interface class или interface struct для определения интерфейса, все члены интерфейса всегда являются public по умолчанию, и вы не можете это изменить. Членами интерфейса могут быть функции, включая функции операций, свойства, статические поля и события — обо всем этом вы узнаете далее в настоящей главе. Интерфейс может также специфицировать статический конструктор и содержать в себе определения вложенных классов любого рода. Несмотря на такое потенциальное разнообразие членов, большинство интерфейсов относительно просты. Обратите внимание, что можно наследовать один интерфейс от другого — почти так же, как наследовать один ссылочный класс от другого. Например:
interface class IController : ITelevison, IRecorder {
// Члены IController...
};
Интерфейс IController включает в себя свои собственные члены, а также наследует члены интерфейсов ITelevision и IRecorder. В классе, который реализует
интерфейс 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
#pragma once
#include "IContainer.h" 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), m_Width(1.0), m_Height(1.0){}
H Конструктор
Box(double Iv, double wv, double hv)
: m_Length(Iv), m_Width(wv), m_Height(hv){} protected:
double m_Length;
double m_Width;
double m_Height;
Имя интерфейса следует за двоеточием в первой строке определения класса — точно так же, как если бы он был базовым классом. Конечно, здесь может также присутствовать и базовый класс, тогда имя интерфейса должно следовать за именем
базового класса, отделяясь от него запятой. Класс может реализовывать множество интерфейсов, при этом их имена разделяются запятыми.
Класс 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(Ь"Массив контейнеров, имеющих следующие объемы:");
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;
}
Этот пример выдаст следующий вывод:
Массив контейнеров, имеющих следующие объемы:
Полезный объем СВох равен 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(IContainerA container in containers) container->ShowVolume();// Вывод объема ящика
Тело цикла показывает полиморфизм в действии; вызывается функция ShowVolume () для специфического типа объекта, на который ссылается container, что видно из вывода.
Элементы массива контейнеров заталкиваются в стек, по сути, точно так же, как и в предыдущем примере. Выталкивание элементов из с^ека также аналогично предыдущему примеру:
ObjectA item;
while((item = stack->Pop ()) != nullptr) safe_cast<IContainerA>(item)->ShowVolume();
Тело цикла доказывает, что вы можете приводить дескриптор к типу интерфейса, используя safe cast, точно таким же образом, как приводили к типу ссылочного класса. Затем этот дескриптор применяется для полиморфного вызова функции ShowVolume().
Использование интерфейсных классов — не только удобный способ определения наборов функций, представляющих стандартные интерфейсы классов, но также мощный механизм для применения полиморфизма в ваших программах.
Классы и сборки
Приложение 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 {
// Детали интерфейса...
1;
Интерфейс 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, чтобы спецификаторы доступа к членам могли обеспечить доступ к ним извне родительской сборки. Когда спецификатор
доступа состоит из двух ключевых слов, например, 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 {
H TODO: добавьте сюда свои методы класса. };
}
Библиотека классов имеет свое собственное пространство имен и здесь оно называется по умолчанию Ех9_16lib. Вы можете изменить это имя на что-то более подходящее по своему желанию. Имена классов в библиотеке квалифицируются названием пространства имен, поэтому следует использовать директиву 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 IContaiper
{
virtual double Volume ();// Функция для вычисления объема
virtual void ShowVolume();// Функция отображения объема
};
// Box.h для Ех9_16
public 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 0 : m_Length(1.0), m_Width(1.0), m_Height(1.0){}
// Конструктор
Box(double Iv, double wv, double hv)
: m_Length(Iv), 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 {
ObjectA Obj; // Дескриптор объекта данного элемента
ItemA Next; // Дескриптор следующего элемента в стеке или nullptr // Конструктор
Item(ObjectA obj, ItemA next): Obj(obj), Next(next){}
};
ItemA Top; // Дескриптор элемента, находящегося на верхушке public:
/ / Затолкнуть объект в стек
void Push(ObjectA obj) {
Top = gcnew Item(obj, Top); // Создать новый элемент и поместить //на верхушку стека } // Вытолкнуть объект из стека ObjectA Pop () {
if (Тор == nullptr)// Если стек пуст,
return nullptr;// вернуть nullptr
ObjectA obj = Top->Obj; // Получить ящик из элемента
Тор = Top->Next;// Сделать следующий элемент верхушкой
return obj;
}
};
}
Интерфейсный класс IContainer, класс Box и класс Stack теперь включены в эту библиотеку. Изменения исходных определений выделены полужирным. Каждый класс теперь является public, что обеспечивает доступ к нему из внешних сборок. Поля класса Box объявлены как public protected, что означает, что они унаследованы в производном классе как protected, но являются public, пока речь идет о классах внутри родительской сборки. В действительности вы не обращаетесь к этим
полям из других классов внутри родительской сборки, поэтому в данном случае можно оставить эти поля класса 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(Ь"\пТеперь затолкнем контейнеры в стек. . .") ;
StackA stack = gcnew Stack;// Создать стек
for each(IContainer* container in containers)
stack->Push(container);
Console:;WriteLine(
L"Выталкивание контейнеров из стека представляет их в обратном порядке:"); Object* item;
while((item = stack->Pop()) ’= nullptr) safe_cast<IContainer*>(item) ->ShowVolume () ;
Console::WriteLine();
return 0;
}
Вам также понадобится добавить заголовочный файл GlassBox.h к проекту с тем же кодом, что и в Ех9_15, так что вы можете скопировать его в каталог этого проекта и затем добавить к проекты правым щелчком мыши на папке Header Files (Заголовочные файлы) во вкладке Solution Explorer (Проводник решений) и выбором пункта Add^Existing Item (Добавить^Существующий элемент) из контекстного меню. Конечно, класс GlassBox унаследован от класса Box, поэтому компилятор должен знать, где найти определение класса Box. В данном случае он находится в библиотеке, созданной в предыдущем проекте, поэтому добавьте следующую директиву в заголовочный файл GlassBox.h после директивы #pragma once:
#using <Ex9_161ib.dll>
Имя класса Box определено внутри пространства имен Ex9_161ib, поэтому также нужно добавить оператор using следом за директивой #using:
using namespace Ex9_161ib;
Чтобы дать возможность компилятору найти библиотеку, скопируйте файл Ex9_161ib.dll из проекта Ex9_161ib в подкаталог debug решения Ех9_16, в котором находится файл Ех9_16. ехе. Вы можете специфицировать полный путь к сборке в директиве #using, но обычно принято помещать все библиотеки классов, используемые проектом, в каталог, содержащий исполняемый файл приложения. Здесь очень легко спутать каталоги. Файл Ex9_161ib.dll находится в подкаталоге debug каталога решения Ех916lib, а не в подкаталоге 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 Iv, 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 () не является полиморфной, поэтому для полиморфного вызова с использованием дескриптора типа базового класса новая версия не вызывается, например:
ВохА 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); }
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()rvalue(1){} Handlerclass(int m)rvalue(m){} protectedr
int value;
};
Класс имеет четыре функции с параметром типа int и типом возврата void. Две из них статические функции и две — функции экземпляра. Он также имеет два конструктора, включая один конструктор без аргументов. Класс не делает ничего особенного, кроме выдачи сообщения на экран, позволяющего определить, какая функция была вызвана, и для функций экземпляра — с каким объектом.
Вы можете создать делегат типа Handler следующим образом:
Handler^ handler = gcnew Handler(Handlerclass г гFunl); // Объект делегата
Объект handler содержит адрес статической функции Funl из класса Handler- Class. Если вызвать делегат, то будет вызвана функция Handlerclass:: Funl () с аргументом, переданным при вызове делегата. Вызов делегата можно написать так:
handler->Invoke(90);
Это вызывает все функции в списке вызовов делегата handler. В данном случае это будет всего одна функция — Handlerclass: : Funl (), поэтому вывод получится такой:
Функция! вызвана со значением 90
Вы можете также вызвать делегат следующим оператором:
handler(90);
Это сокращенный вариант предыдущего оператора, который явно вызвал функцию Invoke (), и это та форма вызова делегата, которую вы обычно видите.
Операция + для типов делегатов перегружена, чтобы комбинировать списки вызовов для двух делегатов в новый объект делегата. Например, вы можете модифицировать список вызовов делегата handler с помощью следующего оператора:
handler += gcnew Handler(Handlerclass г гFun2);
Теперь переменная handler ссылается на объект делегата, список вызовов которого включает две функции: Funl и Fun2. Однако это новый объект делегата. Список вызовов делегата не может быть изменен, поэтому операция + работает подобно тому, как она работает с объектами String (вы всегда при этом создаете новый объект). Вы опять можете вызвать делегат оператором:
handler(80);
Но теперь получите другой вывод:
Функция! вызвана со значением 80
Функция2 вызвана со значением 80
Обе функции в списке вызовов вызываются в последовательности, в которой были добавлены к объекту делегата.
Вы можете исключить элемент списка вызовов делегата, используя операцию -=:
handler -= gcnew Handler(Handlerclass г гFunl);
Это опять создает новый объект делегата, который включает только Handler- Class : : Fun2 () в свой список вызовов, потому что эффект заключается в удалении функции, указанной в правой части выражения (Handlerclass: :Funl), и создании нового объекта, указывающего на оставшуюся функцию.
Обратите внимание, что список вызовов делегата должен содержать хотя бы один указа- тель функции* Если вы удалите все указатели функций, используя операцию вычитания, то получите в результате nullp tr.
Используя конструктор делегата, принимающий два параметра, вы передаете в первом аргументе ссылку на объект из кучи CLR, а во втором — адрес функции экземпляра типа этого объекта. Таким образом, этот конструктор создает делегат, содержащий указатель на функцию экземпляра, специфицированную вторым аргументом, для объекта, заданного первым аргументом. Вот как можно создать такой делегат:
HandlerclassА obj = gcnew HandlerClass;
HandlerA handler2 = gcnew Handler (obj, SHandlerClass::Fun3);
Первый оператор создает объект, а второй — делегат, указывающий на функцию Fun3 () для объекта obj типа HandlerClass. Делегат принимает аргумент типа int, поэтому вы можете вызвать его следующим оператором:
handler2(70);
В результате этого будет вызвана функция Fun3 () для объекта obj со значением аргумента 70, так что вывод получится такой:
ФункцияЗ вызвана со значением 71
Значение, хранящееся в поле value объекта obj, равно 1, так как объект создан с использованием конструктора по умолчанию. Оператор в теле Fun3 () прибавляет значение поля value к аргументу функции — отсюда 71 в выводе.
Поскольку оба они одного типа, вы можете комбинировать список вызовов для handler со списком вызовов делегата handler2:
HandlerА handler = gcnew Handler(HandlerClass::Funl); H Объект делегата
handler += gcnew Handler(HandlerClass::Fun2);
HandlerClassA obj = gcnew Handlerclass;
HandlerA handler2 * gcnew Handler (obj, &HandlerClass::Fun3); handler += handler2;
Здесь пересоздается handler для ссылки на делегат, включающий указатели на статические функции Funl () и Fun2 (). Затем вы можете создать новый делегат, на который ссылается handler, содержащий эти две статические функции плюс функцию экземпляра Fun3 () для объекта obj. После этого вы можете вызвать делегат с помощью следующего оператора:
handler(50);
и получить такой вывод:
Функция! вызвана со значением 50
ФункцияЗ вызвана со значением 50
ФункцияЗ вызвана со значением 51
Press any key to continue . . .
Как видим, вызов делегата запускает две статических функции плюс Fun3 () с объектом obj, так что вы можете комбинировать статические и нестатические функции в одном списке вызовов для делегата.
Практическое занятиеПрактическое занятиеТеперь давайте соберем некоторые из этих фрагментов вместе в одном примере, дабы убедиться, что они действительно работают.
Практическое занятие
Практическое занятие
Создание и вызов делегатов
Этот пример — попурри из того, что вы уже видели относительно делегатов:
// Ех9_17.срр : main project file. II Создание и вызов делегатов #include "stdafx.h"
using namespace System;
public ref class Handlerclass {
public:
static void Funl(int m)
{ Console::WriteLine^"Функция! вызвана co значением {0}”z 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, m+value); } Handlerclass():value(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"\nДeлeгaт с тремя указателями на функции:") ;
handler(70);
Console::WriteLine(L"\nCoкpaщeниe списка вызовов...");
handler -= gcnew Handler(Handlerclass::Funl);
Console::WriteLine
(Е”\пДелегат с указателями на одну статическую и одну функцию экземпляра:”); handler(60) ;
Этот пример выдаст следующий вывод:
Делегат с одним указателем на статическую функцию:
Функция! вызвана со значением 90
Делегат с двумя указателями на статические функции:
Функция! вызвана со значением 80
Функция2 вызвана со значением 80
Делегат с тремя указателями на функции:
Функция! вызвана со значением 70
Функция? вызвана со значением 70
ФункцияЗ вызвана со значением 71
Сокращение списка вызовов...
Делегат с указателями на одну статическую и одну функцию экземпляра:
Функция? вызвана со значением 60
ФункцияЗ вызвана со значением 61
Press any key to continue . . .
Описание полученных результатов
Все операции, присутствующие в main (), вы уже видели в предыдущем разделе. Делегат вызывается с явным использованием функции Invoke (), а также с применением дескриптора делегата, за которым следует список аргументов. Как видно из вывода, все работает так, как надо.
Хотя этот пример показывает делегат, который может содержать указатели на функции с единственным аргументом, но вообще делегат может указывать на функции с таким количеством аргументов, какое необходимо. Например, вы могли бы объявить тип делегата следующим образом:
delegate void MyHandler(double x, StringA description);
Этот оператор объявляет тип делегата MyHandler, который может указывать только на функции с типом возврата void и двумя параметрами: первый типа double, а второй — StringA.
Несвязанные делегаты
Делегаты, которые вы видели до сих пор, были примерами связанных (bound) делегатов. Они называются так потому, что каждый имеет фиксированный набор функция в своем списке вызовов. Но вы также можете создавать и несвязанные (unbound) делегаты; несвязанный делегат указывает на функцию экземпляра с заданным списком параметров и типом возврата для заданного типа объектов. Такой делегат может вызывать функцию экземпляра для любого объекта специфицированного типа. Ниже приведен пример объявления несвязанного делегата.
public delegate void UBHandler(ThisClassA, int value);
Первый аргумент специфицирует тип указателя this, для которого делегат типа UBHandler может вызывать функцию экземпляра; функция должна иметь один параметр типа int и тип возврата void. Таким образом, делегат UBHandler может вызывать только функцию для объекта типа ThisClass, но для любого объекта этого типа. Это может показаться несколько ограниченным, но на самом деле оказывается довольно удобным; вы можете использовать делегат, например, для вызова функции для каждого элемента типа ThisClassA в массиве.
Делегат типа UBHandler можно создать следующим образом:
UBHandlerА ubh = gcnew UBHandler(&ThisClass::Sum);
Аргументом конструктора служит адрес функции в классе ThisClass, которая принимает нужный список параметров и имеет правильный тип возврата.
Вот определение ThisClass:
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. Рассмотрим это в действии.
Iпрактическое занятие. использование несвязанного делегата
В этом примере используются фрагменты кода из предыдущего раздела, чтобы продемонстрировать работу несвязанного делегата.
И Ех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;
public delegate void UBHandler(ThisClassA, int value);
int main(array<System::String A> Aargs) {
array<ThisClassA>A things = { gcnew ThisClass(5.0),gcnew ThisClass(10.0), gcnew ThisClass(15.0),gcnew ThisClass(20.0), gcnew ThisClass(25.0)
};
UBHandlerA ubh = gcnew UBHandler(&ThisClass: :Sum); //Создать объект делегата
// Вызвать делегат с каждым элементом массива
for each(ThisClassA thing in things)
ubh(thing, 3) ;
ubh += gcnew UBHandler(&ThisClass:: Product); // Добавить функцию к делегату
// Вызвать новый делегат с каждым элементом массива for each(ThisClassA thing in things)
ubh(thing, 2);
return 0;
Этот пример выдаст следующий вывод:
Результат сложения = 8
Результат сложения =13
Результат сложения =18
Результат сложения =23
Результат сложения =28
Результат сложения = 7
Результат умножения =10
Результат сложения =12
Результат умножения =20
Результат сложения =17
Результат умножения = 30
Результат сложения =22
Результат умножения =40
Результат сложения =27
Результат умножения = 50
Press any key to continue . . .
Описание полученных результатов
Тип делегата UBHandler объявлен в следующем операторе:
public delegate void UBHandler(ThisClassA, int value);
Объекты типа делегата UBHandler являются несвязанными делегатами, который могут вызывать функции экземпляров для объектов типа ThisClass до тех пор, пока они имеют один аргумент типа int и тип возврата void.
Определение класса ThisClass в этом примере точно такое же, как вы видели в предыдущем разделе. Он имеет две функции экземпляра — Sum () и Product (), — которые принимают параметр типа int и возвращают void, поэтому обе могут быть вызваны делегатом типа UBHandler.
Вы создаете массив дескрипторов объектов ThisClass в функции main () с помощью следующего оператора:
array<ThisClassA>A things = { gcnew ThisClass(5.0),gcnew ThisClass(10.0),
gcnew ThisClass(15.0),gcnew ThisClass(20.0), gcnew ThisClass(25.0)
В списке инициализации создаются пять объектов, каждый из которых инкапсулирует свое значение типа double, так что при вызовах функций Sum () и Product () участвующий объект можно будет легко идентифицировать по выводу.
Объект делегата создается следующим оператором:
UBHandlerА ubh = gcnew UBHandler(&ThisClass::Sum); // Создать объект делегата
Вызов объекта делегата, на который ссылается ubh, вызывает функцию Sum () для любого объекта типа ThisClass, что и делается с каждым объектом в массиве things:
for each(ThisClassA 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(StringA str);
// Класс с членом-событием
public ref class Door
{
public:
// Событие, которое вызовет функции, ассоциированные
//с объектом делегата DoorHandler
event DoorHandlerA Knock;
// Функция для возбуждения событий 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 ImIn(StringA name) {
Console::WriteLine(Ь”3аходи, {0}, открыто.",name);
1
void ImOut(StringA name) (
Console::WriteLine(Ь"Уходи, {0}, меня нет.”,name);
}
};
Класс AnswerDoor включает две функции-члена, которые потенциально могут обработать событие Knock, поскольку обе имеют списки параметров и типы возврата, идентифицированные в объявлении делегата DoorHandler.
Прежде чем зарегистрировать функции, которые должны принимать уведомления о событии Knock, вы должны создать объект Door. Это можно сделать так:
DoorA door = gcnew Door;
Теперь вы можете зарегистрировать функцию для получения уведомления о событии Knock в объекте door следующим образом:
AnswerDoorА answer = gcnew AnswerDoor;
door->Knock += gcnew DoorHandler(answer, &AnswerDoor::Imln);
Первый оператор создает объект типа AnswerDoor — это необходимо, потому что функции Imln () и ImOut () являются нестатическими членами класса. Затем вы добавляете экземпляр делегата типа DoorHandler к члену Knock объекта door. Это аналогично процессу добавления указателей на функции к делегату, и вы можете в дальнейшем точно так же добавлять дополнительные функции-обработчики, которые должны быть вызваны при возникновении события Knock. Рассмотрим это в примере.
Практическое занятие | (^рабОТКЯ СОбЫТИЙ
В следующем примере используются классы из предыдущего раздела для определения, возбуждения и обработки событий.
// Ех9—19.срр : главный файл проекта.
// Определение, возбуждение и обработка событий.
#include "stdafx.h”
using namespace System;
public delegate void DoorHandler(StringA str);
// Класс с членом-событием public ref class Door {
public:
// Событие, которое вызовет функции, ассоциированные
//с объектом делегата DoorHandler event DoorHandlerA Knock;
// Функция для возбуждения событий void TriggerEvents() {
Knock(Ь”Фред”);
Knock(Ь"Джейн”); }
};
// Класс, определяющий функции-обработчики событий Knock public ref class AnswerDoor {
public:
void ImIn(StringA name) {
Console::WriteLine(Ь”3аходи, {0}, открыто.”, name);
}
void ImOut(StringA name) {
Console::WriteLine(Ь”Уходи, {0}, меня нет.”,name); }
};
int main(array<System::String A> Aargs) {
DoorA door = gcnew Door;
AnswerDoorA answer = gcnew AnswerDoor;
// Добавить обработчик события Knock - члена door
door->Knock += gcnew DoorHandler(answer, &AnswerDoor::ImIn);
door->TriggerEvents();// Инициировать событие Knock
// Изменить способ обработки события Knock
door->Knock -= gcnew DoorHandler(answer, &AnswerDoor::Imln);
door->Knock += gcnew DoorHandler(answer, &AnswerDoor::ImOut); door->TriggerEvents() ; H Инициировать событие Knock return 0;
}
Выполнение этого примера даст следующий результат:
Заходи, Фред, открыто.
Заходи, Джейн, открыто.
Уходи, Фред, меня нет.
Уходи, Джейн, меня нет.
Press any key to continue . . .
Описание полученных результатов
Сначала вы определяете два объекта в функции main ():
DoorA door = gcnew Door;
AnswerDoorA answer = gcnew AnswerDoor;
Объект door имеет член-событие Knock, а объект answer — функцию-член, которая может быть зарегистрирована для вызова при наступлении событий Knock.
Следующий оператор регистрирует функцию-член Imln () объекта answer для получения уведомлений событий Knock объекта door:
door->Knock += gcnew DoorHandler(answer, SAnswerDoor::Imln);
Если это имеет смысл, вы можете зарегистрировать и другие функции для вызова при наступлении события Knock.
Следующий оператор вызывает функцию-член TriggerEvents () объекта door:
door->TriggerEvents(); И Инициировать событие Knock
Эта функция инициирует два события Knock — одно с аргументом "Фред", второе— с аргументом "Джейн". В результате этого для каждого события по одному разу вызывается функция Imln (), что дает первые две строки вывода.
Конечно, может возникнуть необходимость реагировать на событие по-разному в разное время, в зависимости от обстоятельств, и это демонстрируют следующие три оператора main ():
door->Knock -= gcnew DoorHandler(answer, SAnswerDoor::Imln);
door->Knock += gcnew DoorHandler(answer, &AnswerDoor::ImOut);
door->TriggerEvents(); H Инициировать событие Knock
Первый оператор исключает указатель на функцию Imln () из события, а второй оператор регистрирует функцию ImOut () для объекта answer с целью получения уведомлений о наступлении событии. Когда инициируются события Knock в третьем операторе, вызывается функция Imln (), поэтому результаты несколько отличаются.
Деструкторы и финализаторы в ссылочных классах
Вы можете определить деструктор для ссылочного класса точно так же, как определяете деструктор в классе “родного” C++. Деструктор для ссылочного класса вызывается, когда дескриптор выходит из области видимости, или объект является частью другого объекта, который уничтожается. Вы можете также применить операцию delete к дескриптору объекта ссылочного класса, в результате чего будет вызван деструктор. Главная причина для реализации деструкторов в классах “родного” C++ заключается в необходимости иметь дело с данными-членам и, динамически размещенными в куче, но очевидно, что это не касается ссылочных классов, поэтому необходимость в деструкторах здесь меньше. Они могут понадобиться, когда объекты класса используют другие ресурсы, не находящиеся под управлением сборщика мусора, такие как файлы, которые должны быть закрыты обычным образом при уничтожении объекта. Также вы можете очистить такие ресурсы в членах классов иного рода, называемых финализаторами (finalizer).
Финализатор — это специальный вид функции-члена ссылочного класса, которая вызывается явно или в результате применения к объекту операции delete. В производных классах финализаторы вызываются в том же порядке, в каком должны были быть вызваны деструкторы, то есть финализатор самого базового класса вызывается первым, за ним идут финализаторы всех последующих классов иерархии, и последним вызывается финализатор последнего производного класса.
Финализатор класса может быть определен примерно так:
public ref class MyClass
{
// Определение финализатора
•MyClass()
{
// Код очистки уничтожаемого объекта...
// Остальная часть определения класса...
};
Вы определяете в классе функцию-финализатор почти так же, как деструктор, но вместо символа - перед именем класса (как в деструкторе), используется символ !. Как и в деструкторе, вы не должны специфицировать тип возврата финализатора, а спецификатор доступа игнорируется. Проиллюстрируем на небольшом примере работу деструкторов и финализаторов.
Практическое занятие | фиНЗЛИЗЭТОрЫ И ДвСТруКТОрЫ
Пример демонстрирует, когда деструкторы и финализаторы вызываются в программе.
// Ех9_20.срр : главный файл проекта.
// Финализаторы и деструкторы
#include "stdafx.h"
using namespace System;
ref class MyClass {
public:
II Конструктор
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) {
MyClassA objl = gcnew MyClass(l);
MyClassA obj2 = gcnew MyClass(2);
MyClassA obj3 = gcnew MyClass(3);
delete objl;
obj2->~MyClass();
Console::WriteLine(Ь”Конец программы");
return 0;
}
Ниже показан вывод этого примера.
Вызван деструктор объекта MyClass (1).
Вызван деструктор объекта MyClass (2).
Конец программы
Вызван финализатор объекта MyClass (3).
Press any key to continue . . .
Описание полученных результатов
Класс MyClass имеет конструктор, деструктор и финализатор. Деструктор и финализатор просто выводят сообщение в командную строку, чтобы было видно, когда каждый из них вызван. Кроме того, вы можете видеть, для которого именно объекта вызван финализатор или деструктор, потому что они выводят значение поля value.
В функции main () создается три объекта типа MyClass, инкапсулирующие значения 1, 2 и 3, чтобы их можно было различать. Затем применяется операция delete к objl и явно вызывается деструктор для obj2. Вызовы деструкторов генерирует две первых строки вывода.
Следующая строка вывода генерируется оператором, предшествующим return в main (), так что последняя строка вывода, генерируемая финализатором для ob j 3, появляется уже после окончания main (). Вывод показывает, что деструкторы вызываются, когда удаляется объект, либо деструктор вызывается явно. При этом в обоих случаях подавляется выполнение финализаторов объектов. Объект, на который ссылается obj3, уничтожается сборщиком мусора по завершении программы, поэтому финализатор вызывается для очистки неуправляемых ресурсов.
Таким образом, если у класса есть и деструктор, и финализатор, только один из них будет вызван при уничтожении объекта. Деструктор вызывается, если вы программно уничтожаете объект, а финализатор — когда он исчезает сам или выходит из области видимости. Отсюда можно сделать вывод, что если вы полагаетесь на финализатор для очистки неуправляемых ресурсов при уничтожении объекта, то вы не должны удалять его явно.
Если закомментировать операторы в main (), которые уничтожают objl и obj2, то вы увидите, что финализаторы этих объектов будут вызваны по завершении программы. С другой стороны, если вы закомментируете финализатор в MyClass, то увидите, что деструктор для ob j 3 не вызывается сборщиком мусора, так что никакой очистки не происходит. Отсюда можно сделать вывод, что если вы хотите быть уверены, что неуправляемые ресурсы, используемые объектом, корректно освобождаются, независимо от того, как уничтожается объект, то вам следует реализовать в классе и деструктор и финализатор.
Обобщенные классы
C++/CLI предоставляет вам возможность определения обобщенных (generic) классов — когда специфический класс создается из типа обобщенного класса во время выполнения. Вы можете определить обобщенные классы значений, обобщенные ссылочные классы, обобщенные интерфейсные классы и обобщенные делегаты. Обобщенный класс определяется с использованием одного или более параметра типа, подобно тому, как определяются обобщенные функции, о которых шла речь в главе 6.
Например, вот как можно определить обобщенную версию класса Stack, который вы видели в Ех9 14:
// Stack.h для Ех9_21
II Обобщенный стек магазинного типа generic<typename Т> ref class Stack { private:
// Определение элементов, помещаемых в стек ref struct Item
{
Т Ob j ;// Дескриптор объекта данного элемента
ItemA Next; // Дескриптор следующего элемента в стеке или nullptr // Конструктор
Item(T obj, ItemA next): Obj(obj), Next(next){}
};
ItemA Top;// Дескриптор элемента, находящегося на верхушке
public:
// Затолкнуть объект в стек void Push(T obj) {
ТоР = gcnew Item(obj, Top); //Создать элемент и поместить его на верхушку } // Вытолкнуть объект из стека Т Pop() { if (Top == nullptr) // Если стек пуст, return Т();// вернуть эквивалент null
Т obj = Top->Obj;// Получить объект из элемента
Тор = Top->Next;// Поместить на верхушку следующий
return obj;
} };
Обобщенная версия класса теперь имеет параметр типа — Т. Обратите внимание, что вы можете использовать ключевое слово class вместо type name, когда специфицируете параметр — в данном контексте между ними разницы нет. Аргумент-тип заменяет Т при использовании обобщенного типа класса; Т заменяется аргументом типа в определении класса, так что основное преимущество перед первоначальной версией в том, что обобщенный тип намного безопаснее, причем без потери гибкости. Член Push () оригинального класса принимает любой дескриптор, поэтому вы можете с успехом поместить в один и тот же стек смесь объектов типа MyClassA, StringA или дескриптор любого другого типа, в то время как экземпляр обобщенного типа принимает только объекты того типа, который специфицирован как аргумент-тип при объявлении обобщенного стека.
Взгляните на реализацию функции Pop (). Оригинальная версия возвращает nullptr, если стек пуст, но вы не можете вернуть nullptr для параметра типа, потому что аргумент-тип может быть типом значения. Решение заключается в возврате Т (), то есть вызова конструктора без аргументов для типа Т. Это дает результат, эквивалентный 0 для типа значения и nullptr для дескриптора.
Обратите внимание, что вы можете специфицировать конструктор в параметре типа обобщенного класса, используя ключевое слово where, точно так же, как это делалось с обоб щенными функциями в главе 6.
Вы можете создать стек из обобщенного типа Stacko, который хранит дескрипторы объектов Box, следующим образом:
Stack<BoxA>A stack = gcnew Stack<BoxA>;
Аргумент-тип ВохА указывается между угловыми скобками, и оператор создает объект Stack<BoxA> в куче CLR. Этот объект позволяет сохранять в стеке дескрипторы типа ВохА, а также всех других производных от него типов. Вы можете убедиться в этом, рассмотрев измененную версию Ех9_14.
практическое занятие Использовзние типе обобщенного клзссз
Создайте новую консольную программу CLR по имени Ех9_21 и скопируйте заголовочные файлы Container .h, Box.h и GlassBox.h из Ех9_14 в каталог этого проекта. Добавьте эти заголовки к проекту, щелкнув правой кнопкой на папке Header Files (Заголовочные файлы) во вкладке Solution Explorer (Проводник решений) и выбрав пункт Add*=> 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(L"MaccnB ящиков, имеющих следующие объемы:");
for each(BoxA box in boxes)
box->ShowVolume(); H Вывод объема ящика
Console::WriteLine(L"\nTenepb затолкаем ящики в стек...");
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;
}
Этот пример генерирует следующий вывод:
Массив ящиков, имеющих следующие объемы:
Полезный
объем
СВох
равен
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.
Для сохранения целых чисел теперь потребуется новый объект Stacko:
Stack<int>A numbers = gcnew Stack<int>; // Создать стек
В оригинальной версии использовался один и тот же необобщенный объект Stack и для хранения ссылок на объекты Box, и для хранения целых, демонстрируя тем самым, что ни о какой безопасности типов там не могло быть речи. Здесь же вы специфицируете аргумент-тип для обобщенного класса как тип значения int, так что только объекты этого типа принимает функция Push ().
Циклы, которые выталкивают объекты из стека, демонстрируют, что возврат Т () из функции Pop () и в самом деле возвращает 0 для типа int, и nullpt г для типа дескриптора ВохА.
Обобщенные интерфейсные классы
Можно определять обобщенные интерфейсы точно так же, как определяются обобщенные ссылочные классы, а обобщенный ссылочный класс может быть определен в терминах обобщенного интерфейса. Чтобы продемонстрировать, как это работает, вы можете определить обобщенный интерфейс, которые может быть реализован обобщенным классом Stacko. Рассмотрим пример определения обобщенного интерфейса.
// Интерфейс для операций со стеком
generic<typename Т> public interface class IStack {
void Push(T obj);H Затолкнуть элемент в стек
Т Pop();// Вытолкнуть элемент из стека
};
Этот интерфейс включает две функции, идентифицирующие операцию заталкивания и выталкивания элемента из стека.
Определение обобщенного класса Stacko, реализующего обобщенный интерфейс I Stacko, выглядит так:
generic<typename Т> ref class Stack : IStack<T> {
private:
// Определение элементов для помещения в стек ref struct Item { Т Ob j ;И Дескриптор объекта в данном элементе
ItemA Next; // Дескриптор следующего элемента в стеке или nullptr // Конструктор Item(T obj, ItemA 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 содержит широкий диа-
пазон обобщенных классов коллекций, которые реализуют строго типизированные коллекции. Доступные обобщенные классы коллекций перечислены в табл. 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);
Здесь добавляется 1000 целых чисел к списку numbers. Список растет автоматически, если его начальная емкость меньше 1000. Когда вы хотите вставить элемент в существующий список, вы можете использовать функцию Insert () для вставки элемента, указанного вторым аргументом в позицию индекса, заданную первым аргументом. Элементы в списке индексируются, начиная с нуля — как и в массивах.
Просуммировать содержимое списка можно следующим образом:
int sum = 0;
for (int i = 0 ; i<numbers->Count ; i++) sum += numbers [ i];
Count — это свойство, которое возвращает текущее количество элементов в списке. Элементы списка могут быть доступны через индексируемое свойство по умолчанию, и вы можете устанавливать и читать значения таким образом. Обратите внима-
ние, что вы не можете увеличить емкость списка, используя индексируемое свойство по умолчанию. Если вы используете индекс вне диапазона элементов списка, будет возбуждено исключение.
Элементы списка можно просуммировать и так:
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);
Функция AddList () добавляет элемент в конец списка. Вы можете добавлять элементы в начало списка с помощью функции AddFirst (). Альтернативно для тех же целей можно использовать функции 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.
Dictionary<TKey, TValue>— обобщенный словарь, хранящий пары “ключ-значение”
Обобщенный класс коллекции Dictionaryo принимает два аргумента-типа, первый — тип ключа, второй — тип значения, ассоциированного с ключом. Словарь особенно удобен, когда нужно хранить пары объектов, из которых один является ключом для доступа к другому объекту. Имя и номер телефона — пример пары “ключ-значение”, которую нужно сохранить в коллекции, поскольку обычно вам нужно извлекать телефонный номер, используя имя в качестве ключа. Предположим, вы определили классы Name и PhoneNumber для инкапсуляции имен и номеров телефонов соответственно. Вы можете определить словарь для хранения пар “имя-номер” следующим образом:
Dictionary<NameA, PhoneNumberА>А phonebook = gcnew Dictionary<NameA, PhoneNumberA>;
Здесь два аргумента-типа — NameA и PhoneNumberA, так что ключ — дескриптор имени, а значение — дескриптор номера телефона.
Добавить элемент в словарь phonebook можно так:
NameA 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.
Объект Dictionaryo имеет свойство Keys, которое возвращает коллекцию, содержащую ключи словаря — так же, как свойство Values возвращает коллекцию значений. Свойство Count возвращает количество пар “ключ-значение”, хранящихся в словаре.
Попробуем применить кое-что из этого в работающем примере.
практическое занятие |Использование обобщенных классов
коллекций
В этом примере испытываются три класса коллекций, описанных выше.
// Ех9_22.срр : главный файл проекта.
И Использование обобщенных классов коллекций
#include "stdafx.h" using namespace System;
using namespace System::Collections::Generic; H Для обобщенных коллекций // Класс, инкапсулирующий имя ref class Name {
public:
Name(StringA namel, StringA name2) : First(namel),Second(name2){}
virtual StringA ToStringO override{ return First +L" "tSecond;} private:
StringA First;
StringA Second;
};
// Класс, инкапсулирующий номер телефона ref class PhoneNumber { public:
PhoneNumber(int area, int local, int number):
Area(area),Local(local), Number(number){}
virtual StringA 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>A 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(П"Итог = {0}", sum);
// Использование LinkedList<T>
Console::WriteLine(П"\пСоздание LinkedList<T> значений double:");
LinkedList<double>A values = gcnew LinkedList<double>; for(int i = 0 ; K1000 ; i++)
values~>AddTail(2. 5*i);
double sumd = 0.0;
for each(double v in values) sumd += v;
Console::WriteLine(П"Итог = {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(L"HTor после добавления элементов = {0}", sumd);
// Использование Dictionary<K,V>
Console::WriteLine(Ъ"\пСоздание Dictionary<K,V> пар имя/номер:"); Dictionary<NameA, PhoneNumberA>A phonebook = gcnew Dictionary<NameA, PhoneNumberA>;
// Добавить в словарь пары "имя-номер"
NameA name = gcnew Name("Джим", "Джонс");
PhoneNumberA number = gcnew PhoneNumber(914, 316, 2233); phonebook->Add(name, number);
phonebook->Add(gcnew Name("Фред","Фонг"), gcnew PhoneNumber(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 () для вставки элемента в заданную позицию индекса.
Следующий блок кода в 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. Это гарантирует корректный выбор деструктора для динамически созданных объектов производных классов.
□ Класс “родного” 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-сайта издательства.
Что неправильно в следующем коде?
class CBadClass
private:
int len;
char* p;
public:
CBadClass(const char* str): p(str), len(strlen(p)) {} CBadClass (){}
};
Предположим, имеется класс CBird, приведенный ниже, который вы хотите использовать в качестве базового для создания иерархии классов, описывающих птиц: class CBird
(
protected:
int wingSpan;
int eggSize;
int airSpeed;
int altitude;
public:
virtual void fly() { altitude = 100; } };
Разумно ли создать класс CHawk (сокол), наследуя его от CBird (птица)? А как насчет COstrich (страус)? Обоснуйте ответы. Предложите иерархию птиц, которая учтет оба эти вида птиц.
Имеется следующий класс:
class CBase { protected:
int m_anlnt;
public:
CBase(int n) : m_anlnt(n) { cout « "Базовый конструктор\п”; } virtual void Print () const = 0;
};
Какого рода класс CBase и почему? Унаследуйте от CBase класс, который устанавливает значение унаследованного члена m_alnt при конструировании и печатает его по запросу. Напишите тестовую программу для проверки корректности вашего класса.
Бинарное дерево — это структура, состоящая из узлов, в которой каждый узел содержит указатель на “левый” и “правый” узлы плюс элемент данных, как показано на рис. 9.7.
Дерево начинается с корневого узла, и он служит стартовой точкой для доступа ко всем узлам дерева. Любой (или оба) указатель узла может быть null. На рис. 9.7 показано упорядоченное бинарное дерево, которое представляет собой дерево, организованное таким образом, что значение каждого узла всегда больше или равно значению его левого подузла и меньше или равно значению правого подузла.
Определите класс “родного” C++ для определения упорядоченного бинарного дерева для хранения целых чисел. Нужно будет также определить класс Node, но он может быть вложенным классом в классе BinaryTree. Напишите программу, тестирующую работу класса BinaryTree, сохраняя в нем произвольную последовательность целых чисел, а затем выводя их в порядке возрастания.
Подсказка: не бойтесь применять рекурсию.
Корневой узел
Упорядоченное бинарное дерево
Рис. 9.7. Бинарное дерево
Реализуйте упражнение 4 в виде программы CLR. Если не получится выполнить упражнение 4, просмотрите решение в загруженном коде и воспользуйтесь им в качестве руководства при выполнении данного упражнения.
Определите обобщенный класс BinaryTree для любого типа, реализующего интерфейсный класс IComparable, и продемонстрируйте его работу, используя экземпляры обобщенного класса для хранения и извлечения сначала случайных целых чисел, а затем — элементов следующего массива:
array<StringA>A words = (L'^uccess", Ь'Чз", L"the", Liability", L”to” ,
L"go", L"from", L"one", L”failure", L”to"f L"another", L"with", L"no", L"loss", L”of", L"enthusiasm"};
Выведите значения, извлеченные из бинарного дерева, в командной строке.
9
Технологии отладки
Если вы выполняли упражнения из предыдущих глав, то более чем вероятно, что вам пришлось сражаться с ошибками в своем коде. В настоящей главе вы ознакомитесь с тем, как базовые возможности отладки, встроенные в Visual C++ 2005, могут помочь в этом. Мы с вами исследуем некоторые дополнительные инструменты, которые можно использовать для нахождения и устранения ошибок в программах, а также рассмотрим некоторые способы, позволяющие снабдить ваши программы специфическим кодом для проверки ошибок.
В настоящей главе вы узнаете о следующих вопросах.
Как запустить вашу программу под управлением отладчика Visual C++ 2005.
Как выполнить вашу программу шаг за шагом.
Как наблюдать изменения значений переменных в ваших программах.
Как наблюдать значение выражений в вашей программе.
Что такое стек вызовов.
Что такое утверждения (assertions) и как их использовать для проверки кода.
Как добавить специфичный отладочный код в программу.
Как обнаружить утечки памяти в программах на “родном” C++.
Как использовать средства слежения за выполнением и сгенерировать отладочный вывод в программах C++/CLI.
Что такое отладка?
Дефекты, или “жучки” (bugs) — это ошибки в вашей программе, а отладка (debugging) — процесс нахождения и устранения этих ошибок. Без сомнений, вы уже знаете, что отладка — неотъемлемая часть процесса программирования. Факты, касающиеся ошибок в ваших программах достаточно удручающи.
Каждая программа, которую вы пишете, если она чуть сложнее совсем тривиальной, содержит ошибки, которые вы должны обнаружить, найти и устранить, если программа претендует на то, чтобы быть надежной и эффективной. Обратите внимание на три фазы этого процесса — ошибка программы не обязательно видна сразу; даже если она видна, вы можете не знать, где именно в вашем коде она находится; и даже когда вы приблизительно знаете, где она может находиться, может оказаться совсем нелегко определить, что именно является причиной проблемы, и затем устранить ее.
Многие программы, которые вы напишете, будут содержать ошибки, даже после того, как вы решите, что уже тщательно протестировали их.
Ошибки могут оставаться скрытыми в программе, которая очевидно работает правильно — иногда в течение нескольких лет. Однако проявиться они могут в самый неподходящий момент.
Программы, выходящие за определенные пределы размеров и сложности почти всегда содержат ошибки, независимо от того, сколько времени и усилий было потрачено на их тестирование. (Точный размер и степень сложности, которые гарантируют наличие ошибок невозможно определить точно, но Visual C++ 2005 и ваша операционная система, несомненно, попадают в эту категорию!)
Неразумно слишком концентрироваться на последнем пункте, если вы склонны к неврозам, особенно когда часто летаете на самолетах или находитесь в непосредственной близости к процессам, связанным с компьютерами, от которых зависит ваше здоровье.
Многие потенциальные ошибки устраняются во время процесса компиляции или компоновки, но некоторое количество их остается даже после окончательной сборки исполняемого модуля вашей программы. К несчастью, несмотря на тот факт, что ошибки в программе неизбежны подобно смерти или налогам, отладка — это не точная наука; тем не менее, вы можете применить формальный структурированный подход для устранения ошибок. Различают четыре основные стратегии, которые вы можете применить для обеспечения безболезненной, насколько возможно, отладки.
Не изобретайте колесо. Разберитесь и используйте библиотечные средства, поставляемые вместе с Visual C++ 2005 (или другими коммерческими программными компонентами, которые вам доступны), чтобы ваши программы использовали как можно больше заранее протестированного кода.
Разрабатывайте и тестируйте ваш код инкрементным образом. Тестируя индивидуально каждый важный класс и функцию, понемножку собирая отдельные компоненты кода и тестируя их, вы можете существенно облегчить процесс разработки и значительно снизить количество скрытых ошибок.
Применяйте защитное кодирование. Это значит, что код нужно писать таким образом, чтобы он сам противостоял потенциальным ошибкам. Например, объявляйте функции-члены классов “родного” C++, которые не модифицируют объекты, как const. Используйте, где возможно, константные параметры. Не применяйте “магических чисел” в своем коде — определяйте константные объекты с требуемыми значениями.
Включайте отладочный код, который с самого начала проверяет и верифицирует данные и условия в вашей программе. Именно с этим вы ознакомитесь в деталях в настоящей главе.
Ввиду важности получения в конечном итоге программ, которые свободны от ошибок, насколько это возможно, 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 и получать ежеквартальные обновления последних ошибок и исправлений.
Может оказаться полезным составить список обнаруженных ошибок в собственном коде для последующего изучения. Проверяя новый код на предмет наличия ошибок, которые были допущены в прошлом, часто удается сократить время отладки новых проектов.
По природе программирования ошибки практически бесконечны в своем разнообразии, но все же есть некоторые их виды, которые особенно распространены. Возможно, вы знаете о большинстве из них, но все равно давайте кратко по ним пройдемся.
Распространенные ошибки
Удобный способ каталогизации ошибок заключается в привязке их к симптомам, которые они вызывают, потому что именно так они проявляются в первой инстанции. Список из пяти распространенных симптомов, представленный в табл. 10.1, без сомнений, нельзя считать исчерпывающим, поэтому вы определенно пополните его по мере накопления программистского опыта.
Таблица 10.1. Пять распространенных симптомов ошибок
СимптомВозможные причины
Повреждение данныхОтсутствие инициализации переменной
Выход за пределы допустимых значений целочисленного типа Неверный указатель
Ошибка в выражении — индексе массива
Ошибка условия цикла
Ошибка в размере динамически распределенного массива Отсутствие реализации конструктора копирования класса, операции присваивания или деструктора
Необработанные исключенияНеверный указатель или ссылка
Отсутствие обработчика catch
Зависание или крах программы Отсутствие инициализации переменной
Бесконечный цикл
Неверный указатель
Двукратное освобождение одного и того же участка памяти
Отсутствие реализации или ошибка в деструкторе класса
Некорректный потоковый ввод данных Чтение с использованием операции извлечения и функции getline()
Некорректные результатыОпечатки: -= вместо ==, i вместо j и тому подобное
Отсутствие инициализации переменной
Выход за пределы допустимых значений целочисленного типа Неверный указатель
Пропуск break в операторе switch
Обратите внимание на то, насколько много видов ошибок может быть вызвано неверными указателями. Это, наверно, наиболее частая причина возникновения таких ошибок, которые трудно обнаружить, поэтому всегда дважды проверяйте все ваши операции с указателями. Если вы будете уверены во всех случаях применения указателей, то сможете избежать многих ловушек. Ниже перечислены частые причины появления “плохих” указателей.
Отсутствие инициализации указателя после его объявления.
Отсутствие установки указателя на свободную память в null после освобождения выделенной памяти.
Возврат из функции адреса локальной переменной.
Отсутствие реализации конструктора копирования и операции присваивания для объектов классов, которые размещаются в памяти свободного хранилища.
Даже если вы учтете все это, в вашем коде все равно останутся какие-то ошибки, поэтому давайте рассмотрим инструментарий, предлагаемый Visual C++ 2005 для выполнения отладки.
Базовые операции отладки
До сих пор вы создавали отладочные версии примеров программ, но не использовали отладчик. Отладчик — это программа, которая управляет выполнением вашей программы таким образом, что вы можете шагать по исходному коду строка за строкой, или запускать ее выполнение до определенной точки, выбранной в исходном коде. В каждой точке вашего кода, где отладчик останавливается, вы можете просмотреть или даже изменить содержимое переменных. Можно также изменить исходный код, перекомпилировать и перезапустить программу сначала. Вы можете даже изменять исходный код в процессе “шагания” по программе. Когда вы переходите к следующему оператору после модификации кода, отладчик автоматически перекомпилирует программу перед выполнением следующего оператора.
Чтобы понять базовые возможности отладки в Visual C++ 2005, используйте отладчик с программой, в правильной работе которой вы относительно уверены. Тогда вы просто сможете “подергать рычаги”, чтобы увидеть, как все работает. Возьмем простой пример из главы 4, использующий указатели:
// Ех4_05.срр
// Работа с указателями
#include <iostream>
using namespace std;
int main()
{
long* pnumber = NULL; // Объявление и инициализация указателя long number 1 = 55, number2 = 99;
pnumber = &numberl;// Сохранить адрес в указателе
*pnumber +=11;// Увеличить number1 на 11
cout « endl
« "numberl = " « numberl
« " &numberl = ” « hex « pnumber;
pnumber = &number2;// Переставить указатель на адрес number?
numberl = *pnumber*10; H умножить number2 на 10
cout « endl
« "numberl = " « dec « numberl
« " pnumber = " « hex « pnumber
« " *pnumber = " « dec « *pnumber;
cout « endl;
return 0;
}
Если у вас сохранился этот пример, просто откройте его проект; иначе вам придется ввести его снова.
Когда вы пишете программу, а она ведет себя неправильно, отладчик позволяет вам исследовать ее работу, выполняя по одному оператору, чтобы найти, где, что и почему идет не так, а также увидеть состояние всех программных данных в любой момент выполнения. Вы можете выполнять этот пример оператор за оператором и наблюдать за содержимым переменных, которые вас интересуют. В данном случае мы
хотим посмотреть на pnumber, содержимое памяти, на которую указывает pnumber (то есть *pnumber), numberl и number2.
Прежде всего, нужно убедиться, что конфигурация сборки примера установлена в Win32 Debug, а не Win32 Release (так по умолчанию, если вы не измените). Конфигурация сборки выбирает набор установок проекта для операции сборки программы, которые можно видеть в пункте меню Project^Settings (Проект*=>Настройки). Текущая активная конфигурация сборки показывается в паре выпадающих списков панели инструментов Standard (Стандартные). Чтобы отобразить или удалить определенную панель инструментов, вы просто щелкаете правой кнопкой на панели и выбираете или отменяете выбор той или иной панели в списке. Убедитесь, что флажок стоит рядом с Debug (Отладка), чтобы отображалась панель инструментов отладки. Она появляется автоматически, когда запущен отладчик, но вам стоит посмотреть, что она содержит, прежде чем запускать отладчик. Конфигурацию сборки можно изменить, открыв выпадающий список и выбрав необходимые альтернативы. Вы также можете использовать пункт меню Build^Configuration Manager... (Сборка^Диспетчер конфигурации). Панель инструментов Standard показана на рис. 10.1.
' Debug т Win32' Debug т Win32’ 9’ 9
' Debug т Win32
' Debug т Win32
’ 9
’ 9
Рис. ЮЛ. Панель инструментов Standard
Вы можете посмотреть, для чего служит каждая из кнопок панели инструментов, просто проводя курсором мыши над кнопками панели. При этом будет всплывать краткая подсказка, идентифицирующая функцию соответствующей кнопки.
Конфигурация проекта Debug включает дополнительную информацию в вашу исполняемую программу во время компиляции, что позволяет потом использовать возможности отладки. Эта дополнительная информация сохраняется в файле .pdb, который создается в папке Debug вашего проекта. Конфигурация Release исключает эту информацию, поскольку она требует накладных расходов, которые излишни в полностью протестированной рабочей программе. В версиях Visual C++ 2005 Professional или Enterprise компилятор также оптимизирует код при компиляции рабочей версии программы. Оптимизация не включается при компиляции отладочной версии, поскольку процесс оптимизации может изменять последовательность выполнения кода, чтобы сделать его более эффективным, или даже вовсе исключать излишний код. Поскольку это разрушает отображение “один к одному” между исходным кодом и соответствующими блоками исполняемого машинного кода, оптимизация может затруднить или запутать пошаговое выполнение программы.
Панель инструментов Debug (Отладка) показана на рис. 10.2.
Нех -|
Рис. 10.2. Панель инструментов Debug
Если вы просмотрите всплывающие подсказки для кнопок этой панели, то получите предварительное представление о том, что они делают — и вскоре вы ими начнете пользоваться. С примером из главы 4 вам не придется применять все доступные возможности отладки, но некоторые наиболее важные вы попробуете. После того, как вы познакомитесь с пошаговым выполнением программы с применением отладчика, вы сможете раскрыть больше средств, исследуя программу, имеющую ошибки.
Запустить отладчик можно щелчком на крайней слева кнопке панели инструментов 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 (Отладка^ отключить все точки прерывания).
Рис. 103. Панель редактора с установленными точками прерывания
Расширенные точки прерывания
Более усовершенствованный способ спецификации точек прерывания предлагается в окне, которое вы можете отобразить, нажав <Alt+F9> или выбрав Breakpoints (Точки прерывания) из списка, отображаемого при выборе кнопки Windows (Окна) в панели инструментов Debug — справа в конце. Это окно показано на рис. 10.4.
Рис. 10.4. Окно Breakpoints (Точки прерывания)
Кнопка Columns (Колонки) в панели инструментов позволяет добавлять дополнительные колонки для отображения в окне. Например, вы можете отображать имя исходного файла или имя функции, в которой установлена точка прерывания, или же вы можете отобразить, что произошло, когда оператор был достигнут.
Дополнительные опции для точки прерывания устанавливаются щелчком правой кнопкой мыши на строке с точкой прерывания в окне Breakpoints и выбором соответствующих пунктов из всплывающего меню. Наряду с установкой точки прерывания в определенном месте кода, вы также можете установить точку прерывания, когда определенное булевское выражение вернет true. Это мощное средство, но оно означает весьма ощутимые накладные расходы в программе, поскольку проверочное выражение должно постоянно вычисляться заново. Как следствие, выполнение за-
медляется, даже на самых быстрых машинах. Можно также указать, что выполнение должно прерываться только после определенного количества проходов потока управления через данную точку. Это наиболее удобно для кода внутри циклов, когда вы не хотите прерывать выполнение на каждой итерации. После установки любого условия для точки прерывания внешний вид глифа изменится — на нем в центре появится знак +.
Установка точек трассировки
Точка трассировки (tracepoint) — специальная разновидность точки прерывания, которая имеет ассоциированное с ней действие. Вы создаете точку трассировки щелчком правой кнопкой мыши по строке, в которой она должна быть установлена, и выбором пункта Breakpoint^When Hit (Точка прерывания^Когда попадают) из всплывающего меню. После этого вы видите диалоговое окно, показанное на рис. 10.5.
Рис. 10.5. Диалоговое окно When Breakpoint Is Hit (Когда происходит попадание в точку прерывания)
Как видите, действием точки трассировки может быть печать сообщения и/или запуск макроса, к тому же вы можете выбрать — нужно ли останавливать выполнение в точке трассировки или нет. Присутствие точки трассировки в строке исходного кода, где выполнение не останавливается, помечается красным глифом в форме ромба. Текст диалогового окна объясняет, как специфицировать печатаемое сообщение. Например, вы можете печатать имя текущей функции и значение pnumber, указав в текстовом поле следующее:
$FUNCTIONf Значение pnumber равно {pnumber}
Вывод, генерируемый по достижении точки трассировки, появляется в панели Output (Вывод) в окне приложения Visual Studio.
Если выставлен флажок Run a macro: (Выполнить макрос:), то вы можете выбрать макрос из длинного списка доступных стандартных макросов.
Запуск отладки
Существуют пять способов запуска вашего приложения в режиме отладки из пунктов меню Debug (Отладка), показанного на рис. 10.6.
Windows
►
Start Debugging
F5
Start With Application Verifier
Shift+Alt+F5
Start Without Debugging
Ctrl+F5
Attach to Process..
Exceptions...
Ctrl+Alt+E
Step Into
Fil
у
Step Over
F10
Toggle Breakpoint
F9
Г lev; Breakpoint
►
>
Delete All Breakpoints
Ctrl+Shift+F9
Disable All Breakpoints
Рис. 10.6. Пункты меню Debug
Опция Start Debugging (Начать отладку), также доступная через кнопку в панели инструментов Debug, просто выполняет программу до первой точки прерывания (если она есть), где выполнение останавливается. После того как вы просмотрите все, что нужно, в этой точке прерывания, выбор того же пункта меню или щелчок на кнопке панели инструментов вызывает продолжение выполнения до следующей точки прерывания. Подобным образом вы можете перемещаться по программе от одной точки прерывания к другой, и в каждой просматривать значение критичных переменных, изменяя их значение при необходимости. Если точек прерывания нет, то запуск отладчика таким способом просто выполняет всю программу без остановок. Конечно, только то, что вы запустили отладчик подобным способом, вовсе не означает, что вы должны продолжать его использовать; при каждом останове выполнения можно выбрать любой другой способ перемещения по коду.
Опция Start With Application Verifier (Начать с верификатором приложений) предназначена для верификации кода родного C++ во время выполнения. Верификатор приложений (Application Verifier) — это расширенный инструмент для идентификации ошибок, связанных с неверными дескрипторами, использованием критических разделов или повреждением кучи. В настоящей книге эта тема не рассматривается.
Опция Attach to Process (Присоединиться к процессу) позволяет отлаживать программу, которая уже запущена. Эта опция отображает список процессов, запущенных на вашей машине, и вы можете выбрать среди них процесс, который хотите отлаживать. Это средство для опытных пользователей и лучше избегать экспериментов с ним, если только вы не уверены в том, что собираетесь делать. Можно очень легко заблокировать машину или вызвать другие проблемы, если вмешаться в критические процессы операционной системы.
Пункт Step Into (Войти), также доступный в виде кнопки в панели инструментов Debug, выполняет программу по одному оператору за раз, заходя в каждый вложенный блок кода, включая каждую вызываемую функцию. Это может оказаться весьма утомительным, если использовать такой режим отладки на протяжении всего процесса, потому что, например, при этом будет выполняться весь код библиотечных функций, что вряд ли вас заинтересует, если только вы не заняты их разработкой. Небольшое количество библиотечных функций написаны на языке ассемблера, включая некоторые из тех, что поддерживают потоковый ввод-вывод. Функции на языке ассемблера выполняются по одной машинной инструкции за раз, что, как и можно ожидать, займет значительное время.
Пункт Step Over (Перешагнуть), также доступный в виде кнопки в панели инструментов Debug, просто выполняет операторы вашей программы по одному за раз, выполняя весь код, используемый функциями, которые могут быть вызваны внутри оператора, как сплошной поток операций, без остановок.
Имеется и шестая опция для запуска в режиме отладки, которая не появляется в меню Debug. Вы можете щелкнуть правой кнопкой мыши на любой строке кода и выбрать из контекстного меню пункт Run to Cursor (Выполнить до курсора). Произойдет именно то, что гласит этот пункт — программа будет запущена до строки, в которой находится курсор, и затем ее выполнение будет прервано, позволив вам просмотреть или изменить переменные программы. Какой бы способ вы ни выбрали для запуска процесса отладки, вы можете продолжить выполнение, применив одну из пяти опций в любой из промежуточных точек прерывания.
А теперь попробуем все это на примере. Запустите программу, используя опцию Step Into, выбрав соответствующий пункт меню или щелкнув на кнопке в панели инструментов, либо нажав клавишу <F11>. После небольшой паузы (предполагается, что проект уже собран), Visual C++ 2005 переключится в режим отладки.
Когда отладчик стартует, появятся два окна с вкладками ниже окна редактора. Вы можете выбрать, что должно отображаться в любой момент времени в каждом из этих окон, выбрав требуемую вкладку. Можно настроить, какие именно окна должны появляться при запуске отладчика. Полный список всех окон отображается в выпадающем меню Debug ^Windows (Отладкам Окна). Окно Autos (Автоматические) слева показывает текущие значения автоматических переменных в контексте функции, выполняемой в данный момент. Окно Call Stack (Стек вызовов) справа идентифицирует вызовы функций, находящиеся в процессе выполнения, но нас сейчас более интересует вкладка Output (Вывод) в том же окне. В панели редактора вы увидите, что открывающая фигурная скобка вашей функции main () выделена стрелочкой, что указывает на то, что это — текущая точка выполнения программы. Все описанное можно видеть на рис. 10.7.
Здесь вы также можете видеть точку прерывания в строке Пи точку трассировки в строке 17. В этот момент выполнения программы вы не можете выбирать какие- либо переменные для просмотра, потому что ни одна из них еще не существует. До тех пор, пока не будет выполнено объявление переменной, вы не сможете ни просмотреть ее значение, ни изменить его.
Во избежание пошагового выполнения всего кода в потоке функций, занимающихся вводом-выводом, вы используете средство Step Over, чтобы продолжить выполнение до следующей точки прерывания. Это просто выполнит операторы вашей функции main () по одному за раз, и выполнит весь код, используемый потоковыми операциями (или любыми другими функциями, которые могут быть вызваны внутри оператора) без остановок.
Рис. 10.7. Панельредактора во время отладки
Просмотр знамений переменных
Определение переменной, значение которой вы хотите просматривать, называется установкой наблюдения (setting a watch) над переменной. Для того чтобы установить наблюдение, необходимо уже иметь переменные, объявленные в программе. Операторы объявления переменной можно выполнить, три раза выбрав Step Over. Воспользуйтесь пунктом меню Step Over, кнопкой панели инструментов или нажмите <F10> три раза, чтобы стрелка переместилась в начало строки 11:
pnumber = snumberl; // Сохранить адрес в указателе
Теперь, если вы посмотрите в окно Autos, оно должно будет выглядеть, как показано на рис. 10.8 (хотя значение snumberl может отличаться в вашей системе, поскольку представляет местоположение в памяти). Обратите внимание, что значения Snumberl и pnumber не равны друг другу, потому что строка, в которой pnumber присваивается адрес numberl (строка, на которую указывает стрелочка), еще не была выполнена. Вы инициализировали pnumber нулевым указателем в первой строке функции, вот почему адрес содержит ноль. Если вы не инициализируете этот указатель, он может содержать произвольное “мусорное” значение, включая и ноль, конечно, поскольку он содержит значение, оставшееся там от последней программы, которая использовала эти конкретные четыре байта памяти.
Окно Autos содержит пять вкладок, включая вкладку Autos, отображаемую в данный момент, и все они описываются ниже.
Вкладка AutOS (Автоматические) показывает автоматические переменные, используемые в текущем операторе и его непосредственном предшественнике (другими словами, в операторе, указанном стрелочкой в окне редактора и предшествующем ему).
Вкладка Locals (Локальные) показывает значения переменных, локальных по отношению к текущей функции. В общем случае новые переменные входят в
область видимости по мере продвижения по программе и покидают ее при выходе из блока, в котором они объявлены. В данном случае это окно всегда показывает значения number 1, number2 и pnumber, поскольку в программе имеется только одна функция main (), состоящая их единственного блока кода.
Вкладка Threads (Потоки) позволяет просматривать и управлять потоками в многопоточных приложениях.
Вкладка Modules (Модули) перечисляет детали модулей кода, выполняемых в данный момент. Если ваше приложение терпит крах, вы можете определить, в каком модуле это произошло, сравнив адрес, по которому произошел крах, с диапазонами адресов в колонке Address (Адрес) этой вкладки.
На вкладке Watch 1 (Наблюдение 1) вы можете добавлять переменные, за которыми хотите наблюдать. Щелкните на строке в окне и введите имя переменной. Вы также можете наблюдать за значением выражения C++, которое вводится точно так же, как и имя переменной. Можно добавлять до трех дополнительных окон Watch (Наблюдение) через пункт меню Debug^Windows^Watch (Отл адка^Окна^Наблюдение).
Autos
NameValueType
+/Jnuimberl0x0012^.54ng*
jnumber 155long
Vnumber299:ong
+vpnumber0x00000000long*
^ Autos |^J Locals [^Threads | i^l Modules |^1 Watch 1
Puc. 10.8. Окно Autos отображаетпеременные отлаживаемой программы
Обратите внимание, что рядом с именем pnumber в окне Autos стоит знак плюса. Этот знак появляется возле каждой переменной, для которой может быть отображена дополнительная информация — для таких, как массив, указатель или объект класса. В данном случае вы можете раскрыть представление переменной pnumber, щелкнув на значке “плюс”. Если вы нажмете <F10> два раза и щелкнете на + возле pnumber, то отладчик отобразит значение, хранимое в памяти по адресу, содержащемуся в указателе, как показано на рис. 10.9.
Autos
ж X
Name
Value
Type
/ *pnumber
66
long
/ number 1
66
long
i- / pnumber
0x0012ff54
lung*
♦
66
long
S AutosLocals |yj}Threads 11 '■J Modules |^| Watch 1
Рис. 10.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, это будет интерпретироваться как шестнадцатеричное значение. Понятно, что вы должны быть осторожны с новыми значениями, которые “вбрасываете” в свою программу. Если только вы точно не знаете, какой эффект можно ожидать от таких изменений, то легко можете обеспечить изменчивое поведение программы, что вряд ли приблизит вас к получению нормально функционирующей программы.
Возможно, вы сочтете полезным запустить под управлением отладчика еще несколько примеров из предыдущих глав. Это позволит вам лучше ощутить то, как отладчик работает в различных условиях. Наблюдение за переменными и выражениями здорово помогает разобраться в проблемах с вашим кодом, однако существуют и другие возможности для поиска и устранения ошибок. Посмотрим, как можно добавить к программе код, который предоставит больше информации о том, когда и почему что-то идет не так.
Добавление отладочного кода
Разрабатывая крупные программы, вы определенно нуждаетесь в добавлении специального кода, предназначенного для выявления ошибок, где только возможно, и предоставлении трассирующего вывода, который поможет найти местоположение ошибок. Наверняка вы не захотите заниматься пошаговым выполнением кода до тех пор, пока не имеете представления о том, что собой представляет ошибка, и в какой части кода ее следует искать. Код, который выполняет упомянутые вещи, необходим только на этапе тестирования программы. Необходимость в нем отпадает, когда вы уверены, что программа полностью работоспособна, и нет смысла нести дополнительные расходы, связанные с его выполнением, а также терпеть неудобства, наблюдая отладочный вывод в завершенном продукте. По этой причине код, который вы добавляете только для отладки, работает в отладочной версии программы, но не в рабочей версии (если, конечно, она правильно реализована).
Вывод, генерируемый отладочным кодом, должен предоставлять возможность вскрыть причины возникновения проблем, и если вы как следует продумаете его реализацию, способ встраивания его в вашу программу, то с его помощью можно будет легко получить представление о том, в какой части программы следует искать ошибку. Затем вы можете применить отладчик, чтобы точно найти причину и местоположение ошибки, после чего устранить ее.
Первый способ, которым вы можете проверить поведение программы, представлен библиотечной функцией C++.
Использование утверждений
Заголовочный файл стандартной библиотеки <cassert> объявляет функцию assert (), которую вы можете использовать для проверки логических условий внутри вашей программы, когда не определен специальный символ препроцессора — NDEBUG. Эта функция объявлена следующим образом:
void assert(int expression);
Аргумент функции специфицирует условие, которое должно быть проверено, но эффект от функции assert () подавляется, если определен специальный символ препроцессора NDEBUG. Символ NDEBUG автоматически определяется в рабочей версии программы, но не в отладочной версии. Поэтому утверждение (assertion) проверяет свой аргумент в отладочной версии программы, но ничего не делает в рабочей версии. Если вы хотите отключить этот механизм в отладочной версии программы, то можете определить NDEBUG явно, используя директиву #def ine. Чтобы это возымело эффект, такую директиву необходимо поместить ранее директивы #include для заголовочного файла <cassert> в исходном файле:
#define NDEBUG// Отключить механизм утверждений в коде
#include <cassert> // Объявление assert()
Если выражение, переданное в качестве аргумента функции 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\ testassertAtestassert \ testassert.cpp, строка 11
Утверждение также отображает окно сообщения, предлагающее три варианта выбора, как показано на рис. 10.10.
Рис. 10.10. Окно сообщения, отображаемое утверждением
Щелчок на кнопке Abort (Прервать) немедленно завершает программу. Кнопка Retry (Повторить) запускает отладчик Visual C++ 2005, так что вы можете по шагам выполнить программу, чтобы подробнее выяснить причину отказа утверждения. В принципе, кнопка Ignore (Игнорировать) позволяет продолжить выполнение программы, несмотря на ошибку, но обычно это неразумный выбор, поскольку результат, скорее всего, будет непредсказуемым.
В качестве аргумента assert () можно использовать логическое выражение любого рода. Можно сравнивать значения, проверять указатели, верифицировать типы объектов или выполнять любую другую полезную проверку, чтобы убедиться в корректности операций вашего кода.
Получение сообщения о невыполнении некоторого логического условия помогает мало, так что обычно вам потребуется дополнительная помощь для обнаружения и исправления ошибок. Посмотрим, как можно добавить диагностический код более общей природы.
Добавление собственного отладочного кода
Используя директивы препроцессора, вы можете добавить любой код, который хотите, к своей программе — так, чтобы он компилировался и выполнялся только в отладочной ее версии. Отладочный код полностью исключается из рабочей версии, потому он вообще не влияет на эффективность протестированной программы. Отсутствие символа NDEBUG может служить управляющим механизмом для включения отладочного кода; то есть символ, используемый для управления функцией assert () стандартной библиотеки, о которой говорилось выше, может послужить и вашему отладочному коду. В качестве альтернативы, для лучшего и более позитивного управляющего механизма можно применять другой символ препроцессора — _DEBUG, который всегда определяется автоматически Visual C++ в отладочной версии программы, но который в рабочей версии не определен. Вы просто заключаете свой отладочный код, который должен компилироваться и выполняться только во время отладки, между парой директив препроцессора — #ifdef и #endif, проверяя наличие символа _DEBUG, как показано ниже:
#ifdef _DEBUG
// Код для целей отладки. . .
#endif // _DEBUG
Код между #ifdef и #endif компилируется только при условии наличия определения символа _DEBUG. Это значит, что когда ваша программа будет полностью протестирована, вы можете собрать рабочую версию, полностью свободную от накладных расходов, связанных с отладочным кодом. Этот код может делать все, что помогает в процессе отладки — от простого вывода сообщения для трассировки последовательности выполнения (каждая функция может фиксировать свой вызов), до выполнения дополнительных вычислений с целью проверки и верификации данных или вызова функций, обеспечивающих отладочный вывод.
Конечно, вы можете иметь столько блоков отладочного кода в своих программах, сколько хотите. Можно также использовать свои собственные символы препроцессора для более выборочного включения отладочного кода. Одной из причин для этого может быть то, что ваш отладочный код выдает слишком объемный вывод, так что вы хотите генерировать его только тогда, когда это действительно необходимо. Другой причиной может рассматриваться необходимость обеспечения “детализированного” отладочного вывода, чтобы можно было выбирать и указывать — какой вывод следует генерировать при каждом конкретном запуске. Но даже в этих случаях все равно будет хорошей идеей использовать символ _DEBUG для обеспечения общего контроля, поскольку это позволяет автоматически обеспечить полную свободу рабочей версии от накладных расходов отладочного кода.
Рассмотрим простой случай. Предположим, что вы используете два собственных символа для управления кодом отладки: MYDEBUG, управляющий “нормальным” отладочным кодом, и VOLUME DEBUG, который применяется для управления кодом, производящим гораздо больше вывода, необходимость в котором возникает лишь изредка. Вы можете организовать эти символы так, что они будут зависеть от символа DEBUG:
#ifdef _DEBUG
#define MYDEBUG
#define VOLUMEDEBUG
#endif
Чтобы предотвратить объемный отладочный вывод, вам нужно просто закомментировать определение VOLUME DEBUG, и никакой символ не будет определен, если не определен _DEBUG. Если ваша программа состоит из нескольких отладочных файлов, то возможно, будет удобно собрать управляющие символы отладки где-то в одном заголовочном файле и включить его с помощью #include в каждый файл, содержащий отладочный код.
Давайте рассмотрим простой пример, чтобы увидеть, как добавление отладочного кода в программу работает на практике.
Практическое занятие| ДобаВЛвНИв ОТЛЭДОЧНОГО КОДЗ
Чтобы раскрыть этот и ряд других общих подходов к отладке, возьмем пример программы, которая, будучи довольно простой, содержит ошибки, которые нужно найти и устранить. Поэтому вы должны рассматривать весь код в остальной части настоящей главы как подозрительный, в частности потому, что он не обязательно воплощает хорошую практику программирования.
Для экспериментирования с операциями отладки начнем с определения класса, представляющего имя персоны, и протестируем его в работе. В этом коде должно быть много неправильного, поэтому не поддавайтесь искушению сразу исправить явно ошибочный код; задача состоит в том, чтобы научиться применять средства отладки для нахождения ошибок. Однако на практике многие ошибки проявляются сразу же при запуске программы. Вам не обязательно использовать отладчик или дополнительный код для их нахождения.
Создайте пустое консольное приложение Win32 под названием ExlOOl. Далее добавьте заголовочный файл Name. h, в котором будет находиться определение класса Name. Класс представляет имя из двух членов-данных — указателей на строки, хранящие имя и фамилию человека. Если вы хотите иметь возможность объявлять массивы объектов Name, то должны предоставить конструктор по умолчанию в дополнение к любым другим конструкторам. Вам понадобится возможность сравнения объектов Name, поэтому придется включить в класс перегруженные операции для их осуществления. Определение класса Name в файле Name. h будет таким:
// Name.h — определение класса Name
#pragma 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;
Теперь вы можете добавить файл 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, например. Однако для проверки этого утверждения лучше не использовать. Такие вещи могут происходить в результате пользовательского ввода, так что для этого должны быть предусмотрены обычные программные проверки, обрабатывающие ошибки, которые случаются в процессе нормального течения событий. Важно улавливать разницу между ошибками в коде и ошибочными условиями, которые могут случаться во время обычного выполнения программы. Конструктор никогда не должен получать нулевые указатели, но имя
нулевой длины легко может получиться при нормальной работе (от клавиатурного ввода, например). В этом случае было бы лучше читать в коде имена, проверяя это условие перед вызовом конструктора класса 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)] = 1 1;// Добавить пробел
// Добавить фамилию и вернуть результат
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)+strlen(pSurname);
}
Функция, которая намерена вызвать getName (), может использовать возвращенное getNameLength () значение, чтобы определить, сколько места нужно для того, чтобы уместить полное имя. Также здесь присутствует код трассировки.
В интересах инкрементной разработки класса вы можете пока пропустить определение перегруженных операций сравнения. Необходимы только определения функций-членов, которые в действительности используются в программе, а эта начальная версия тестовой программы будет предельно простой.
Символы препроцессора, предназначенные для управления выполнением отладочного кода, можно определить в заголовочном файле Debugs tuff. h:
// 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", "Horton"); // Попробовать одиночный объект
// Получить и сохранить имя в локальном символьном массиве char theName[10);
cout « ”\пПолное имя: " « myName.getName (theName) ;
// Сохранить имя в массиве из свободного хранилища
char* pName = new char[myName.getNameLength()+1];
cout « "ХпПолное имя: " « myName.getName(pName);
cout « endl;
return 0;
}
Теперь, когда весь код введен, дважды проверен и полностью корректен, вы можете запустить его, дабы воочию в этом убедиться.
Описание полученных результатов
Но он даже не компилируется! Основная проблема кроется в конструкторе Name. Параметры указаны как const, что и должно быть, но данные-члены — нет. Вы могли бы объявить их как const, но в любом случае придется копировать строки имен, а не просто копировать указатели. Исправим определение конструктора следующим образом:
// Конструктор
Name::Name (const char* pFirst, const char* pSecond) {
// Проверка аргумента на равенство null
assert(pFirst != 0) ;
assert(pSecond != 0) ;
#ifdef CONSTRUCTOR-TRACE
// Трассировка вызова конструктора
cerr « "ХпВызван конструктор Name.";
#endif
pFirstname = new char[strlen(pFirst)+1];
strcpy (pFirstname, pFirst) ;
pFirstname = new char [strlen (pSecond)+1] ; strcpy (pSurname, pSecond) ;
}
Теперь вы копируете строки, так что все должно быть хорошо, не правда ли?
При перекомпиляции программы будут выданы некоторые предупреждения о том, что функция strcpy () устарела и лучше использовать strcpy_s (), но strcpy () успешно работает, поэтому проигнорируем предупреждение в данном упражнении. Однако при перезапуске программы она выдаст сбой почти немедленно. Вы можете видеть это в консольном окне, где получите сообщение от конструктора, так что приблизительно видно, как далеко зашло выполнение. Перезапустите программу под управлением отладчика, и вы увидите, что случилось.
Отладка программы
Когда начинается отладка, вы получаете окно сообщений, указывающее на необработанное исключение. В отладчике есть всесторонний набор средство для пошагового выполнения вашего кода и трассировки последовательности вызовов. Щелкните на кнопке Break (Прервать) в диалоговом окне, сообщающем о необработанном исключении, чтобы остановить выполнение. Программа будет находиться в точке, где произошло исключение, и текущий выполняемый код окажется в окне редактора. Исключение вызвано обращением к области памяти, находящейся вне юрисдикции программы, поэтому сразу подозрение падает на неверный указатель.
Стек вызовов
Стек вызовов хранит информацию обо всех функциях, которые были вызваны и которые все еще выполняются, поскольку еще не вернули управления. Как вы видели ранее, окно Call Stack (Стек вызовов) показывает последовательность вызовов функций, приведших к текущей точке программы (рис. 10.11).
Call Stack▼ fl X
NameLanguage
ms: crSOa.dll'sircat^ nagnea char * dst=0x00417714, unsigned char *src=0x0012ff68' Line Asm Exl0_01.exe Name: :Name(ccnst char *pFirst =0x0041770c, const char *pSecond =0x00417 C++ Exl0_01.exe!main(intargc=l, char **arg ;=0x003565f0} Line 14C++
Exl0_01.exe!tmainCRTStartupO Line 586 + 0x19 bytesC
ExlOJ)l.exe!mainCRTStartup() Line 403C
кете - I2.all!7c8399r3(Ш '^Call Stack fl Breakpoints |3 Output
Puc. 10.11. Окно Call Stack
Последовательность вызовов функций отображается, начиная с самого последнего вызова в верхней части, библиотечной функции strcat (), и продолжается вниз до вызовов Кегпе132 в самом низу окна, показанного на рис. 10.11. Каждая функция была вызвана прямо или непрямо той, которая находится ниже ее, и ни одна из отображенных функций еще не выполнила возврат. Строки Kerne 132 показывают системные процедуры, которые стартовали до запуска нашей функции main (). Вас интересует роль вашего кода в этом, и вы можете видеть, начиная со второй строки, что конструктор класса Name находится в процессе выполнения (еще не вернул управления), когда было возбуждено исключение. Если выполнить двойной щелчок на этой строке, в окне редактора отобразится код этой функции и с помеченной строкой исходного кода, которая выполнялась, когда возникла проблема. В данном случае это:
strcpy(pSurname, pSecond);
Этот вызов стал причиной возбуждения необработанного исключения — но почему? Исходная проблема не обязательно кроется здесь; здесь она только проявилась. Это типичная ошибка, связанная с указателями. Взгляните на окно, показывающее значение переменных в контексте конструктора 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() вызвана.
Вывод имени определенно неправильный, однако вы не знаете точно, где именно находится проблема. Перезапуск и повторное пошаговое выполнение программы должно помочь разобраться.
Переход к ошибке
Функция getNameLength () в данный момент отображается в окне редактора, и отладчик указывает строку, в которой возникла проблема:
return strlen(pFirstname)tstrlen(pSurname)+1;
В окне Call Stack вы можете видеть, что точка выполнения программы находится в функции-члене getNameLength (), которая только вызывает библиотечную функцию strlen (), чтобы получить общую длину имени. Маловероятно, что сбой произошел в функции strlen (), следовательно, что-то не так с частью объекта. Окно AutOS, отображая переменные в контексте этой функции, показывает, что текущий объект был поврежден, что видно на рис. 10.13.
Рис. 10.13. Окно AutOS показывает, что текущий объект был поврежденРис. 10.13. Окно AutOS показывает, что текущий объект был поврежден
Рис. 10.13. Окно AutOS показывает, что текущий объект был поврежден
Рис. 10.13. Окно AutOS показывает, что текущий объект был поврежден
На текущий объект указывает this и, щелкнув на значке + рядом с this, вы можете просмотреть его данные-члены. Сразу видно, что проблема в члене pSurname. Адрес, который он содержит, должен ссылаться на строку “Horton”, но это явно не так. Более того, отладчик пометил его как недопустимый указатель.
Предполагая, что ошибки подобного рода не возникают в точке, где они проявляются, вы можете вернуться назад, перезапустить программу и, выполняя ее по шагам, следить, где объект Name будет “испорчен”. Для этого вы можете выбрать Step Over (Перешагнуть) или нажать <F10>, чтобы перезапустить приложение, и повторно нажимая <F10>, пройти по шагам все операторы программы. После выполнения оператора, определяющего объект myName, окно AutOS для функции main () покажет, что он был сконструирован успешно, что и можно видеть на рис. 10.14.
Autos
NamsNamsf| "i>Name3 _y pFrswame ti у pSamame+ # theNamef| "i>Name3 _y pFrswame ti у pSamame+ # theNameValue Type {pFirstname =0x00356678 Ivor'pSun Name 0x00356678 Ivor' -. char *Value Type {pFirstname =0x00356678 Ivor'pSun Name 0x00356678 Ivor' -. char *0x003566c0 "Horton'0x003566c0 "Horton'char *char *CxCC12fTrt ШППШШППШхЧ. char [10]CxCC12fTrt ШППШШППШхЧ. char [10]
Nams
Nams
f| "i>Name
3 _y pFrswame ti у pSamame
+ # theName
f| "i>Name
3 _y pFrswame ti у pSamame
+ # theName
Value Type {pFirstname =0x00356678 Ivor'pSun Name 0x00356678 Ivor' -. char *
Value Type {pFirstname =0x00356678 Ivor'pSun Name 0x00356678 Ivor' -. char *
0x003566c0 "Horton'
0x003566c0 "Horton'
char *
char *
CxCC12fTrt ШППШШППШхЧ. char [10]
CxCC12fTrt ШППШШППШхЧ. char [10]
0 Autos I^J] Locals ^Threads |S Modules ^Watch 1
Puc. 10.14. Окно Autos показывает, что объ-ект Name сконструирован успешно
В результате выполнения следующего оператора, который выводит имя, объект myName повреждается. Это ясно видно в окне Autos для main () на рис. 10.15.
NameNameValueValueTypeType*Name: :getftime rel 0x0012ff5c Horton'*Name: :getftime rel 0x0012ff5c Horton'char *char *► / std::operator<<<s {...}► / std::operator<<<s {...}sta:: basic_ostream <char std:sta:: basic_ostream <char std:- | sra::operator<«s {...}- | sra::operator<«s {...}std: :basic_ostream<;cnar,sta:std: :basic_ostream<;cnar,sta:
Name
Name
Value
Value
Type
Type
*Name: :getftime rel 0x0012ff5c Horton'
*Name: :getftime rel 0x0012ff5c Horton'
char *
char *
► / std::operator<<<s {...}
► / std::operator<<<s {...}
sta:: basic_ostream <char std:
sta:: basic_ostream <char std:
- | sra::operator<«s {...}
- | sra::operator<«s {...}
std: :basic_ostream<;cnar,sta:
std: :basic_ostream<;cnar,sta:
/ m /Namer ,# pFirstnameВ ^ pSurname + / phame 3 у theName/ m /Namer ,# pFirstnameВ ^ pSurname + / phame 3 у theName{pFirstname =0x00356678 Ivor'pSun Name
/ m /Name
r ,# pFirstname
В ^ pSurname + / phame 3 у theName
/ m /Name
r ,# pFirstname
В ^ pSurname + / phame 3 у theName
0x00 356678 Ivor"- char *
0x74726f48 <Bad Ptr>v char *
Oxcccccccc <Bad Ptr>fl char *
0x0012ff44*Ivornnnnnnnixf Ц ^ char [10]
S3 Autos |Ж1 Locals |+g|Th reads, ^>1 Modules |^1 Watch 1
Рас. 10.15. Окно Autos отображает факт раз-рушения объекта Name
Если исходить из предположения, что операция потокового вывода работает корректно, значит ваша функция-член getName () делает что-то такое, чего делать не должна. Перезапустите отладчик еще раз, но на этот раз используйте Step Into, когда выполнение достигнет оператора вывода. Когда точка выполнения окажется в первом операторе функции getName (), вы можете продолжить шагать по ее операторам, используя Step Over. Следите за контекстным окном по мере продвижения по функции. Вы увидите, что все замечательно до тех пор, пока не будет выполнен следующий оператор:
strcpy(pName+strlen(pName)+1, pSurname); // Добавить второе имя после пробела
Этот оператор вызывает повреждение pSurname для текущего объекта, на который указывает this. Вы можете видеть это в окне Autos на рис. 10.16.
Рис. 10.16. Повреждение pSurname для текущего объектаРис. 10.16. Повреждение pSurname для текущего объекта
Рис. 10.16. Повреждение pSurname для текущего объекта
Рис. 10.16. Повреждение pSurname для текущего объекта
Как может копирование из объекта в другой массив испортить объект, особенно учитывая, что pSurname передано в качестве аргумента для параметра const? Вы должны посмотреть на адрес, находящийся в pName, чтобы понять это. Сравните его с адресом, хранящимся в this. Между ними разница всего в 20 байт. Они не могут находиться так близко! Вычисление адреса позиции в pName неверно — просто потому, что вы забыли, что копирование пробела перезаписало ограничивающий \0 в массиве pName, и strlen (pName) не может вычислить корректную длину pName. Вся проблема вызвана этим оператором:
pName [strlen(pName) ] = ’ 1; II Добавить пробел
Он перезаписал \0, и это привело к тому, что следующий вызов strlen () выдал неверный результат.
Этот код все равно излишне запутан. Применять библиотечную функцию strcat () для соединения строки намного лучше, чем вызывать strcpy О , поскольку это позволит исключить все излишние модификации указателя. То есть вы должны переписать оператор вот так:
Конечно, должен быть изменен и следующий оператор:
return strcat(pName, pSurname); // Добавить второе имя и вернуть итог
После этих изменений вы можете перекомпилировать и попробовать снова. На этот раз программа работает удовлетворительно, что можно видеть из вывода:
Вызван конструктор Name.
Name::getName() вызвана.
Полное имя: Ivor Horton
Name::getNameLength() вызвана.
Name::getName()вызвана.
Полное имя: Ivor Horton
Press any key to continue . . .
Однако получение правильного вывода не всегда означает, что все хорошо, и в данном случае это определенно не так. Вы получаете окно сообщения, отображаемое библиотекой отладки (рис. 10.17), которое сообщает о повреждении стека.
xrosoft Visual C++ Debug Library
Debug Error!
Program: d:\begTnning visual C++2Q05\examples\exW_01Xdebug\ExlQ_01.exe Module: d:\beginning visual C++ 2005\cxamples\exl0_01tdebug\Exl0_01.exe File:
Run-Time Check Failure #2- Stack around the variable 'theName1 was corrupted.
(Press Retry to debug the application)
AbortAbortRetryRetryIgnoreIgnore
Abort
Abort
Retry
Retry
Ignore
Ignore
Puc. 10.17. Окно сообщения о повреждении стека
Следующий код демонстрирует, в чем состоит проблема:
int main(int argc, char* argv[]) {
Name myName("Ivor", "Horton"); H Попробовать одиночный объект
/ / Получить и сохранить имя в локальном символьном массиве char theName [10];
cout « "ХпПолное имя: " « myName.getName (theName) ;
// Сохранить имя в массиве из свободного хранилища
char* pName = new char[myName.getNameLength()];
cout « "ХпПолное имя: 11 « myName.getName (pName) ;
cout « endl; return 0;
}
Обе выделенные полужирным строки ошибочны. Первая представляет массив из 10 символов для хранения имени. Фактически требуется 12 символов: 10 для двух имен, один для пробела и один для \0 в конце. Вторая выделенная полужирным строка должна добавить 1 к значению, возвращенному функцией getNameLength (), чтобы учесть \0 в конце. Таким образом, код main () должен быть таким:
int main(int argc, char* argv[]) {
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;
1
Однако в определении члена класса getNameLength () присутствует и более серьезная проблема. Там не добавляется 1 для пробела между именем и фамилией, так что возвращенное значение всегда на один символ короче. Определение должно выглядеть следующим образом:
size_t Name::getNameLength() const {
#ifdef FUNCTION_TRACE
// Трассировка вызова функции
cout « "XnName::getNameLength() вызвана.";
#endif
return strlen (pFirstname) +strlen (pSurname) +1 ;
}
Но и это еще не все. Возможно, вы изначально заметили, что наш класс содержит серьезные ошибки, но давайте проведем тестирование, чтобы посмотреть, все ли из них устранены.
Тестирование расширенного класса
Судя по выводу, все работает, так что теперь можно добавить к классу Name определения перегруженных операций сравнения. Я полагаю, что это будет новый консольный проект Win32 под названием Ех10_02. Чтобы реализовать операции сравнения для объектов Name, вы можете использовать функции сравнения, объявленные в заголовочном файле <cstring>. Начните с операции “меньше чем”:
// Операция "меньше чем"
bool Name::operator<(const Names name) const {
int result = strcmp(pSurname, name.pSurname);
if(result < 0) return true;
if(result == 0 SS strcmp(pFirstname, name.pFirstname) < 0) return true;
else return false;
}
Теперь можно легко определить операцию > в терминах операции <:
// Операция "больше чем”
bool Name::operator>(const Names name) const {
return name > *this;
Для определения равенства имен вы используете функцию st гетр () — вновь из стандартной библиотеки:
// Операция равенства
bool Name::operator==(const Names name) const {
if(stremp(pSurname, name.pSurname) == 0 SS
stremp(pFirstname, name.pFirstname) == 0) return true;
else return false;
1
Теперь расширим тестовую программу. Вы можете создать массив объектом 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] ;
for (int i = 0 ; i<count ; i++) {
if(i%2)
first = 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];// Попробовать массив
/ / Инициализировать имена 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]; И Массив для хранения H первого имени
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;
}
Функция init () создает последовательную комбинацию из имен и фамилий, взятых из массива имен, для инициализации массива объектов Name. Имена повторяются после генерации 25-го, но здесь вам нужны только 10.
Поиск следующей ошибки
Если вы запустите программу под управлением отладчика, используя кнопку Start Debugging (Начать отладку) в панели инструментов Debug, она снова завершится сбоем. Будет отображено окно сообщений, показанное на рис. 10.18.
Рис. 10.18. Окно сообщения о сбое программы
Окно сообщения указывает на переполнение доступной памяти стека, и если вы выберете кнопку Break, то окно Call Stack покажет, что именно произошло не так. Вы увидите последовательные вызовы функции operator () >, то есть она вызывает сама себя. Если взглянуть на ее код, то причина станет ясной: банальная опечатка. Единственная строка в теле функции должна выглядеть так:
return name < *this;
Исправьте, перекомпилируйте и попробуйте снова. На этот раз она работает правильно, но, к сожалению, класс все еще содержит дефект. В нем происходит утечка памяти, которая не проявляется никакими симптомами, но в другом контексте они могут оказаться губительными. Утечки памяти трудно обнаружить непосредственно, но вы можете воспользоваться помощью 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;
Вам не придется непосредственно иметь дело с подробностями этого состояния свободного хранилища, поскольку вы используете функции, предоставляющие информацию в более читабельной форме. Существует довольно много функций, участвующих в трассировке операций со свободным хранилищем, но мы рассмотрим только пять наиболее интересных из них. Эти функции предлагают следующие возможности.
Запись состояния свободного хранилища в любой точке.
Определение разницы между двумя состояниями свободного хранилища.
Вывод информации о состоянии.
Вывод информации об объектах в свободном хранилище.
Обнаружение утечек памяти.
Ниже приведены объявления этих функций вместе с кратким описанием того, что они делают:
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 —CrtMemDumpAHObjectsSince(const _CrtMemState* state);
Функция сбрасывает информацию об объектах, размещенных в свободном хранилище с момента состояния свободного хранилища, указанного в аргументе; это состояние должно быть ранее записано вызовом _CrtMemCheckpoint (). Если этой функции передать null, она сбрасывает информацию обо всех объектах, размещенных с момента запуска вашей программы.
int _CrtDumpMemoryLeaks();
Это функция, которая понадобится вам для данного примера, поскольку проверяет утечки памяти и сбрасывает информацию о любой обнаруженной утечке. Вы можете вызвать эту функцию в любое время, но очень удобный механизм может вызывать эту функцию автоматически по завершении вашей программы. Если вы включите этот механизм, то получите автоматический детектор любых утечек памяти, произошедших за время выполнения программы, так что давайте посмотрим, как это можно сделать.
Управление отладочными операциями свободного хранилища
Вы можете управлять отладочными операциями свободного хранилища, устанавливая флаг crtDbgFlag типа int. Этот флаг включает в себя пять отдельных управляющих бит, в том числе один для включения автоматической проверки утечек памяти. Вы специфицируете эти управляющие биты, используя идентификаторы, перечисленные в табл. 10.2.
Таблица 10.2. Управляющие биты флага _crtDbgFlag
Управляющий бит
Описание
_CRTDBG_ALLOC_MEM_DF
Когда этот бит установлен, включается отладка, так что можно отслеживать состояние свободного хранилища.
-CRTDBG_DELAY_FREE_MEM_DF
Когда этот бит установлен, он предотвращает освобождение памяти операцией delete, так что вы можете определить, что случится при условии нехватки памяти.
-CRTDBG_CHECK-ALWAYS_DF
Когда этот бит установлен, включается автоматический вызов функции _CrtCheckMemory () при каждой операции new и delete. Эта функция проверяет целостность свободного хранилища, проверяя, например, что блоки не были перезаписаны сохранением значений, с выходом за пределы массива. Если обнаруживается любой дефект, выводится отчет. Замедляет выполнение, но позволяет быстро перехватывать ошибки.
_CRTDBG_CHECK_CRT_DF
Когда этот бит установлен, память, использованная внутри библиотеки исполняющей системы, отслеживается в операциях отладки.
-CRTDBG_LEAK_CHECK_DF
Заставляет выполнять проверку утечек памяти при завершении программы, автоматически вызывая _CrtDumpMemoryLeaks (). Вы получаете вывод только в том случае, если ваша программа не освобождает всю память, которая была выделена.
По умолчанию бит _CRTDBG_ALLOC_MEM_DF установлен, а все остальные — нет. Для включения и отключения комбинаций этих бит вы должны использовать битовые операции. Чтобы установить флаг __crtDbgFlag, вы передаете флаг типа int функции _CrtDbgFlag (), которая реализует необходимую комбинацию индикаторов. Она активизирует переданный вами флаг и возвращает предыдущее состояние _CrtDbgFlag. Один способ установки нужных индикаторов заключается в предварительном получении текущего состояния флага __crtDbgFlag. Это делается вызовом функции —CrtSetDbgFlag () с аргументом _CRTDBG_REPORT__FLAG, как показано ниже:
int flag = _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG); // Получить текущий флаг
Затем вы можете установить или снять индикаторы, комбинируя идентификаторы индивидуальных индикаторов с этим флагом посредством битовых операций. Для включения индикатора выполняется операция логического ИЛИ с флагом. Например, чтобы включить автоматическую проверку утечек памяти в flag, можно записать так:
flag 1= _CRTDBG_LEAK_CHECK_DF;
Чтобы отключить индикатор, нужно воспользоваться операцией логического И для обращенного значения идентификатора и флага. Например, для отключения
трассировки памяти, используемой внутри библиотеки, вы можете записать следующий код:
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.
Таблица 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, которая позволит использовать средства отладки свободного хранилища, в общем, и обнаружение утечек памяти, в частности.
int main(int argc, char* argv[]) f
// Включить отладку свободного хранилища и проверку утечек памяти _CrtSetDbgFlag( __CRTDBG__LEAK_CHECK__DF|_CRTDBG_ALLOC_MEM_DF ) ;
// Направить предупреждения в stdout
_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()+!]; 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
После этого перекомпилируйте пример и запустите его снова.
Описание полученных результатов
Код работает так, как и можно было ожидать. Вы получаете отчет о том, что ваша программа действительно имеет утечки памяти, и в конце работы программы получаете список объектов в свободном хранилище. Вывод, сгенерированный средством отладки свободного хранилища, начинается так:
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
{14 3} нормальный блок по адресу 0x00355F08, 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 0x00355E90, 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 0x003559A0, 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: CHorton > 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 {
// Конструктор по умолчанию pSecond); // Конструктор// Конструктор копирования// Деструктор// Получить полное имя// Получить длину полного имени// Конструктор по умолчанию pSecond); // Конструктор// Конструктор копирования// Деструктор// Получить полное имя// Получить длину полного имениpublic:
// Конструктор по умолчанию pSecond); // Конструктор
// Конструктор копирования
// Деструктор
// Получить полное имя
// Получить длину полного имени
// Конструктор по умолчанию pSecond); // Конструктор
// Конструктор копирования
// Деструктор
// Получить полное имя
// Получить длину полного имени
Name();
Name(const char* pFirst, const char* Name (const Names rName) ;
~Name();
char* getName(char* pName) const;
size_t getNameLength () const;
II Операции сравнения имен
bool operator<(const Names name) const;
bool operator==(const Names name) const;
bool operator>(const Names name) const;
Names operator=(const Names rName) ;// Операция присваивания
private:
char* pFirstname;
char* pSurname;
};
Конструктор копирования можно определить так:
Name:: Name(const Names 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;
}
В операции присваивания вы должны сначала выполнить обычную проверку на идентичность левой и правой частей:
Names Name::operator=(const Names rName) {
if (this == SrName)// Если 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];
pSurname = new char[l];
pFirstname[0] = pSurname[0] = ’\0’; // Записать нулевой символ
}
Если вы добавите в main () оператор для освобождения памяти, которая там была выделена динамически, то программа должна будет выполняться без каких-либо сообщений об утечках памяти. В main () потребуется добавить следующий оператор в конце внутреннего цикла, управляемого j:
delete [] jName;
Кроме того, необходимо также добавить следующий оператор в конец внешнего цикла, управляемого i:
delete[] iName;
И, наконец, следует освободить память, выделенную pName после всех циклов в main ():
delete[] pName;
Отладка программ C++/CLI
GeneralGeneralПри программировании на C++/CLI жизнь намного легче. Никаких сложностей, связанных с поврежденными указателями или утечками памяти, в программах CLR не возникает, так что это существенно упрощает задачу отладки по сравнению с “родным” C++. Вы устанавливаете точки прерывания и трассировки в программах CLR точно так же, как делаете это в коде на “родном” C++. Существует специфическая для кода C++/CLI опция, предотвращающая вход отладчика в библиотечный код. Если вы выберете пункт меню Tools^ Options (Сервис^Параметры) при открытом отладчике и выберете набор опций General (Общие) в группе DebGgging (Отладка), то диалоговое окно будет выглядеть, как показано на рис. 10.19.
General
General
У Environment Ferforma псе Тсс з+ Projects and 5olutions+ Source Controlit Texteditor Datebase ToolsВ DebuggingGeneralEdit and continueJust-m-TimeNativeSymbolsDevice Tools+■ HTML Designer+■ MLcrosnft; Office Keyboard Settings4- Test Tools* Windows Forms DesignerУ Environment Ferforma псе Тсс з+ Projects and 5olutions+ Source Controlit Texteditor Datebase ToolsВ DebuggingGeneralEdit and continueJust-m-TimeNativeSymbolsDevice Tools+■ HTML Designer+■ MLcrosnft; Office Keyboard Settings4- Test Tools* Windows Forms DesignerОрт г
У Environment Ferforma псе Тсс з
+ Projects and 5olutions
+ Source Control
it Texteditor Datebase Tools
В Debugging
General
Edit and continue
Just-m-Time
Native
Symbols
Device Tools
+■ HTML Designer
+■ MLcrosnft; Office Keyboard Settings
4- Test Tools
* Windows Forms Designer
У Environment Ferforma псе Тсс з
+ Projects and 5olutions
+ Source Control
it Texteditor Datebase Tools
В Debugging
General
Edit and continue
Just-m-Time
Native
Symbols
Device Tools
+■ HTML Designer
+■ MLcrosnft; Office Keyboard Settings
4- Test Tools
* Windows Forms Designer
П Asb before deleting all bro,крайня
Г-1 Вгеак all processes when one process breaks
□ Егеак when except! ms a oes AppDomain or rr.a'.age'V'nahve boon da
П Enable -dd ess-level debugging
□ Show disassembly if source is not available
0 Enable breakpoint filters
П Enable the exception assistant
■1exceptions
Enable Just My Code J jraged only)
Й show all "mberefor toPuser objects In variables windows ;vi: 0 Warn if no user code on launch
Г 1 Enable property eval nation and other implied: function calls 0 Call ToStringQ on objects m vandhles windows (C# only) Enable source server support
■ r। . । *-я- <■ 'ie -< j ' г
caitelcaitel
caitel
caitel
□ Highlight entire source line for breakpoints and current statement Feajire source ileste exactly match the anginal ersion
I Berti rent ail riutnut Windriw text Гл th^ ImmfiriiatP Window
Puc. 10.19. Опции General (Общие) группы Debugging (Отладка)
Отметка выделенной на рис. 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)
Пишет message в место назначение вывода.
Debug::Write(StringA message, StringA category)
Пишет categoryname, за которым следует message, в место назначения вывода. Имя категории используется для организации вывода.
Debug::Write(ObjectA value)
Пишет строку, возвращенную value->ToString (), в место назначения.
Debug::Write(ObjectA value, StringA category)
Пишет categoryname, за которым следует строка, возвращенная value->ToString(), в место назначения.
Writelf () и WriteLinelf () — условные версии функций Write () и WriteLine () в классе Debug; они перечислены в табл. 10.7.
Как видите, функции Writelf () и WriteLinelf () имеют дополнительный параметр типа bool в начале списка параметров соответствующих функций Write () и WriteLine (), и он определяет, должен ли осуществляться вывод.
Вы можете также выполнять вывод, используя функцию Debug: : Print (), которая существует в двух перегруженных версиях (табл. 10.8).
Таблица 10.7. Варианты функции Debug::Writelf ()
Функция
Описание
Debug::Writelf(bool condition, StringA message)
Пишет message в место назначения вывода, если condition равно true; иначе никакого вывода не генерирует.
Debug::Writelf(bool condition,
StringA message,
StringA category)
Пишет categoryname, за которым следует message, в место назначения вывода, если condition равно true; иначе никакого вывода не генерирует.
Debug::Writelf(bool condition, ObjectA value)
Пишет строку, возвращенную value->ToString (), в место назначения, если condition равно true; иначе никакого вывода не генерирует.
Debug::Writelf(bool condition,
ObjectA value,
StringA category)
Пишет categoryname, за которым следует строка, возвращенная value->ToString (), в место назначения, если condition равно true; иначе никакого вывода не генерирует.
Таблица 10.8. Варианты функции Debug::Print ()
Функция
Описание
Print(StringA message)
Пишет message в место назначение вывода, за которым следует символ новой строки.
Print(StringA format, ...array<ObjectA>A args)
Работает аналогично форматированному выводу функции Console: :WriteLine (). Форматная строка определяет, как будут располагаться в выводе следующие за ней аргументы.
Установка места назначения вывода
По умолчанию выходные сообщения отправляются в окно вывода IDE-среды, но вы можете изменить это, используя слушатель (listener). Слушатель — это объект, который направляет вывод отладки и трассировки в одно или множество мест назначения. Вот как можно создать слушатель и направить отладочный вывод в стандартный выходной поток:
TextWriterTraceListenerA listener = gcnew TextWriterTraceListener ( Console::Out);
Debug::Listeners->Add(listener);
Первый оператор создает объект TextWriterTraceListener, который направляет вывод в стандартный выходной поток, который возвращает статическое свойство Out класса Console. (Свойства In и Error класса Console возвращают, соответственно, стандартный входной поток и стандартный поток ошибок.) Свойство Listeners класса Debug возвращает коллекцию слушателей отладочного вывода, так что этот оператор добавляет объект-слушатель в коллекцию. Вы можете добавить и другие слушатели, которые дополнительно направят вывод еще куда-нибудь (возможно, в файл).
Отступы вывода
Вы можете управлять отступами при выводе сообщений отладки и трассировки. Это, в частности, удобно в ситуациях, когда функции вызываются на разной глубине. Добавляя отступ в начале функции, и удаляя его в ее конце, можно легче идентифицировать отладочный или трассирующий вывод, и вы сразу можете видеть глубину вызова функции по величине отступа перед сообщением.
Чтобы увеличить текущий уровень отступа на единицу (по умолчанию одна единица отступа — это четыре пробела), вызывается статическая функция Indent () класса Debug:
Debug::Indent ();// Увеличить уровень отступа на 1
Чтобы уменьшить текущий уровень отступа, вызывается функция Un indent ():
Debug::Unindent();// Уменьшить уровень отступа на 1
Текущий уровень отступа сохраняется в свойстве IndentLevel класса Debug, так что с его помощью вы можете как получить, так и установить текущий уровень отступа, например:
Debug::IndentLevel = 2*Debug::IndentLevel;
Этот оператор удваивает текущий уровень отступа для последующего отладочного вывода.
Количество пробелов, приходящихся на единицу отступа, записываются в статическом свойстве Indentsize класса Debug. Вы можете получить текущее значение единицы отступа и изменить его. Например:
Console::WriteLine(Ь”Текущая единица отступа = {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()”);
// Еще код...
}
// Остальная часть класса.. .
Здесь показан объект error — статический член класса MyClass. Первый аргумент конструктора BooleanSwitch — это отображаемое имя переключателя, используемое для инициализации свойства DisplayName, а второй аргумент устанавливает значение свойства Description переключателя. Имеется и другой конструктор, принимающий третий аргумент типа String^, который устанавливает свойство Value переключателя.
Свойство Enabled класса BooleanSwitch имеет тип bool и по умолчанию равно false. Чтобы установить его в true, вы устанавливаете значение свойства как обычно:
errors->Enabled = true;
Функция Dolt () класса MyClass выводит отладочное сообщение об ошибке, только когда переключатель error включен.
Ссылочный класс Trace Switch имеет два конструктора, принимающих такие же параметры, как и конструкторы класса BooleanSwitch. Вы можете создать объект TraceSwitch следующим образом:
TraceSwitchA traceCtrl =
gcnew TraceSwitch(Ь’’Обновление", L"Трассирует операции обновления”);
Первый аргумент конструктора устанавливает свойство DisplayName, а второй — значение свойства Description.
Свойство Level объекта TraceSwitch имеет тип класса перечисления TraceLevel, и вы можете устанавливать это свойство для управления выводом трассировки в любое из значений, перечисленных в табл. 10.9.
Таблица 10.9. Перечисление TraceLevel
Значение
Описание
TraceLevel::Off
Никакого трассирующего вывода.
TraceLevel::Info
Выводить информацию, предупреждения и сообщения об ошибках.
TraceLevel::Warning
Выводить предупреждения и сообщения об ошибках.
TraceLevel::Error
Выводить сообщения об ошибках.
TraceLevel::Verbose
Выводить все сообщения.
Значение, которое вы устанавливаете, определяет генерируемый вывод. Чтобы получить все сообщения, вы устанавливаете свойство следующим образом:
traceCtrl->Level = TraceLevel::Verbose;
Должно ли конкретное сообщение быть выдано, определяется вашим кодом трассировки и отладки через проверку состояния одного из четырех свойств типа bool объекта TraceSwitch (табл. 10.10).
Таблица 10.10. Свойства объекта TraceSwitch для определения необходимости выдачи сообщений
Свойство
Описание
TraceVerbose
Возвращает значение true, когда все сообщения должны быть выведены.
Traceinfo
Возвращает значение true, когда должны быть выданы информационные сообщения.
TraceWarning
Возвращает значение true, когда должны быть выданы предупреждающие сообщения.
TraceError
Возвращает значение true, когда должны быть выданы сообщения об ошибках.
По значениям этих свойств вы можете видеть, что установка свойства Level также изменяет состояния этих свойств. Например, если вы устанавливаете свойство Level равным TraceLevel: : Warning, то TraceWarning и TraceError устанавливаются в true, a TraceVerbose и Traceinfo — в false.
Чтобы решить, нужно ли выводить определенное сообщение, вы проверяете соответствующее свойство:
if(traceCtrl->TraceWarning)
Debug::WriteLine(L”3to ваше последнее предупреждение!");
Сообщение выводится, только когда свойство TraceWarning для traceCtrl установлено в true.
Утверждения
Классы Debug и Trace имеют статическую функцию Assert (), которая предоставляет возможности, аналогичные функции assert () в “родном” C++. Первый аргумент функции Debug: :Assert () — булевское значение или выражение, которое заставляет программу реагировать, когда значение аргумента false. При этом отображается стек вызовов в диалоговом окне, как показано на рис. 10.20.
Assertion Failed: Abo t=Quit, Retry=Debug, lgnore=Co tinue
8
at TraceTest.FunCQ 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.FunБ ) d:\beginning visual c++
2005\exa mp I e s\exl 0_03\exl 0_03\exl 0_03. cp p (39)
at TraceTest.Fun^Q d:\beginning visual c++
2005\exa m p I e s\exl 0_03\exl 0_03\exl0_0 3. cp p (28)
at <Module>.main(Stririg[] args) d:\beginning visual c++
2005\exam p I e s\exl 0_03\exl0_03 \exl 0_03. 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 (313)
AbortAbortRetryRetryIgnoi eIgnoi e
Abort
Abort
Retry
Retry
Ignoi e
Ignoi e
Puc. 10.20. Диалоговое окно, отображаемое при нарушении утверждения
На рис. 10.20 показано утверждение, которое генерируется следующим примером. Когда программа реагирует на утверждение, в диалоговом окне отображается стек вызовов с номерами строк кода и именами функций, выполнявшихся в этой точке. Здесь мы видим выполнение пяти функций, включая main ().
После возникновения утверждения вам предлагаются три варианта действий. Щелчок на кнопке Abort (Прервать) завершает программу немедленно, кнопка Ignore (Игнорировать) позволяет продолжить выполнение программы, а щелчок на кнопке Retry (Повторить) дает возможность перейти к выполнению программы в режиме отладки.
Доступны три перегруженных версии функции Assert О , перечисленные в табл. 10.11.
Таблица 10.11. Перегруженные версии функции Assert ()
Функция
Описание
Debug::Assert(bool condition)
Когда condition равно false, отображается диалог, показывающий стек вызовов в этой точке.
Debug::Assert(bool condition, StringA message)
То же, что и предыдущий вариант, но над информацией о стеке отображается message.
Debug::Assert(bool condition,
StringA message,
StringA details)
То же, что и предыдущая версия, но в диалоге дополнительно отображается details.
Этот механизм легче всего понять, увидев его в действии, так что соберем вместе пример, который продемонстрирует код отладки и трассировки в действии.
практическое занятие | Использование классов Debug и Trace
Этот пример — просто упражнение по использованию некоторых отладочных и трассирующих функций, описанных выше. Создайте консольный проект 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(L"Завершение FunA");
Trace::Unindent();
}
void FunB() {
Trace::Indent();
Trace::WriteLine(Е"Старт FunB");
if(sw->TraceWarning)
Debug::WriteLine^"Предупреждение в FunB...");
FunCO ;
Trace::WriteLine(П"3авершение FunB");
Trace::Unindent();
}
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
Уровень трассировки 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:
for each(TraceLevel level in levels)
{
obj->Level = level; M Установить уровень сообщений
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.
Основы программирования для Windows
При работе в Visual C++ 2005 вам доступны три основных способа создания интерактивных приложений Windows.
Использование 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-программы. Однако я все равно перечислю их — просто, чтобы быть уверенным, что у нас имеется общее понимание значения используемых терминов. Лучший способ понять, чем могут быть элементы окна — рассмотреть один из них. Аннотированная версия окна, отображенного примером, который вы видели в главе 1, показана на рис. 11.1.
Фиксатор размера
Рис. 11.1. Стандартные окна ЖпДо^упрограммы.
Этот пример в действительности генерирует два окна. Большее окно, с панелями меню и инструментов, является главным, или родительским окном, а меньшее окно — дочерним окном по отношению к родительскому. Хотя дочернее окно может быть закрыто без закрытия родительского двойным щелчком на пиктограмме панели заголовка, находящейся в его левом верхнем углу, закрытие родительского окна также автоматически закрывает дочернее окно. Это связано с тем, что дочернее окно принадлежит и зависит от родительского. В общем случае одно родительское окно может иметь множество дочерних окон, как вы вскоре увидите.
Наиболее фундаментальными частями типичного окна являются рамка, панель заголовка, отображающая имя, которое вы присвоили окну, пиктограмма панели заголовка, находящаяся в левой части панели заголовка, и клиентская область, пред-
ставляющая собой область в центре окна, не занятую панелью заголовка или рамкой. Все это вы получаете бесплатно в Windows-программе. Как вы увидите, все, что вам нужно сделать — это предоставить некоторый текст для панели заголовка.
Рамка определяет границы окна и может быть фиксированной либо изменяемого размера. Если рамка с изменяемым размером, вы можете двигать ее, изменяя размеры окна. Окно также может предоставлять фиксатор размера, который можно использовать для изменения размера окна при установке его соотношения сторон — то есть отношения ширины к высоте. Когда вы определяете окно, то можете модифицировать поведение рамки и ее внешний вид по своему желанию. Большинство окон в правом верхнем углу также имеют кнопки разворачивания, сворачивания и закрытия. Они позволяют распахнуть окно до полного размера экрана, свернуть его в пиктограмму или полностью закрыть.
Когда вы щелкаете на пиктограмме панели заголовка левой кнопкой мыши, отображается стандартное меню для изменения или закрытия окна, которое называется системным или управляющим меню. Системное меню также появляется, когда выполняется щелчок правой кнопкой мыши на панели заголовка окна. Хотя это и не обязательно, всегда неплохо включать пиктограмму панели заголовка в любое главное окно, создаваемое вашей программой. Включение пиктограммы панели заголовка предоставляет очень удобную возможность закрытия программы, когда что-то работает не так во время отладки.
Клиентская область — это часть окна, в которую программа обычно выводит текст или графику. Вы обращаетесь с клиентской областью для этих целей точно так же, как с двором, который был показан на рис. 7.1 в главе 7. Верхний левый угол клиентской области имеет координаты (0, 0), причем х растет слева направо, а у — сверху вниз.
Панель меню в окне не обязательна, но предоставляет, вероятно, наиболее удобный способ управления приложением. Каждое меню в панели отображает выпадающий список элементов меню, когда вы на нем щелкаете. Содержимое меню и физическое расположение многих объектов, отображаемых в окне, таких как пиктограммы в панели инструментов, находящейся ниже, курсор и многие другие, определяются ресурсным файлом. Мы еще обратимся к ресурсным файлам, когда приступим к написанию более сложных Windows-программ.
Панель инструментов предлагает набор пиктограмм, которые обычно действуют как альтернатива наиболее часто используемым опциям меню. Поскольку они обеспечивают прямой путь обращения к функциям, которые ими вызывается, их применение часто облегчает и ускоряет использование программы.
Прежде чем двигаться дальше, я должен предупредить о терминологии, которой вы должны придерживаться. Пользователи обычно склонны воспринимать окно как вещь, которая появляется на экране с рамкой, ограничивающей его, и конечно же, так оно и есть, но это только один из возможных видов окон. Тем не менее, окно Windows — это общий термин, описывающий весь диапазон видимых сущностей. Фактически почти все, что отображается, является окном, например, диалоговое окно — это окно, и каждая его кнопка — тоже окно. Обычно я буду использовать терминологию, называющую объекты тем, чем они являются — кнопки, диалоги и тому подобное, однако вы должны иметь в виду, что все они также являются окнами, поскольку с каждым из них вы можете делать то, что и с обычным окном. Например, вы вполне можете рисовать на поверхности кнопки.
Windows-программы и операционная система
Когда вы пишете код для Windows, ваша программа подчиняется операционной системе, и Windows управляет ею. Ваша программа не должна напрямую взаимодействовать с оборудованием, и все коммуникации с внешним миром должны проходить через Windows. Когда вы используете Windows-программу, то первоначально взаимодействуете с Windows, а она уже взаимодействует с вашей прикладной программой. Ваша Windows-программа — это “хвост”, а сама Windows — “собака”, и ваша программа “виляет хвостом” только тогда, когда Windows приказывает ей делать это.
Существует масса причин подобного положения вещей. Первая и важнейшая причина заключается в том, что ваша программа почти всегда разделяет компьютер с другими программами, которые могут выполняться в одно и то же время. Windows постоянно должна сохранять контроль над разделяемыми ресурсами машины. Если одному приложению будет разрешено захватить контроль над всей средой Windows, это неизбежно потребует его значительного усложнения, из-за необходимости учитывать работу других программ, и информация, предназначенная для других программ, может быть потеряна. Вторая причина для сохранения контроля за Windows состоит в том, что Windows заключает в себе стандартный пользовательский интерфейс, и должна отвечать за соблюдение стандарта. Вы можете отображать информацию на экране, только используя инструменты, предусмотренные в Windows, и только тогда, когда это разрешено Windows.
Программирование, управляемое событиями
В главе 1 уже было показано, что Windows-программы являются управляемыми событиями, поэтому такая программа, по сути, постоянно ожидает, когда что-нибудь произойдет. Значительная часть кода, необходимого Windows-приложению, предназначена для обработки событий, вызванных внешними действиями пользователя, но действия, которые не ассоциированные напрямую с вашим приложением, могут все же потребовать выполнения некоторого фрагмента кода вашей программы. Например, если пользователь перетаскивает окно другого приложения, которое работает параллельно с вашей программой, и это действие открывает часть клиентской области окна, выделенной вашему приложению, ваше приложение должно будет перерисовать часть вашего окна.
Сообщения Windows
События в Windows-приложении представляют собой происшествия, подобные щелчку кнопкой мыши или нажатию клавиши либо истечению определенного периода времени. Операционная система Windows записывает каждое событие в сообщение и помещает его в очередь сообщении программы, которой это сообщение предназначено. Таким образом, сообщение Windows — это просто запись данных, имеющих отношение к событию, а очередь сообщений приложения — это просто последовательность таких сообщений, ожидающая обработки приложением. Отправляя сообщения, Windows может известить вашу программу о том, что нечто должно быть сделано, или о том, что некоторая информация стала доступной, или же о том, что произошло событие вроде щелчка кнопкой мыши. Если ваша программа правильно организована, она соответствующим образом реагирует на сообщение. Может существовать множество видов сообщений, и они могут поступать очень часто — по много раз в секунду — например, когда передвигается мышка.
Windows-программа должна содержать функцию, специально предназначенную для обработки таких сообщений. Эта функция часто называется WndProc () или WindowProcO, хотя она не обязана иметь какое-то определенное имя, потому что 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
В Windows определено значительное количество типов данных, используемых для спецификации типов параметров и типов возврата функций в Windows API. Эти специфичные для Windows типы также распространяются на функции, определенные в МЕС. Каждый из этих типов Windows отображается на некоторый тип C++, но поскольку отображение между типами Windows и типами C++ может изменяться, вы всегда должны применять типы Windows, где это возможно. Например, в прошлом тип Windows WORD был определен в одной версии Windows как unsigned short, а в другой версии — как unsigned int. На 16-разрядных машинах эти типы эквивалентны, но на 32-разрядных машинах они определенно отличаются, так что всякий, кто использует тип C++ вместо типа Windows, рискует столкнуться с проблемами.
Вы можете найти полный список типов данных Windows в документации, но в табл. 11.1 перечислено несколько наиболее распространенных типов, с которыми вам придется столкнуться.
Таблица 11.1.
Наиболее распространенные типы данных Windows
Тип
Описание
BOOL ИЛИ BOOLEAN
Булевская переменная, которая может принимать значения true или false.
Обратите внимание, что это не то же самое, что тип 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 представлены некоторые примеры префиксов, с которыми вы можете столкнуться.
Таблица 11.2. Примеры префиксов, указывающих вид значений в переменных
Префикс
Значение
ь
Логическая переменная типа bool — эквивалент int.
by
Тип unsigned char или byte.
с
Тип char.
dw
Тип DWORD, то есть unsigned long.
fn
Функция.
h
Дескриптор, используется для ссылки на что-либо.
i
Тип int.
1
Тип long.
Ip
Длинный (long) указатель.
n
Тип int.
p
Указатель.
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-программы
Для минимальной 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.
Рис. 11.2. Взаимодействие между функциями WinMain () и WindowProc ()
Функция WinMain ()
Функция 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, даже если запущен другой экземпляр приложения.
Следующий аргумент, 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++: МЕС определяет класс, представляющий окно, но это совсем другое. Вы должны создать переменную типа WNDCLASSEX и присвоить значения каждому из ее членов (подобно заполнению формы). После того, как вы заполните значениями все переменные, эту структуру можно передать Windows (через функцию, которую вы увидите ниже), чтобы зарегистрировать класс окна. Когда это сделано, то всякий раз, когда вы хотите создать окно этого класса, вы можете указать Windows, чтобы она искала класс, который уже был зарегистрирован.
Определение структуры WNDCLASSEX выглядит следующим образом:
struct WNDCLASS {
UINT cbSize;// Размер этого объекта в байтах
UINT style;И Стиль окна
WNDPROC IpfnWndProc; // Указатель на функцию обработки сообщений
int cbClsExtra;int cbWndExtra;HINSTANCE hlnstance;HICON hlcon;HCURSOR hCursor;HBRUSH hbrBackground;LPCTSTR IpszMenuName;LPCTSTR IpszClassName;HICON hlconSm;int cbClsExtra;int cbWndExtra;HINSTANCE hlnstance;HICON hlcon;HCURSOR hCursor;HBRUSH hbrBackground;LPCTSTR IpszMenuName;LPCTSTR IpszClassName;HICON hlconSm;H Дополнительный байт после класса окна
int cbClsExtra;
int cbWndExtra;
HINSTANCE hlnstance;
HICON hlcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR IpszMenuName;
LPCTSTR IpszClassName;
HICON hlconSm;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hlnstance;
HICON hlcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR IpszMenuName;
LPCTSTR IpszClassName;
HICON hlconSm;
// Дополнительные байты после экземпляра окна
/ / Дескриптор экземпляра приложения
/ / Пиктограмма приложения
// Курсор окна
// Кисть, определяющая цвет фона
И Указатель на имя ресурса меню
// Указатель на имя класса
// Малая пиктограмма, связанная с окном
Вы конструируете объект типа 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 дополнительное пространство для ваших собственных целей. Примером может служить ситуация, когда вы хотите ассоциировать дополнительные данные с каждым экземпляром окна, чтобы ассистировать в обработке сообщений для каждо-
го экземпляра окна. Обычно вам не потребуется выделять это дополнительное пространство, и в этом случае вы просто устанавливаете значение членов cbClsExtra и cbWndExtra равными нулю.
Член hlnstance содержит дескриптор текущего экземпляра приложения, так что вы должны установить его равным значению hlnstance, переданному Windows функции WinMain().
Члены hl con, hCursor и hbrBackground представляют собой дескрипторы, которые, в свою очередь, определяют пиктограмму, представляющую приложение в свернутом виде, курсор мыши, используемый окном, и цвет фона клиентской области окна. (Как упоминалось ранее, дескриптор — это 32-разрядное целое число, используемое в качестве идентификатора, представляющего что-либо.) Они устанавливаются с использованием функций Windows API. Например:
Windowclass.hlcon = Loadicon(О, 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 szAppName[] = "OFWin”; // Определить имя класса окна WindowClass.IpszClassName = szAppName; // Установить имя класса
Последний член — hlconSm, который идентифицирует маленькую пиктограмму, ассоциированную с классом окна. Если вы специфицируете его как null, то Windows будет искать маленькую пиктограмму, связанную с членом hlcon, и использует ее.
Фактически структура WNDCLASSEX заменяет другую структуру, WNDCLASS, которая была использована для тех же целей. Старая структура не включала член cbSize, хранящий размер структуры в байтах, и член hlconSm.
Создание окна программы
После того, как всем членам вашей структуры WNDCLASSEX присвоены нужные значения, следующий шаг состоит в том, чтобы сообщить об этом Windows. Вы делаете это с использованием функции Windows API RegisterClassEx (). Если ваша структура называется WindowClass, то оператор, с помощью которого это делается, должен быть таким:
RegisterClassEx(&WindowClass);
Просто, не правда ли? Адрес структуры передается в функцию, a Windows извлекает и сохраняет все значения, установленные в членах структуры. Этот процесс называется регистрацией класса окна. Просто чтобы напомнить: термин класс используется здесь в смысле классификации, и никак не связан с классом C++, так что не путайте их. Каждый экземпляр приложения должен обеспечить регистрацию своего класса окна. Если вы используете устаревшую структуру WNDCLASS, упомянутую выше, то должны будете применить здесь другую функцию — Registerclass ().
После того, как Windows узнает характеристики требуемого вам окна и функцию, предназначенную для обработки его сообщений, вы можете двинуться дальше и создать его. Для этого используется функция CreateWindow (). Определенный вами класс окна описывает широкий диапазон характеристик окна, а дополнительные аргументы функции CreateWindow () добавляют к ним еще некоторые. Поскольку приложение в общем случае может состоять из нескольких окон, функция CreateWindow () возвращает дескриптор созданного окна, который вы можете сохранить, чтобы позднее обращаться к этому конкретному окну. Существует много вызовов API, которые требуют специфицировать дескриптор окна в качестве параметра, если вы хотите использовать их. Рассмотрим пример типичного использования функции CreateWindow ():
HWND hWnd; // Дескриптор окна
hWnd = CreateWindow(
szAppName, // "A Basic Window the Hard Way", И WS_OVERLAPPEDWINDOW, // CW_USEDEFAULT, H CW_USEDEFAULT, // CW_USEDEFAULT, // CW USEDEFAULT, H 0, // 0, // hlnstance, II 0 //szAppName, // "A Basic Window the Hard Way", И WS_OVERLAPPEDWINDOW, // CW_USEDEFAULT, H CW_USEDEFAULT, // CW_USEDEFAULT, // CW USEDEFAULT, H 0, // 0, // hlnstance, II 0 //Имя класса окна Заголовок окна Стиль окна - перекрываемое Позиция на экране по умолчанию левого верхнего угла как х,у. . . Размер окна по умолчанию - ширина... . . .и высотаНет родительского окна Нет меню Дескриптор экземпляра программы Никаких данных для создания окнаИмя класса окна Заголовок окна Стиль окна - перекрываемое Позиция на экране по умолчанию левого верхнего угла как х,у. . . Размер окна по умолчанию - ширина... . . .и высотаНет родительского окна Нет меню Дескриптор экземпляра программы Никаких данных для создания окна
szAppName, // "A Basic Window the Hard Way", И WS_OVERLAPPEDWINDOW, // CW_USEDEFAULT, H CW_USEDEFAULT, // CW_USEDEFAULT, // CW USEDEFAULT, H 0, // 0, // hlnstance, II 0 //
szAppName, // "A Basic Window the Hard Way", И WS_OVERLAPPEDWINDOW, // CW_USEDEFAULT, H CW_USEDEFAULT, // CW_USEDEFAULT, // CW USEDEFAULT, H 0, // 0, // hlnstance, II 0 //
Имя класса окна Заголовок окна Стиль окна - перекрываемое Позиция на экране по умолчанию левого верхнего угла как х,у. . . Размер окна по умолчанию - ширина... . . .и высота
Нет родительского окна Нет меню Дескриптор экземпляра программы Никаких данных для создания окна
Имя класса окна Заголовок окна Стиль окна - перекрываемое Позиция на экране по умолчанию левого верхнего угла как х,у. . . Размер окна по умолчанию - ширина... . . .и высота
Нет родительского окна Нет меню Дескриптор экземпляра программы Никаких данных для создания окна
);
Переменная hWnd типа HWND — это 32-разрадный целочисленный дескриптор окна. Вы используете эту переменную для записи значения, возвращенного функцией CreateWindow (), которое идентифицирует окно. Первый аргумент, который вы передаете в функцию — это имя класса. Оно используется Windows для идентификации структуры WNDCLASSEX, которую вы передали до этого при вызове RegisterClassEx (), так что информация из этой структуры может быть использована в процессе создания окна.
Второй аргумент CreateWindow () определяет текст, который должен появиться в панели заголовка. Третий аргумент специфицирует стиль, который имеет окно после создания. Указанная здесь опция, WS_OVERLAPPEDWINDOW, в действительности комбинирует несколько опций. Она определяет окно как обладающее стиля-
ми WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX и WS_MAXIMIZEBOX. Это дает в результате перекрываемое окно, предназначенное для того, чтобы служить главным окном приложения, с панелью заголовка на толстой рамке, имеющей пиктограмму, системное меню, а также кнопки разворачивания и сворачивания. Окно, которое вы специфицируете, имеет толстую линию рамки, с помощью которой можно менять размеры окна.
Следующие четыре аргумента определяют положение и размер окна на экране. Первые два — это экранные координаты левого верхнего угла окна, а вторые два — ширина и высота окна. Значение CW_USEDEFAULT говорит о том, что вы хотите, чтобы Windows назначила значение окна по умолчанию. Это сообщает Windows о том, чтобы она располагала последовательно открываемые окна каскадом — последовательно друг за другом. CW_USEDEFAULT применяется только к окнам, специфицированным как WS_OVERLAPPED.
Следующее значение аргумента — ноль, говорящее о том, что создаваемое окно не является дочерним (то есть окном, зависящим от родительского окна). Если нужно создать дочернее окно, то в этом аргументе необходимо указать дескриптор родительского окна. Следующий аргумент также установлен в ноль, и это говорит о том, что никакого меню не требуется. Затем вы специфицируете дескриптор текущего экземпляра программы, который был передан данной программе от Windows. Последний аргумент при создании окна равен нулю, поскольку в данном примере вам нужно простое окно. Если вы захотите создать клиентское окно многодокументного интерфейса (MDI — multipledocument interface), то последний аргумент должен указывать на структуру, связанную с этим. Позднее из настоящей книги вы узнаете подробнее об MDI.
Обратите внимание, что Windows API также включает функцию CreateMndowEx (), которую вы используете для создания окон с расширенной информацией о стиле*
После вызова функции CreateWindow () окно существует, но пока еще не отображено на экране. Чтобы отобразить его, вы должны вызвать другую функцию Windows API:
Showwindow(hWnd, nCmdShow); H Отобразить окно
Здесь требуются только два аргумента* Первый идентифицирует окно и равен дескриптору, возвращенному функцией CreateWindow (). Второй имеет значение nCmdShow, переданное WinMain (), и идентифицирует способ отображения окна на экране.
Инициализация окна программы
После вызова функции ShowWindow () окно появляется на экране, но все еще не имеет содержимого, так что вы должны заставить программу нарисовать клиентскую область окна. Вы можете просто собрать вместе весь необходимый для этого код непосредственно в функции WinMain (), но этого будет недостаточно: в этом случае содержимое клиентского окна останется статичным, и вы не можете вывести в окно то, что необходимо, после чего забыть об этом. Любое действие пользователя, которое модифицирует окно каким-либо образом, подобное перетаскиванию границы или окна целиком, обычно требует перерисовки окна и его клиентской области.
Когда клиентская область должна быть перерисована по любой причине, Windows посылает определенное сообщение вашей программе, на которое ваша функция WindowProc () должна отреагировать, реконструируя клиентскую область окна.
Таким образом, лучший способ обеспечить перерисовку клиентской области “в первой инстанции” — это поместить код перерисовки клиентской области в функцию 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
/ / Трансляция сообщения/ / Диспетчеризация сообщения/ / Трансляция сообщения/ / Диспетчеризация сообщенияTranslateMessage(&msg);DispatchMessage(&msg);TranslateMessage(&msg);DispatchMessage(&msg);while(GetMessage(&msg, 0, 0, 0) == TRUE) H Получить сообщение {
/ / Трансляция сообщения
/ / Диспетчеризация сообщения
/ / Трансляция сообщения
/ / Диспетчеризация сообщения
TranslateMessage(&msg);
DispatchMessage(&msg);
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Обработка сообщения состоит из трех шагов.
GetMessage () — получает сообщение из очереди.
TranslateMessage () — выполняет все необходимые преобразования извлеченного сообщения.
DispatchMessage () — заставляет Windows вызвать функцию WindowProc () вашего приложения для обработки сообщения.
Операция GetMessage () важна, поскольку она вносит существенный вклад в способ одновременной работы Windows с множеством приложений. Рассмотрим ее более подробно.
struct MSG {HWND hwnd;UINT message;WPARAM wParam;LPARAM IParam;DWORD time;POINT pt;struct MSG {HWND hwnd;UINT message;WPARAM wParam;LPARAM IParam;DWORD time;POINT pt;Функция GetMessage () извлекает сообщение, помещенное в очередь окна приложения, и сохраняет информацию о сообщении в переменной msg, на которую указывает первый аргумент. Переменная msg, представляющая собой структуру типа MSG, содержит множество членов, которые вы здесь не используете. Для полноты картины рассмотрим определение этой структуры.
struct MSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM IParam;
DWORD time;
POINT pt;
struct MSG {
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM IParam;
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 (), то
не можете извлечь сообщение WM_QUIT, и ваша программа не сможет корректно завершиться.
Последние два аргумента GetMessage () — целые числа, содержащие минимальное и максимальное значения идентификаторов сообщений, которые вы хотите извлечь из очереди. Это позволяет извлекать сообщения выборочно. Обычно диапазоны указываются символическими константами. Использование WM_MOUSE FIRST и WM_MOUSBLAST в качестве этих двух аргументов, например, позволяет выбрать только сообщения мыши. Если оба аргумента равны нулю, как в данном примере, то извлекаются все сообщения.
Многозадачность
Если в очереди нет сообщений, функция GetMessage () не возвращает управление в вашу программу. Windows позволяет выполняться другим программам, и вы получите значение, возвращенное GetMessage () только тогда, когда сообщение появится в очереди. Этот механизм был основным в обеспечении возможности выполнения множества приложений в старых версиях Windows и назывался кооперативной многозадачностью, поскольку зависел от того, как конкурирующие приложения передавали управление процессору время от времени. После того, как ваша программа вызвала GetMessage (), если только для вашей программы нет сообщений, выполняется другое приложение, и ваша программа получает другую возможность сделать что-либо только после того, как другое приложение освободит процессор — возможно, вызовом GetMessage (), когда для него не будет сообщений, хотя это не единственная возможность.
В современных версиях Windows операционная система может прервать приложение после истечения периода времени и передать управление другому приложению. Этот механизм называется вытесняющей многозадачностью, поскольку приложение может быть прервано в любом событии. Однако при вытесняющей многозадачности вы все равно должны программировать цикл сообщений в WinMain (), используя 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); H Установить размер структуры
И Перерисовать окно при изменении размера WindowClass.style = CS_HREDRAW | CS_VREDRAW;
// Определить функцию - обработчик сообщений
WindowClass.IpfnWndProc = WindowProc;
Windowclass.cbClsExtra = 0; // Никаких дополнительных байт после структуры
Windowclass.cbWndExtra = 0; // класса окна в экземпляре окна
Windowclass.hlnstance = hlnstance; // Дескриптор экземпляра приложения
/ / Установить пиктограмму приложения по умолчанию WindowClass.hlcon = Loadicon(0, IDI APPLICATION);
II Установить стандартный курсор мыши в виде стрелочки WindowClass.hCursor = LoadCursor(0, IDC_ARROW);
/ / Установить серую кисть для рисования фона
WindowClass.hbrBackground =
static_cast<HBRUSH>(GetStockObject(GRAY_BRUSH));
WindowClass.IpszMenuName = 0;// Нет меню
WindowClass.IpszClassName = szAppName; // Установить имя класса
WindowClass.hlconSm = 0;// Маленькая пиктограмма по умолчанию
/ / Теперь зарегистрировать класс окна RegisterClassEx(&WindowClass);/ / Теперь можно создать окно HWnd = CreateWindow( szAppName,"A Basic Window the Hard Way", WS_OVERLAPPEDWINDOW, CW-USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT,0,0,hlnstance, 0) ;ShowWindow(hWnd, nCmdShow);UpdateWindow(hWnd);/ / Теперь зарегистрировать класс окна RegisterClassEx(&WindowClass);/ / Теперь можно создать окно HWnd = CreateWindow( szAppName,"A Basic Window the Hard Way", WS_OVERLAPPEDWINDOW, CW-USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT,0,0,hlnstance, 0) ;ShowWindow(hWnd, nCmdShow);UpdateWindow(hWnd);// имя класса окна// Заголовок окна/ / Стиль окна - перекрываемое// Позиция на экране по умолчанию // левого верхнего угла как х,у. . //Размер окна по умолчанию - ширина. // . . .и высота// Нет родительского окна// Нет меню// имя класса окна// Заголовок окна/ / Стиль окна - перекрываемое// Позиция на экране по умолчанию // левого верхнего угла как х,у. . //Размер окна по умолчанию - ширина. // . . .и высота// Нет родительского окна// Нет меню// Дескриптор экземпляра программы // Никаких данных для создания окна// Отобразить окно// Дескриптор экземпляра программы // Никаких данных для создания окна// Отобразить окно// Заставить перерисовать клиентскую область окна// Заставить перерисовать клиентскую область окна
/ / Теперь зарегистрировать класс окна RegisterClassEx(&WindowClass);
/ / Теперь можно создать окно HWnd = CreateWindow( szAppName,
"A Basic Window the Hard Way", WS_OVERLAPPEDWINDOW, CW-USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT,
0,
0,
hlnstance, 0
) ;
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
/ / Теперь зарегистрировать класс окна RegisterClassEx(&WindowClass);
/ / Теперь можно создать окно HWnd = CreateWindow( szAppName,
"A Basic Window the Hard Way", WS_OVERLAPPEDWINDOW, CW-USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW USEDEFAULT,
0,
0,
hlnstance, 0
) ;
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
// имя класса окна
// Заголовок окна
/ / Стиль окна - перекрываемое
// Позиция на экране по умолчанию // левого верхнего угла как х,у. . //Размер окна по умолчанию - ширина. // . . .и высота
// Нет родительского окна
// Нет меню
// имя класса окна
// Заголовок окна
/ / Стиль окна - перекрываемое
// Позиция на экране по умолчанию // левого верхнего угла как х,у. . //Размер окна по умолчанию - ширина. // . . .и высота
// Нет родительского окна
// Нет меню
// Дескриптор экземпляра программы // Никаких данных для создания окна
// Отобразить окно
// Дескриптор экземпляра программы // Никаких данных для создания окна
// Отобразить окно
// Заставить перерисовать клиентскую область окна
// Заставить перерисовать клиентскую область окна
// Цикл сообщений
while(GetMessage(&msg, 0, 0, 0) == TRUE) // Получить сообщение {
TranslateMessage(&msg);II Трансляция сообщения
DispatchMessage(&msg);H Диспетчеризация сообщения
} return static_cast<int>(msg.wParam); II Конец, возврат в Windows
Описание полученных результатов
После объявления переменных, которые вам понадобятся в функции, все члены структуры WindowClass инициализированы и окно зарегистрировано.
Следующий шаг — вызов функции CreateWindows О для создания данных, необходимых для физического появления окна, на основе переданных аргументов и данных, установленных в структуру WindowClass, которая была ранее передана Windows с использованием функции RegisterClassEx (). Вызов ShowWindow () заставляет отобразить окно в соответствии с режимом, специфицированным в nCmdShow, а функция UpdateWindow () сигнализирует, что должно быть генерировано сообщение для перерисовки клиентской области окна.
И в конце запускается цикл сообщений, который продолжает извлекать сообщения до тех пор, пока не получит сообщение 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
Идентификатор сообщения — 32-битное значение, указывающее тип сообщения. 32-битное значение, содержащее дополнительную информацию, зависящую от типа сообщения.
LPARAM IParam
32-битное значение, содержащее дополнительную информацию, зависящую от типа сообщения.
Окно, к которому относится входящее сообщение, идентифицируется первым аргументом — HWnd, передаваемым функции. В данном случае есть только одно окно, поэтому вы можете проигнорировать этот аргумент.
Сообщения идентифицируются значением 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:
/ / Код обработки отпускания левой кнопки мыши 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 о своем намерении и получить от Windows разрешение на это. Это делается вызовом функции Windows API BeginPaint (), которая должна быть вызвана только в ответ на сообщение WM_PAINT. Она используется следующим образом:
HDC hDC;// Дескриптор дисплейного контекста
PAINTSTRUCT PaintSt; // Структура, определяющая область для перерисовки hDC = BeginPaint(hWnd, &PaintSt); // Подготовиться к рисованию в окне
Тип HDC определяет то, что называется дисплейным контекстом, или в более широком смысле — контекстом устройства. Контекст устройства представляет связь между независимыми от устройств функциями 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,// Дескриптор контекста устройства
L"But, soft! What light through yonder window breaks?”,
-1,// Индикатор строки, ограниченной null
&aRect,// Прямоугольник, в котором выполняется рисование текста
DT_SINGLELINE|// Формат текста - одна строка
DT_CENTER|//- центрирование в строке
DT—VCENTER//- центрирование по высоте aRect
) ;
Первый аргумент функции DrawText () — ваше разрешение на рисование в окне; это дисплейный контекст hDC. Следующий аргумент — текстовая строка, которую нужно вывести. С тем же успехом вы могли бы определить ее в переменной и передать
указатель на текст в качестве второго аргумента при вызове функции. Следующий аргумент, имеющий значение -1, указывает на то, что ваша строка ограничена символом null. Иначе здесь нужно было бы указать количество символов строки. Четвертый аргумент — указатель на структуру RECT, определяющую прямоугольник, в котором вы хотите выводить текст. В данном случае это полная клиентская область окна, определенная в aRect. Последний аргумент определяет формат текста в прямоугольнике. Здесь скомбинированы спецификации, объединенные битовым ИЛИ (|). Строка выводится в одну линию с центрированием текста по горизонтали и по вертикали внутри прямоугольника. Это красиво располагает ее по центру окна. Существует также множество других опций, которые включают возможность размещения текста в верхней или нижней часть прямоугольника с выравниванием влево или вправо.
После того, как вы вывели все, что хотели отобразить, следует сообщить Windows, что рисование в клиентской области завершено. Для каждого вызова функции BeginPaint () должен существовать соответствующий вызов EndPaint (). Поэтому для завершения обработки сообщения WM_PAINT необходим такой оператор:
EndPaint(hWnd, &PaintSt); // Завершить операцию перерисовки окна
Аргумент 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); II Подготовиться к перерисовки окна
И Получить верхний левый и нижний правый углы клиентской области GetClientRect(hWnd, &aRect);
// Теперь отобразить текст в клиентской области окна// Теперь отобразить текст в клиентской области окнаSetBkMode(hDC, TRANSPARENT); //Установить режим отображения фона текста
// Теперь отобразить текст в клиентской области окна
// Теперь отобразить текст в клиентской области окна
DrawText(HDC, L"But, soft! What -1, &aRect,DT_SINGLELINE| DT_CENTER| DT_VCENTER) ;EndPaint(hWnd, &PaintSt); return 0;DrawText(HDC, L"But, soft! What -1, &aRect,DT_SINGLELINE| DT_CENTER| DT_VCENTER) ;EndPaint(hWnd, &PaintSt); return 0;// Дескриптор контекста устройства light through yonder window breaks?”, // Индикатор строки, ограниченной null // Прямоугольник, в котором выполняется И рисование текста
DrawText(HDC, L"But, soft! What -1, &aRect,
DT_SINGLELINE| DT_CENTER| DT_VCENTER
) ;
EndPaint(hWnd, &PaintSt); return 0;
DrawText(HDC, L"But, soft! What -1, &aRect,
DT_SINGLELINE| DT_CENTER| DT_VCENTER
) ;
EndPaint(hWnd, &PaintSt); return 0;
// Формат текста — одна строка
//- центрирование в строке
//- центрирование по высоте 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, как вы делали до сих пор, вам придется создать про-
ект, используя шаблон проекта Win32. Вы должны создать его как пустой проект и затем добавить файл Exll Ol. срр с кодом.
Практическое занятие | Простая прогрЗММЗ WmdOWS 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.
Рис. 11.5. Отношения между функциями WinMain () и WindowProc ()
Это может быть не совсем очевидно на основе кода, который вы видите, но такая структура является сердцем всех Windows-программ. Понимание организации Windows-приложений часто может помочь, когда вы пытаетесь разобраться, почему в приложении что-то работает не так, как должно. Функция WinMain () вызывается Windows в начале выполнения программы, а функция WindowProc (), которая иногда называется WndProc (), вызывается операционной системой при каждой передаче сообщения окну вашего приложения. Обычно в приложении присутствует отдельная функция WindowProc () для каждого окна приложения.
Функция WinMain () обрабатывает все сообщения для данного окна, которые не были поставлены в очередь, включая инициированные в цикле сообщений WinMain (). Таким образом, WindowProc () имеет дело с обоими видами сообщений. Это потому, что код в цикле сообщений определяет, какого рода сообщение он извлек из очереди, и затем осуществляет диспетчеризацию его для обработки в WindowProc (). Функция WindowProc () — это место, где находится код специфичной для приложения реакции на каждое сообщение Windows, который должен обрабатывать все коммуникации с пользователем, обрабатывая сообщения Windows, сгенерированные действиями поль-
зователей, такими как перемещения мышки и щелчки ее кнопками или ввод информации с клавиатуры.
Сообщения, помещенные в очередь, в большей степени вызваны пользовательским вводом посредством мыши или клавиатуры. Неочередизованные сообщения, для которых 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”. Такая практика явного указания типа переменной в ее имени была важна в среде С из-за недостаточного контроля типов; благодаря тому, что можно было определить тип по имени, снижалась вероятность неправильной интерпретации значений. Отрицательной стороной таких имен переменных была некоторая громоздкость, из-за которой код выглядел сложнее, чем
он был на самом деле. Поскольку 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. Объект этого класса включает все необходимое для
запуска, инициализации и закрытия приложения. Вам нужно создать приложение, наследуя собственный класс приложения от CWinApp. Вы определите специализированную версию класса, подходящую для нужд вашего приложения. Ниже показан необходимый для этого код.
class COurApp: public CWinApp
{
public:
virtual BOOL Initlnstance();
I;
Как можно ожидать от столь простого примера, в этом случае нет никакой особой специализации. Вы включили только один член в определение класса: функцию 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"Our Dumb MFC Application’’);
}
};
Функция Create (), которая вызывается в конструкторе, унаследована от базового класса. Она создает окно и присоединяет его к создаваемому объекту COurWnd. Обратите внимание, что объект COurWnd — это не то же самое, что окно, отображаемое Windows — объект класса и физическое окно представляют собой разные вещи.
Значение первого аргумента функции 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 () объекта приложения для конструирования объекта окна, и потому неявно предполагает, что объект предположения уже существует.
Готовый продукт
Теперь, когда вы увидели весь код, можете добавить его в исходный файл проекта Exll—02 . срр. В Windows-программе классы обычно определяются в файлах .h, а функции-члены, которые не определены внутри определения класса, помещаются в файлы . срр. Ваше приложение коротко, так что вы можете поместить все необходимое в единственный файл . срр. Преимущество этого заключается в том, что вы можете видеть весь код сразу в одном месте. Таким образом, код программы структурирован следующим образом:
// Exll_02.cpp
// Элементарная программа MFC
#include <afxwin.h> // Для библиотеки классов
// Определение класса приложения
class COurApp:public CWinApp
{
public:
virtual BOOL Initlnstance();
};
/ / Определение класса окна
class COurWnd:public CFrameWnd
{
public:
// Конструктор
COurWnd() {
Create (0, L"Our Dumb MFC Application’’);
} };
// Функция для создания экземпляра главного окна приложения
BOOL COurApp::Initlnstance(void) {
H Конструировать объект окна в свободном хранилище 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 Forms со стандартным меню.
— Our Dumb ^FC Application
Рис. 11.6. Окно приложения Ех11_2
Практическое занятие | ПрИЛОЖвНИв 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 (Вид).
New Pro" -ct
froject types.
lem plates.
S Visual C++
ML г_к
General
MFC
Smart Device
Visual Studio i stalled templates
9 ASP.HET Web Service 7сл Console Application
"SQL Server Pioje't
1 Windows Forms Control Library
Class Library
^QR Imply Project "3 Windows Farms Application
1 Windows Service
Test
Win32
+ Other Languages
D stributed System Solutions
+ othe Project Туры
• test Projects
My Templates
jOSeardi Online Templates..,
A project fc г creating an application with a Windows user interface
Name:
гх::_оз|
Location
d^Beginning Visual C+-*- 2DD5\Example£v ।
Crov.'se...
Solution
Create new Solution^ Pl Create directory for solution
SdlutloFi Name:
Exll_P3
—
UK
cancel
Рис. 11.7. Диалоговое окно New Project
Рис. 11.8. Окно конструктора с пустой формой
Найдите опцию MenuStrip (Полоса меню) в списке и перетащите ее в окно формы на панели вкладки конструктора. Щелкните правой кнопкой мыши на элементе Menus tripl. который появится под окном формы, и выберите Insert Standard Items (Добавить стандартные элементы) из контекстного меню. После этого вы получите в окне формы панель меню, наполненную стандартными меню File (Файл), Edit (Правка), Tools (Сервис), Help (Справка) — каждое со своим выпадающим списком элементов. Результат этой операции показан на рис. 11.9.
Если вы соберете проект, нажав <Ctrl+Shift+B>, и затем запустите его на выполнение, нажав <Ctrl+F5>, то увидите окно формы, отображенное вместе с его меню. Естественно, пункты меню пока ничего не делают, поскольку вы не добавили никакого кода, обрабатывающего события их выбора, но пиктограммы в правой части панели заголовка работают, так что вы можете закрыть приложение. Далее в настоящей книге вы узнаете о том, как разработать полноценное приложение Windows Forms, включающее обработку событий.
Рис. 11.9. Стандартные элементы управления Windows Forms
Резюме
В настоящей главе были показаны три разных способа создания элементарного 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 Document Interface — SDI) и многодокументного интерфейса (Multiple Document Interface — MDI).
Как использовать мастер MFC Application Wizard для генерации программ SDI и MDI.
Какие файлы генерирует MFC Application Wizard, и каково их содержимое.
Как структурированы программы, генерируемые MFC Application Wizard.
Ключевые классы программ, сгенерированных MFC Application Wizard, и как они взаимосвязаны.
Общий подход к настройке программ, сгенерированных MFC Application Wizard.
В последующих главах вы будете постепенно расширять программы, сгенерированные в этой главе, путем инкрементного добавления кода. В конечном итоге вы получите масштабную работающую Windows-программу, которая будет включать почти
все базовые приемы программирования пользовательского интерфейса, изученные на этом пути.
Концепция “документ-представление” в MFC
Когда вы пишете приложения с применением МЕС, это подразумевает следование определенной структуре построения вашей программы с размещением и обработкой данных приложения определенным образом. Это может показаться ограничивающим фактором, но на самом деле большей частью это вовсе не так, и выигрыш в скорости и простоте реализации, который вы получаете, перевешивает любые вероятные недостатки. Структура программы МЕС включает две ориентированные на приложение сущности: документ (document) и представление (view), поэтому давайте рассмотрим, в чем состоит их суть и как они используются.
Что такое документ?
Документ — это имя, присвоенное коллекции данных вашего приложения, с которыми взаимодействует пользователь. Хотя слово документ выглядит как нечто, подразумевающее текстовую природу, на самом деле документ не ограничивается текстом. Это могут быть данные игры, геометрическая модель, текстовый файл, коллекция данных о распределении апельсиновых деревьев в Калифорнии — словом, все, что хотите. Термин документ — это просто принятое обозначение данных приложения, рассматриваемых как единое целое.
Для вас не должно быть сюрпризом, что документ в вашей программе определяется как объект класса документа. Ваш класс документа наследуется от класса библиотеки МЕС по имени С Document, и вы добавляете свои собственные данные-члены доя хранения элементов, необходимых вашей программе, а также функции-члены, поддерживающие их обработку. Ваше приложение не ограничено единственным типом документа; вы можете определить множество классов документов, если приложение имеет дело с несколькими разными представлениями документов.
Обработка данных приложения подобным образом позволяет использовать стандартные механизмы, предусмотренные в МЕС для управления коллекцией данных приложения как единым элементом, а также сохранять и извлекать данные, содержащиеся в объектах документов на диске. Эти механизмы наследуются кодом класса вашего документа от базового класса, определенного в библиотеке МЕС, так что вы автоматически получаете широкий набор функциональности, встроенной в ваше приложение, не написав при этом никакого собственного кода.
Документные интерфейсы
У вас есть выбор, со сколькими документами одновременно будет иметь дело ваша программа — только с одним либо с несколькими. О дно документный интерфейс, обозначаемый сокращенно как SDI (Single Document Interface), поддерживается библиотекой МЕС для программ, которым нужно открывать по одному документу за раз. Программа, использующая этот интерфейс, называется SDI-приложением.
Для программ, которые должны открывать по несколько документов одновременно, вы можете применить многодокументный интерфейс, сокращенно называемый MDI (Multiple Document Interface). В MDI, наряду с возможностью открывать множество документов одного типа, существует также возможность организовать одновре-
менную обработку документов разного типа, причем каждый документ отображается в своем собственном окне. Конечно, вы должны предоставить код соответствующей обработки для всех различных представлений документов, которые собираетесь поддерживать. В приложении MDI каждый документ отображается в дочернем окне приложения. У вас есть также дополнительный вариант построения интерфейса, который называется архитектурой множества документов верхнего уровня (multiple top-level document architecture), когда окно каждого документа является дочерним по отношению к рабочему столу.
Что такое представление?
Представление всегда относится к определенному объекту документа. Как вы уже видели, документ в вашей программе содержит набор данных приложения, а представление — это объект, предлагающий механизм отображения некоторых или всех данных, хранящихся в документе. Он определяет, как данные отображаются в окне, и как пользователь может взаимодействовать с ними. Подобно тому, как вы определяете документ, вы также будете определять собственный класс представления, наследуя его от класса MFC по имени CView. Обратите внимание, что объект представления и окно, в котором он отображается — не одно и то же. Окно, в котором появляется представление, называется обрамляющим окном (frame window). В действительности представление отображается в своем собственном окне, которое в точности заполняет клиентскую часть обрамляющего окна. На рис. 12.1 показан документ с двумя представлениями.
ДокументДокументJanuary300February400March400April500May400June400July500August300September500October600November700December800January300February400March400April500May400June400July500August300September500October600November700December800Sales by MonthSales by MonthПредставление 1Представление 1Представление 2Представление 2Рис. 12.1. Документ с двумя тфедставлениямиРис. 12.1. Документ с двумя тфедставлениями
Документ
Документ
January
300
February
400
March
400
April
500
May
400
June
400
July
500
August
300
September
500
October
600
November
700
December
800
January
300
February
400
March
400
April
500
May
400
June
400
July
500
August
300
September
500
October
600
November
700
December
800
Sales by Month
Sales by Month
Представление 1
Представление 1
Представление 2
Представление 2
Рис. 12.1. Документ с двумя тфедставлениями
Рис. 12.1. Документ с двумя тфедставлениями
В примере на рис. 12.1 каждое представление отображает все данные, которые содержит документ, в разной форме, хотя при необходимости представление также может отображать только часть данных документа.
Объект документа может иметь столько объектов, ассоциированных с ним, сколько вам нужно. Каждый объект представления может обеспечивать свое отличающееся представление данных документа или подмножества его данных. Например, если вы имеете дело с текстом, то разные представления могут отображать независимые блоки текста из одного и того же документа. Если программа имеет дело с графическими данными, вы можете отобразить все данные документа в отдельных окнах в разном масштабе или в разных форматах. На рис. 12.1 показан документ, содержащий числовые данные — месячные объемы продаж продукта, причем одно представление отображает гистограмму, отражающую производительность продаж, а второе — те же данные в форме графика.
Связь документа с его представлениями
Библиотека МЕС включает механизм интеграции документа с его представлениями и каждого обрамляющего окна с текущим активным представлением. Объект документа автоматически поддерживает список указателей на ассоциированные с ним представления, а объект представления имеет член данных, содержащий указатель на документ, с которым он связан. Каждое обрамляющее окно сохраняет указатель на текущий активный объект представления. Координация между документом, представлением и обрамляющим окном устанавливается объектами другого класса МЕС, называемыми шаблонами документа (document templates).
Шаблоны документа
Шаблон документа управляет объектами документов в вашей программе, а также тем, как окна и представления ассоциированы с каждым из них. Существует один шаблон документа для каждого из типов документов, которые определены в вашем приложении. Если есть два или более документов одного и того же типа, для управления ими понадобится только один шаблон документа. Чтобы точнее описать роль шаблона документа, можно сказать, что шаблон документа создает объекты документов, а представления документа создаются объектом обрамляющего окна. Объект приложения, являющийся фундаментальным для каждого приложения МЕС, создает сам объект шаблона документа. На рис. 12.2 показано графическое представление этих отношений.
На этой диаграмме с помощью пунктирных стрелок показано использование указателей для связи объектов. Эти указатели позволяют функциям-членам объекта одного класса обращаться к общедоступным (public) данным и функциям-членам в интерфейсе другого объекта.
Классы шаблонов документа
Библиотека МЕС включает два класса для определения шаблонов документа. Для SDI-приложений используется библиотечный класс CSingleDocTemplate. Он относительно прост, потому что приложение SDI имеет только один документ и обычно только одно представление. MDI-приложения несколько более сложны. Они могут иметь множество одновременно активных документов, поэтому для определения их шаблона документа требуется другой класс — CMultiDocTemplate. С большей частью этих классов вы познакомитесь по мере продвижения в разработке кода приложения.
Рис. 12.2. Отношения между шаблоном документа, документом, обрамляющим окном и представлением
Ваше приложение и MFC
На рис. 12.3 показаны четыре базовых класса, которые появляются почти в каждом Windows-приложении на основе MFC.
Класс приложения СМуАРр.
Класс обрамляющего окна CMyWnd.
Класс представления CMyView, который определяет, как должны отображаться данные, содержащиеся в CMyDoc, в клиентской области окна, созданного объектом CMyWnd.
Класс документа CMyDoc, который определяет документ, содержащий данные приложения.
Действительные наименования этих классов специфичны для каждого конкретного приложения, но наследование от MFC в основном одно и то же, хотя могут быть и альтернативные базовые классы, в частности, класс представления. Как вы увидите чуть позже, в MFC предусмотрены разные вариации класса представления, включающие широкий набор предварительно разработанной функциональности, что по-
зволяет сэкономить немало усилий по кодированию. Обычно вам не придется расширять класс, определяющий шаблон документа для вашего приложения, так что стандартного класса MFC CSingleDocTemplate для SDI-программы обычно достаточно. Когда вы создаете программу MDI, вашим классом шаблона документа является CMultiDocTemplate, который также унаследован от CDocTemplate.
Стрелки на диаграмме направлены от базового класса к производному. Показанные здесь библиотечные классы MFC формируют довольно сложную структуру наследования, но фактически представляют лишь малую часть полной структуры MFC. Обычно вам не обязательно знать все детали иерархии MFC, но важно иметь общее представление об унаследованных членах ваших классов. Вы не увидите никаких определений базовых классов в своей программе, но унаследованные члены производных классов вашей программы происходят от непосредственного базового класса, а также от всех непрямых базовых классов из иерархии MFC. Чтобы понять, какие члены присутствуют в классах вашей программы, вы должны знать, от каких классов они унаследованы. После того, как вы это узнаете, вы можете поискать их описание в справочной системе.
Еще одно, о чем вам не нужно беспокоиться — это помнить о том, какие классы должны присутствовать в вашей программе и какие базовые классы использовать в их определении. Как вы увидите ниже, обо всем этом заботится Visual C++ 2005.
Создание приложений MFC
При разработке Windows-программ на базе MFC используются четыре основных инструмента.
Мастер создания приложений (Application Wizard) для начального создания базового кода прикладной программы. Вы пользуетесь этим мастером при каждом создании проекта, в результате чего код генерируется автоматически.
Контекстное меню проекта в Class View (Представление классов) для добавления новых классов и ресурсов к вашему проекту. Контекстное меню отображается при щелчке правой кнопкой мыши в Class View, а новый класс создается выбором пункта Add^Class (Добавить^Класс) в этом меню. Ресурсы — это нечто, включающее неисполняемые данные, вроде графических изображений, пиктограмм, меню и диалоговых окон. Пункт Add1^ Resource (Добавить^ Ресурс) того же контекстного меню позволяет добавить новый ресурс.
Контекстное меню класса используется в Class View для расширения и настройки существующих классов программы. Для этого применяются пункты Add^Add Function (Добавить^ Добавить функцию) и Add^Add Variable (Добавить^ Добавить переменную) этого меню.
Редактор ресурсов применяется для создания или модификации таких объектов, как меню и панели инструментов.
На самом деле доступно несколько редакторов ресурсов; каждый используется в определенной конкретной ситуации, в зависимости от вида ресурса, который вы редактируете. Редактирование ресурсов рассматривается в следующей главе, но пока позвольте забежать немного вперед и создать приложение MFC.
Процесс создания приложения MFC почти так же прост, как создание консольной программы; на этом пути у вас будет лишь немногим больше вариантов выбора. Как вы уже видели, все начинается с создания нового проекта выбором пункта меню
File^New*^ Project (Файл1^Создать^Проект) или же нажатием комбинации клавиш <Ctrl+Shift+N>. После этого отображается диалоговое окно New Project (Новый проект), в котором вы можете выбрать MFC в качестве типа проекта и MFC Application (Приложение MFC) — в качестве используемого шаблона приложения. Вы также должны ввести имя проекта, которое может быть любым. Я использовал TextEditor, как показано на рис. 12.4. Вам не придется превращать этот конкретный пример в какое-то серьезное приложение, так что можете выбрать любое имя на свой вкус.
Рис. 123. Базовые классы, присутствующие почти в каждом Windows-приложении на основе MFC
Как вы знаете, имя, присвоенное проекту — TextEditor в данном случае — служит именем папки, содержащей все файлы проекта, но также оно используется и для создания имен классов, которые Application Wizard генерирует для вашего проекта. Когда вы щелкаете на кнопке ОК в диалоговом окне New Project, то видите перед собой диалоговое окно MFC Application Wizard (Мастер создания приложений МЕС), в котором можете указать опции своего приложения (рис. 12.5).
Как видите, диалоговое окно объясняет все активные установки проекта, и в правой его части доступен набор опций, которые можно выбрать. Вы можете выбрать любую из них, дабы посмотреть, что получится — у вас всегда есть возможность вернуться к базовому диалоговому окну Application Wizard, щелкнув на кнопке Previous (Назад). Выбор любой из этих опций справа предоставит вам полный набор дальнейших уточняющих настроек, так что в сумме их очень много. Я не стану обсуждать их все, а только опишу вкратце те из них, которые, скорее всего, представят наибольший интерес, а остальные оставлю для вашего самостоятельного исследования. Изначально Application Wizard позволяет вам выбрать SDI-приложение, MDI-приложение либо приложение на основе диалогового окна. Давайте для начала создадим приложение SDI и рассмотрим все опции, которые потребуется выбрать в процессе.
Создание SDI-приложения
Выберите опцию Application Туре (Тип приложения) из списка в правой части диалогового окна. По умолчанию будет выбран переключатель Multiple documents (Множество документов), что означает многодокументный интерфейс — MDI, и внешний вид MDI-приложения показан в левой верхней части диалогового окна, чтобы вы знали, чего ожидать. Выберите переключатель Single document (Один документ), и представление приложения слева вверху изменится, как показано на рис. 12.6.
Рис. 12.4. Создание проекта Text Edi tor
MFC Applicatfo i Wizard - TextEditc
Welcome to the MFC Application Wizard
OverviewApplication TypeCompound Document SupportDocument Template StringsDatabase SupportUser Interface FeaturesAdvanced FeaturesGenerated ClassesOverviewApplication TypeCompound Document SupportDocument Template StringsDatabase SupportUser Interface FeaturesAdvanced FeaturesGenerated ClassesThese are the current project settings:
Overview
Application Type
Compound Document Support
Document Template Strings
Database Support
User Interface Features
Advanced Features
Generated Classes
Overview
Application Type
Compound Document Support
Document Template Strings
Database Support
User Interface Features
Advanced Features
Generated Classes
Multiple document interface
No database support
No compound document support
Click FmHh from any window to accept the current settings
[ Next>[ Next>FinishFinishCancelCancelPuc. 12.5. Диалоговое окно MFC Application Wizard (Мастер создания при-ложений MFC)Puc. 12.5. Диалоговое окно MFC Application Wizard (Мастер создания при-ложений MFC)Рис. 12.6. Выбор в качестве типа приложения Single document (Один документ)Рис. 12.6. Выбор в качестве типа приложения Single document (Один документ)After you create the project, see the project's readme.txt file for information about thie project features and files that are generated.
[ Next>
[ Next>
Finish
Finish
Cancel
Cancel
Puc. 12.5. Диалоговое окно MFC Application Wizard (Мастер создания при-ложений MFC)
Puc. 12.5. Диалоговое окно MFC Application Wizard (Мастер создания при-ложений MFC)
Рис. 12.6. Выбор в качестве типа приложения Single document (Один документ)
Рис. 12.6. Выбор в качестве типа приложения Single document (Один документ)
Теперь рассмотрим прочие опции, которые вы видите здесь для данного типа приложений; они перечислены в табл. 12.1.
Таблица 12.1. Опции SDI-приложений
Опция
Описание
Dialog based (Основанное на диалоге)
Окном приложения является диалоговое окно, а не обрамляющее окно.
Multiple top-level documents (Множество документов верхнего уровня)
Документы отображаются в дочерних окнах рабочего стола, а не в дочерних окнах приложения, как это происходит в MDI- приложении.
Document/View architecture support (Поддержка архитектуры “документ- представление")
Эта опция выбрана по умолчанию, так что вы получаете встроенный код для поддержки архитектуры “документ-представление". Если вы отключите эту опцию, то эта поддержка не предоставляется, и вы должны реализовать все необходимое самостоятельно.
Resource language (Язык ресурсов)
Этот выпадающий список отображает выбор языков, применяемых в ресурсах вашего приложения, таких как меню и текстовые строки.
Use Unicode libraries (Использовать библиотеки Unicode)
Поддержка 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 (Имя фильтра), что будет именем фильтра, который будет появлять-
ся в диалоговых окнах 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 (Начальная панель состояния)
Эта опция добавляет панель состояния в нижнюю часть окна приложения, включающую индикаторы клавиш <CAPS LOCK>, <NUM LOCK> и <SCROLL LOCK>, а также строку сообщений, отображающую вспомогательные сообщения для меню и кнопок панели инструментов. Эта опция также добавляет команды меню для сокрытия и отображения панели состояния.
Split window (Разделить окно)
Эта опция предоставляет разделительную черту для каждого из основных представлений приложения.
Standard docking toolbar (Стандартная стыкуемая панель инструментов)
Эта опция добавляет в окно приложения панель инструментов со стандартным набором кнопок, являющихся альтернативой стандартным пунктам меню. Панель инструментов предоставляется по умолчанию. Стыкуемая (dockable) панель инструментов может быть прикреплена к любой грани окна приложения, так что вы можете поместить ее там, где вам удобно. Добавление кнопок к панели инструментов будет описано в главе 13.
Browser style toolbar (Панель инструментов в стиле браузера)
Это добавляет в окно приложения панель инструментов в стиле браузера Internet Explorer.
Имеется еще пара средств в наборе опций Advanced Features (Дополнительные возможности), о которых вам следует знать. Одно из них — Printing and print preview (Печать и предварительный просмотр), которое выбрано по умолчанию, а другое — Context-sensitive help (Контекстно-чувствительная справка), которое вы получите, если отметите соответствующий флажок. Отметка флажка Printing and print preview добавляет стандартные пункты Page Setup (Параметры страницы), Print Preview (Предварительный просмотр) и Print (Печать) в меню File (Файл), а мастер Application Wizard предоставит код для их поддержки. Включение опции Context- sensitive help дает базовый набор возможностей поддержки контекстной подсказки. Понятно, что вы должны будете добавить специфическое содержимое в справочные файлы, если хотите использовать это средство.
Если в диалоговом окне MFC Application Wizard выбрать опцию Generated Classes (Сгенерированные классы), вы увидите список классов, которые Application Wizard генерирует в коде вашей программы, как показано на рис. 12.7.
Рис. 12.7. Просмотр сгенерированных классов
Можно выделить любой класс в списке, щелкнув на его имени, и в находящихся ниже полях будет отображено имя класса, имя заголовочного файла, в котором находится его определение, имя базового класса, а также имя файла, содержащего реализацию функций-членов класса. Определение класса всегда содержится в файле . h, а исходный код функций-членов — в файле . срр.
В случае класса CTextEditorDoc вы можете изменить все за исключением базового класса; однако если выбрать CtextEditorApp, то единственное, что можно будет изменить — это имя класса. Попробуйте щелкнуть на именах других классов в списке. Для СМа in Frame вы сможете изменить все за исключением базового класса, а для класса CTextEditorView , показанного на рис. 12.7, вы сможете изменить также и базовый класс. Щелкните на кнопке со стрелкой вниз для отображения списка классов, из которого можно выбрать базовый класс; этот список показан на рис. 12.7. Возможности, встраиваемые в ваш класс представления, зависят от выбранного базового класса (табл. 12.3).
Поскольку мы назвали приложение TextEditor, что говорит о его способности редактировать текст, выберем в качестве базового класса CEditView, чтобы автоматически получить базовые возможности редактирования.
Щелкните на кнопке Finish (Готово), чтобы получить исходные файлы полностью рабочей базовой программы, сгенерированные мастером MFC Application Wizard на основе выбранных вами опций.
Таблица 12.3. Базовые классы и возможности
Базовый класс
Возможности класса представления
CEditView
Предоставляет простую возможность редактирования многострочных текстов, включая поиск с заменой, а также печать.
СFormView
Обеспечивает представление формы; форма — это диалоговое окно, содержащее элементы управления, управляющие отображением данных и пользовательским вводом. По сути это та же функциональность, что предлагается в приложении Windows Forms для CLR, о чем пойдет речь в главе 21.
CHtmlEditView
Этот класс расширяет класс CHtmlView и добавляет возможность редактирования HTML-страниц.
CHtmlView
Обеспечивает представление, в котором могут отображаться Web-страницы и локальные HTML-документы.
CListView
Позволяет использовать архитектуру “документ-представление” со списочными элементами управления.
CRichEditView
Предоставляет возможность отображения и редактирования документов, содержащих форматируемый текст.
CScrollView
Обеспечивает представление, автоматически добавляющее линейки прокрутки, когда отображаемые данные того требуют.
CTreeView
Предоставляет возможность использовать архитектуру “документ-представление” с древовидными элементами управления.
CView
Предоставляет базовые возможности просмотра документов.
Вывод мастера MFC Application Wizard
Все программные файлы, сгенерированные мастером создания приложений, сохраняются в папке проекта TextEditor, вложенной в папку решения с тем же именем. Кроме того, во вложенной в папку решения папке res будут находиться файлы ресурсов. IDE-среда поддерживает несколько способов просмотра информации, имеющих отношение к вашему проекту, как показано в табл. 12.4.
Таблица 12.4. Способы просмотра информации, относящейся к проекту
Вкладка/панель
Содержимое
Solution Explorer (Проводник решения)
Class View (Представление классов)
Показывает файлы, включенные в ваш проект. Файлы размещены по категориям в виртуальных папках под названиями Header Files (Заголовочные файлы), Resource Files (Ресурсные файлы) и Source Files (Исходные файлы).
Отображает классы, присутствующие в вашем проекте, вместе с их членами. Также выводятся глобальные сущности, определенные вами. Классы показаны в верхней панели, а в нижней — члены класса, выбранного в данный момент .в верхней панели. Щелкая правой кнопкой мыши на элементах, отображаемых в Class View, можно получить меню, используемое для просмотра определения выбранной сущности.
Resource View (Представление ресурсов) Property Pages (Страницы свойств)
Здесь отображаются ресурсы вроде пунктов меню и кнопок панели инструментов, использованных в вашем проекте. Щелчок правой кнопкой мыши на ресурсе отображает меню, позволяющее редактировать текущий ресурс или добавлять новые. Здесь отображаются версии, которые вы можете собрать из вашего проекта. Отладочная версия включает дополнительные возможности для облегчения отладки кода. Рабочая версия дает в результате более компактный исполняемый файл и собирается тогда, когда код полностью протестирован и отлажен для производственного применения. Щелкнув правой кнопкой мыши на версии — Debug (Отладочная) или Release (Рабочая) —• вы можете вызвать контекстное меню, в котором добавить панель свойств или отобразить свойства, установленные для версии в данный момент. Панель свойств позволяет установить настройки для компилятора и компоновщика.
Если вы щелкнете правой кнопкой мыши на TextEditor в панели Solution Explorer и выберете в контекстном меню пункт Properties (Свойства), будет отображено окно свойств проекта, показанное на рис. 12.8.
1
ext Ed or Prape t ' Pa es
Configu ration. Actrve(Debug)Platform: Act^e(Win32)v
Configuration anagen,..
i±i Common Propertiesв uenerali
В Configuration PropertiesOutput Directo--/$(SolutionDir)$( Configuration Name)rv I
GeneralIntermediate Directory$(ConfigurationName)
Debugfl ingExte nsions to De lete onClea n’ .obj;ж. il lq^ ,tib; ’ .4i; ’ .tfh; *.tri । p;’. rsp; ^gcj’.pgd;!
t) C/C-i-»Build iLog File$(IntDir)\B u.ldLog.htm
n erInherited Project Property Sheets
® anifestToolв inject Defaults
® ResourcesConfiguration fvpeApplication (.exe)
i±J XML Document Generate Use of МГСUse MFC sr э Shared DLL
ш Browse information
.Lise of ATLhot Using ATL
w Emld Events„3
_ .„ . ,Minimize CRT Use in ATLNo
- Custom Build Step
_ Code AnalysisCharacter SetUre UnicodeCharacter S^t
ffi Web DeploymentCommon Language Runtime support NoCommonlanguage Runtime support
*i Application Verifier^no e fro9ram OptimizationNoWhole Program Optimization
Output Directory
Specifies a relative path to the output tile directory; can include enviranmer t vanaolos
OK
Cancel
Рис. 12.8. Окно свойств проекта
В левой панели отображаются группы свойств, которые можно выбрать для показа на правой панели. В данный момент отображается группа свойств General (Общие), и вы можете менять значение свойства в правой панели щелчком на нем и выбором нового значения из выпадающего окна списка справа от имени свойства, либо в некоторых случаях в результате ввода нового значения с клавиатуры.
Просмотр файлов проекта
Если вы выберете вкладку Solution Explorer и развернете список щелчком на + рядом с TextEditor, а затем щелкнете на + рядом с каждой из папок Source Files, Header Files и Resource Files, то увидите полный список всех файлов проекта, как показано на рис. 12.9.
На рис. 12.9 показана панель в виде плавающего окна, чтобы сделать список файлов полностью видимым; вы можете сделать любую из панелей плавающей, щелкнув на стрелке вниз на вершине панели и выбрав из списка возможных позиций. Как видите, всего в проекте присутствует 17 файлов. Вы можете просмотреть содержимое любого файла, выполнив двойной щелчок на его имени. Содержимое выбранного файла отображается в окне редактора. Попробуйте это с файлом ReadMe .txt. Вы увидите, что он содержит краткое описание содержимого каждого из файлов, составляющих проект. Я не стану здесь повторять описание этих файлов, поскольку они предельно ясно описаны в ReadMe. txt.
Рис. 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 (Глобальные функции и переменные), вы увидите, что оно содержит два определения: объект приложения the Арр и indicators, представляющий собой массив индикаторов, записывающих состояния клавиш <CAPS LOCK>, <NUM LOCK> и <SCROLL LOCK>, отображаемые в панели состояния.
Чтобы просмотреть код определения класса в панели редактора, вы просто выполняете двойной щелчок на имени соответствующего класса в дереве Class View. Аналогично, чтобы просмотреть код функции-члена, дважды щелкните на имени функции.
Class Viewж 4 x
<Search>x
Q TextEditorл
A = Maps
Global Functions and Variables
^ Maa os and Constants
+1-^ CAboutDIg
A CMainFrameL I
A CTextEditorApp
'+ CTextEditorDotv
* ~CTextEditorDoc(void) AssertValid(void) const CreateObject(void)
* CTextEditorDoc(void) Dump(CDumpConteAt &dc) const _G e tBase Class( vo id)
♦ GetMessageMap(void) const GetRuntimeClass(void) const GetTh isClass(vo id)
* GetTh isMessage Ma p( void) О n NewDocu me nt(vo id)
-■ Serialize^ Archive Rar) Q classCTextEditorDoc
^S^'j^c" Е.^Чсу^ Class View i^glRw-j.-ci
Puc. 12.10. Просмотр членов классаCTextEditorDoc в панели Class View
Обратите внимание, что вы можете перетаскивать грани любой из панелей в окно IDE, чтобы увидеть содержимое кода. Вы можете скрыть или показать набор панелей Solution Explorer, щелкнув на кнопке Autohide (Автоматически скрывать) в правой части панели заголовка этой панели.
Определения классов
Я не стану здесь погружаться в детали реализации классов — вы просто получите представление о том, как они выглядят, а я подчеркну несколько важных аспектов. Если выполнить Двойной щелчок на имени класса в Class View, отображается код определения класса. Для начала взглянем на класс приложения — CTextEditorApp. Определение этого класса приведено ниже.
// TextEditor.h : главный заголовочный файл для приложения TextEditor
#pragma once
#ifndef AFXWIN_H
#error "включите 'stdafx.h' перед включением этого файла для РСН"
#endif
#include "resource.h" H главные символы
U CTextEditorApp:
М В TextEditor.срр находится реализация этого класса
class CTextEditorApp : public CWinApp
{
public:
CTextEditorApp();
/ / Переопределения
public:
virtual BOOL Initlnstance();
II Реализация
afx__msg void OnAppAboutO ;
DECLARE—MESSAGE_MAP()
};
extern CTextEditorApp theApp;
Класс CTextEditorApp унаследован от CWinApp и включает конструктор, виртуальную функцию InitInstance (), функцию OnAppAbout () и макрос DECLARE—MESSAGE—МАР ().
Макрос — это не код C++. Это имя, определенное директивой препроцессора # de fine, заменяемое некоторым текстом, который обычно является кодом C++, но также может быть и константами или символами определенного рода.
Макрос DECLARE_MESSAGE_MAP () касается определения того, какие сообщения Windows какими функциями-членами класса обрабатываются. Макрос присутствует в определении любого класса, который может обрабатывать сообщения Windows. Конечно, наш класс приложения наследует множество функций и данных-членов от базового класса, и вы познакомитесь с ними по мере расширения примеров программ. Если посмотреть в начало кода определения класса, можно заметить директиву #pragma once, предотвращающую многократное включение данного файла. Далее идет группа директив препроцессора, гарантирующих включение файла stdafх.h перед данным файлом. Обрамляющее окно приложения нашей SDI-программы создается объектом класса CMainFrame, который определен следующим образом:
class CMainFrame : public CFrameWnd { protected: // создавать только из сериализации
CMainFrame();
DECLARE_DYNCREATE(CMainFrame)
// Атрибуты public:
I/ Операции public:
// Переопределения public:
virtual BOOL PreCreateWindow(CREATESTRUCT& cs) ;
// Реализация public:
virtual -CMainFrame();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& de) const;
#endif
protected: // встроенные члены управляющий панелей
CStatusBar m_wndStatusBar;
CToolBar m_wndToolBar;
/ / Сгенерированные функции отображения сообщений protected:
afx_msg int OnCreate(LPCREATESTRUCT IpCreateStruct); DECLARE_MESSAGE_MAP()
};
Этот класс унаследован от CFrameWnd, который предоставляет большую часть функциональности, необходимой обрамляющему окну нашего приложения. Производный класс включает защищенные (protected) данные-члены — m wndStatusBar и
m wndToolBar, которые являются экземплярами классов MFC CStatusBar и CToolBar соответственно. Эти объекты создают и управляют панелью состояния, появляющейся в нижней части окна приложения, и панелью инструментов, предлагающей кнопки для быстрого доступа к стандартным функциям меню.
Определение класса CTextEditorDoc , созданного мастером MFC Application Wizard, выглядит так:
class CTextEditorDoc : public CDocument
{
protected: H создавать только из сериализации
CTextEditorDoc();
DECLARE_DYNCREATE(CTextEditorDoc)
H Атрибуты
public:
/ / Операции public:
/ / Переопределения
public:
virtual BOOL OnNewDocument();
virtual void Serialize(CArchive& ar);
// Реализация
public:
virtual ~CTextEditorDoc();
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& de) const;
#endif
protected:
// Сгенерированные функции отображения сообщений
protected:
DECLARE_MESSAGE_MAP()
};
Как и в случае предыдущего класса, большая часть “мяса” поступает от базового класса, и потому здесь не видна. Макрос DECLARE—DYNCREATE (), который следует за конструктором (он также использовался в классе CMainFrame), позволяет создавать объект данного класса динамически, синтезируя его из данных, прочитанных из файла. Когда вы сохраняете объект SDI-документа, обрамляющее окно, содержащее представление, сохраняется наряду с вашими данными. Это позволяет потом все восстановить, прочтя обратно. Чтение и запись объекта документа в файл поддерживается процессом, называемым сериализацией. В последующих примерах, которые мы будет разрабатывать, вы увидите, как с помощью сериализации можно записывать свои собственные документы в файл и затем реконструировать их по данным этого файла.
Класс документа также содержит в своем определении макрос DECLARE MESSAGE- MAP (), чтобы при необходимости позволить обрабатывать сообщения Windows функциями-членами класса.
Класс представления в нашем SDI-приложении определен так, как показано ниже.
class CTextEditorView : public CEditView {
protected: // создавать только из сериализации
CTextEditorView();
DECLARE_DYNCREATE(CTextEditorView)
/ / Атрибуты
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;
#endif
protected:
// Сгенерированные функции отображения сообщений
protected:
DECLARE_MESSAGE_MAP() };
#ifndef —DEBUG // отладочная версия в TextEditorView.epp
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 () просто предоставляет связь с объектом документа. Вы можете вызывать любую из этих функций в интерфейсе класса документа, используя указатель на документ, возвращенный функцией.
По умолчанию отладочные возможности включены в вашу программу. Наряду со специальной версией Get Document (), в этом случае в код MFC включается множество разнообразных проверок. Если вы хотите изменить это, можете воспользоваться выпадающим списком в панели инструментов Build для выбора конфигурации версии, которая не содержит отладочного кода.
При компиляции вашей программы с включенной отладкой компилятор не обнаруживает неинициализированных переменных, так что может оказаться полезным иногда выполнять сборку рабочей версии даже на этапе тестирования вашей программы.
Предварительно скомпилированные заголовочные файлы
Первый раз, когда вы компилируете и компонуете программу, это занимает некоторое время. Второй и все последующие разы это должно происходить немного быстрее благодаря средству Visual C++ 2005, называемому предварительно скомпилированными заголовочными файлами (precompiled headers). Во время начальной компиляции компилятор сохраняет вывод от компиляции заголовочных файлов в специальном файле с расширением . pch. При последующих сборках этот файл используется повторно, если источники в заголовках не изменились, что позволяет сэкономить время на компиляцию заголовков.
Вы можете определить, используются ли предварительно скомпилированные заголовки, и управлять тем, как они обрабатываются, через вкладку Properties (Свойства). Щелкните правой кнопкой мыши на TextEditor и выберите в контекстном меню пункт Properties. Если вы развернете узел C/C++ в отображенном диалоговом окне, то сможете выбрать Precompiled Headers (Предварительно скомпилированные заголовочные файлы), чтобы установить это свойство.
Запуск программы
Чтобы выполнить программу, нажмите <Ctrl+F5>. Поскольку в качестве базового для CTextEditorView выбран класс CEditView, программа представляет полностью функционирующий простой текстовый редактор. Вы можете вводить текст в окне, как показано на рис. 12.11.
——
Untitled - TextEditor- □
File Edit View Help
D Й_В £5 6 § ?
My 7ather had a small estate m Nott ighamshire: I was the third of Г-ve a sons. He sent me to Fmanue College In Cambr dge at fourteen years a d, where I resided three years, and applied myse F dose to my stud es; but the charge of manta ^ ng me, although I had a very scanty a owance, being too great for a narrow fortune, I was bound apprent ce to Mir. James Bates, an eminent surgeon in London, w th whom I continued four years. My father now and then send ng me smaiJ sums of money, I la d them out in earning navgat on, and other parts of the mathemat cs, useful to those who intend to travel, as I v
Ready
Puc. 12.11. Работа программы TextEditor
Обратите внимание, что приложение имеет линейки прокрутки для просмотра текста за пределами видимой области внутри окна, и конечно, вы можете изменять размеры окна, перетаскивая его границы. Все пункты меню полностью функционируют, так что вы можете сохранять и загружать файлы, вырезать и вставлять текст, а
также печатать текст в окне — и все это без написания даже одной строки кода! При перемещении курсора над кнопками панели инструментов или над пунктами меню в панели состояния появляются сообщения, описывающие функции этих кнопок или пунктов меню, а если задержать курсор над кнопкой на секунду, всплывает подсказка, отображающая ее назначение. (Вы узнаете подробнее об этих всплывающих подсказках в главе 13.)
Как работает программа
Как и в тривиальном примере MFC-приложения, который рассматривался ранее в этой главе, объект приложения создается в глобальном контексте нашей SDI-npo- граммы. Вы можете видеть это, развернув элемент 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() {
H InitCommonControlsEx() требуется под Windows ХР, если манифест приложения // указывает на использование ComCtl32.dll версии 6 и последующих для разрешения Н визуальных стилей. В противном случае создание окна завершится неудачей. INITCOMMONCONTROLSEX InitCtrls;
InitCtrls.dwSize = sizeof(InitCtrls);
// Установите это для включения всех классов общих элементов управления, // которые хотите использовать в своем приложении.
InitCtrls.dwICC = ICC_WIN95_CLASSES;
InitCommonControlsEx(&InitCtrls);
CWinApp::Initlnstance();
II Инициализировать библиотеки OLE if (!AfxOlelnit())
{
AfxMessageBox(IDP_OLE_INIT_FAILED);
return FALSE;
}
AfxEnableControlContainer();
И Стандартная инициализация.
// Если вы не используете эти средства и желаете уменьшить размер финального // исполняемого файла, можете удалить специфические процедуры инициализации, /I в которых не нуждаетесь.
// Измените ключ реестра, под которым будут сохраняться настройки,
// 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 (1 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 () следующим оператором:
pDocTemplate = new CSingleDocTemplate(
IDR_MAINFRAME,
RUNTIME_CLASS(CTextEditorDoc) ,
RUNTIME_CLASS(CMainFrame), // главное обрамляющее окно SDI RUNTIME-CLASS(CTextEditorView));
Первый параметр конструктора CSingleDocTemplate — символ IDR_MAINFRAME — определяет меню и панель инструментов, используемую типом документа. Следующие три параметра определяют документа, главное обрамляющее окно и объекты класса представления, которые должны быть связаны вместе внутри шаблона документа. Поскольку здесь мы имеем дело с SDI-приложением, в программе присутствует только одно представление, управляемое через объект шаблона документа. RUNTIME_CLASS () — макрос, позволяющий определять тип объекта класса во время выполнения.
Здесь присутствует много чего другого для настройки экземпляра приложения, о чем вам беспокоиться не следует. Вы можете добавить в функцию InitInstance () свою собственную инициализацию, необходимую приложению.
Функция Run ()
Функция Run () в классе CTextEditorApp унаследована от базового класса приложения CWinApp. Поскольку функция объявлена как virtual, вы можете заменить ее версию из базового класса своей собственной, но обычно в этом нет необходимости, так что вам не нужно об этом беспокоиться.
Функция Run () получает все сообщения от Windows, предназначенные приложению, и обеспечивает доставку каждого из них соответствующей функции программы, которая, если она существует, должна его обработать. Таким образом, эта функция продолжается до тех пор, пока выполняется приложение. Она прекращает свою работу, когда вы закрываете приложение.
Таким образом, вы можете разделить работу приложения на четыре шага.
Создание объекта приложения the Арр.
Выполнение функции WinMain (), которую предоставляет MFC.
WinMain () вызывает Initlnstance (), которая, в свою очередь, создает шаблон документа, главное обрамляющее окно, документ и представление.
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.
Рис. 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.
Два пред-ставлениядокументаSketch2Два пред-ставлениядокументаSketch2
Два пред-ставлениядокументаSketch2
Два пред-ставлениядокументаSketch2
Два представления документа Sketch 1
Это создано экземпляром CMainFrame
Меню и панель инструментов созданы объектом CmainFrame, однако они разделяются
Создаются как экземпляры CChildFrame
Рис. 12.14. Работа приложения Sketcher с двумя активными документами
Резюме
В этой главе внимание было сосредоточено в основном на способах использования мастера 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- сайта издательства.
Что такое отношение между документом и представлением?
В чем состоит назначение шаблона документов в MFC-программе для Windows?
Почему следует проявлять внимательность и планировать структуру программы заранее при использовании мастера создания приложений?
Напишите код простой программы текстового редактора. Выполните сборку отладочной и рабочей версий и оцените типы и размеры файлов, полученных в обоих случаях.
Сгенерируйте приложений текстового редактора несколько раз, пробуя различные оконные стили в окне Advanced Options (Дополнительные параметры) мастера создания приложений.
13
Работа с меню и панелямиинструментов
В предыдущей главе было показано, как устроено простое каркасное приложение, сгенерированное мастером MFC Application Wizard, и как взаимодействуют его части. В настоящей главе мы начнем изменять каркасное MDI-приложение под названием Sketcher, постепенно превращая его в полезную программу. Первый шаг на этом пути — разобраться, как в Visual C++ 2005 определяются меню и как создаются функции для обслуживания специфичных для приложения пунктов меню, которые вы добавляете к своей программе. Вы также увидите, как добавить к приложению кнопки панели инструментов. В этой главы вы изучите следующие вопросы.
Как программы на основе MFC обрабатывают сообщения.
Ресурсы меню, их создание и модификация.
Свойства меню, их создание и модификация.
Как создать функцию для обслуживания сообщения, генерируемого при выборе пункта меню.
Как добавить обработчики для обновления свойств меню.
Как добавить кнопки панели инструментов и ассоциировать их с имеющимися пунктами меню.
Взаимодействие с Windows
Как было показано в главе 11, операционная система Windows общается с вашей программой, посылая ей сообщения. Большую часть рутинной работы по обработке сообщений берет на себя MFC, так что вам вообще не нужно беспокоиться о создании функции WndProc (). Библиотека MFC позволяет предоставить функции для обработки индивидуальных сообщений, в которых вы заинтересованы, и игнорировать все
остальные. Эти функции мы будем называть обработчиками сообщений, или просто обработчиками. Поскольку ваше приложение построено на базе MFC, обработчик сообщений — это всегда функция-член одного из классов вашего приложения.
Ассоциация между определенным сообщением и функцией вашей программы, предназначенной для его обработки, устанавливается картой сообщений — каждый класс в вашей программе, который может обрабатывать сообщения Windows, имеет такую карту. Карта сообщений класса — это просто таблица функций-членов, обрабатывающих сообщения Windows. Каждая позиция в карте сообщений ассоциирует функцию с определенным сообщением; когда данное сообщение поступает, вызывается соответствующая функция. В карте сообщений класса присутствуют только те сообщения, что имеют отношение к этому классу.
Карта сообщений класса создается автоматически мастером MFC Application Wizard, когда вы создаете проект, или мастером Add Class Wizard, когда вы добавляете класс, обрабатывающий сообщения, в свою программу. Добавления и удаления позиций в карте сообщений в основном осуществляется с помощью мастера Class Wizard, но бывают случаи, когда приходится модифицировать карту сообщений вручную. Начало карты сообщений в вашем коде отмечается макросом BEGIN_MESSAGE_MAP (), а его конец — макросом END_MESSAGE_MAP ().
Давайте посмотрим, как работает карта сообщений на основе примера Sketcher.
Что такое карты сообщений?
Карта сообщений устанавливается мастером MFC Application Wizard для каждого из основных классов вашей программы. В экземпляре MDI-программы, такой как Sketcher, карта сообщений определена для каждого из классов СSketcherАрр, CSketcherDoc, CSketcherView, CMainFrame и CChildFrame. Вы можете видеть карту сообщений для класса в файле . срр, содержащем его реализацию. Конечно, функции, включенные в карту сообщений, также должны быть объявлены в определении класса, но они идентифицируются здесь специальным образом. Взгляните на приведенное ниже определение класса CSketcherApp.
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, который вы будете использовать для добавления ново-
го класса в проект, в зависимости от того, что именно отвечает за его создание. На рис. 13.1 показаны классы MFC, унаследованные от CCmdTarget, который применялся в наших примерах до сих пор.
Классы, используемые непосредственно, либо прямые предки классов нашего приложения, выделены полужирным. Таким образом, CSketcherApp имеет CCmdTarget как непрямой базовый класс, и потому всегда включает макрос DECLARE_MESSAGE_MAP (). Все классы представлений (и прочие), унаследованные от CWnd, также имеют его.
Рис. 13.1. Классы MFC, унаследованные от CCmdTarget
Если вы добавляете в класс собственные члены непосредственно, то лучше оставить DECLARE_MESSAGE_MAP () в последней строке определения класса. Если вы будете добавлять члены после DECLARE_MESSAGE_MAP (), вам придется указывать спецификаторы доступа для них: public, protected или private.
Определения обработчиков сообщений
Если определение класса включает макрос DECLARE_MESSAGE_MAP (), то реализация класса должна содержать BEGIN_MESSAGE_MAP () и END_MESSAGE_MAP (). Если вы посмотрите на Sketcher.срр, то увидите следующий код, представляющий часть реализации CSketcherApp:
BEGIN_MESSAGE_MAP(CSketcherApp, CWinApp)
ON_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 () указывают границы карты сообщений, и каждый из обработчиков сообщений появляется в классе между этими двумя макросами. В предыдущем случае код обрабаты-
вает только одну категорию сообщений — тип сообщений 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 — это идентификатор, соответствующий выбору пункта меню File^ New (Фай л ^Создать), a ID_APP_ABOUT соответствует пункту меню Help1^ About (Справка^О программе).
Есть и другие символы помимо WM_COMMAND, которые Windows использует для идентификации стандартных сообщений. Каждое из них снабжено префиксом WM_, что означает “Windows Message” (“сообщение Windows ”). Эти символы определены в Winuser. h, который включен в Windows . h. Если вы хотите ознакомиться с ними, то найдете Winuser .h в одной из вложенных в VC папок, содержащих установленную систему Visual C++ 2005.
Существует достаточно симпатичный способ просмотра файла .h. Если имя такого файла присутствует в акне редактора, вы можете щелкнуть на нем правой кнопкой мыши и выбрать в контекстном меню пункт Open Document "Filename, h" (Открыть документ "Filename.h").
Сообщения Windows часто содержат дополнительные данные, используемые для уточнения идентификации определенного сообщения, специфицированного заданным ID. Сообщение WM_COMMAND, например, посылается для большого диапазона команд, включая те, что вызваны выбором пунктов меню или щелчками на кнопках панели инструментов.
Следует отметить, что когда вы добавляете обработчики сообщений вручную, то не должны отображать сообщение (или в случае командных сообщений — идентификатор команды) на более чем один обработчик в классе. Если вы это сделаете, ничего не плохого не произойдет, но второй обработчик никогда не будет вызван. Обычно обработчики сообщений добавляются через окно свойств, и в таком случае отобразить сообщение на более чем один обработчик нельзя. Если вы хотите увидеть окно свойств класса, щелкните правой кнопкой мыши на имени класса в панели Class View
(Представление классов) и выберите Properties (Свойства) из контекстного меню. Затем вы добавляете обработчик сообщений, выбирая кнопку Messages (Сообщения) в верхней части окна Properties (рис. 13.2). Определить, какая кнопка является кнопкой Messages, можно, наводя курсор мыши на каждую из кнопок и прочитав всплывающую подсказку.
Щелчок на кнопке Messages вызывает список идентификаторов сообщений; однако прежде чем двинуться дальше, я должен рассказать немного больше о типах сообщений, которые вы можете обрабатывать.
▼ 1= X▼ 1= XProperties
▼ 1= X
▼ 1= X
CM □inFraire VCCode Class
В CMainFrame
WM_ACnVATE WM_ACTT/ATEAPP VJM_ASKCBFORMATЦA^ WM_CANCELMODE
WM_CAPTU RECHAN G ED WM_CHANGECB CHAIN WM_CHANG EUISTATE WM_CHAR
WM_CHARTOITEM WM_CHHD ACTIVATE WM_CLOSE
WM_COMPACTING WM_COMPAREIFEM WM_CO NT EXTMENU WM_COPYDATA
WM_CREATEOnCreate
WM_CTLCOLOR
WM_DE4DCHAR WM_DELETEITEM WM_DESTROY WM_DESTROYCLIPBOAR WM_DEVMO DECHANGE WM_D RAWCLIPBOARD
CMainFrame
Puc. 13.2. Окно Properties (Свойства) для сообщений
Категории сообщений
Существуют три категории сообщений, с которыми может иметь дело ваша программа, и категория, к которой оно относится, определяет то, как это сообщение обрабатывается. Категории сообщений перечислены в табл. 13.1.
Стандартные сообщения Windows из первой категории отмечены идентификаторами, снабженными префиксом WM_, и определены в Windows. Мы напишем обработчики для некоторых из этих сообщений в следующей главе. Сообщения из второй категории — особая группа сообщений WM_COMMAND, с которыми вы познакомитесь в главе 16, когда речь пойдет о диалоговых окнах. С сообщениями из последней категории, поступающими от пунктов меню и панели инструментов, мы будем иметь дело в настоящей главе. В дополнение к идентификаторам сообщений, определенным MFC для стандартных меню и панелей инструментов, вы можете определять собственные идентификаторы для пунктов меню и кнопок панели инструментов, специфичных для вашей программы. Если вы не предусмотрите идентификаторов для них, то MFC автоматически сгенерирует собственные, на основе текста соответствующих пунктов меню.
Таблица 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, оно обрабатывается обработчиком по умолчанию, определенным в базовом классе. Если вы предусматриваете обработчик в своем классе, то иногда все равно нуждаетесь в вызове обработчика базового класса, чтобы правильно обработать сообщение. При создании своего собственного обработчика его скелетная реализация предоставляется, когда вы выбираете нужный обработчик в окне свойств класса, и эта реализация включает вызов базового обработчика, когда это необходимо.
Обработка командных сообщений построена более гибко. Вы можете поместить такие обработчики в класс приложения, в класс документа и в класс шаблона документа, и, конечно, в классы представлений и окна вашей программы. Но что случится, если командное сообщение отправлено вашему приложению, и при этом есть множество мест, где оно может быть обработано?
Как обрабатываются командные сообщения
Все командные сообщения поступают в главное обрамляющее окно приложения. Главное обрамляющее окно затем пытается обработать сообщение, маршрутизируя его в специфической последовательности в классы вашей программы. Если один класс не может обработать сообщение, оно передается следующему.
Последовательность, в которой классы получают возможность обработать командное сообщение, для SDI-программы описана ниже.
Объект представления.
Объект документа.
Объект шаблона документа.
Объект главного обрамляющего окна.
Объект приложения.
Объект представления первым получает возможность обработать командное сообщение, и если в нем не определено никакого обработчика, следующий объект класса получает шанс его обработать. Если ни один из классов не имеет определенного обработчика, осуществляется обработка Windows по умолчанию, что, по сути, означает, что сообщение отбрасывается.
Для MDI-программы все лишь ненамного сложнее. Хотя существует возможность наличия множества документов, каждый с множеством представлений, в маршрутизацию командного сообщения вовлечены только активное представление и ассоциированный с ним документ. Последовательность маршрутизации командного сообщения в MDI-программе описана ниже.
Активный объект представления.
Объект документа, ассоциированного с активным представлением.
Объект шаблона документа для активного документа.
Объект обрамляющего окна активного представления.
Объект главного обрамляющего окна.
Объект приложения.
Можно изменить последовательность маршрутизации сообщений, однако подобная необходимость возникает настолько редко, что в этой книге она не рассматривается.
Расширение программы Sketcher
Теперь добавим код к программе Sketcher, созданной в предыдущей главе, для реализации функциональности, необходимой для создания рисунков. Нужно будет предоставить код для рисования линий, окружностей, прямоугольников и кривых с различным цветом и толщиной линий, а также для добавления аннотаций к рисунку. Данные рисунка будут храниться в документе и нужно будет обеспечить возможность иметь несколько представлений одного и того же документа в разных масштабах.
Для представления всего необходимого при реализации всех этих возможностей потребуется несколько глав, но хорошей стартовой точкой будет добавление элементов меню для выбора типов элементов, которые вы собираетесь рисовать, а также для выбора цвета рисования. Мы сделаем постоянными выбранный тип элемента и цвет, чтобы то и другое оставалось в силе до тех пор, пока вы не измените его.
Для того чтобы добавить пункты меню к программе Sketcher, необходимо выполнить следующие шаги.
Определить элементы меню, которые должны появиться в панели главного меню, а также каждое из их подменю.
Решить, какие классы вашего приложения должны обрабатывать сообщения для каждого пункта меню.
Добавить функции обработки сообщений меню в соответствующие классы.
Добавить в классы функции для обновления внешнего вида меню, чтобы показать текущие выбранные установки.
Для каждого нового элемента меню добавить соответствующую кнопку в панель инструментов, снабженную всплывающей подсказкой.
Элементы меню
Мы рассмотрим два аспекта обращения с меню в MFC: создание и модификация внешнего вида меню в приложении и обработка выбора определенного пункта меню, то есть определение обработчика сообщений для него. Сначала посмотрим, как создаются новые пункты меню
Создание и редактирование ресурсов меню
Меню определены вне кода программы — в ресурсном файле, и спецификации меню называют ресурсом. Существует еще несколько других типов ресурсов, которые можно включить в приложение; типичными примерами являются диалоги, панели инструментов и кнопки этих панелей. С большинством из них вы познакомитесь в процессе усовершенствования приложения Sketcher.
Наличие определения меню в ресурсах позволяет изменять физическое представление меню, не затрагивая кода, обрабатывающего события этого меню. Например, вы можете перевести пункты меню с английского на русский, французский, норвежский или любой другой язык, без необходимости модификации или перекомпиляции кода программы. Код, обрабатывающий сообщения, поступающие от пользовательского выбора пунктов меню, никак не зависит от того, как выглядит пункт меню, а только от того факта, что он был выбран. Конечно, если вы добавляете элементы в меню, то должны добавить и некоторый код для каждого из них, дабы они действительно делали что-то полезное!
Наша программа Sketcher уже имеет меню, а это означает, что она уже имеет ресурсный файл. Доступ к содержимому ресурсного файла программы Sketcher можно получить, выбрав панель Resource View (Представление ресурсов) или, если у вас уже отображается панель Solution Explorer, дважды щелкнув на Sketcher. гс. Это переключит вас на Resource View, где отображаются ресурсы. Если вы развернете ресурс меню, щелкнув на символе +, то увидите, что там определено два меню, помеченные идентификаторами IDR_MAINFRAME и IDR SketcherTYPE. Первое из них применяется, когда в приложении нет открытых документов, а второе — когда открыт хотя бы один документ. Библиотека MFC использует префикс IDR_ для идентификации ресурса, определяющего полное меню для окна.
Вы будете модифицировать только меню, имеющее идентификатор IDR_ SketcherTYPE. Вам незачем обращать внимание на IDR MAINFRAME, поскольку ваши новые пункты меню имеют смысл только в том случае, когда документ открыт. Вы
можете вызвать редактор ресурса для меню двойным щелчком на его ID в панели Resource View. Если сделать это с IDR_SketcherTYPE, появится панель редактора, показанная на рис. 13.3.
.St#' сЬчг.гс (IDR.. Л h- ~¥Pf - Menu)File I Editviewwindew Help
Newari+H
Open...Ctri*O
£lcse 5 weCtrl+5
Saw As..
Erint,Qrl+P
Print Prey few Print Setjp. .
Recent file
Egt
Puc. 13.3. Панель редактора
Добавление пункта к панели меню
Чтобы добавить новый элемент меню, вы просто щелкаете на рамке меню с текстом Туре Неге (Вводите здесь), чтобы выбрать его, и затем вводите его имя. Если вы введете символ амперсанда (&) перед буквой в элементе меню, то эта буква будет идентифицирована как “горячая клавиша” для вызова этого меню с клавиатуры. Введите в качестве первого пункта меню E&lement (Элемент). Этим вы выберете 1 как букву сокращенного вызова, так что сможете вызывать элемент меню, нажимая комбинацию <Alt+l>. Вы не можете использовать Е, поскольку она уже задействована для пункта Edit (Правка). Завершив ввод имени меню, вы можете выполнить двойной щелчок на новом пункте меню, чтобы отобразить его свойства, как показано на рис. 13.4.
Свойства — это просто параметры, определяющие, как пункт меню будет выглядеть и вести себя. На рис. 13.4 показаны свойства пункта меню, сгруппированные по категориям. Если вы хотите, чтобы они отображались в алфавитном порядке, просто щелкните на второй кнопке слева. Обратите внимание, что свойство Popup установлено в True по умолчанию; это объясняется тем, что новый элемент меню находится на вершине панели меню, поэтому обычно при выборе он представляется в виде всплывающего меню. Щелчок на любом свойстве в левой колонке позволяет модифицировать его значение в правой колонке. В данном случае вы хотите оставить все, как есть, так что можете просто закрыть окно Properties. Никакого идентификатора для пункта всплывающего меню не требуется, потому что его выбор просто отображает меню, находящееся под ним, и нет никакого события, которое нужно было бы обрабатывать. Заметьте, что при этом вы получаете новый пустой пункт для первого элемента всплывающего меню — точно такой же, как и в панели меню.
Было бы лучше, чтобы меню Element появилось между меню View (Вид) и Window (Окно), поэтому поместите курсор на пункте Element и, не отпуская левую кнопку мыши, перетащите его в позицию между элементами меню View и Window и отпустите кнопку. После того, как будет изменена позиция нового элемента меню Element,
следующий шаг состоит в наполнении соответствующего ему всплывающего меню новыми пунктами.
Properties^ - X
Menu Editor IMenuEd
3
Appearance
Caption
E&lement
Checked
False
Enabled
True
Grayed
False
Popup
True
я
Behavior
Break
None
Right Justify
False
Right Order
False
3
Mise
nu Ec
Help
False
П I
F
(Наше)
this is the name.
Puc. 13.4. Свойства нового пункта меню
Добавление пунктов к меню Element
Выберите первый пункт (помеченный в данный момент как Туре Неге) во всплывающем меню, щелкнув на нем; затем введите &Line (Линия) в качестве заголовка и нажмите клавишу <Enter>. Выполнив двойной щелчок на этом первом элементе всплывающего меню, можно просмотреть его свойства (рис. 13.5).
Свойства модифицируют внешний вид пункта меню, а также специфицируют идентификатор сообщения, передаваемого вашей программе при выборе этого пункта. Здесь мы видим уже определенный пункт ID ELEMENT LINE, но вы можете изменить его на что-либо другое по своему желанию. Иногда удобно специфицировать идентификатор самостоятельно, например, когда сгенерированный получается слишком длинным, либо его смысл не ясен. Если вы решите определить свой собственный ID, то должны следовать соглашению, принятому в МЕС относительно префиксов ID_, чтобы указать, что это — идентификатор команды для пункта меню.
Поскольку этот элемент — часть всплывающего меню, свойство Popup по умолчанию установлено в False. Вы можете сделать его еще одним всплывающим меню со своим списком элементов, установив свойство Popup равным True. Как видно на рис. 13.5, допустимые значения свойства Popup можно отобразить, выбрав стрелку вниз. Разве вам не нравится такой способ вкладывать всплывающие меню друг в друга?
Вы можете ввести текстовую строку в качестве значения свойства Prompt, и когда пункт меню подсвечен, эта строка появляется в панели состояния вашего приложения. Если вы оставите его пустым, в панели состояния не появится ничего. Я советую вам ввести Line в качестве значения свойства Prompt. Обратите внимание, что
вы получаете краткую подсказку о назначении выбранного свойства в нижней части окна свойств. Вы хотите, чтобы элементом рисования, выбранным по умолчанию при запуске приложения, была линия, поэтому можете установить значение свойства Checked в True, чтобы получить метку возле данного пункта меню, что сообщит о том, что выбран режим Line. Не забудьте добавить код для обновления меток пунктов меню при выборе другого элемента для рисования. Свойство Break может изменить внешний вид всплывающего меню, сдвигая пункт в новую колонку. Здесь это вам не понадобится, так что оставьте все, как есть. Закройте окно Properties, чтобы сохранить установленные значения.
Properties
Menu Editor IMenuEd
^ Al 1
В AppearanceCaption CheckedEnabled GrayedPopupВ BehaviorBreakRight JustifyRight OrderВ MiseameHelp ID PromptSeparatorВ AppearanceCaption CheckedEnabled GrayedPopupВ BehaviorBreakRight JustifyRight OrderВ MiseameHelp ID PromptSeparator&LineFalse TrueFalse&LineFalse TrueFalseFalsevTrueFalse тчтз jFalsevTrueFalse тчтз jFalseMenu EunurFalseID_ELEMENT_LINEFalseFalseMenu EunurFalseID_ELEMENT_LINEFalse
В Appearance
Caption Checked
Enabled Grayed
Popup
В Behavior
Break
Right Justify
Right Order
В Mise
ame
Help ID Prompt
Separator
В Appearance
Caption Checked
Enabled Grayed
Popup
В Behavior
Break
Right Justify
Right Order
В Mise
ame
Help ID Prompt
Separator
&Line
False True
False
&Line
False True
False
Falsev
True
False тчтз j
Falsev
True
False тчтз j
False
Menu Eunur
False
ID_ELEMENT_LINE
False
False
Menu Eunur
False
ID_ELEMENT_LINE
False
Popup
Specifies that the menu item can contain menu items and submenus.
Puc. 13.5. Свойства элемента всплывающего меню Une
Модификация существующих пунктов меню
Если вы обнаружите, что допустили ошибку и захотите изменить существующий элемент меню, или просто вам понадобится проверить корректность установок свойств, то вернуться к элементу меню очень легко. Просто выполните двойной щелчок на элементе, который вас интересует, и будет отображено окно его свойств. Вы можете изменить любые из них и закрыть окно, когда все будет сделано. Если необходимый вам пункт находится во всплывающем меню, которое не отображено в данный момент, просто щелкните на элементе в панели меню, чтобы отобразить требуемое всплывающее меню.
Завершение меню
Теперь можно двигаться дальше и создать остальные элементы всплывающего меню Element, которые вам нужны: & Rectangle (Прямоугольник), &Circle (Окружность) и Cur&ve (Кривая). Конечно, все они должны иметь свойство Checked,
равное False. Вы не можете использовать С в качестве горячей клавиши последнего элемента, поскольку эти клавиши должны быть уникальными, а С уже назначено пункту меню Circle. Можно принять наименования идентификаторов по умолчанию: ID_ELEMENT_RECTANGLE, ID_ELEMENT_CIRCLE и ID_ELEMENT_CURVE. Можно также установить значения свойства Prompt для этих пунктов соответственно в Rectangle, Circle и Curve.
Вам еще понадобится меню Color в панели меню — для выбора цвета рисования — в виде всплывающего меню с пунктами Black, Red, Green и Blue. Вы можете создать его, начиная с пустого пункта в панели меню, используя ту же процедуру, что описана выше. Установите метку (Checked) на пункт Black, чтобы цветом по умолчанию был черный. Для пунктов нового меню вы можете использовать идентификаторы, созданные по умолчанию (ID_COLOR_BLACK и так далее). Можете также добавить к каждому пункту подсказки для показа в панели состояния. После того, как все будет готово, перетащите пункт Color в место, находящееся справа от Element, чтобы меню выглядело, как показано на рис. 13.6.
Обратите внимание, что следует позаботиться о том, чтобы одна и та же буква не использовалась более одного раза в качестве горячей клавиши во всплывающем меню или в главном меню. При создании новых пунктов меню никакой проверки “двойников” не выполняется, но если по завершении редактирования вы щелкнете правой кнопкой мыши, установив курсор на панель меню, то получите всплывающее меню, содержащее пункт Check Mnemonics (Проверить мнемонические сокращения). Выбор его запустит проверку всего меню на предмет наличия дублированных горячих клавиш. Неплохо бы выполнять такую проверку после каждого редактирования меню, поскольку очень легко непреднамеренно наплодить двойников.
На этом завершается расширение меню добавления элементов и цветов. Не забудьте сохранить файл, убедившись, что внесенные дополнения надежно сохранены. Далее потребуется решить, какие классы должны будут обрабатывать сообщения от новых пунктов меню, и добавить в них соответствующие функции-члены для обработки каждого сообщения. Для этого используется мастер добавления обработчиков событий (Event Handler Wizard).
S!:etcher.rc (ИЖ... herTYPE - Menu )*▼ X ।
File Edit View Element Color Window Help
[ Type Here ]у Black J
Red
Green
Blue
Puc. 13.6. Внешний вид завершенного меню
Добавление обработчиков сообщений меню
Чтобы создать обработчик пункта меню, щелкните правой кнопкой мыши на нужном пункте и выберите из появившегося контекстного меню пункт Add Event Handier (Добавить обработчик событий). Если вы сделаете это с пунктом меню 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
Эти сообщения генерируются, когда меню должно быть обновлено — установлена или снята отметка, например, в зависимости от его состояния. Это сообщение возникает перед отображением всплывающего меню, так что вы можете настроить внешний вид пунктов меню перед тем, как пользователь увидит их.
Это работает довольно просто. Когда вы щелкаете на отдельном пункте в панели меню, посылается сообщение UPDATE_COMMAND_UI для каждого элемента меню до того, как оно будет отображено. Это дает возможность выполнить любое необходимое обновление свойств пунктов меню перед тем, как пользователь их увидит. Когда эти сообщения обработаны, и любые изменения свойств элементов завершены, меню отображается. Когда затем вы щелкаете на одном из пунктов меню, посылается сообщение COMMAND для этого пункта. Сначала поговорим о сообщении COMMAND, а к UPDATE_COMMAND_UI в этой главе вернемся чуть позже.
Поскольку события в пунктах меню приводят к выдаче командных сообщений, вы можете выбрать в качестве места их обработки любой из классов, определенных в данный момент в приложении Sketcher. Но как решить, где именно следует обрабатывать сообщение пункта меню?
Выбор класса для обработки сообщений меню
Прежде чем вы сможете решить, какой класс должен обрабатывать сообщения добавленных вами пунктов меню, необходимо решить, что вы хотите делать с этими сообщениями.
Нужно, чтобы тип элемента и выбранный цвет были модальными, то есть установленный текущий элемент и цвет должны сохраняться до тех пор, пока они не будет изменены. Это позволит вам создать столько синих окружностей, сколько вы захотите, а когда вам понадобятся красные, вы просто смените цвет. Существуют две основные возможности обработки установки цвета и выбора типа элемента: установка их представлением или документом. Если устанавливать представлением, то поскольку у одного документа может быть несколько представлений, у каждого представления будет свой текущий цвет и элемент. Это значит, что вы сможете рисовать красные окружности в одном представлении, переключиться на другое и обнаружить, что там вы рисуете синие прямоугольники. Это приводит к путанице и вряд ли соответствует тому, как вы хотите, чтобы они работали.
Поэтому лучше иметь текущий цвет и элемент на уровне документа. Тогда вы, переключаясь от одного представления на другое, продолжите рисовать те же элементы тем же цветом. Могут существовать другие отличия между представлениями вроде масштаба отображения документа, но все же операция рисования будет согласована между представлениями.
Это говорит о том, что лучше сохранять текущий цвет и элемент в объекте документа. Тогда они будут доступны объектам представлений, ассоциированным с объектом документа. Конечно, если у вас одновременно активно более одного документа, то каждый документ должен иметь свои собственные установки цвета и элемента. Поэтому имеет смысл обрабатывать сообщения от ваших новых пунктов меню в классе С Sketcher Do с и сохранять информацию о текущем выборе в объекте этого класса. Я думаю, что вы готовы приступить к созданию обработчика для пункта меню Black.
Создание функций сообщений меню
Выберите имя класса CSketcherDoc в диалоговом окне мастера Event Handler Wizard, щелкнув на нем. Также потребуется щелкнуть на типе сообщений COMMAND. Затем вы можете щелкнуть на кнопке Add and Edit (Добавить и редактировать). При этом диалоговое окно закроется, а код созданного обработчика в классе CSketcherDoc будет отображен в окне редактора. Функция выглядит следующим образом:
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:
afx__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 ()
public:
afx_msg void OnColorBlack () ;
afx_msg void OnColorRed () ;
afx_msg void OnColorGreen() ;
afx_msg void OnColorBlue () ;
afx_msg void OnElementLine () ;
af x__msg void OnElementRectangle ();
afx__msg void OnElementCircle () ;
afx_msg void OnElementCurve() ;
}
Как видите, для каждого из обработчиков, специфицированных вами в диалоговом окне мастера Event Handler Wizard, добавляется объявление. Каждое из объявлений функций снабжено префиксом af х_msg, указывающим на то, что это — обработчик сообщений.
Мастер Event Handler Wizard также автоматически обновляет карту сообщений в реализации вашего класса CSketcherDoc, добавляя в нее все новые обработчики. Если вы заглянете в 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_RECTANGLEf OnElementRectangle)
ON_COMMAND(ID_ELEMENT_CIRCLE, OnElementCircle)
ON_COMMAND(ID_ELEMENT_CURVE, OnElementCurve)
END_MESSAGE_MAP()
Мастер Event Handler Wizard добавил макрос 0N_COMMAND () для каждого обработчика, который вы идентифицировали. Это ассоциирует имя обработчика с идентификатором сообщения, поэтому, например, функция-член OnColorBlack () вызывается для обработки сообщения COMMAND для пункта меню с идентификатором ID_COLOR_BLACK.
Каждый из обработчиков, сгенерированных Event Handler Wizard, является, по сути, скелетом. Например, взгляните на код OnColorBlue (). Он также находится в файле SketcherDoc.срр, и вы найдете его там, прокрутив текст вниз, или переключившись на Class View и дважды щелкнув на имени функции после разворачивания дерева для класса CSketcherDoc (только сначала убедитесь, что файл сохранен).
void CSketcherDoc::OnColorBlue ()
// TODO: добавьте сюда код обработчика команды
}
Как видите, обработчик не принимает аргументов и ничего не возвращает. На данный момент он вообще ничего не делает, и в этом нет ничего удивительного, поскольку Event Handler Wizard не знает, что вы собираетесь делать с этими сообщениями!
Кодирование функций сообщений меню
Теперь подумаем, что вы должны делать с сообщениями COMMAND для новых пунктов меню. Я уже говорил, что если вы хотите записывать текущий элемент и цвет в документе, то для того и другого потребуется добавить данные-члены в класс CSketcherDoc.
Добавление членов для хранения режимов цвета и элемента
Вы можете добавить необходимые данные-члены в определение класса CSketcherDoc, просто непосредственно редактируя существующее определение, но давайте лучше воспользуемся мастером добавления переменной-члена (Add Member Variable Wizard). Отобразите диалоговое окно мастера, щелкнув правой кнопкой мыши на имени класса CSketcherDoc в Class View и затем выбрав Add^Add Variable (Добавить1^ Добавить переменную) из появившегося контекстного меню. После этого вы увидите диалоговое окно мастера, показанное на рис. 13.8.
d ember variable v izard - S etc sr
Access:Access:intintVariable name:Variable name:protectedVariable type:protectedVariable type:C iroi vpe.C iroi vpe.Min value:Min value:Comment C7 notation not required):Comment C7 notation not required):Category.ControlCategory.ControlWelcome to the Add Member Variable Wizard
Access:
Access:
int
int
Variable name:
Variable name:
protected
Variable type:
protected
Variable type:
C iroi vpe.
C iroi vpe.
Min value:
Min value:
Comment C7 notation not required):
Comment C7 notation not required):
Category.
Control
Category.
Control
Current element type
FinishFinishCancel
Finish
Finish
Puc. 13.8. Мастер добавления переменной-члена
Я уже вводил информацию в диалоговое окно для переменной m Element, хранящей текущий тип элемента, который должен быть нарисован. Я выбрал для него модификатор доступа protected, потому что он не должен быть доступен непосредственно извне класса. Я также выбрал для него тип unsigned int, поскольку для идентификации каждого типа элемента используются положительные целые числа. Когда вы щелкаете на кнопке Finish (Готово), переменная добавляется в определение класса в файле CSketcherDoc .h.
Добавим вручную член класса CSketcherDoc для хранения цвета элемента — просто чтобы показать, что это возможно. Его именем будет m_Color, типом — COLORREF, определенный в Windows API для представления цвета как 32-разрядного целого. Вы можете добавить объявление члена m_Color в класс CSketcherDoc следующим образом:
class CSketcherDoc : public CDocument
{
II Сгенерированные функции отображения сообщений
protected:
DECLARE MESSAGE МАР()
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:
11 Текущий тип элемента
unsigned int m_Element;
COLORREF m__Color; // Текущий цвет рисования };
Член m Color также является protected, поскольку нет смысла открывать к нему доступ извне. Вы всегда можете добавить функции для обращения или изменения значений защищенных или приватных членов класса, пользуясь тем преимуществом, что можно сохранить полный контроль над тем, какие значения могут быть установлены.
Инициализация новых данных-членов класса
Теперь вы должны решить, как представлять тип элемента. Можно просто установить в m Element уникальное числовое значение, но это будет означать введение в программу “магического числа”, значение которого будет менее очевидно любому, кто станет читать код. Лучше определить набор констант, которые можно будет использовать для установки значений переменной-члена m 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) соответствует черному цвету, поскольку
в нем нет ни красной, ни зеленой, ни синей составляющих. RGB (255,0,0) создает значение цвета с максимальным значением красного компонента при полном отсутствии зеленого и синего. Все прочие цвета вы можете создать путем комбинирования красной, зеленой и синей компонент.
Эти константы нужно куда-то поместить, поэтому давайте создадим новый заголовочный файл и назовем его OurConstants . h. Создать новый файл можно, щелкнув правой кнопкой мыши на папке Header Files (Заголовочные файлы) на вкладке Solution Explorer и выбрав в контекстном меню Add^Add New Item (Добавить^ Добавить новый элемент). Введите в диалоговом окне имя заголовочного файла OurConstants, затем щелкните на кнопке Open (Открыть). После этого вы сможете ввести определения констант в окне редактора, как показано ниже.
// Определения констант
#pragma once
// Определения типов элементов.
// Каждое значение типа должно быть уникальным
const unsigned int LINE = 101U;
const unsigned int RECTANGLE = 102U;
const unsigned int CIRCLE = 103U;
const unsigned int CURVE = 104U;
III / III / III III III III III IIIШ / / H //
// Значения цвета для рисования
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 только в том случае, если они не были включены ранее. После того, как заголовок включен в файл, он уже не будет включен снова.
После сохранения заголовочного файла вы можете добавить оператор #includee начало файла Sketcher.h:
#include "OurConstants.h"
Любому файл . срр, содержащему директиву #include для Sketcher. h, будут доступны константы.
Вы можете проверить, что новые константы стали частью проекта, развернув ветку Global Functions and Variables (Глобальные функции и переменные) на вкладке Class View. Если все в порядке, вы увидите там имена цветов и типов элементов наряду с глобальной переменной theApp.
Модификация конструктора класса
Важно убедиться, что данные-члены, которые вы добавили в класс CSketcherDoc, правильно инициализируются при создании документа. Вы можете добавить код инициализации к конструктору класса, как показано ниже:
CSketcherDoc: :CSketcherDoc() : mJElement (LINE) , m_Color (BLACK)
{
// TODO: добавьте сюда код конструирования объекта
}
Мастер уже решил, что член m Element необходимо инициализировать нулем, поэтому исправьте начальное значение на LINE. Затем нужно будет добавить инициализатор для члена m Color со значением BLACK, чтобы все соответствовало определенным вами меткам в пунктах меню.
Теперь вы готовы к добавлению кода в функции-обработчики для пунктов меню Element и Color. Это можно сделать на вкладке Class View. Щелкните на имени первой функции-обработчика — OnColorBlack (). Вам понадобится добавить всего одну строку к этой функции, так что код станет выглядеть так:
void CSketcherDoc::OnColorBlack()
{
mjColor = BLACK; // Установить черный цвет рисования
}
Единственное, что должен сделать обработчик — установить соответствующий цвет. В интересах согласованности, новая строка кода заменяет комментарий (// TODO:), который был представлен первоначально. Затем вы можете аналогичным образом добавить новые строки во все обработчики пунктов меню.
Обработчики пунктов меню элементов очень похожи. Так, обработчик для пункта Elements Line (Элемент^Линия) принимает следующий вид.
void CSketcherDoc::OnElementLine()
{
m_Element = LINE; // Установить тип элемента - линию
}
В этой модели несложно написать и другие обработчики для пунктов меню Element. Таким образом, у вас будут готовы восемь обработчиков сообщений. Теперь можно заново собрать пример и посмотреть, как он работает.
Запуск расширенного примера
Если только не было допущено опечаток, компиляция и компоновка программы должна пройти без ошибок. Когда вы запустите программу, то увидите окно, показанное на рис. 13.9.
Рис. 13.9. Робота программы Sketcher
Как видите, новые меню расположены в правильных местах панели меню, к ним добавлены все необходимые пункты, и вы можете видеть сообщения Prompt в панели состояния в нижней части главного окна, когда курсор мыши находится на пункте меню. Вы также можете проверить, что комбинации клавиш <Alt+C> и <Alt+l> работают корректно. Что не работает, как надо — так это отметки текущего выбранного цвета и элемента, которые остаются на своих начальных позициях. Посмотрим, как это можно исправить.
Добавление обработчиков сообщений для обновления пользовательского интерфейса
Чтобы корректно устанавливать отметки новых пунктов меню, к каждому такому пункту вам придется добавить обработчик сообщений другого вида — UPDATE— COMMAND_UI (означает команду обновления пользовательского интерфейса).
Обработчики сообщений этого типа специально предназначены для обновления свойств пунктов меню непосредственно перед их отображением.
Вернемся к просмотру файла Sketcher.гс в окне редактора. Щелкните правой кнопкой мыши на элементе Black в меню Color и выберите из контекстного меню пункт Add Event Handler (Добавить обработчик событий). Затем можете выбрать UPDATE—СOMMAND_UI в качестве типа сообщения и класс CSketcherDoc (рис. 13.10).
Рис. 13.10. Добавления обработчика сообщений UPDATE_COMMAND__UI
Имя функции обновления будет сгенерировано автоматически: OnUpdateColorBlack (). Поскольку это кажется подходящим именем функции, щелкайте на кнопке Add and Edit, позволяя мастеру Event Handler Wizard сгенерировать ее. Наряду с генерацией определения скелета функции в SketcherDoc. срр, ее объявление будет добавлено к определению класса. Также для нее будет предусмотрена строка в карте сообщений:
ON_UPDATE_COMMAND_UI(ID_COLOR_BLACK, OnUpdateColorBlack)
Здесь используется макрос 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);
}
Добавленный оператор вызывает функцию 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::OnUpdateColorGreen(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__Element=CURVE) ;
}
void CSketcherDoc::OnUpdateElementCircle(CCmdUI *pCmdUI)
{
// Пометить пункт меню, если текущий элемент — окружность pCmdUI->SetCheck(m_Element==CIRCLE) ;
}
void CSketcherDoc: :OnUpdateElementRectangle(CCmdUI* pCmdUI)
{
// Пометить пункт меню, если текущий элемент — прямоугольник pCmdUI->SetCheck(m_Element=RECTANGLE);
}
Если вы ухватили идею, все довольно просто, не правда ли?
Испытание обработчиков обновления
Добавив код для всех обработчиков обновления, вы снова можете собрать и запустить приложение Sketcher. Теперь, когда вы изменяете цвет или тип элемента, это отражается и в меню, как показано на рис. 13.11.
Рис. 13.11. Работа расширенной программы Sketcher
На этом весь код, необходимый для пунктов меню, готов. Не забудьте все сохранить, прежде чем переходить к следующему этапу. В наше время панели инструментов — обязательный компонент любой Windows-программы, так что следующим шагом, который мы предпримем, будет добавление кнопок панели инструментов для поддержки наших меню.
Добавление кнопок панели инструментов
Выберите вкладку Resource View (Представление ресурсов) и откройте ресурс — панель инструментов. Вы увидите, что там содержится тот же ID, что и в главном меню — IDR_MAINFRAME. Если выполнить двойной щелчок на этом ID, откроется окно редактора, показанное на рис. 13.12.
Рис. 13.12. Редактирование пиктограммы для кнопки панели инструментов
Кнопка панели инструментов — это массив точек 16x15, содержащий графическое представление функции, которую она вызывает. На рис. 13.12 видно, что редактор ресурсов предлагает увеличенное представление кнопки панели инструментов, так
что вы можете видеть и манипулировать отдельными пикселями. Если вы щелкнете на новой кнопке в правом конце представленного ряда, то сможете нарисовать эту кнопку. Прежде чем начать редактирование, перетащите новую кнопку примерно на половину ее ширины вправо. Это отделит ее от соседней слева, и тем самым будет начат новый блок кнопок.
Вы должны сохранять последовательность блоков кнопок в том же порядке, что и панель меню, так что сначала создавайте кнопки для выбора типа элемента. Ниже перечислены кнопки редактирования из редактора ресурсов, который появляется в окне приложения Visual C++ 2005, которыми вы будете пользоваться.
Карандаш для рисования индивидуальных точек.
Резинка для стирания индивидуальных точек.
Заполнитель областей текущим цветом.
Увеличитель представления кнопки.
Рисование прямоугольника.
Рисование эллипса.
Рисование кривой.
Если оно не показано, вы можете отобразить окно для выбора цвета, щелкнув правой кнопкой мыши на кнопке панели инструментов и выбрав Show Colors Window (Показать окно выбора цвета) из контекстного меню. Убедитесь, что выбран черный цвет и используете инструмент “карандаш” для того, чтобы нарисовать диагональную линию в увеличенном образе новой кнопки. Фактически, если вам нужно ее увеличить еще больше, можно воспользоваться кнопкой редактирования Magnification Tool (Инструмент увеличения), чтобы увеличить ее в 8 раз относительно реального размера. Если в процессе рисования допущена ошибка, вы можете выбрать кнопку редактирования Erase Tool (Инструмент очистки), но при этом нужно убедиться, что выбранный для нее цвет соответствует цвету фона редактируемой кнопки. Вы также можете стирать индивидуальные точки, щелкая на них правой кнопкой мыши, но опять-таки при этом следует убедиться, что установлен правильный цвет фона. Для установки цвета фона просто щелкните на соответствующем цвете правой кнопкой мыши. После того, как вы будете удовлетворены тем, что нарисовали, следующим шагом будет редактирование свойств кнопки панели инструментов.
Редактирование свойств кнопки панели инструментов
Дважды щелкните на новой кнопке панели инструментов, чтобы вызвать окно свойств, показанное на рис. 13.13.
Окно свойств показывает ID кнопки по умолчанию, но вы хотите ассоциировать кнопку с пунктом меню Elements Line (Элемент^ Линия), который был определен ранее, поэтому щелкните на ID и затем на стрелке вниз, чтобы отобразить альтернативные значения. Затем можете выбрать в выпадающем списке ID_ELEMENT_LINE. Если щелкнуть на Prompt, можно обнаружить, что в панели состояния будет отображена та же строка, что и для пункта меню, поскольку она ассоциирована с ID. Для завершения определения кнопки закройте окно свойств.
Теперь можете двигаться дальше и спроектировать остальные три кнопки элементов. С помощью кнопки редактирования с прямоугольником можно нарисовать прямоугольник, а посредством кнопки с кругом — эллипс. Кривую можно нарисовать карандашом, составив ее из отдельных точек. Затем каждую кнопку потребуется ассоциировать с соответствующим ID пункта меню, который был определен ранее.
Properties
Toolbar Editor ICTBEd £ 64 в
Height15
IDID_BUTTON32785
Prompt
Width16
Prompt
Specifies the ToolTip text that will be displayed when the button Is passed over.
Pac. 13.13. Окно свойств кнопка панели инструментов
Теперь добавьте кнопки выбора цвета. Первую кнопку выбора цвета также нужно будет сдвинуть вправо, чтобы начать новую группу кнопок. Кнопки цветов можно сделать очень простыми, и просто покрасить каждую из них в цвет, который она выбирает. Это можно сделать, выбрав соответствующий цвет переднего плана и затем редактирующую кнопку для заливки сплошным цветом. Опять-таки вы должны использовать для новых кнопок уже готовые идентификаторы ID COLOR BLACK, ID_COLOR_RED и так далее. Окно редактирования панели инструментов должно выглядеть подобно тому, как показано на рис. 13.14.
Рас. 13.14. Создание пиктограммы для кнопка установки цвета
Вот и все, что необходимо сделать на данный момент, так что сохраните ресурсный файл и выполните сборку новой версии программы Sketcher.
Испытание кнопок панели инструментов
Итак, еще раз соберем приложение и запустим его. Вы должны увидеть окно приложения, показанное на рис. 13.15.
Sketcher - SketchedSketcher - Sketched] File Edit View Element Color Window Help] File Edit View Element Color Window HelpReadyReadyPuc. 13.15. Работа программы Sketcher после добавления кнопок панели инструментовPuc. 13.15. Работа программы Sketcher после добавления кнопок панели инструментов
Sketcher - Sketched
Sketcher - Sketched
] File Edit View Element Color Window Help
] File Edit View Element Color Window Help
Ready
Ready
Puc. 13.15. Работа программы Sketcher после добавления кнопок панели инструментов
Puc. 13.15. Работа программы Sketcher после добавления кнопок панели инструментов
И тут происходит нечто удивительное. Добавленные вами кнопки панели инструментов уже отображают установки по умолчанию, которые вы определили для пунктов меню. Если вы наведете курсор на одну из новых кнопок, то в панели состояния появится соответствующая подсказка. Новые кнопки работают как полноценная замена пунктов меню, и каждый новый выбор, выполненный через меню или через панель инструментов, отображается в виде нажатой кнопки и пометки соответствующего пункта меню.
Если вы закроете окно представления документа Sketcherl, то увидите, что ваши кнопки панели инструментов автоматически станут серыми и недоступными. При открытии нового окна документа они опять становятся активными и доступными. Вы можете также попробовать перетащить панель инструментов курсором мыши. Ее можно пристыковать к любому краю главного окна приложения либо оставить в “свободном плавании”. Можно также включать и выключать ее, выбирая опцию меню View^Toolbar (Вид1^Панель инструментов). И все это вы получаете, не написав ни одной строчки кода!
Добавление всплывающих подсказок
Есть еще одна деталь, которую вы можете добавить к вашим кнопкам панели инструментов замечательно просто: всплывающие подсказки (tooltips). Всплывающая подсказка — это маленькая рамка, которая появляется рядом с кнопкой панели инструментов, когда вы задерживаете курсор на ее поверхности. Эта рамка содержит текстовую строку, дающую дополнительное пояснение назначения данной кнопки.
Чтобы добавить всплывающие подсказки, выберите вкладку Resource View и после раскрытия списка ресурсов щелкните на папке String Table (Таблица строк) и дважды щелкните на ресурсе. Он содержит идентификаторы и строки подсказок, ассоциированные с пунктами меню, которые вы добавили ранее вместе с этими подсказками под каждым заголовком. Чтобы добавить всплывающую подсказку, вы просто добавляете в конец текста заголовка символ новой строки (\п), за которым следует текст всплывающей подсказки. Что касается текста подсказки для строки состояния, кото-
рую вы ввели ранее, то можете выполнить двойной щелчок, чтобы войти в режим редактирования, и добавить \п в конец текста подсказки в колонке заголовка, тем самым изменив существующий заголовок для идентификатора ID ELEMENT LINE с Line на Ыпе\пУстанавливает режим рисования линий. Таким образом, текст заголовка будет иметь две части, разделенные символом \п, где первая часть будет подсказкой для строки состояния, а вторая — текстом всплывающей подсказки.
Добавьте \п, за которым следует текст всплывающей подсказки, к тексту заголовка для каждого ID пунктов меню Element и Color. Не забывайте начинать текст каждой всплывающей подсказки с \п. Это все, что потребуется сделать. После сохранения ресурса String Table вы можете выполнить сборку приложения и запустить его. Помещение курсора над одной из новых кнопок через секунду или две вызовет появление всплывающей подсказки.
Резюме
В этой главе вы узнали, как MFC соединяет сообщение с функцией-членом класса для его обработки, и написали свои первые обработчики сообщений. Большая часть работы при создании Windows-программ заключается в разработке обработчиков сообщений, поэтому важно иметь четкое представление о том, как происходит этот процесс. Когда мы приступим к рассмотрению других обработчиков сообщений, вы увидите, что процесс их добавления почти такой же.
Вы расширили стандартное меню и панель инструментов, используя программу, сгенерированную мастером MFC Application Wizard, которая является хорошей основой для кода приложения, который мы добавим в следующей главе. Хотя в программе еще и нет никакой скрытой функциональности, операции меню и панели инструментов выглядят очень профессионально — благодаря сгенерированному мастером каркасу и мастеру Event Handler Wizard.
Ниже перечислены важнейшие моменты, с которыми вы познакомились в этой главе.
В MFC определены обработчики сообщений для класса в карте сообщений, которая появляется в файле . срр с определением класса.
Командные сообщения, поступающие от меню и панелей инструментов, могут быть обработаны в любом классе, унаследованном от CCmdTarget. К ним относятся класс приложения, классы обрамляющего окна — главного и дочерних, класс документа и класс представления.
Сообщения, отличные от командных, могут быть обработаны только в классах, производных от CWnd. К ним относятся обрамляющие окна и классы представлений, но не классы приложений и документов.
Библиотека MFC соблюдает предопределенную последовательность поиска классов в вашей программе, чтобы найти обработчик для командного сообщения.
Вам всегда следует использовать мастер Event Handler Wizard для добавления в программу обработчиков событий.
Физическое представление меню и панелей инструментов определяется в ресурсных файлах, которые редактируются с помощью встроенного редактора ресурсов.
Пункты меню, которые могут порождать командные сообщения, идентифицируются символическими константами с префиксом ID. Эти идентификаторы используются для установления ассоциации обработчика с сообщением от пункта меню.
Чтобы ассоциировать кнопку панели инструментов с определенным пунктом меню, вы присваиваете ей тот же самый ID, что и для пункта меню.
Чтобы добавить всплывающую подсказку для кнопки панели инструментов, соответствующую элементу меню, вы добавляете текст всплывающей подсказки к элементу с ID данного пункта меню в колонку заголовка ресурса String Table. Текст всплывающей подсказки отделяется от текста подсказки меню символом \п.
В следующей главе вы добавите код, необходимый для рисования элементов в представлении, и воспользуетесь созданными в этой главе меню и кнопками панели инструментов для выбора того, что нужно рисовать и какого цвета. Именно там программа Sketcher начнет соответствовать своему названию.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Добавьте пункт Ellipse (Эллипс) к всплывающему меню Element (Элемент).
Реализуйте обработчики сообщений команды и обновления этого пункта в классе документа.
Добавьте кнопку панели инструментов, соответствующую пункту меню Ellipse, и затем добавьте всплывающую подсказку для этой кнопки.
Модифицируйте обработчик команды обновления для пунктов меню цвета, чтобы текущий выбранный пункт меню отображался заглавными буквами, а остальные — прописными.
14
Рисование в окне
В этой главе мы добавим немного “мяса” к приложению Sketcher. Мы сосредоточим внимание на получении графического вывода, отображаемого в окне приложения. До конца этой главы вы научитесь рисовать почти все элементы, для которых добавили пункты меню в прошлой главе. Ниже перечислены основные вопросы, рассматриваемые в настоящей главе.
Какие координатные системы представляет Windows для рисования в окне.
Контекст устройства и для чего он необходим.
Как и когда ваша программа рисует в окне.
Как определить обработчики для сообщений мыши.
Как определять ваши собственные классы фигур.
Как программировать рисование фигур в окне с помощью мыши.
Как заставить программу захватить мышь.
Основы рисования в окне
Прежде чем мы приступим к рисованию с применением MFC, будет полезно получить представление о том, что происходит за кулисами операционной системы Windows, когда вы рисуете в окне. Подобно любой другой операции под Windows, запись в окно на экране вашего дисплея обеспечивается функциями Windows API. Хотя, в принципе, это не очень точно отражает ситуацию; способ работы Windows некоторым образом ее усложняет.
Для начала: вы не можете просто что-то записать в окно и забыть об этом. Существует множество событий, которые требуют, чтобы ваше приложение перерисовывало окно, такие как изменение размера окна, в котором вы рисуете, или когда часть вашего окна, которая была ранее скрыта, открывается пользователем при перемещении другого окна.
К счастью, вам не нужно беспокоиться обо всех деталях подобных случаев, поскольку на самом деле всеми этими сообщениями управляет Windows; однако, это значит, что вы можете только писать постоянные данные в окно, когда приложение получает специфическое сообщение Windows, которое требует этого действия. Это также означает, что вы должны быть готовы в любой момент заново реконструировать все, что ранее нарисовали в окне.
Когда все окно или его часть должна быть перерисована, Windows посылает вашему приложению сообщение WM PAINT. Оно перехватывается библиотекой МЕС, которая посылает сообщение функции-члену одного из ваших классов. Чуть позже в настоящей главе я объясню, как вы можете реализовать обработку.
Клиентская область окна
Окно не имеет фиксированного положения на экране и даже не имеет фиксированной видимой области, потому что может быть передвинуто с помощью мыши, и его размер может быть изменен перетаскиванием его границ. Как же вы узнаете, где именно на экране следует рисовать?
К счастью, вам этого знать и не нужно. Поскольку Windows предоставляет согласованный способ рисования в окне, вам не приходится беспокоиться о точном месте на экране; если бы это было не так, рисование в окне чересчур бы усложнилось. Windows обеспечивает это, поддерживая координатную систему для клиентской области окна, локальную по отношению к окну. Она всегда использует левый верхний угол клиентской области в качестве точки отсчета. Все точки внутри клиентской области определены относительно этой начальной точки, как показано на рис. 14.1.
Горизонтальное и вертикальное расстояние точки от левого верхнего угла клиентской области всегда одно и то же, независимо от местоположения окна на экране и его текущего размера. Конечно, Windows необходимо отслеживать местоположение окна, и когда вы рисуете что-то в точке клиентской области, она должна определять, где именно эта точка находится на экране.
Это точка отсчета для клиентской области окна
Местоположение этой точки определяется расстояние
Рис. 14.1. Клиентская область окна
Интерфейс графических устройств 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.
Таблица 14.1. Режимы отображения, поддерживаемые Windows
Режим отображения
Описание
MMJTEXT
Логической единицей является один пиксель устройства с положительными х слева направо и положительными у от вершины до дна клиентской области.
MM_LOENGLISH
Логической единицей является 0,01 дюйма с положительными х слева направо и положительными у, отсчитываемым от вершины клиентской области вверх.
MM_HIENGLISH
Логической единицей является 0,001 дюйма с направлениями х и у как ВMM_LOENGLISH.
MM_LOMETRIC
Логической единицей является 0,1 миллиметра с направлениями х и у как ВMM_LOENGLISH.
MM_HIMETRIC
Логической единицей является 0,01 миллиметра с направлениями х и у как ВMM-LOENGLISH.
MM_ISOTROPIC
Логическая единица произвольной длины, но одинаковая для обеих осей — хиу. Направления х и у— как в mm_loenglish.
MM_ANISOTROPIC
Режим подобен mm isotropic, но допускает разные длины логических единиц по осямхиу.
MMJTWIPS
Логической единицей является twip, причем каждый twip составляет 0,05 точки, а точка — 1/72 дюйма. Таким образом, twip соответствует 1/1440 дюйма, то есть 6,9x10"4 дюйма. (Точка — единица измерения для шрифтов.) Направления х и у — как в mm loenglish.
Рис. 14.2. Режим отображения MMJTEXT
По умолчанию точка в верхнем левом углу клиентской области имеет координаты (О, 0) во всех режимах отображения, хотя и возможно переместить начало координат из верхнего левого угла клиентской области, если вы этого хотите. Например, некоторые приложения, представляющие данные в графическом виде, перемещают начало координат в центр клиентской области, чтобы упростить рисование графиков по числовым данным. При начале координат в левом верхнем углу в режиме ММ_ТЕХТ точка, отстоящая на 50 пикселей от левой границы и на 100 пикселей вниз от вершины клиентской области, будет иметь координаты (50, 100). Конечно, поскольку единицами измерения являются пиксели, эта точка окажется ближе к верхнему левому углу клиентской области, если ваш монитор имеет разрешение 1280x1024, чем при разрешении 1024x768. Обратите внимание, что установка DPI (dot per inch — точек на дюйм) вашего дисплея влияет на представление во всех режимах отображения.
Установка по умолчанию предполагает 96 DPI, поэтому если DPI для вашего дисплея установлено в другое значение, это повлияет на внешний вид. Координаты всегда измеряются 32-битными целыми числами, если только вы не программируете для старых операционных систем Windows95/98 — в этом случае они ограничены 16-ю битами. Максимальный физический размер всего рисунка варьируется с физической длиной единицы координат, что определяется режимом отображения.
Направления координатных осей х и у в режиме MM LOENGLISH и всех остальных режимах отображения одинаковы, но отличаются от ММ_ТЕХТ. Координатные оси для MM LOENGLISH показаны на рис. 14.3. Хотя положительные значения у согласуются с тем, что вы учили в школе (значение у увеличивается по мере движения по экрану снизу вверх), MM LOENGLISH — все же несколько необычный режим, поскольку точка начала координат находится в левом верхнем углу клиентской области, так что для точек внутри видимой клиентской области у всегда отрицательно.
Sketcherl
► направление положительной оси х
Режим отображения MM_LOENGLISH ▼ направление отрицательной оси у
Рис. 14.3. Режим отображения MMJLOENGLISH
В режиме отображения MM LOENGLISH единицы измерения вдоль осей составляют 0,01 дюйма, так что точка с координатами (50, -100) находится на полдюйма от левой границы и на один дюйм ниже вершины клиентской области. Объект имеет всегда один и тот же размер на экране, независимо от разрешения монитора, на котором он отображается. Если вы рисуете что-либо в режиме MM_LOENGLISH с отрицательной координатой х или положительной у, это оказывается за пределами клиентской области и потому невидимо, поскольку точка отсчета (0, 0) по умолчанию расположена в левом верхнем углу. Однако точку начала координат можно сдвинуть, вызвав функцию Windows API SetViewportOrg () (или функцию-член класса MFC CDC SetViewportOrg (), о которой мы поговорим ниже).
Механизм рисования в Visual C++
Библиотека МЕС инкапсулирует интерфейс Windows для вашего экрана и принтера, избавляя вас от необходимости беспокоиться о большей части деталей программирования графического вывода. Как было показано в предыдущей главе, программа, сгенерированная мастером Application Wizard, уже включает класс, унаследованный от MFC-класса CView, специально спроектированного для отображения данных документа на экране.
Класс представления в вашем приложении
Мастер создания приложений 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(IpDoc) return;
// TODO: добавить сюда код рисования для собственных данных
}
Указатель на объект типа класса С DC передается функции-члену класса представления OnDraw (). Этот объект включает функции-члены, вызывающие функции Windows API, который позволяют вам рисовать в контексте устройства. Обратите внимание, что имя параметра закомментировано, так что вы должны либо убрать комментарий с него, либо подставить собственное имя перед тем, как сможете использовать указатель.
Поскольку вы поместите весь код, необходимый для рисования документа, в эту функцию, мастер Application Wizard включил объявление указателя pDoc и инициализировал его, применив функцию GetDocument (), которая возвращает адрес объекта документа, связанного с текущим представлением:
CSketcherDoc* pDoc = GetDocument();
Функция GetDocument () в действительности извлекает указатель на документ из m_pDocument, унаследованной переменной-члена объекта представления. Функция выполняет важную задачу приведения указателя, хранящегося в этой переменной, к типу, соответствующему классу документа приложения — CSketcherDoc. Это необходимо, чтобы компилятор получил доступ к членам класса документа, определенного вами, иначе он имел бы доступ только к членам базового класса. Поэтому pDoc указывает на объект документа в вашем приложении, ассоциированный с текущим представлением, и будет использовать его для обращения к данным, хранящимся в объекте документа, когда понадобится нарисовать его.
Следующая строка:
ASSERT_VALID(pD0C);
просто проверяет, что pDoc содержит корректный адрес, а оператор if, следующий за ней, проверяет, что pDoc не равен null.
Имя параметра pDC для функции OnDraw () означает “указатель на контекст устройства” (pointer to Device Context). Объект класса CDC, на который указывает аргумент pDC, передаваемый функции OnDraw () — это ключ к рисованию окна. Он представля-
ет контекст устройства плюс инструменты, необходимые для рисования графики и текста, так что вам определенно следует познакомиться с ним поближе.
Класс 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 То () для обеспечения гибкости в указании позиции, которую вы хотите указать в качестве текущей. Существуют две версии этой функции, объявленные в классе CDC следующим образом:
CPoint MoveTo(int х, int у);// Перейти в позицию х, у
CPoint MoveTo(POINT aPoint);// Перейти в позицию, определенную aPoint
>
Первая версия принимает координаты х и у как отдельные аргументы. Вторая принимает один аргумент типа POINT, который представляет собой структуру, определенную, как показано ниже:
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 (), которая рисует линию в клиентской области от текущей позиции в точку, специфицированную ее аргументами, как проиллюстрировано на рис. 14.4.
Класс С DC также определяет две версии функции LineTo () со следующими про- тотипами:
BOOL LineTo (int х, int у); // Рисовать линию до позиции х, у
BOOL LineTo(POINT aPoint); // Рисовать линию до позиции, определенной aPoint
Это предлагает вам такую же гибкость в указании аргумента, как и MoveTo (). Вы можете использовать объект С Point в качестве аргумента второй версии функции. Функция возвращает TRUE, если линия была нарисована, и FALSE — в противном случае.
Когда выполняется функция LineTo (), текущая позиция изменяется в точку, указанную в конце линии. Это позволяет рисовать серии соединенных линий простыми последовательными вызовами LineTo () для каждой из них. Взгляните на следующую версию функции OnDraw ():
void CSketcherView::OnDraw(CDC* pDC)
{
CSketcherDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if(’pDoc)
return;
pDC->MoveTo(50,50); // Установить текущую позицию
pDC->LineTo(50,200); // Нарисовать вертикальную линию вниз на 150 единиц pDC->LineTo (150,200); И Нарисовать горизонтальную линию вправо на 100 единиц pDC->LineTo(150,50); // Нарисовать вертикальную линию вверх на 150 единиц pDC->LineTo (50,50) ;// Нарисовать горизонтальную линию влево на 100 единиц
}
Если вы включите это в программу Sketcher и запустите ее на выполнение, будет отображено окно документа, показанное на рис. 14.5.
Четыре вызова функции LineTo () обеспечили рисование прямоугольника против часовой стрелки, начиная с верхнего левого угла. Первый вызов использует текущую позицию, установленную функцией MoveTo (); последующие вызовы применяют текущую позицию, установленную предыдущим вызовом функции LineTo (). Вы можете использовать это для рисования любой фигуры, состоящей из последовательности линий, каждая из которых соединена с предыдущей. Конечно, можно также в любой момент использовать MoveTo () для изменения текущей позиции.
Рис. 14.5. Работа программы Sketcher с прямоугольниками
Рисование окружностей
У вас есть выбор среди нескольких функций-членов класса С DC для рисования окружностей, но все они предназначены для рисования эллипсов. Как вы знаете из школьного курса геометрии, окружность — это частный случай эллипса, у которого большая и малая оси равны. Поэтому вы можете использовать функцию-член Ellipse () для рисования окружности. Как и все другие замкнутые фигуры, поддерживаемые классом CDC, функция Ellipse () заполняет свою внутреннюю часть установленным вами цветом. Внутренний цвет определяется кистью (brush), выбранной в контексте устройства. Текущая кисть контекста устройства определяет способ заполнения замкнутых фигур.
Библиотека МЕС предлагает класс 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-ю (большую) окружность
// Определить ограничивающий прямоугольник для 2-й (меньшей) окружности CRect* pRect = new CRect(250,50,300,100);
CPoint Start(275,100);// Начальная точка дуги
CPoint End(250,75);// Конечная точка дуги
pDC->Arc(pRect,Start, End);// Нарисовать вторую окружность
delete pRect;
Обратите внимание, что для определения ограничивающего прямоугольника здесь используется класс CRect вместо структуры RECT и объекты класса CPoint вместо структур POINT. Вы также позднее будете работать с объектами CRect, хотя, как вы увидите, они обладают некоторыми ограничениями. Функция Аге () не требует установки текущей позиции, поскольку положение и размер дуги полностью определяются передаваемыми ей аргументами. Текущая позиция при рисовании дуги не важна — она остается на прежнем месте, где и была до начала рисования дуги. Хотя координаты могут быть до 32 768, максимальная ширина и высота прямоугольника, ограничивающего фигуру, составляет 32 767, потому что это максимальное положительное значение, которое может быть представлено 16-битным целым числом.
Теперь попробуйте запустить Sketcher с этим кодом в функции OnDraw (). Вы должны получить результат, показанный на рис. 14.6.
Рис. 14.6. Работа программы Sketcher с дугами
Попробуйте изменить размеры границ. Клиентская область будет автоматически перерисована при сокрытии и открытии дуг на рисунке. Вспомните, что разрешение экрана влияет на масштаб отображения. Чем меньше разрешение экрана, тем больше и дальше от верхнего левого угла получатся дуги.
Рисование в цвете
Все, что мы рисовали до сих пор, появлялось на экране в черном цвете. Рисование подразумевает использование объекта пера (реп), который имеет цвет и толщину, и пока что мы применяли объект пера по умолчанию, предоставленный контекстом устройства. Конечно, вы не обязаны поступать так — можно создать свое собственное перо требуемой толщины и цвета. В MFC определен класс С Реп, призванный помочь в этом.
Все замкнутые кривые, которые вы рисуете, заполнялись текущей кистью контекста устройства. Как уже упоминалось, вы можете определить кисть как экземпляр класса СВ rush. Давайте рассмотрим некоторые возможности объектов С Реп и CBrush.
Создание пера
Простейший способ создать объект пера — это объявить объект класса СРеп:
СРеп аРеп; И Объявление объекта пера
Этот объект нуждается в инициализации нужными вам свойствами. Это делается с помощью функции-члена 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_INS I DEFRAME
Перо рисует сплошную линию, но в отличие от 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 (). Чтобы выбрать перо, которое вы хотите использовать, упомянутая функция вызывается с указателем на объект пера в качестве аргумента. Функция возвращает указатель
на предыдущий использованный объект пера, так что вы можете сохранить его и восстановить старое перо по завершении рисования. Ниже показан типичный оператор выбора пера.
CPen* pOldPen = pDC->SelectObject(&aPen);// Выбрать аРеп в качестве пера
Чтобы восстановить старое перо, вы просто вызываете эту функцию опять, передавая ей указатель, возвращенный предыдущим вызовом:
pDC->SelectObject(pOldPen);// Восстановить старое перо
Вы можете увидеть все это в действии, изменив предыдущую версию функции OnDraw () в классе CSketcherView:
void CSketcherView::OnDraw(CDC* pDC) {
CSketcherDoc* pDoc = GetDocument ();
ASSERT_VALID(pDoc);
if(!pDoc) return;
// Объявить объект пера и инициализировать его
// как красное сплошное перо шириной в 2 пикселя
CPen аРеп;
аРеп.CreatePen(PS_SOLID, 2, RGB(255, 0, 0) ) ;
CPen* pOldPen = pDC->SelectObject(&aPen); // Выбрать аРеп в качестве пера 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->SelectObject(pOldPen);// Восстановить старое перо
Если вы соберете и запустите приложение Sketcher с этой версией функции OnDraw (), то получите те же дуги, что и раньше, но линии будут толще и красного цвета. Можно с пользой поэкспериментировать с этим примером, пробуя разные комбинации аргументов функции CreatePen () и наблюдая эффект от их применения. Обратите внимание, что возвращаемое значение функции CreatePen () игнорируется, так что вы рискуете столкнуться со сбоем этой функции, который не будет обнаружен в программе. В данном случае это не имеет значения, поскольку программа все еще достаточно тривиальна, но когда вам придется разрабатывать серьезные программы, важность проверки сбоев подобного рода возрастет.
Создание кисти
Объект класса СВ rush инкапсулирует кисть Windows. Вы можете определять кисть как сплошную, штрихованную или закрашенную по шаблону. Кисть — это на самом деле блок пикселей размером 8x8, повторяющийся в пределах области, которую кисть заполняет.
Чтобы определить кисть сплошного цвета, вы можете специфицировать цвет при создании объекта кисти, например:
CBrush aBrush(RGB(255,0,0)) ; // Определить кисть красного цвета
Этот оператор определяет кисть красного цвета. Значение, переданное конструктору, должно иметь тип 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); // Выбрать кисть в контекст устройства
Доступно множество стандартных кистей. Каждая из стандартных кистей идентифицируется предопределенной символической константой, и всего их существует семь. Вот они:
DKGRAY_BRUSHDKGRAY_BRUSHGRAY_BRUSHLTGRAY_BRUSH
DKGRAY_BRUSH
DKGRAY_BRUSH
BLACK_BRUSHWHITE_BRUSH
HOLLOW_BRUSHNULL_BRUSH
Имена этих кистей достаточно самоочевидны. Чтобы использовать одну из них, вы вызываете функцию-член класса CDC по имени SelectStockObject (), передавая ей в качестве аргумента символическое имя кисти, которую вы хотите использовать.
Чтобы выбрать нулевую кисть, которая оставляет внутренность замкнутой фигуры незакрашенной, вы можете написать:
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.
По мере перемещения курсора линия непрерывно обновляется
Рис. 14.7. Рисование линии
Окружность фиксируется
По мере перемещения курсора окружность непрерывно обновляется
Рис. 14.8. Рисование окружности
Точно так же легко, как вы рисуете линию, можно рисовать и прямоугольник, как показано на рис. 14.9.
Левая кнопка
По мере перемещения курсора прямоугольник непрерывно обновляется
Рис. 14.9. Рисование прямоугольника
Первая точка определяет позицию, в которой нажата левая кнопка мыши. Это — один угол прямоугольника. Позиция курсора в процессе его перемещения с нажатой кнопкой мыши определяет противоположный по диагонали угол прямоугольника. Прямоугольник определяется окончательно, когда отпускается левая кнопка мыши.
С кривой немного иначе. Кривую можно определять с указанием произвольного количества точек. Предлагаемый для использования механизм проиллюстрирован на рис. 14.10.
Кривая определяется набором сегментов линий, соединяемых в позициях курсора
Рис. 14.10. Рисование кривой
Как и с другими фигурами, первая точка определяется позицией курсора в момент нажатия левой кнопки мыши. Последовательные позиции фиксируются по мере перемещения мыши и соединяются сегментами линий, формирующими кривую, так что след курсора мыши определяет нарисованную кривую.
Теперь, когда вы знаете, как пользователь определяет элемент, очевидным следующим шагом для понимания того, как это реализовать, должно быть изучение программирования для мыши.
Программирование для мыши
Чтобы обеспечить программе возможность рисования фигур описанным выше способом, необходимо знать некоторые сведения, касающиеся мыши.
Нажатие кнопки мыши сигнализирует о начале операции рисования.
Местоположение курсора при нажатии кнопки мыши определяет начальную точку фигуры.
Перемещение мыши после обнаружения нажатия кнопки мыши — сигнал рисования фигуры, и позиция курсора представляет точку определения фигуры.
Позиция курсора во время отпускания кнопки мыши сигнализирует о том, что нарисована финальная версия фигуры.
Как вы можете предположить, вся эта информация предоставляется Windows в форме сообщений, отправленных вашей программе. Реализация процесса рисования линий и окружностей почти полностью состоит из написания обработчиков сообщений.
Сообщения от мыши
Когда пользователи программы рисуют фигуру, они взаимодействуют с определенным представлением документа. Поэтому класс документа является очевидным местом для размещения обработчиков сообщений мыши. Выполните щелчок правой кнопкой мыши на имени класса CSketcherView в Class View и затем отобразите окно его свойств, выбрав пункт Properties из контекстного меню. Если затем вы щелкнете на кнопке сообщений (подождите отображения всплывающих подсказок, если не знаете, где она находится), то увидите список идентификаторов сообщений. Затем вы увидите список идентификаторов стандартных сообщений Windows, отправляемых классу, с префиксом WM_.
Пока вам нужно знать о трех сообщениях от мыши, перечисленных в табл. 14.4, поэтому прокрутим список вниз, чтобы добраться до них (рис. 14.11).
Таблица 14.4. Три сообщения от мыши
Сообщение
Описание
WM_LBUTTONDOWN
Сообщение посылается, когда нажимается левая кнопка мыши.
WM_LBUTTONUP
Сообщение посылается, когда отпускается левая кнопка мыши.
WM_MOUSEMOVE
Сообщение посылается, когда мышь перемещается.
Эти сообщения достаточно независимы друг от друга и посылаются представлениям документа в вашей программе, даже если вы не предусматриваете обработчиков для них. Вполне вероятно получение в окне сообщения WM_LBUTTONUP без предвари-
тельного получения WM_LBUTTONDOWN. Это может случиться, если кнопка нажата, когда курсор находился на другом окне, а затем был перемещен на ваше окно представления прежде, чем кнопка была отпущена.
Properties▼ X
CSket t herVtew VCGode Class
Аи sij
WM_LBUTTONDBLCLK*
WM_LBUTTONDOWN
WM_LBUTTON'JP
WM_MBI JTTONDBLCLK
WM_MBUTTONDOWN
WM_MBUTTONUP
^M_MDIACTIVATE
WM_MEASURETTEM
WM_MEHUCHAR
WM_MENUSELECT
WM_MOUSEACnVATE
WM_MOUSEMOVE
,7M_MOUSEWHEEL
WM_MOVE
WM_MOVING
WM_NCACFIVATE
WM_NCGALCSIZE
WM_NCCREATE
WM_NCDE5FROY
WM_MCHIFTE3T
WM_NCLBUTFONDBLCL
WM_NCLB UTTO NDOW N
WM_NCLBJUTTONUP
WM_NCMBUTTONDBLCLv
CSket< herVew
Puc. 14.11. Список сообщений от мыши
Если вы посмотрите на список в окне свойств, то увидите там и другие сообщения мыши, которые могут произойти. Вы можете выбрать для обработки любое из них или же все, в зависимости от требований вашего приложения. Определите в общих чертах, что необходимо делать с этими тремя сообщениями, которые вас интересуют, на основе процесса рисования фигур, который вы видели ранее:
WM^LBUTTONDOWN
С этого начинается процесс рисования элемента. Получив это сообщение, вы должны предпринять следующие действия.
Отметить начало процесса рисования.
Записать текущую позицию как первую точку определения элемента.
WM_MOUSEMOVE
Это сообщение означает промежуточную стадию, когда вы хотите создать и нарисовать временную версию текущего элемента, но только при условии, что левая кнопка мыши нажата. Ниже перечислены выполняемые действия.
Проверить, что левая кнопка мыши нажата.
Если да, удалить предыдущую версию текущего элемента, которая была нарисована.
Нарисовать новую версию элемента по двум известным точкам.
WM_LBUTTONUP
Это указывает на то, что процесс рисования элемента завершен, так что осталось предпринять следующие действия.
Сохранить финальную версию элемента на основе записанной начальной точки, вместе с позицией курсора, в которой кнопка была отпущена, в качестве второй точки.
Зафиксировать конец процесса рисования элемента.
Теперь сгенерируем обработчики для этих трех сообщений мыши.
Обработчики сообщений мыши
Вы можете создать обработчик для одного из сообщений мыши, щелкнув на 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.
Таблица 14.5. Символические значения, используемые в аргументе nFlags
Флаг
Описание
MK_CONTROL
Соответствует нажатой клавише <Ctrl>.
MK_LBUTTON
Соответствует нажатию левой кнопки мыши.
MK_MBUTTON
Соответствует нажатию средней кнопки мыши.
MK_RBUTTON
Соответствует нажатию правой кнопки мыши.
MK_SHIFT
Соответствует нажатой клавише <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, то увидите, что добавлены объявления этих трех функций:
// Сгенерированные функции отображения сообщений
protected:
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);
};
Это идентифицирует функции, которые добавлены в качестве обработчиков сообщений.
Теперь, получив представление об информации, передаваемой созданным обработчикам сообщений, вы можете начать добавление своего собственного кода, чтобы заставить их делать то, Что запланировано.
Рисование с помощью мыши
Для сообщения WM LBUTTONDOWN необходимо записывать позицию курсора в первой точке, определяющей элемент. Нужно также записывать позицию курсора после перемещения мыши. Очевидное место для хранения этой информации — класс CSketcherView, так что вы можете добавить для этого в класс данные-члены. Щелкните правой кнопкой мыши на имени класса CSketcherView в Class View и выберите из контекстного меню пункт Add^Add Variable (Добавить1^Добавить переменную). Затем вы сможете добавить детали для дополнительной переменной-члена класса, как показано на рис. 14.12.
Рис. 14.12. Добавление к классу новой переменной-члена
Выпадающий список типов включает только фундаментальные типы, так что для ввода типа С Point вы просто высвечиваете отображенный тип двойным щелчком и затем вводите тип, который хотите. Новый член данных должен быть protected, чтобы предотвратить непосредственную модификацию его извне класса. Когда вы щелкаете на кнопке Finish (Готово), будет создана переменная, и ее начальное значение будет установлено в 0 в списке инициализации конструктора. Но вам нужно назначить начальное значение* С Point (0,0), так что код будет таким:
// Конструирование/уничтожение CSketcherView
CSketcherView::CSketcherView():
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 () клас-
са 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 область обновления сбрасывается в пустое состояние.
Таким образом, чтобы перерисовать фигуру, потребуется выполнить перечисленные ниже действия.
Убедиться, что функция OnDraw О в вашем представлении включает вновь созданный элемент, когда она перерисовывает окно.
Вызвать InvalidateRect () с указателем на прямоугольник, описывающий фигуру, подлежащую перерисовке, который передается в первом аргументе.
Аналогично, если вы хотите удалить фигуру из клиентской области окна, то должны выполнить следующие шаги.
Удалить фигуру из перечня элементов, которые функция OnDraw () будет рисовать.
Вызвать InvalidateRect () с первым аргументом, указывающим на прямоугольник, который ограничивает фигуру, подлежащую удалению.
Поскольку фон указанного прямоугольника очищается автоматически, до момента, когда OnDraw () нарисует фигуру заново, эта фигура исчезает. Конечно, это означает, что вы должны быть готовы получить прямоугольник, ограничивающий любую созда-
ваемую вами фигуру, так что для этого вы добавите функцию-член в классы, определяющие элементы, которые сможет рисовать Sketcher.
Определение классов элементов
Немного забегая вперед, отметим, что вам придется каким-то образом сохранять элементы в документе. Кроме того, должна быть возможность сохранять документ в файле для последующего извлечения, если рисунок должен обладать хоть каким-то постоянством. С деталями файловых операций мы разберемся чуть позднее, а пока достаточно будет знать, что в классе MFC по имени CObject предусмотрены необходимые для этого инструменты, так что вы используете CObject в качестве базового класса для классов элементов.
Еще одна проблема заключается в том, что вы не можете знать заранее, в какой последовательности пользователь будет создавать элементы разных типов. Программа Sketcher должна уметь обрабатывать любую последовательность элементов. Это предполагает, что применение указателя базового класса для выбора функции конкретного класса элемента с целью его рисования может несколько упростить задачу. Например, вам не нужно знать, что это за элемент, чтобы нарисовать его. До тех пор, пока вы обращаетесь к элементу через указатель его базового класса, вы всегда можете заставить его нарисовать себя с помощью виртуальной функции. Это еще один пример полиморфизма, о котором говорилось, когда речь шла о виртуальных функциях. Все, что необходимо для этого — убедиться, что классы, определяющие конкретные элементы, разделяют общий базовый класс, и что в этом классе все функции, которые должны выбираться автоматически во время выполнения, являются виртуальными. Все это указывает на то, что классы элементов могут быть организованы, как показано на рис. 14.13.
Рис. 14.13. Диаграмма классов для программы Sketcher
Стрелки на диаграмме в каждом случае указывают на базовый класс. Если вы захотите добавить новый тип элемента, то все, что для этого понадобится — унаследовать его от CElement. Поскольку эти классы тесно связаны, стоит поместить определения всех этих классов в один новый файл .h, который назовем Elements .h.
Вы можете создать новый класс CElement, выполнив щелчок правой кнопкой мыши на Sketcher в Class View и выбрав из контекстного меню пункт Add*=> Class (Добавить *=>Класс). Выберите в качестве категории класса MFC, а в качестве шаблона— MFC Class (Класс MFC). После щелчка на кнопке Add (Добавить) в диалоге по-
явится другой диалог, в котором нужно будет специфицировать имя класса, как показано на рис. 14.14.
Рис. 14.14. Добавление нового класса, представляющего элемент
В поле имени класса вписано СЕ 1 ement, а в качестве базового класса выбран С 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.
Рис. 14.15. Создание нового класса CLine
Мастер C++ Class Wizard предлагает имена по умолчанию для заголовочного файла и файла . срр — Line. h и Line. срр, но вы можете изменить это, указав другие имена либо воспользовавшись существующими. Чтобы код класса CLine был вставлен в те же файлы, что и код класса CElement, просто щелкните на кнопке по соседству с именем файла и выберите соответствующий существующий файл. На рис. 14.14 это уже было сделано. Вам может понадобиться стереть имя файла по умолчанию, предложенное в диалоге, и повторно выбрать каталог Sketcher, чтобы отобразить список файлов. После щелчка на кнопке Finish (Готово) вы увидите запрос подтверждения вашего намерения включить новый класс в существующий файл. Просто щелкните на кнопке Yes (Да), чтобы подтвердить это, и сделайте то же самое в ответ на запрос относительно файла Elements. срр. Создав определение класса CLine, повторите то же самое для CRectangle, CCircle и CCurve. Когда все будет готово, вы должны увидеть определения всех четырех подклассов CElement в файле Elements .h, каждый со своим конструктором и виртуальным деструктором.
Хранение временного элемента в представлении
Когда я говорил о том, как должны быть нарисованы фигуры, было очевидно, что при перемещении мыши с нажатой левой кнопкой должна создаваться и отображаться серия объектов. Теперь, когда вы знаете, что базовым классом для всех фигур является CElement, можно добавить указатель на класс представления, который будет использоваться для хранения адреса временного элемента. Щелкните правой кнопкой мыши на классе CSketcherView еще раз и еще раз выберите пункт Add^Add Variable (Добавить*=> Добавить переменную) из контекстного меню. Элемент m_pTempElement должен быть типа CElement* с модификатором доступа protected, как и предыдущие две переменные-члены, которые были добавлены ранее (рис. 14.16).
Рис. 14.16. Добавление в класс CSketcherView плена m_pTenopElement
Мастер добавления переменных-членов (Add Member Variable Wizard) обеспечивает инициализацию переменной при конструировании объекта представления значением по умолчанию NULL.
CSketcherView::CSketcherView()
:m_FirstPoint (CPoint (0,0))
, m__SecondPoint (CPoint (0,0))
, m_pTempElement (NULL)
{
// TODO: добавьте сюда код конструирования
}
Вы сможете использовать указатель m_pTempElement в обработчике сообщений WM MOUSEMOVE в качестве теста для предыдущих временных элементов, поскольку решено, что он равен null, когда таковые отсутствуют.
Если вы посмотрите, что было добавлено к заголовочному файлу SketcherView. h, то увидите в его начале строку:
#include "atltypes.h"
Она вставлена потому, что мастер предположил, что CElement — это тип ATL (active template library — библиотека активных шаблонов). Вы можете удалить эту строку, поскольку она не нужна. Чтобы класс CSketcherView корректно компилировался, сразу за директивой #pragma once потребуется добавить следующий оператор:
class CElement; // Опережающее объявление класса
Это просто укажет, что идентификатор CElement представляет собой имя класса, определенного где-то в другом месте, так что компилятор обработает его как таковой.
Поскольку вы создаете объекты класса CElement в функциях-членах класса представления и ссылаетесь на этот класс при определении члена данных, указывающего на временный элемент, то должны обеспечить, чтобы определение класса CElement
было включено до определения класса CSketcherView там, где SketcherView. h включен в файл . срр. Вы можете сделать это для CSketcherView, добавив директиву #include для Elements.h в файл SketcherView.срр перед директивой #include для SketcherView.h:
#include "Elements.h"
#include "SketcherView.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 (GDC* pDC) {} // Виртуальная операция рисования
CRect GetBoundRect () ; // Получить ограничивающий прямоугольник элемента protected:
CElement();// Здесь — для предотвращения вызова
};
Я изменил доступ к конструктору с public на protected, чтобы предотвратить его вызов извне класса. В этот момент к членам, подлежащим наследованию в производных классах, относится определяющая цвет переменная-член m Color и функция- член, вычисляющая ограничивающий прямоугольник элемента — GetBoundRect (). Эта функция возвращает значение типа CRect, описывающее прямоугольник, который ограничивает фигуру.
Мы имеем также виртуальную функцию Draw (), реализованную в производных классах, для рисования конкретного объекта. Функция Draw () нуждается в указателе на объект С DC, переданном ей для обеспечения доступа к функциям рисования, описанным выше и выполняющим рисование в контексте устройства.
Может возникнуть искушение объявить член Draw () как пустую виртуальную функцию в классе CElement — в конце концов, она не может иметь осмысленного содержимого в этом классе. Кроме того, это сделало бы обязательным ее определение в каждом производном классе. Обычно вы так и будете поступать, но класс CElement
наследует от CObject возможность, называемую сериализацией, к услугам которой вы прибегнете позже для сохранения объектов в файле, а это потребует возможности создания экземпляров класса СЕ 1 ement. Класс с пустой виртуальной функцией- членом является абстрактным, а экземпляры абстрактного класса не могут быть созданы. Если вы хотите использовать возможности сериализации MFC для сохранения объектов, ваши классы не должны быть абстрактными. Для сериализуемого класса вы также должны предусмотреть конструктор без аргументов.
У вас также может возникнуть искушение объявить функцию GetBoundRect () как возвращающую указатель на объект CRect — в конце концов, вы ведь передаете указатель функции-члену InvalidateRect () в классе представления; однако это может привести к возникновению проблем. Вы создадите объект CRect как локальную переменную функции, так что возвращенный указатель при возврате из GetBoundRect () станет указывать на несуществующий объект. Это можно обойти, создав объект CRect в куче, но тогда придется позаботиться об его удалении после использования; в противном случае вы наполните кучу объектами CRect — по одному на каждый вызов GetBoundRect (). Другая возможность заключается в том, чтобы сохранить ограничивающий прямоугольник элемента в виде члена класса и генерировать его во время создания элемента. Это разумная альтернатива, но если впоследствии вы измените элемент, скажем, переместив его, то всякий раз должны будете обеспечить повторное вычисление координат этого прямоугольника.
Класс СЫпе
Определение класса CLine можно усовершенствовать следующим образом:
class CLine: public CElement
{
public:
-CLine(void);
virtual void Draw(CDC* pDC);// Функция отображения линии
// Конструктор объекта линии
CLine(CPoint Start, CPoint End, COLORREF aColor);
protected:
CPoint m_StartPoint;// Начальная точка линии
CPoint mJSndPoint;// Конечная точка линии
CLine (void) ; // Конструктор по умолчанию (не должен быть использован) );
Переменные-члены, определяющие линию — это m_StartPoint и m_EndPoint, и обе объявлены как protected. Класс имеет public-конструктор, принимающий параметры значений, определяющих линию, а также конструктор по умолчанию без аргументов, перемещенный в раздел protected, чтобы предотвратить его внешнее использование.
Реализация класса CLine
Вы добавляете реализацию функций-членов в файл Elements. срр, который был сгенерирован мастером при создании класса CElement. Файл stdafx.h был включен в этот файл, чтобы сделать доступными определения стандартных системных заголовочных файлов. Вам также могут понадобиться директивы #include для файлов, содержащих определения сгенерированных мастером MFC Application Wizard классов, если вы используете любой из них в своем коде.
Конечно, каждое из определений функций-членов придется добавлять вручную, потому что мастер 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, унаследованной от класса СЕ 1 ement. Позднее вы добавите код для обеспечения перемещения элемента, и это перемещение будет реализовано простым изменением начальной точки, а конечная точка должна быть определена относительно начальной. Вы сделаете это вычитанием координат х и у для Start из End. Оба члена класса CPoint, х и у, являются общедоступными, так что вы можете обращаться к ним непосредственно. И, наконец, вы сохраняете цвет в переменной-члене m_Color, унаследованной от класса СЕ 1 ement.
Рисование линии
Функция Draw () класса CLine не так сложна, хотя вы должны принять во внимание цвет, используемый при рисовании линии.
// Рисование объекта CLine void CLine::Draw(CDC* pDC) {
// Создать перо для этого объекта и инициализировать
// его цветом объекта и шириной линии в 1 пиксель
СРеп аРеп;
if (!aPen.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 () специфицирует сообщение, появляющееся в окне, а второй говорит о том, что окно должно иметь кнопку ОК. Вы можете получить подробную ин-
формацию о каждой из этих функций, поместив курсор на ее имя в окне редактора и нажав <F1>.
После выбора пера вы перемещаете текущую позицию в начальную точку линии, определенную в унаследованной переменной m StartPoint, а затем рисуете линию от этой точки до конечной. Наконец, вы восстанавливаете старое перо в контексте устройства, тем самым завершая работу. Переменная m_Pen — второй аргумент функции CreatePen () — пока не существует; мы добавим ее в класс CElement в этой главе чуть позже.
Создание ограничивающих прямоугольников
На первый взгляд получение ограничивающего прямоугольника для фигуры — задача тривиальная. Например, линия всегда направлена по диагонали своего описанного прямоугольника, а окружность определена своим описанным прямоугольником, однако существует ряд небольших сложностей. Фигура должна находиться 'полностью внутри прямоугольника, иначе ее часть может быть не нарисована, так что при вычислении ограничивающего прямоугольника необходимо учитывать толщину линий, которыми нарисована фигура. К тому же, вычисляя поправки координат, определяющих описывающий прямоугольник, следует помнить о режиме отображения и также принимать его во внимание.
Взгляните на рис. 14.17, который поясняет метод получения ограничивающих прямоугольников для линии и окружности.
Рис. 14.17. Получение ограничивающих прямоугольников для линии и окружности
Я называю прямоугольник, используемый для рисования фигуры, “описанным прямоугольником”, в то время как прямоугольник, принимающий во внимание ширину пера, “ограничивающим прямоугольником”, дабы отличать их друг от друга. На рис. 14.17 показаны фигуры с их описанными прямоугольниками, а их ограничивающие прямоугольники смещены на толщину линии. Очевидно, что здесь это смещение преувеличено, зато вы наглядно видите, что происходит.
Разница в вычислении координат ограничивающих прямоугольников в разных режимах отображения касается только координаты у; вычисление координаты х одинаково во всех режимах отображения. Чтобы получить углы ограничивающего прямоугольника в режиме отображения ММ_ТЕХТ, вычтите толщину линий из координаты у левого верхнего угла описанного прямоугольника и добавьте ее к правому нижнему. Однако в режиме ММ LOENGLISH (и всех прочих режимах) ось у направлена противоположно, так что вы должны прибавить толщину линии к координате у верхнего левого угла описанного прямоугольника и отнять ее от координаты правого нижнего угла. И для всех режимов отображения вы отнимаете толщину линии от координаты х левого верхнего угла описанного прямоугольника и добавляете ее к координате х правого нижнего его угла.
Чтобы реализовать в Sketcher типы элементов насколько возможно согласованно, вы можете хранить описанный прямоугольник каждой фигуры в переменной- члене базового класса. Описанный прямоугольник должен быть вычислен при конструировании фигуры. Задача функции GetBoundRect () базового класса состоит в вычислении ограничивающего прямоугольника добавлением смещения к контуру описанного прямоугольника, равному толщине линии. Вы можете усовершенствовать определение класса CElement добавлением двух данных-членов.
class CElement: public CObject { protected:
COLORREF m_Color;//Цвет элемента
CRect in_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;// Установить цвет линии
m_Pen = 1;// Установить ширину пера
}
Теперь вы можете реализовать функцию-член базового класса GetBoundRect (), предполагая режим отображения ММ_ТЕХТ:
Н Получить ограничивающий прямоугольник элемента
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;
Вспомните, что индивидуальные данные-члены объекта CRect — это левая и верхняя граница (координаты х и у верхнего левого угла), а также правая и нижняя (координаты нижнего правого угла). Это общедоступные члены, так что вы можете обращаться к ним непосредственно. Часто допускаемая ошибка (по крайней мере, мною) — запись пар координат как (у, х) вместо правильного порядка (х,у).
Риск обоих подходов — этого и с использованием Inf lateRect () — заключается в том, что делается предположение о режиме отображения ММ ТЕХТ, что означает, что положительное направление оси у — сверху вниз. Если вы измените режим отображения, оба варианта будут работать неправильно, хотя это и не сразу может быть заметно.
Нормализованные прямоугольники
Функция Inf lateRect () работает, вычитая переданные ей значения из членов top и left прямоугольника и добавляя их членам bottom и right. Это значит, что вы можете обнаружить, что ваш прямоугольник на самом деле уменьшился в размере, если вы не обеспечите его нормализацию. Нормализованный прямоугольник имеет значение left меньше или равным right, а значение top — меньше или равным bottom. Вы можете обеспечить нормализацию прямоугольника, вызвав функцию- член объекта NormalizeRect (). Большинство функций-членов для корректной работы CRect требуют нормализованного объекта, так что при сохранении описанного прямоугольника в m_EnclosingRect вы должны обеспечить его нормализацию.
Вычисление описанного прямоугольника для линии
Ниже показано все, что понадобится в коде конструктора линии для вычисления описанного прямоугольника.
CLine::CLine(CPoint Start, CPoint End, COLORREF aColor)
m_StartPoint = Start; // Установить начальную точку линии
m_EndPoint = End;// Установить конечную точку линии
m_Color = aColor;// Установить цвет линии
ш_Реп = 1;// Установить ширину пера
// Определить описанный прямоугольник
m__EnclosingRect = CRect(Start, End) ;
m__EnclosingRect. Normal!zeRect ();
Используемые здесь аргументы конструктора 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;// Установить ширину пера
// Определить описанный прямоугольник
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 пиксель СPen аРеп;
if(laPen.CreatePen(PS_SOLID, m_Pen, m_Color)) {
// Создание пера не удалось
AfxMessageBox (_Т ("Не удалось создать перо для рисования прямоугольника"), МВ-ОК) ;
AfxAbort () ;
}
И Выбрать перо
CPen* pOldPen = pDC->SelectObject(&аРеп);
И Выбрать кисть
CBrush* pOldBrush = (CBrush*)pDC->SelectStockObject(NULL_BRUSH);
II Теперь нарисовать прямоугольник pDC->Rectangle(m_EnclosingRect); pDC->SelectObject(pOldBrush);// Восстановить старую кисть
pDC->SelectObject(pOldPen);// Восстановить старое перо
}
После установки пера и кисти вы просто передаете весь прямоугольник непосредственно функции Rectangle (), чтобы нарисовать его. Все, что остается сделать — это очистить и восстановить старое перо и кисть контекста устройства.
Класс CCircle
Интерфейс класса CCircle не отличается от интерфейса класса CRectangle. Вы можете определить окружность целиком через ее описанный прямоугольник, так что определение класса будет таким, как показано ниже.
// Класс, определяющий объект окружности
class CCircle: public CElement
{
public:
-CCircle(void);
virtual void Draw(CDC* pDC) ; / / Функция отображения окружности
M Конструктор объекта окружности
CCircle(CPoint Start, CPoint End, COLORREF aColor) ;
protected:
CCircle (void) ; // Конструктор по умолчанию — не должен использоваться };
Вы определили общедоступный конструктор, создающий окружность по двум точкам, а также объявили конструктор без аргументов — опять как protected. Кроме того, в определение класса добавлено также объявление функции рисования.
Реализация класса CCircle
Как я говорил ранее, когда вы создаете окружность, точка, в которой вы нажимаете левую кнопку мыши, становится ее центром, а после перемещения курсора с нажатой левой кнопкой точка, в которой кнопка отпущена, находится на линии готовой окружности. Работа конструктора заключается в том, чтобы преобразовать эти точки в форму, используемую в классе для определения круга.
Конструктор класса CCircle
Точка, в которой вы отпускаете левую кнопку мыши, может находиться в любом месте окружности, так что координаты точек, специфицирующих описанный прямоугольник, нужно вычислять, как показано на рис. 14.18.
Рис. 14.18. Вычисление координат точек, специфицирующих описанный прямоугольник для окружности
На рис. 14.18 видно, как можно вычислить координаты левого верхнего и правого нижнего углов описанного прямоугольника по отношению к центру окружности (xl, у!) — точке, записанной в момент нажатия левой кнопки мыши. Предполагая, что режимом отображения является ММ ТЕХТ, для левой верхней точки вы просто отнимаете радиус от каждой из координат центра. Аналогично правая нижняя точка вычисляется прибавлением радиуса к координатам х и у центра. Отсюда вы можете закодировать конструктор следующим образом:
// Конструктор объекта окружности
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.у)*(End.y-Start. y))));
// Теперь вычислить прямоугольник, описывающий окружность, исходя
Н из предположения, что режимом отображения является ММ_TEXT m_EnclosingRect = CRect(Start.x-Radius, Start.y-Radius, Start.x+Radius, Start.y+Radius);
m_Color = aColor; // Установить цвет окружности
Ш-Pen = 1;И Установить 1
}
Для использования функции sqrt () вы должны добавить такую строку в начало файла Elements. срр:
#include <math.h>
Вы можете поместить это после директивы #include для stdafx.h.
Максимальное значение координат составляет 32 бита, и члены хи у из CPoint объявлены как long, так что вычисление аргументов функции sqrt () может быть безопасно выполнено в целочисленном виде. Результат вычисления квадратного корня имеет тип double, поэтому вы приводите его к long, поскольку вам нужен целочисленный результат.
Рисование окружности
Вы уже видели, как рисовать окружность с помощью функции Аге () класса CDC, так что здесь воспользуемся функцией Ellipse (). Реализация функции Draw () в классе CCircle выглядит следующим образом.
И Нарисовать окружность
void CCircle::Draw(CDC* pDC) (
// Создать объект пера и инициализировать его
// цветом объекта и шириной линии в 1 пиксель СРеп аРеп;
if(!aPen.CreatePen(PS_SOLID, m_Pen, m_Color)) {
// He удалось создать перо
AfxMessageBox(_T("Создать перо для рисования окружности не удалось”), МВ-ОК);
AfxAbort();
}
СРеп* pOldPen = pDC->SelectObject(&аРеп) ; // Выбрать перо
// Выбрать нулевую кисть
CBrush* pOldBrush = (CBrush*)pDC->SelectStockObject(NULL_BRUSH);
H Нарисовать окружность
pDC->Ellipse(m_EnclosingRect);
pDC->SelectObject(pOldPen);// Восстановить старое перо
pDC->SelectObject(pOldBrush);// Восстановить старую кисть
}
После выбора пера соответствующего цвета и нулевой кисти окружность рисуется вызовом функции 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_MOUSEMOVE и доделать детали. Это можно сделать, выбрав CSketcherView в Class View и дважды щелкнув на имени обработчика OnMous eMove ().
Этот обработчик рассматривается только для рисования последовательности временных версий элемента по мере движения курсора, поскольку финальный элемент создается, когда вы отпускаете левую кнопку мыши. Поэтому вы можете трактовать рисование временных элементов в процессе движения как полностью локальное для данной функции, оставляя финальную версию элемента для прорисовки в функции- члене представления OnDraw (). Этот подход обеспечивает относительно эффектив-
ное рисование промежуточных элементов, поскольку не включает вызова функции 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_MERGENOTPEN
Рисование цветом, порожденным операцией ИЛИ над цветом фона и инверсией цвета пера.
R2_MASKNOTPEN
Рисование цветом, порожденным операцией И над цветом фона и инверсией цвета пера.
R2_MERGEPEN
Рисование цветом, порожденным операцией ИЛИ над цветом фона и цветом пера.
R2_N0TMERGEPEN
Рисование цветом, инверсным по отношению к цвету r2_mergepen.
R2_MASKPEN
Рисование цветом, порожденным операцией И над цветом фона и цветом пера.
R2_NOTMASKPEN
Рисование цветом, инверсным по отношению к цвету R2_maskpen.
R2_XORPEN
Рисование цветом, полученным в результате операции исключающего ИЛИ над цветом пера и цветом фона.
R2_NOTXORPEN
Рисование в цвете, инверсном по отношению к R2_xorpen.
Каждый из этих символов предопределен и соответствует определенному режиму рисования. Здесь предлагается множество опций, но одна из них может выполнить для нас некоторую волшебную работу — последняя, то есть R2_NOTXORPEN.
Когда вы устанавливаете режим R2_NOTXORPEN, то первый раз, когда вы рисуете конкретную фигуру цветом фона по умолчанию, она рисуется нормальным цветом пера, который вы специфицировали. Если же вы рисуете эту фигуру опять, перекрывая первый рисунок, то фигура исчезает, потому что цвет, которым она рисуется, соответствует тому, что получается в результате операции исключающего ИЛИ цвета пера с ним самим. В результате получается белый цвет рисования. Яснее это можно увидеть на примере.
Белый цвет формируется из равных пропорций максимальной яркости красного, синего и зеленого. Для простоты это можно представить, как 1, 1, 1 — три значения, представляющие компоненты RGB цвета. В той же схеме красный определяется как 1, 0, 0. Это дает комбинации, перечисленные в табл. 14.7.
Таблица 14.7. Комбинации цветов фона и пера при первом рисовании
R
G
в
Фон — белый
1
1
1
Перо — красное
1
0
0
Объединение по XOR
0
1
1
NOT XOR — дает красный
1
0
0
Поэтому первый раз вы рисуете красную линию на белом фоне, она остается красной, как указано в последней строке. Если теперь вы нарисуете ту же линию второй раз, перекрывая существующую, то пиксели фона перекроют красный. Результирующие цвета рисования показаны в табл. 14.8.
Таблица 14.7. Комбинации цветов фона и пера при втором рисовании
R
G
в
Фон — белый
1
0
0
Перо — красное
1
0
0
Объединение по XOR
0
0
0
NOT XOR — дает белый
1
1
1
Как показано в последней строке, линия рисуется белым, и поскольку фон белый, линия исчезает из виду.
Здесь вам нужно позаботиться о правильном выборе цвета фона. Вы можете убедиться, что рисование белым пером на красном фоне не будет работать так же хорошо, поскольку первый раз вы нарисуете что-то красным, а потому это будет невидимо. Второй раз оно появится в белом цвете. Если вы станете рисовать на черном фоне, фигура появится и исчезнет, как это происходит и на белом фоне, но при этом она не будет рисоваться выбранным цветом пера.
Кодирование обработчика OnMouseMove ()
Начнем с добавления кода, который создает элемент после сообщения о перемещении мыши. Поскольку вы собираетесь рисовать элемент в функции-обработчике,
понадобится создать объект контекста устройства. Наиболее удобный класс для этого — 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; // Сохранить текущую позицию курсора
И Проверить предыдущий временный элемент {
// Мы попадаем сюда, если было предыдущее перемещение мыши, / / поэтому сюда добавить код удаления старого элемента
}
// Создать временный элемент типа и цвета, который
П записан в объекте документа, и нарисовать его mjpTenpElement = CreateElement ();// Создать новый элемент
m_pTenpElement->Draw(6aDC);II Нарисовать элемент}
}
Первая новая строка кода создает объект CClientDC. Указатель this, передаваемый конструктору, идентифицирует текущий объект представления, поэтому объект CClientDC имеет контекст устройства, соответствующий клиентской области текущего представления. Наряду с упомянутыми мною характеристиками, этот объект обладает всем функциями рисования, которые вам нужны, поскольку наследован от класса CDC. Первая функция-член, используемая здесь — SetROP2 (), которая устанавливает режим рисования R2_NOTXORPEN.
Чтобы создать новый элемент, вы сохраняете текущую позицию курсора в переменной-члене m_SecondPoint, а затем вызываете функцию-член представления CreateElement (). (Функцию CreateElement () мы определим сразу после того, как покончим с данным обработчиком.) Это функция должна создавать элемент, используя две точки, сохраненные в текущем объекте представления, со спецификациями цвета и типа, хранимыми в объекте документа, и возвращать адрес элемента. Сохраните его в m_pTempElement.
Используя указатель нового элемента, вы вызываете функцию-член Draw (), заставляя объект нарисовать себя. Адрес объекта CClientDC передается как аргумент. Поскольку вы определили функцию Draw () , как виртуальную в базовом классе СЕ 1 ement, автоматически выбирается версия этой функции для того типа объекта, на который указывает m_pTempElement. Новый элемент рисуется нормально в режиме R2_NOTXORPEN, поскольку вы рисуете его первый раз на белом фоне.
Вы можете использовать указатель m_pTempElement в качестве индикатора существования предыдущего временного элемента. Код этой части обработчика показан ниже.
void CSketcherView::OnMouseMove(UINT nFlags, CPoint point)
{
// Определение объекта контекста устройства для представления
CClientDC aDC(this); // DC для данного представления
aDC.SetROP2(R2_NOTXORPEN);// Установить режим рисования
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); // Проверить указатель
// Теперь выбрать элемент, используя тип, указанный в документе switch (pDoc->GetElementType ())
{
case RECTANGLE:
return new CRectangle (m_FirstPoint, m_SecondPoint, pDoc->GetElementColor());
case CIRCLE:
return new CCircle (m__FirstPoint, mJSecondPoint, pDoc->GetElementColor());
case CURVE:
return new CCurve (pDoc->GetElementColor ());
case LINE:
return new CLine (m^FirstPoint, m_SecondPoint,
pDoc->GetElementColor()); default:
// Что-то идет не так
AfxMessageBox (_Т("Неверный код элемента"), МВ__ОК) ;
AfxAbort ();
return NULL;
Рас. 14.19. Добавление в класс CSketcherView функции CSketcherView
Строки, не выделенные полужирным — это те, которые добавляются автоматически, когда вы добавляете функцию к классу, используя пункт меню Add^Add Function. Первое, что делается здесь — получение указателя на документ вызовом GetDocument (), как было показано ранее. Для целей безопасности применяется макрос ASSERT_VALID (), дабы убедиться, что возвращен правильный указатель. В отладочной версии MFC, которая применяется с отладочной версией вашего приложения, этот макрос вызывает функцию-член объекта AssertValidO , которая специфицирована как аргумент макроса. Она проверяет корректность текущего объекта,
и если переданный указатель равен 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)
{
// Вызвать функцию класса документа для сохранения элемента,
// указанного m_pTempElement в объекте документа delete m_pTempElement; // Этот код временный mjDTempElement = 0;// Сбросить указатель элемента
}
}
Оператор if проверяет, что m_pTempElement не равен нулю, перед тем, как его обработать. Всегда существует возможность, что пользователь нажмет и отпустит левую кнопку мыши без ее перемещения — в этом случае никакой элемент не создается. Но если элемент существует, указатель на него передается объекту документа; соответ-, ствующий код будет добавлен в следующей главе. А пока вы просто удаляете элемент из памяти, чтобы не переполнять кучу. И, наконец, m_pTempElement сбрасывается в ноль, что готовит его к началу рисования пользователем следующего элемента.
Испытание программы Sketcher
Прежде чем запустить пример с обработчиками событий мыши, вы должны обновить функцию OnDraw () в реализации класса CSketcherView, чтобы избавиться от старого кода, добавленного ранее.
Дабы убедиться, что функция OnDraw () чиста, перейдите в панель Class View и выполните двойной щелчок на имени функции, чтобы увидеть ее реализацию в SketcherView. срр. Удалите любой код, который вы добавили, но оставьте первых четыре строки, предоставленные мастером для получения указателя на объект документа. Он понадобится вам позднее, чтобы получить элементы, когда они будут храниться в документе. Код функции должен быть таким:
void CSketcherView::OnDraw (CDC* pDC) {
CSketcherDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc) ;
if(!pDoc) return;
}
Поскольку у вас еще нет никаких элементов в документе, пока не нужно ничего добавлять в эту функцию. Когда в следующей главе мы начнем сохранять данные в документе, тогда придется добавить код для рисования элементов в ответ на сообщение WM^PAINT. А пока, как вы увидите, элементы просто будут исчезать при изменении размера представления.
Запуск примера
После того, как вы сохраните все исходные файлы, вновь соберите программу. Если при вводе кода не было допущено ошибок, компиляция и компоновка должны пройти гладко, так что после этого можно будет сразу запустить программу. Типичное окно, которое вы при этом увидите, показано на рис. 14.20.
Рис. 14.20. Работа программы Sketcher
Поэкспериментируйте с пользовательским интерфейсом. Обратите внимание, что вы можете перемещать окно по экрану, и фигурки остаются в нем до тех пор до тех пор, пока вы не выдвинете их за пределы границ окна приложения. Если это сделать, элементы не появятся опять при перемещении окна обратно. Когда клиентская область скрывается и открывается, Windows посылает приложению сообщение WM_PAINT, которое инициирует вызов функции-члена объекта представления OnDraw (). Как вы знаете, функция представления OnDraw () пока ничего не делает. Это будет исправлено, когда вы будете использовать документ для хранения нарисованных элементов.
Когда вы изменяете размеры окна, все фигуры немедленно исчезают, но когда вы перемещаете представление целиком, они остаются (до тех пор, пока не выходят за границы окна приложения). Почему так происходит? Дело в том, что когда вы изменяете размеры окна, Windows делает недействительной всю клиентскую область и ожидает, что ваше приложение перерисует ее в ответ на сообщение WM_PAINT. Если вы перемещаете представление целиком, Windows заботится о перемещении клиентской области, как она есть. В этом можно убедиться, переместив представление так, чтобы фигура была скрыта частично. Когда вы возвращаете ее обратно, то по-прежнему видите только ту ее часть, которая оставалась видимой — остальное исчезает.
Если попытаться рисовать в процессе перемещения курсора за пределами клиентской области, то обнаружится необычный эффект. Вне окна представления вы теряете слежение за курсором мыши, что разрушает механизм “растягивания” фигуры. Что же происходит?
Захват сообщений мыши
Проблема вызвана тем фактом, что Windows посылает сообщения мыши тому окну, которое находится в данный момент под курсором. Как только курсор покидает клиентскую область окна представления вашего приложения, сообщения WM_MOUSEMOVE поступают в какое-то другое место. Вы можете исправить это, используя некоторый унаследованный механизм в классе CSketcherView.
Класс представления наследует функцию SetCapture (), которую вы можете вызвать для уведомления Windows, что вы хотите, чтобы ваше окно представления получало все сообщения мыши до тех пор, пока вы это не отмените вызовом другой унаследованной функции класса представления — Releasecapture (). Можно захватить мышь сразу после нажатия левой кнопки, модифицировав обработчик сообщения WM_LBUTTONDOWN:
// Обработчик нажатия левой кнопки мыши
void CSketcherView::OnLButtonDown(UINT nFlags, CPoint point)
m_FirstPoint = point; // Записать позицию курсора
SetCapture () ; // Перехватывать все последующие сообщения мыши
}
После этого вы должны вызвать функцию Releasecapture () в обработчике WM_LBUTTONUP. Если не сделать этого, другие программы не смогут получить никаких сообщений мыши до тех пор, пока выполняется ваша программа. Конечно, отпускать мышь следует только после того, как она ранее была захвачена. Функция GetCapture (), которую наследует ваш класс представления, возвращает указатель на окно, захватившее мышь, и это дает вам возможность узнать, были ли захвачены сообщения мыши. Для этого нужно просто добавить следующий оператор в обработчик WM LBUTTONUP:
void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point)
if(this = GetCapture())
Releasecapture () ;// Прекратить захват сообщений мыши
// Убедиться в наличии элемента
if (mjpTempElement) {
// Вызвать функцию класса документа для сохранения элемента,
U тквавввото m_j)TempElement в объекте документа
delete m_pTempElement; // Этот код временный
mjpTempElement = 0;// Сбросить указатель на элемент
}
}
Если указатель, возвращенный функцией GetCapture (), эквивалентен указателю this, значит, мышь захвачена вашим представлением, так что вы можете ее освободить.
И последнее изменение, которое следует провести — модифицировать обработчик WM MOUSEMOVE, чтобы он имел дело только с сообщениями, захваченными вашим представлением. Это изменение совсем небольшое:
void CSketcherView::OnMouseMove(UINT nFlags, CPoint point) {
// Определить объект контекста устройства для представления
CClientDC aDC(this);II DC для данного представления
aDC.SetROP2(R2_NOTXORPEN);/ / Установить режим рисования
if((nFlags & MK_LBUTTON) && (this == GetCapture())) {
m_SecondPoint = point;// Сохранить текущую позицию курсора
if (m_pTempElement)
{
// Перерисовать старый элемент, чтобы он исчез из представления mjpTempElement->Draw (&aDC) ;
delete mjpTempElement;// Удалить старый элемент
mjpTempElement = 0;// Сбросить указатель в 0
}
// Создать временный элемент типа и цвета, записанного
// в объекте документа, и нарисовать его
mjpTempElement = CreateElement () ;// Создать новый элемент mjpTempElement->Draw(&aDC) ;// Нарисовать элемент
}
}
Этот обработчик теперь обрабатывает сообщения только в том случае, если левая кнопка нажата и ранее был вызван обработчик нажатия левой кнопки вашего представления, так что мышь была захвачена окном вашего представления.
Если вы перекомпилируете программу Sketcher с этими дополнениями, то обнаружите, что проблемы, возникавшие ранее при выходе курсора за пределы клиентской области, исчезли.
Резюме
После прочтения этой главы у вас должно появиться четкое понимание того, как писать обработчики сообщений мыши, и как организовать операции рисования в ваших Windows-программах. Ниже перечислены важнейшие моменты, раскрытые в настоящей главе.
По умолчанию Windows обращается к клиентской области окна, используя клиентскую координатную систему с точкой отсчета, находящейся в левом верхнем углу клиентской области. Положительное направление оси х идет слева направо, а положительное направление оси у — сверху вниз.
Вы можете рисовать в клиентской области окна только с использованием контекста устройства.
Для обращения к клиентской области окна контекст устройства предоставляет набор логических координатных систем, называемых режимами отображения.
По умолчанию точка начала координат режима отображения находится в левом верхнем углу клиентской области. По умолчанию используется режим отображения ММ ТЕХТ, который предусматривает измерение координат в пикселях. Положительная ось х в этом режиме направлена слева направо, а положительная ось у — сверху вниз.
Ваша программа всегда должна рисовать постоянное содержимое клиентской области окна в ответ на сообщение WM PAINT, хотя временные сущности могут быть нарисованы и в другие моменты. Все рисование документа вашего приложения должно управляться функцией-членом OnDraw () класса представления. Эта функция вызывается в ответ на получение вашим приложением сообщения WM_PAINT.
Вы можете идентифицировать часть клиентской области, которую необходимо перерисовать, вызвав функцию-член InvalidateRect () вашего класса представления. Переданная в качестве аргумента область добавляется Windows к общей области, подлежащей перерисовке на момент отправки следующего сообщение WM PAINT вашему приложению.
Windows посылает стандартные сообщения вашему приложению для событий мыши. Вы можете подготовить обработчики, имеющие дело с этими сообщениями, применив для этого мастер создания классов.
Можно перенаправить все сообщения мыши вашему приложению, если вызвать функцию SetCaptire {) класса представления. Завершив работу, вы должны освободить мышь, вызвав функцию Releasecapture (). Если этого не сделать, другие приложения не смогут получать сообщения мыши.
Можно реализовать “растягивание” фигур при создании геометрических сущностей, перерисовывая их в обработчике сообщений о перемещении мыши.
Функция-член класса С DC под названием SetROP2 () позволяет устанавливать режимы рисования. Выбор правильного режима рисования существенно упрощает реализацию операций “растягивания”.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Добавьте пункт меню и кнопку панели инструментов для элемента типа эллипс, как в упражнениях к главе 13, и определите класс для поддержки рисования эллипсов, заданных по двум точкам противоположных углов описанного прямоугольника.
Какие функции нужно модифицировать для поддержки рисования эллипса? Внесите изменения программу, чтобы она могла рисовать эллипсы.
Какие функции следует модифицировать в примере из предыдущего упражнения, чтобы первая точка определяла центр эллипса, а текущая позиция курсора — угол описанного прямоугольника? Модифицируйте пример, чтобы он работал подобным образом. (Подсказка: изучите члены класса CPoint в справочной системе.)
Добавьте новое всплывающее меню IDR__SketcherTYPE для задания стиля пера (Pen Style), чтобы можно было рисовать линии сплошные, пунктирные, точечные, штрихпунктирные и штрихпунктирные с двойной точкой.
Какие части программы должны быть модифицированы для поддержки операций меню и рисования элементов линиями этих типов?
Реализуйте поддержку нового всплывающего меню и рисование элементов линиями любых типов.
15
Создание документаи усовершенствованиепредставления
В этой главе вы познакомитесь со средствами, предоставляемыми MFC для управления коллекциями элементов данных. Вы используете их для завершения определения класса и реализации геометрического элемента — кривой линии, которая осталась недоделанной в предыдущей главе. Вы также расширите приложение Sketcher для хранения данных в документе, и сделаете представление документа более гибким, добавив несколько новых приемов в процесс.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
Коллекции и работа с ними.
Как использовать коллекцию для хранения точки кривой.
Как использовать коллекцию для хранения данных документа.
Как реализовать рисование документа.
Как реализовать прокрутку представления.
Как создать контекстное меню в месте положения курсора.
Как выделить элемент, ближайший к курсору, чтобы позволить пользователю перемещать и удалять элементы.
Как программировать мышь для перемещения и удаления элементов.
Что такое классы коллекций?
В связи с особой природой Windows-приложений вам часто понадобится управлять коллекциями элементов данных в условиях, когда вы заранее не знаете, сколько будет этих элементов, или даже какого типа они будут. Это отчетливо видно на примере приложения Sketcher. Пользователь может нарисовать произвольное количество элементов, которые могут быть линиями, прямоугольниками, окружностями и кривыми, причем в любой последовательности. Библиотека MFC предлагает группу классов коллекций, предназначенных специально для решения проблем подобного рода. Коллекция — скопление произвольного количества элементов данных, организованных определенным образом.
Типы коллекций
MFC предоставляет огромное количество классов коллекций для управления данными. Вы используете на практике лишь пару из них, но будет полезно иметь представление и о других доступных классах коллекций. MFC поддерживает три вида коллекций, отличающихся способом организации данных. Способ организации коллекции называется формой коллекции. Существуют три таких формы, и они перечислены в табл. 15.1.
Таблица 15.1. Формы коллекций
Форма
Как организована информация
Массив (array)
Массив в данном контексте похож на массив, который вы видели в языке C++. Это упорядоченная организация элементов, где любой элемент извлекается по целочисленному значению индекса. Коллекция-массив может автоматически расти, чтобы вместить больше элементов данных; однако, другие типы коллекций обычно более предпочтительны, потому что коллекции-массивы могут оказаться относительно медленными в работе.
Список (list)
Коллекция-список — упорядоченное множество элементов, в котором каждый элемент имеет два ассоциированных с ним указателя, ссылающихся на следующий и предыдущий элементы списка. Вы уже сталкивались со связным списком в главе 7, когда речь шла о структурах. Такой список называется двусвязным списком, потому что его элементы имеют как прямые, так и обратные ссылки. В нем можно выполнять поиск в обоих направлениях, и, подобно массиву, коллекция-список при необходимости автоматически растет. Коллекция-список проста в использовании и работает быстро, когда нужно добавлять элементы. Поиск элемента может быть медленным, если в списке содержится большое количество элементов данных.
Карта (тар)
Карта — это неупорядоченная коллекция элементов данных, в которой каждый элемент ассоциируется с ключом, используемым для извлечения элемента из карты. Обычно ключом служит строка, но это также может быть числовое значение или объект любого типа. Карты работают быстро при сохранении элементов данных и при их поиске, поскольку ключи обеспечивают прямой доступ к нужным элементам. Может показаться, что карта — идеальный выбор, и часто так оно и есть, но при последовательном доступе массивы работают быстрее. Кроме того, существует проблема выбора уникального ключа для каждого элемента в списке.
Классы коллекций MFC предлагают два подхода к реализации каждого типа коллекций. Один подход основан на использовании шаблонов классов и представляет безопасную к типам обработку данных в коллекции. Безопасная к типам обработка
означает, что данные, передаваемые функции-члену класса коллекции, проверяются на принадлежность к типу, который может быть обработан функцией.
Другой подход предусматривает использование широкого диапазона классов коллекций (вместо шаблонов), но не обеспечивает никакой проверки данных. Если вы хотите, чтобы ваши классы коллекций были безопасными к типам, то должны включить собственный код для обеспечения этого. Такие классы были доступны в старых версиях Visual C++ под Windows, а шаблонные классы коллекций — нет. Я сосредоточу внимание на версиях коллекций на базе шаблонов, поскольку они дают лучшие шансы избежать ошибок в приложении.
Безопасные к типам классы коллекций
Безопасные к типам классы коллекций на базе шаблонов поддерживают коллекции объектов любого типа, а также коллекции указателей на объекты любых типов. Коллекции объектов поддерживаются шаблонными классами CArray, CList и СМар, а коллекции указателей на объекты — шаблонными классами CTypedPtrArray, CTypedPtrList и CTypedPtrMap. Мы не будем погружаться в детали всех этих классов, а рассмотрим только два, которые будут использованы в программе Sketcher. Один из них вы используете для хранения объектов, а второй — для хранения указателей на объекты, так что получите представление о коллекциях обоих сортов.
Коллекции объектов
Все шаблонные классы для определения коллекций объектов наследуются от класса MFC CObject. Они определены подобным образом, и поэтому наследуют свойства класса CObject, что особенно удобно по ряду причин. Сюда входит операции файлового ввода и вывода объектов, обычно называемые сериализацией; вы познакомитесь с ней в главе 17.
Эти шаблонные классы могут хранить и управлять объектами любого рода, включая базовые типы C++, плюс любые классы и структуры, которые можете определить вы или кто-нибудь другой. Поскольку эти классы хранят объекты, то всякий раз, когда вы добавляете элемент в список, массив или карту, объект шаблонного класса должен выполнить копирование вашего объекта. Следовательно, любой тип класса, который вы хотите хранить в любой из этих коллекций, должен иметь конструктор копирования. Конструктор копирования вашего класса используется для создания дубликата объекта, подлежащего сохранению в коллекции.
Взглянем на общие свойства каждого шаблонного класса, который предоставляет безопасное к типам управление объектами. Это не будет исчерпывающим перечнем всех существующих функций-членов Скорее это нужно только для того, чтобы дать вам достаточное ощущение того, как они работают, дабы вы могли решить — стоит их использовать или нет. Вы можете получить информацию обо всех функциях-членах, используя справочную систему по определению шаблонного класса.
Шаблонный класс CArray
Вы можете применять этот шаблон для хранения объектов любого рода в массиве, автоматически расширяющемся по мере необходимости. Коллекция-массив показана на рис. 15.1.
Как и в обычных массивах “родного” C++, элементы коллекции-массива индексируются, начиная с 0. Объявление коллекции-массива принимает два аргумента. Первый аргумент — тип хранимого объекта, так что, например, если коллекция-массив долж-
на хранить объекты типа CPoint, в качестве первого аргумента вы специфицируете CPoint. Второй аргумент — это тип, используемый в вызовах функции-члена. Чтобы избежать накладных расходов, связанных с копированием объектов, переданных по значению, вторым аргументом обычно бывает ссылка, так что пример коллекции-массива для хранения объектов CPoint, выглядит следующим образом:
CArraycCPoint, CPoint&> PointArray;
Это определяет объект класса коллекции-массива PointArray, который хранит объекты CPoint. Когда вызываются функции-члены объекта, аргумент является ссылкой, поэтому, чтобы добавить объект CPoint, вы должны записать:
PointArray.Add(aPoint) ;
и аргумент aPoint передается по ссылке.
Тип сохраняемогообъектаТип сохраняемогообъектаТип используемого аргументаТип используемого аргументаИндексОИндексО- Возвращаемый объектGetAt(2)По индексу -- Возвращаемый объектGetAt(2)По индексу -112233445Возвращаемый индекс ——/g5Возвращаемый индекс ——/gAdd (AnObject)Add (AnObject)Сохраняет объектСохраняет объектРис. 15.1. Коллекция-массивРис. 15.1. Коллекция-массивObject 1 Object2 ObjectsObject4 ObjectsObjectsAnObject IObject 1 Object2 ObjectsObject4 ObjectsObjectsAnObject ISetSize(S) определяет начальный размерSetSize(S) определяет начальный размерРасширение > происходит автоматическиРасширение > происходит автоматическиКоллекция-массив: CArray<ObjectType, ObjectType&> anArray
Тип сохраняемогообъекта
Тип сохраняемогообъекта
Тип используемого аргумента
Тип используемого аргумента
ИндексО
ИндексО
- Возвращаемый объект
GetAt(2)
По индексу -
- Возвращаемый объект
GetAt(2)
По индексу -
1
1
2
2
3
3
4
4
5
Возвращаемый индекс ——/g
5
Возвращаемый индекс ——/g
Add (AnObject)
Add (AnObject)
Сохраняет объект
Сохраняет объект
Рис. 15.1. Коллекция-массив
Рис. 15.1. Коллекция-массив
Object 1 Object2 Objects
Object4 Objects
Objects
AnObject I
Object 1 Object2 Objects
Object4 Objects
Objects
AnObject I
SetSize(S) определяет начальный размер
SetSize(S) определяет начальный размер
Расширение > происходит автоматически
Расширение > происходит автоматически
После объявления коллекции-массива важно вызвать функцию-член SetSize (), чтобы зафиксировать начальное количество элементов, которое вам понадобится перед тем, как использовать его. Он будет работать, даже если этого и не сделать, но начальное размещение элементов и последующие расширения будут малыми, что снижает эффективность операций и требует частого перераспределения памяти для массива. Начальное количество элементов, которое вы должны указать, зависит от типичного размера массива, который, как вы ожидаете, вам понадобится, и от того, в каких пределах будет изменяться его размер. Если, например, вы ожидаете, что вашей программе понадобится минимум 400-500 элементов, с последующим увеличением до 700-800, то подойдет начальное значение 600.
Чтобы извлечь содержимое элемента, вы можете применить функцию Get At (), как показано на рис. 15.1. Чтобы сохранить третий элемент PointArray в переменной aPoint, вы записываете следующим образом:
aPoint = PointArray.GetAt(2);
Класс также перегружает операцию [ ], так что вы можете извлечь третий элемент 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<Tnn06bexTa, ТипОбъекта&> aList;
При объявлении коллекции-списка вы должны применить два аргумента к шаблону: тип хранимого в коллекции объекта и способ спецификации объекта в аргументах функций. Пример показывает, что второй аргумент — ссылка, поскольку так делается
наиболее часто. Он не обязательно должен быть ссылкой — вы можете применить и указатель, или даже просто тип объекта (тогда объекты будут передаваться по ссылке), но это будет медленнее.
Вы можете воспользоваться списком для управления кривыми линиями в программе Sketcher. Можно объявить коллекцию-список для хранения точек, описывающих объект-кривую, в следующем операторе:
CListcCPoint, CPoint&> PointList;
Это объявляет список по имени PointList, хранящий объекты типа CPoint, которые передаются функциям класса по ссылке. Мы вернемся к этому, когда позднее в этой главе продолжим совершенствовать программу Sketcher.
Добавление элементов в список
Добавление объектов в начало или конец списка осуществляется с использованием функций-членов AddHead () или AddTail (), как показано на рис. 15.2.
Коллекция-список: CLisKObjectType, ObjectType&> aList
Тип хранимогообъектаТип хранимогообъектаAddHead (ThisObject)AddHead (ThisObject)Тип используемого аргументаТип используемого аргументаСохраняет объектСохраняет объектРасширение происходит автоматическиРасширение происходит автоматическиAddTail (ThatObject)AddTail (ThatObject)Рис. 15.2. Коллекция-списокРис. 15.2. Коллекция-список
Тип хранимогообъекта
Тип хранимогообъекта
AddHead (ThisObject)
AddHead (ThisObject)
Тип используемого аргумента
Тип используемого аргумента
Сохраняет объект
Сохраняет объект
Расширение происходит автоматически
Расширение происходит автоматически
AddTail (ThatObject)
AddTail (ThatObject)
Рис. 15.2. Коллекция-список
Рис. 15.2. Коллекция-список
На рис. 15.2 показаны прямые и обратные указатели, которые “склеивают” объекты списка вместе. Это внутренние связи, к которым вы не можете получить доступ непосредственно, но можете делать почти все, что угодно, используя функции, представленные в общедоступном интерфейсе класса.
Для добавления объекта a Point в хвост списка PointList потребуется записать так:
PointList.AddTail(aPoint);II Добавить элемент в конец
Когда новые элементы добавляются, размер списка автоматически увеличивается.
Обе функции — AddHead () и AddTail () — возвращают значение типа POSITION, которое специфицирует позицию вставляемого объекта в списке. Способ использования переменной типа POSITION показан на рис. 15.3.
Коллекция-список: CLisKObjectType, ObjectType&> aList
Тип используемого аргументаТип используемого аргументаТип хранимого объектаТип хранимого объектаРис. 15.3. Использование переменной типа POSITIONРис. 15.3. Использование переменной типа POSITION
Тип используемого аргумента
Тип используемого аргумента
Тип хранимого объекта
Тип хранимого объекта
Рис. 15.3. Использование переменной типа POSITION
Рис. 15.3. Использование переменной типа POSITION
Вы можете использовать тип POSITION для извлечения объекта в заданной позиции списка с помощью функции GetNext(). Обратите внимание, что вы не можете применять арифметические действия к значениям типа POSITION — вы можете только модифицировать значение позиции через функции-члены объекта списка. Более того, вы не можете установить значение позиции в определенное число. Переменные POSITION могут устанавливаться только функциями-членами списка.
Наряду с возвратом объекта функция GetNext () увеличивает переменную позиции, переданной ей, так что она после этого указывает на следующий объект в списке. Вы можете, таким образом, использовать повторные вызовы GetNext () для пошагового перебора списка — элемент за элементом. Переменная позиции устанавливается в NULL, если вы используете GetNext () для извлечения последнего объекта в списке, так что вы можете применять это для управления операцией цикла. Всегда следует убеждаться в том, что вы имеете правильное значение позиции, когда вызываете функцию-член объекта списка.
Имея значение POSITION, можно вставить элемент в определенную позицию списка. Чтобы вставить объект ThePoint в список PointList непосредственно перед элементом в позиции aPosition, используйте следующий оператор:
PointList.insertBefore(aPosition, ThePoint);
Функция InsertBefore () также возвращает позицию нового объекта. Для вставки элемента после объекта из заданной позиции предусмотрена функция InsertAfter (). Эти функции часто применяются со списком, содержащим отображаемые геометрические элементы. Элементы рисуются на экране в последовательности, в которой вы-
полняется проход по списку. Элементы, появившиеся в списке позднее, перекрывают помещенные ранее элементы, так что порядок элементов определяет, как они перекрывают друг друга. Таким образом, вы можете определить, какие из существующих элементов перекрывают новый элемент, поместив его в соответствующую позицию списка.
Когда вам нужно установить существующий объект списка с определенное значение, вы можете использовать функцию 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 () для сравнения объектов в списке с аргументом. Это вспомогательная функция — одна из тех, о которых упоминалось ранее, — предназначенная для обслуживания процесса поиска. Реализация этой функции по умолчанию сравнивает адрес аргумента с адресом каждого объекта в списке. Это подразумевает, что для того, чтобы поиск был успешным, аргумент должен быть
в действительности элементом списка, а не копией. Если объект найден в списке, возвращается позиция элемента. Если он не найден, возвращается 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);
У этой функции нет возвращаемого значения. Ответственность за правильность значения позиции, переданной ей в аргументе, возлагается на вас. Если необходимо удалить все содержимое списка, используется функция-член 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.
Коллекция-карта: СМар<КеуТуре, КеуТуре&, ObjectType, ObjectType&> аМар
Тип аргумента ключаТип аргумента ключаТип объекта аргументаТип объекта аргументаТип аргумента ключа Тип сохраняемого объектаТип аргумента ключа Тип сохраняемого объектаРис. 15.4. Коллек^ия-4са{>таРис. 15.4. Коллек^ия-4са{>та
Тип аргумента ключа
Тип аргумента ключа
Тип объекта аргумента
Тип объекта аргумента
Тип аргумента ключа Тип сохраняемого объекта
Тип аргумента ключа Тип сохраняемого объекта
Рис. 15.4. Коллек^ия-4са{>та
Рис. 15.4. Коллек^ия-4са{>та
Процесс хеширования может и не произвести уникального значения хеша по ключу, в этом случае элемент — ключ вместе с ассоциированным объектом — вводится и связывается с элементом или элементами, сохраненными ранее и имеющими то же значение ключа (часто в виде списка). Конечно, чем меньше сгенерировано уникальных хеш-значений, тем менее эффективен процесс извлечения из карты, поскольку поиск требует просмотра всех элементов, имеющих одно и то же значение хеша.
Для объявления карты требуется пять аргументов:
CMap<LONG,. LONG&, CPoint, CPoint&> PointMap;
Первые два специфицируют тип ключа и способ передачи его в аргументе. Обычно он передается по ссылке. Вторая пара аргументов специфицирует тип объекта и способ его передачи в аргументе, как было показано ранее.
Вы можете сохранить объект в карте с помощью операции [ ] (см. рис. 15.4). Для сохранения объекта вы можете также воспользоваться функцией-членом Set At (), передав ей значение ключа и объект в качестве аргументов. Следует отметить, что вы не можете применять операцию [ ] в правой части операции присваивания для извлечения объекта, поскольку эта версия операции в классе не реализована.
Чтобы извлечь объект, используйте функцию Lookup (), показанную на рис. 15.4. Это извлечет объект, соответствующий указанному ключу; функция возвращает TRUE, если объект найден, и FALSE — в противном случае. Вы можете также выполнить итерацию по всем объектам в карте, применив переменную типа POSITION, хотя последовательность извлечения объектов при этом не связана с последовательностью их добавления в карту. Это объясняется тем, что объекты сохраняются в карте в места, определяемые значением хеша, а не последовательностью их ввода.
Вспомогательные функции, использованные СМар
Наряду с вспомогательными функциями, которые мы обсуждали в контексте массивов и списков, коллекции-карты также используют глобальную функцию HashKey (), определенную следующим щаблоном:
template<class ARG_KEY>
UINT HashKey(ARG_KEY key);
Эта функция преобразует значение ключа в хеш-значение типа UINT, что эквивалентно unsigned int. Версия по умолчанию делает это простым сдвигом значения ключа вправо на 4 бита. Если операция по умолчанию не отвечает вашим требованиям, вы должны реализовать собственную версию этой функции.
Существуют разные приемы хеширования, которые варьируются в зависимости от используемого в качестве ключа типа данных, и множество элементов, которые вы, вероятно, захотите сохранять в карте. Вероятное количество элементов, подлежащих сохранению, указывает количество уникальных хеш-значений, которые вам понадобятся. Общий метод хеширования значения числового ключа — вычислять хеш-значение как остаток от деления ключа на ^(модуль Л/), где N— количество различных значений, которые вам нужны. По причинам, которые слишком долго объяснять здесь, чтобы это хорошо работало, 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 и соответствует минимальному значению, большему или
равному количеству возможных различных символов в строке. Для длинных строк это может генерировать очень большие числа, так что применяются специальные приемы для вычисления значения модуля N. Подробное обсуждение этих приемов выходит за рамки настоящей книги, но вы можете найти множество Web-ссылок, запустив поиск по слову “хеширование”.
Типизированные коллекции указателей
Шаблонные классы типизированных коллекций указателей предназначены для хранения указателей вместо самих объектов. Это основное отличие между этими шаблонными классами и шаблонными классами, описанными выше. Рассмотрим применение шаблонного класса CTypedPtrList, поскольку именно его вы будете использовать в качестве основы для управления элементами вашего класса документа CSketcherDoc.
Шаблонный класс CTypedPtrList
Объявить типизированный класс списка можно с помощью оператора следующего вида:
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.
Таблица 15.2. Функции, определенные в CTypedPtrList
Функция
Описание
GetHead()
Возвращает указатель на начало списка. Прежде чем вызывать эту функцию, необходимо с помощью функции IsEmpty О убедиться, что список не пуст.
GetTail ()
Возвращает указатель на конец списка. Прежде чем вызывать эту функцию, необходимо с помощью функции IsEmpty () убедиться, что список не пуст.
RemoveHead()
Удаляет первый указатель в списке. Прежде чем вызывать эту функцию, необходимо с помощью функции IsEmpty () убедиться, что список не пуст.
RemoveTail()
Удаляет последний указатель в списке. Прежде чем вызывать эту функцию, необходимо с помощью функции IsEmpty () убедиться, что список не пуст.
GetNext()
Возвращает указатель на позицию, которая задана переменной типа position, переданной в аргументе-ссылке. Переменная обновляется для указания на следующий элемент в списке. Когда достигнут конец списка, переменная позиции устанавливается в null. Эта функция может быть использована для итерации в прямом направлении по всем указателям списка.
GetPrev ()
Возвращает указатель на позицию, которая задана переменной типа position, переданной в аргументе-ссылке. Переменная обновляется для указания на предыдущий элемент списка. Когда достигается начало списка, переменная позиции устанавливается в null. Эта функция может быть использована для итерации в обратном направлении по всем указателям списка.
GetAt()
Возвращает указатель, хранящийся в позиции, которая задана переменной типа position, переданной в аргументе, причем переменная не изменяется. Поскольку функция возвращает ссылку, до тех пор, пока список не определен как const, эта функция может использоваться в левой части операции присваивания для модификации элемента списка.
Таблица 15.3. Функции CTypedPtrList, унаследованные от CQbList
Функция
Описание
AddHead()
Добавляет указатель, переданный в аргументе, в начало списка и возвращает значение типа position, соответствующее новому элементу. Существует другая версия этой функции, добавляющая в начало списка другой список.
AddTail ()
Добавляет указатель, переданный в аргументе, в конец списка и возвращает значение типа position, соответствующее новому элементу. Существует другая версия этой функции, добавляющая в конец списка другой список.
RemoveAll ()
Удаляет все элементы из списка. Обратите внимание, что это не уничтожает объектов, на которые указывали элементы списка. Вы должны позаботиться об этом самостоятельно.
GetHeadPosition()
GetTailPosition()
SetAt()
Возвращает позицию элемента в начале списка.
Возвращает позицию элемента в конце списка.
Устанавливает указатель, специфицированный вторым аргументом, в позицию списка, определенную первым аргументом. Неверное значение позиции вызывает ошибку.
RemoveAt()
Удаляет указатель из позиции списка, специфицированной аргументом типа position. Неверное значение позиции вызывает ошибку.
InsertBefore()
Вставляет новый указатель, специфицированный вторым аргументом, перед позицией, специфицированной первым аргументом. Возвращается позиция нового элемента.
InsertAfter()
Вставляет новый указатель, специфицированный вторым аргументом, после позиции, специфицированной первым аргументом. Возвращается позиция нового элемента.
Find ()
Ищет указатель в списке, идентичный заданному в аргументе. Если найден, возвращает его позицию, в противном случае — null.
FindNext()
Возвращает позицию указателя в списке, специфицированную целочисленным аргументом — индексом, начинающимся с нуля.
GetCount ()
IsEmpty ()
Возвращает количество элементов в списке.
Возвращает true, если в списке нет элементов, и false — в противном случае.
Некоторые из этих функций-членов вы увидите в действии в настоящей главе чуть позже — в контексте реализации класса документа для программы Sketcher.
Использование шаблонного класса CList
Вы можете использовать шаблон коллекции CList в определении объекта кривой нашего приложения Sketcher. Кривая определяется двумя или более точками, так что сохранение их в списке должно быть удобным способом управления ими. Сначала вы должны определить класс коллекции CList как член класса CCurve. Вы используете эту коллекцию для хранения точек. Выше мы с вами уже в некоторых деталях изучили шаблонный класс CList, так что это должно быть не сложно.
Шаблонный класс CList имеет два параметра, так что общая форма объявления класса коллекции этого типа выглядит следующим образом:
CList<YourObjectType, FunctionArgType> ClassName;
Первый аргумент, YourObjectType, специфицирует тип объектов, которые вы собираетесь хранить в списке. Второй аргумент указывает тип аргумента для использования в функциях-членах класса коллекции при ссылках на объект. Обычно это — ссылка на тип объекта, чтобы минимизировать копирование аргументов при вызову функции. Объявим объект класса коллекции, подходящего для наших нужд, в классе 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.
ОсьХ
Рисование кривой в режиме отображения ММ_ТЕХТ
Рис. 15.5. Рисование кривой
Точки, определенные последовательностью сообщений о перемещении мыши, определяют дополнительные сегменты к существующему объекту кривой. Вам понадобится добавить функцию AddSegment () к классу CCurve для расширения кривой после ее создания конструктором.
Следующий момент, который нужно рассмотреть — это каким образом вычислять описанный прямоугольник. Это определяется получением пары минимальных значений х и у для левого верхнего угла и пары максимальных значений — для правого нижнего угла. Это потребует прохода по всем точкам в списке. Таким образом, вы будете вычислять описывающий прямоугольник инкрементным образом, в функции 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); H Конструктор по умолчанию — не должен использоваться CList<CPoint, CPoint&> m_PointList; // Безопасный к типам список указателей };
Вы должны модифицировать определение класса в Elements . h в соответствии с приведенным кодом. Конструктор принимает в качестве параметра две точки и цвет, так что он определяет кривую из одного сегмента. Он вызывается в функции CreateElement (), вызванной, в свою очередь, из обработчика OnMouseMove () класса представления, когда сообщение WM_MOUSEMOVE впервые принимается для кри-
вой, так что не забудьте модифицировать определение функции CreateElement () в CSketcherView для вызова конструктора класса CCurve с правильными аргументами. Оператор, использующий конструктор CCurve в операторе switch функции CreateElement (), должен быть изменен:
case CURVE:
return new CCurve(m_FirstPoint, m^SecondPoint, pDoc-X3etElementColor());
После вызова конструктора все последующие сообщения WM_MOUSEMOVE должны инициировать вызов функции AddSegment () для добавления сегмента к существующей кривой, как показано на рис. 15.6.
OnMouseMoveO вызывает AddSegment() с точками х4,у4
Рис. 15.6. Посяедоватпелъностъ вызовов обработчиков сообщений при рисовании кривой
Здесь показана полная последовательность вызовов обработчиков сообщений для кривой, состоящей из девяти сегментов. Последовательность обозначена пронумерованными стрелками. Код функции OnMouseMOve () в CSketcherView должен быть обновлен следующим образом:
void CSketcherView::OnMouseMove(UINT nFlags, CPoint point) {
CClientDC aDC(this); // Контекст устройства для текущего представления if((nFlags&MK_LBUTTON)&&(this==GetCapture())) {
m_SecondPoint = point; // Сохранить текущую позицию курсора if(m_pTempElement)
{
if (CURVE = GetDocument()->GetElementType()) // Это кривая?
{ // Рисуем кривую, поэтому добавляем
// сегмент к существующей кривой
static_cast<CCurve*> (m_j)TempE lament) ->AddSegment(m_SecondPoint); m_pTexnpElement->Draw(&aDC); // Теперь рисуем ”
return;// Готово
}
aDC. Sa tROP2 (R2_N0TX0RPEN);// Установить режим рисования
И Перерисовать старый элемента, чтобы он исчез из представления m_pTempElement->Draw(&aDC);
delete m__pTempElement;/1 Удалить старый элемент
m_pTempElement - 0;/ / Сбросить указатель в 0
}
// Создать элемент по типу и цвету, // записанному в объекте документа m_pTempElement = CreateElement(); m_pTempElement->Draw(&aDC);
}
}
Вы должны трактовать элемент типа CURVE как специальный случай после его создания, поскольку при всех последующих вызовах обработчика OnMouseMove () вы должны вызывать функцию AddSegment () для существующего элемента вместо конструирования нового взамен старого. Вам не нужно устанавливать режим рисования в этом экземпляре, поскольку нет необходимости каждый раз стирать предыдущую версию кривой. Поэтому вы перемещаете вызов SetROP2 () в позицию, находящуюся после кода, обрабатывающего кривую.
Добавление сегмента кривой и рисование расширенной кривой выполняется внутри добавленного блока if. Обратите внимание, что вы должны привести указатель 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.х, m_EnclosingRect.left) , min(aPoint.у, m_EnclosingRect.top), max(aPoint.x, m_EnclosingRect.right), max(aPoint.у, m_EnclosingRect.bottom));
}
Используемые здесь функции min () и max () — это стандартные макросы, эквивалентные применению условных операций для выбора минимума и максимума из двух значений. Новая точка добавляется в конец списка точно так же, как в конструкторе. Важно, чтобы каждая новая точка добавлялась в список способом, согласованным с конструктором, поскольку вы рисуете сегменты, используя точки в последовательности — от начала до конца списка. Каждый сегмент линии рисуется от конечной точки предыдущей линии к новой точке. Если точки располагаются в неправильной последовательности, сегменты линий не будут правильно направлены. После добавления новой точки описанный прямоугольник переопределяется с учетом координат этой новой точки.
Последняя функция-член, которую потребуется определить для интерфейса класса CCurve — это Draw ():
void CCurve::Draw(CDC* pDC) {
// Создать перо для этого объекта, инициализировать
// его цветов объекта и линией шириной в 1 пиксель
СРеп аРеп;
if(!aPen.CreatePen(PS_SOLID, m_Pen, m_Color)) {
// Создать перо не удалось. Закрыть программу
AfxMessageBox(_Т("Не удалось создать перо для рисования кривой"), МВ_ОК) ; AfxAbort();
}
СРеп* pOldPen = pDC->SelectObject(&аРеп); Н Выбрать перо
// Теперь нарисовать кривую.
// Получить позицию первого элемента в списке POSITION aPosition = m__PointList.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.
Рис. 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; // Тип текущего элемента
CTypedPtrList<CObList, CElement*> mJElementList; // Список элементов
// Остальная часть класса — как раньше... };
Теперь класс 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
M Остальная часть класса — как раньше...
// Операции
public:
unsigned int GetElementType()// Получить тип элемента
{ return m_Element; }
COLORREF GetElementColor ()// Получить цвет элемента
{ return m_Color; }
void AddElement (CElement* pElement) // Добавить элемент в список { m_ElementList.AddTail(pElement); }
II Остальная часть класса — как раньше..., };
Добавление элемента в список потребует единственного оператора, который вызывает функцию-член AddTail (). Это все, что необходимо для создания документа, но вы еще должны рассмотреть, что должно случиться, когда документ закрывается. Нужно гарантировать, чтобы список указателей и все элементы, на которые они указывают, правильно уничтожались. Для этого потребуется добавить код деструктора для объектов CSketcherDoc.
Реализация деструктора документа
В деструкторе вы сначала проходите по списку, удаляя элементы, на которые указывает каждый указатель. После этого вы должны удалить указатели из списка. Код, который это сделает, выглядит следующим образом:
CSketcherDoc:: ~CSketcherDoc (void)
I
/ / Получить позицию начала списка
POSITION aPosition = m_ElementList.GetHeadPosition();
// Удалить элементы, на которые указывает каждое вхождение списка while(aPosition)
delete m__E lenient Li st. GetNext (aPosition);
m_ElementList.RemoveAll(); // И, наконец, удалить все указатели
С помощью функции GetHeadPosition () вы получаете значение позиции вхождения начала списка, и затем этим значением инициализируете переменную aPosition. Затем вы используете a Position в цикле while для прохода по списку и удаления объекта, на который указывает каждый элемент списка. Функция GetNext () возвращает текущий элемент-указатель и обновляет переменную aPosition, чтобы она указывала на следующий элемент. Когда получен последний элемент списка, aPosition устанавливается в NULL функцией GetNext О , и цикл завершается. После того, как вы удалили все объекты, на которые указывали указатели из списка, можно удалить и сами указатели. Все они удаляются одним вызовом функции RemoveAll () для объекта списка.
Этот код вы должны добавить в определение деструктора в SketcherDoc. срр. Перейти сразу к коду деструктора можно через вкладку Class View.
Рисование документа
Поскольку документ владеет списком элементов и этот список является защищенным (protected), вы не можете использовать его непосредственно из представления. Функция-член представления OnDraw () не должна вызывать функцию-член Draw () для каждого элемента в списке, так что нужно подумать, как это сделать лучше всего. Ниже предложены варианты.
Вы можете сделать список public, но это не даст объекту поддерживать protected-члены класса документа, потому что он показывает все функции- члены объекта списка.
Вы можете добавить функцию-член, возвращающую указатель на список, но это, по сути, сделает список public и также повлечет за собой накладные расходы на доступ к ним.
Вы можете добавить public-функцию к документу, которая вызовет функции- члены Draw () каждого элемента. Вы можете затем вызвать этот член через функцию OnDraw () класса представления. Это будет неплохим решением, поскольку обеспечит то, что вы хотите, и при этом сохранит приватность списка. Единственный аргумент против заключается в том, что функция нуждается в доступе к контексту устройства, а это — принадлежность представления.
Вы можете добавить функцию, предоставляющую значение POSITION первого элемента, и второй член — для итерации по элементам. Это не откроет списка, но сделает доступными содержащиеся в нем указатели.
Последний вариант кажется наилучшим выбором, поэтому на нем и остановимся. Вы можете расширить определение класса документа следующим образом:
class CSketcherDoc: public CDocument {
И Остальная часть класса — как раньше...
/ / Операции
public:
unsigned int GetElementType()// Получить тип элемента
{ return m_Element; }
COLORREF GetElementColor()H Получить цвет элемента
{ return m_Color; }
void AddElement(CElement* pElement) // Добавить элемент в список
{ m_ElementList.AddTail(pElement); }
POSITION GetListHeadPosition() / / Возвратить значение POSITION начала списка { return m ElementList.GetHeadPosition(); }
CElement* GetNext (POSITIONS aPos) // Возвратить указатель на текущий элемент { return m_ElementList.GetNext (aPos) ; }
II Остальная часть класса — как раньше...
};
Используя эти две дополнительные функции класса документа, функция 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) ; M Рисовать текущий элемент
}
}
Эта реализация функции OnDraw () всегда рисует все элементы, содержащиеся в документе. Оператор в цикле while сначала получает указатель на элемент из документа выражением pDoc->GetNext (). Возвращенный им указатель используется для вызова функции Draw () для этого элемента. Оператор работает подобным образом без скобок, благодаря ассоциативности слева направо операции ->. Цикл while проходит список от начала до конца. Хотя, вы можете реализовать его лучше, и тем самым сделать программу более эффективной.
Нередко когда программе посылается сообщение WM PAINT, должна перерисовываться лишь часть окна. Когда Windows посылает окну сообщение WM_PAINT, также определяется область клиентской части окна, и только эта часть должна быть перерисована. Класс С DC предлагает функцию-член Re с tVisible (), которая проверяет, перекрывается ли прямоугольник, переданный в аргументе, с областью, которую Windows требует перерисовать, тем самым повышая производительность приложения.
void CSketcherView: :OnDraw (CDC* pDC)
{
CSketcherDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
if(IpDoc)
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, который извлекает указатель на каждый элемент по очереди, так что цикл продолжается до
тех пор, пока aPos не примет значения NULL. Вы извлекаете ограничивающий прямоугольник для каждого элемента, вызывая функцию-член GetBoundRect () объекта, и передаете его функции RectVisible () в операторе if. В результате перерисовываются только элементы, перекрывающие область, которую Windows идентифицирует как недействительную. Рисование на экране — относительно дорогая операция в смысле времени, поэтому выборочная перерисовка только тех элементов, которые нужно перерисовать, вместо рисования каждый раз всего существенно повышает производительность.
Добавление элемента в документ
Последнее, что вы должны сделать, чтобы получить работающий документ в вашей программе — это добавить в обработчик OnLButtonUp () класса CSketcherView код, который будет добавлять временный элемент к документу:
void CSketcherView: :OnLButtonUp (UINT nFlags, CPoint point) {
if(this == GetCapture ())
Releasecapture();H Прекратить захват сообщений мыши
U Если есть элемент, добавить его к документу
if(m_pTempElement)
{
GetDocument()->AddElement(m_pTempElement);
InvalidateRect(0); H Перерисовать текущее окно m_pTempElement = 0;// Сбросить указатель элемента
}
}
Конечно, вы должны проверить, действительно ли есть элемент, прежде чем добавлять его к документу. Пользователь может просто щелкнуть кнопкой, не передвигая мышь. После добавления элемента к списку в документе вызывается InvalidateRect (), чтобы инициировать перерисовку клиентской области текущего представления.
Аргумент 0 объявляет недействительной всю клиентскую область представления. Из-за особенностей работы процесса “растяжения” некоторые элементы могут не отображаться правильно, если этого не сделать. Если вы рисуете горизонтальную линию, например, а затем растягиваете прямоугольник в том же цвете так, что его верхняя или нижняя грань перекрывают линию, перекрытая часть линии исчезнет. Это потому, что грань прямоугольника рисуется операцией XOR с лежащей ниже линией, так что вы получаете цвет фона. Вы также сбрасываете указатель m_pTempElament в ноль, дабы избежать путаницы при создании другого элемента.
Испытание документа
После сохранения всех модифицированных файлов можно выполнить сборку последней версии программы Sketcher и запустить ее. Теперь вы можете нарисовать картинку “счастливый программист”, показанную на рис. 15.8.
Теперь программа работает более реалистично. Она сохраняет указатель на каждый элемент в объекте документа, так что все они автоматически перерисовываются при необходимости. Программа также выполняет корректную очистку данных документа, когда он удаляется.
Однако в программе все еще присутствует ряд ограничений, над которыми вы можете поработать. Эти ограничения перечислены ниже.
Рис. 15.8. Работа программы Sketcher с документом
Можно открыть другое окно представления, используя пункт меню Windows New Window (Окно^Новое окно) программы. Эта возможность встроена в приложение MDI и открывает новое представление существующего документа, а не новый документ. Однако если вы рисуете в одном окне, то элементы не рисуются в другом окне. Элементы никогда не появляются в окне, отличном от того, в котором были нарисованы, если только место, которое они занимают, не должно быть перерисовано по какой-то другой причине.
Вы можете рисовать только в той клиентской области, которую видите. Было бы неплохо иметь возможность прокручивать представление и рисовать в большей области.
Никак невозможно удалить элемент, так что если вы допустили ошибку, то придется либо мириться с ней, либо начинать новый документ.
Все это довольно-таки серьезные недостатки, которые существенно снижают практическую полезность программы. До конца настоящей главы мы их устраним.
Усовершенствование представления
Первое, что вы можете попытаться исправить — это обновление всего окна документа, отображающегося при рисовании документа. Проблемы возникают из-за того, что о новом элементе известно только представлению, в котором рисуется элемент. Каждое представления работает независимо от других, и между ними нет никакого взаимодействия. Вы должны заставить любое представление, которое добавляет элемент к документу, каким-то образом известить об этом другие представления, чтобы они предприняли соответствующее действие.
Обновление множественных представлений
Класс документа оснащен функцией UpdateAllViews О , которая помогает справиться с этой конкретной проблемой. Эта функция, по сути, предоставляет для документа средство отправки сообщений всем его представлениям. Вам просто нужно
вызвать ее из функции OnLButtonUp () в классе CSketcherView, всякий раз, когда вы добавляете новый элемент к документу:
void CSketcherView::OnLButtonUp(UINT nFlags, CPoint point)
if (this == GetCapture ())
Releasecapture(); H Прекратить перехват сообщений мыши
// Если есть элемент, добавить его к документу if(m_pTempElement)
GetDocument()->AddElement(mjpTempElement);
GetDocument()->UpdateAllViews(0,O,m_pTempElement); H Сообщить всем // представлениям mjpTempElement = 0;// Сбросить указатель элемента
}
}
Когда указатель mjpTempElement не равен NULL, то специфическое действие функции расширено вызовом функции Update All Views () — члена вашего класса документа. Эта функция взаимодействует с представлениями, инициируя вызов функции OnUpdate () в каждом представлении. Три аргумента UpdateAllViews () описаны на рис. 15.9.
Первый аргумент вызова функции UpdateAllViews () — часто указатель this текущего представления. Это подавляет вызов функции OnUpdate () для текущего представления. Это удобное средство, когда текущее представление уже обновлено. В случае программы Sketcher, поскольку вы занимаетесь “растягиванием” в процессе создания элемента, то хотите также заставить перерисовываться и текущее представление, поэтому, специфицируя первый аргумент как 0, вы инициируете вызов функции OnUpdate () для всех представлений, включая текущее. Это исключает необходимость в вызове InvalidateRect (), как делалось раньше.
Здесь вы не используете второй аргумент UpdateAllViews (), поскольку передаете указатель на новый элемент в третьем аргументе. Передача указателя на новый элемент позволяет представлениям определить, какая часть их клиентской области должна быть перерисована.
void UpdateAIIView( CView* pSender, LPARAM Hint = OL, CObject* pHint = NULL);
Эти два значения аргументов передаются функциям OnUpdate() в представлениях
Лог. 15.9. Аргументы функции UpdateAllViews ()
Чтобы перехватить информацию, переданную функции UpdateAllViews (), вы добавляете в класс представления функцию-член OnUpdate (). Это можно сделать в мастере создания классов (Class Wizard), в свойствах класса CSketcherView. Вы должны помнить, что для того, чтобы увидеть свойства класса, следует выполнить щелчок левой кнопкой мыши на имени класса и выбрать из контекстного меню пункт Properties (Свойства). Если вы щелкнете на кнопке Overrides (Переопределения) в окне Properties (Свойства), то сможете выбрать OnUpdate в списке функций, которые можно переопределить. Щелкните на имени функции, затем выберите опцию <Add> OnUpdate, показанную в выпадающем списке в соседней колонке. Когда вы закроете окно Properties, то сможете отредактировать код переопределения OnUpdate () в панели редактора. Вам потребуется добавить в определение функции следующий выделенный полужирным код:
void CSketcherView::OnUpdate(CView* pSender, LPARAM IHint, CObject* pHint) {
// Объявить недействительной область, соответствующую указанному элементу, If если он есть, иначе объявить недействительной всю клиентскую область 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. Этот новый базовый класс облада-
ет встроенной функциональностью прокрутки, так что вы можете изменить определение класса CSketcherView следующим образом:
class CSketcherView: public CScrollView
{
// Определение класса — как раньше...
};
Вы должны также модифицировать две строки кода в начале файла SketcherView. срр, которые ссылаются на базовый класс для CSketcherView:
IMPLEMENT__DYNCREATE(CSketcherView, CScrollView)
BEGIN_MESSAGE_MAP (CSketcherView, CScrollView)
Однако этого все еще недостаточно. Новая версия класса представления должна кое-что знать о рисуемой области, например, размер и насколько далеко должна выполняться прокрутка представления, когда вы используете ползунок прокрутки. Эта информация должна применяться перед первым рисованием представления. Вы можете поместить код для выполнения этой задачи в функцию OnlnitialUpdate О класса представления.
Необходимую информацию вы устанавливаете вызовом функции, унаследованной от класса CScrollView — SetScrollSizes (). Аргументы этой функции объясняются на рис. 15.10.
Рис. 15.10. Аргументы функции SetScrollSizes ()
Прокрутка на одну строку происходит, когда вы щелкаете на верхней или нижней стрелке линейки прокрутки, а прокрутка на страницу происходит при щелчке на теле самой линейки. Здесь вы имеете возможность изменить режим отображения. MM_LOENGLISH будет хорошим выбором для приложения Sketcher, но сначала обеспечим работу прокрутки в режиме отображения ММ_ТЕХТ, поскольку нужно будет преодолеть некоторые трудности.
Чтобы добавить код для вызова SetScrollSizes (), вы должны переопределить версию по умолчанию функции OnlnitialUpdate () представления. Вы обращаетесь к ней точно так же, как делали это с переопределением функции OnUpdate () — через окно Properties для класса CSketcherView. После добавления переопределения просто добавьте код, помеченный комментариями:
void CSketcherView: .'OnlnitialUpdate ()
CScrollView::OnlnitialUpdate();
/ / Определить размер документа
CSize DocSize(20000,20000);
// Установить режим отображения и размер документа.
SetScrollSizes(ММ_ТЕХТ,DocSize);
Это устанавливает режим отображения ММ ТЕХТ и определяет общий размер пространства, в котором вы можете рисовать, в 20 000 пикселей по каждому измерению.
Проделанного достаточно, чтобы получить наглядно работающий механизм прокрутки. Выполните сборку программы и запустите ее с этими дополнениями. Вы сможете нарисовать несколько элементов, а затем прокрутить представление. Однако, хотя прокрутка окна работает нормально, если вы попытаетесь нарисовать что-либо, когда представление прокручено, окажется, что все работает не так, как должно. Элементы появляются не в той позиции, где вы их рисуете, к тому же отображаются неправильно. Что происходит?
Логические координаты и клиентские координаты
Проблема кроется в используемых координатных системах — и эта множественность не случайна. В действительности во всех рассмотренных примерах мы использовали две координатные системы, хотя вы могли этого и не заметить. Как было показано в предыдущей главе, при вызове такой функции, как LineTo (), предполагается применение логических координат. Функция — член класса С ОС, определяющего контекст устройства, а контекст устройства имеет собственную систему логических координат. Режим отображения, являющийся свойством контекста устройства, определяет единицу измерения координат при рисовании чего-либо.
С другой стороны, данные координат, которые вы принимаете вместе с сообщениями мыши, никак не связаны с контекстом устройства или объектом С DC — и вне контекста устройства логические координаты неприменимы. Точки, передаваемые обработчикам OnLButtonDown () и OnMouseMove (), имеют координаты, всегда выраженные в единицах устройства, то есть в пикселях, и измеряются относительно верхнего левого угла клиентской области. Их называют клиентскими координатами. Аналогично, когда вы вызываете InvalidateRect (), предполагается, что прямоугольник определен в терминах клиентских координат.
В режиме ММ^ТЕХТ клиентские координаты и логические координаты контекста устройства совпадают — обе измеряются в пикселях, а потому являются одними и теми же — до тех пор, пака вы не прокручиваете окно. Во всех предыдущих примерах не было никакой прокрутки, поэтому все работало без проблем. В последней версии
Sketcher все работает отлично, пока вы не прокручиваете представление, причем точка начала логических координат (точка 0, 0) перемещается механизмом прокрутки, так что уже не находится в том же месте, что и начало клиентских координат. Единицы измерения логических и клиентских координат здесь совпадают, но начальные точки этих координатных систем отличаются. Эта ситуация проиллюстрирована на рис. 15.11.
В левой части показано положение клиентской области, в которой вы рисуете, и точки — положения мыши, определяющие линию. Они записаны в клиентских координатах. В правой стороне показано, где именно в действительности рисуется линия. Рисование выполняется в логических координатах, но вы используете значения клиентских координат. В случае прокрученного окна линия появляется в неправильном месте из-за смещения логических координат.
Это значит, что для определения элементов в программе Sketcher применяются неверные значения, и когда части клиентской области объявляются недействительными, чтобы обеспечить их перерисовку, то прямоугольники, передаваемые функции, также неправильны — отсюда загадочное поведение программы. С другими режимами отображения все еще хуже, поскольку отличаются не только единицы измерения двух координатных систем, но к тому же направления оси у могут быть противоположными!
Логические координаты 0,0¥Логические координаты 0,0¥Левая кнопка Левая кнопка нажата отпущена
Логические координаты 0,0
¥
Логические координаты 0,0
¥
Рисование в непрокручиваемом окнеРисование в непрокручиваемом окнеЛиния появляется здесь в логических координатахЛиния появляется здесь в логических координатахЛиния рисуется здесь в клиентских координатах
Рисование в непрокручиваемом окне
Рисование в непрокручиваемом окне
Линия появляется здесь в логических координатах
Линия появляется здесь в логических координатах
Левая кнопка нажатаЛевая кнопка отпущенаЛевая кнопка нажатаЛевая кнопка отпущенаЛогические координаты 0,0х ◄Логические координаты 0,0х ◄Линия рисуется здесь в клиентских координатахЛиния рисуется здесь в клиентских координатахПредставление прокручено, поэтому начальная точка теперь здесьПредставление прокручено, поэтому начальная точка теперь здесьЛиния появляется здесь в логических координатахЛиния появляется здесь в логических координатахРис. 15.11. Рисование в непрокручиваемом и прокручиваемом окнахРис. 15.11. Рисование в непрокручиваемом и прокручиваемом окнахРисование в прокручиваемом окне
Левая кнопка нажата
Левая кнопка отпущена
Левая кнопка нажата
Левая кнопка отпущена
Логические координаты 0,0
х ◄
Логические координаты 0,0
х ◄
Линия рисуется здесь в клиентских координатах
Линия рисуется здесь в клиентских координатах
Представление прокручено, поэтому начальная точка теперь здесь
Представление прокручено, поэтому начальная точка теперь здесь
Линия появляется здесь в логических координатах
Линия появляется здесь в логических координатах
Рис. 15.11. Рисование в непрокручиваемом и прокручиваемом окнах
Рис. 15.11. Рисование в непрокручиваемом и прокручиваемом окнах
Работа с клиентскими координатами
Рассмотрим, что можно сделать для решения проблемы.
Преобразовать клиентские координаты, полученные в сообщениях мыши, в логические, а затем использовать их для создания элементов.
Преобразовать ограничивающий прямоугольник, созданный в логических координатах, обратно в клиентские координаты, если необходимо использовать их для вызова InvalidateRect ().
В результате гарантируется, что при работе с функциями контекста устройства всегда используются логические координаты, а для всего прочего взаимодействия в окне всегда применяются клиентские координаты. Функции, которые должны использоваться для выполнения необходимых преобразований, ассоциированы с контекстом устройства, поэтому вы должны получать этот контекст устройства всякий раз, когда хотите преобразовать логические координаты в клиентские и наоборот. Для выполнения работы можно применять функции преобразования координат класса CDC, унаследованного от CClientDC.
Ниже показана новая версия обработчика OnLBut ton Down (), включающая преобразования.
// Обработчик сообщения нажатия левой кнопки мыши
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); H Контекст устройства для текущего представления OnPrepareDC(&aDC);// Получить уточненную начальную точку
if((nFlags&MKJLBUTTON)&&(this==GetCapture()))
{
aDC.DPtoLP(Spoint);// преобразовать точку в логические координаты
m_SecondPoint = point; // Сохранить текущую позицию курсора // Остаток функции — как раньше. . .
}
Код преобразования значения точки, переданной обработчику, по сути, тот же самый, что и в предыдущем обработчике, и это — все, что необходимо на данный момент. Легко догадаться, какую последнюю функцию вы должны изменить — OnUpdate () в классе представления. Она должна приобрести такой вид:
void CSketcherView::OnUpdate(CView* pSender, LPARAM IHint, CObject* pHint) {
// Объявить недействительной область, соответствующую указанному элементу, Н если он есть, иначе объявить недействительной всю клиентскую область if (pHint)
{
CClientDC aDC(this) ;// Создать контекст устройства
OnPrepareDC (&aDC);// Получить уточненную начальную точку
//Получить ограничивающий прямоугольник и преобразовать в клиентские координаты CRect aRect=( (CElement*) pHint) ->GetBoundRect () ;
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();
J J Определить размер документа 30x30 дюймов в MM_LOENGLISH CSize DocSize(3000,3000);
II Установить режим отображения и размер документа. SetScrollSizes(MM_LOENGLISH, DocSize);
}
Вы просто изменяете аргументы в вызове 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); H Создать контекст устройства
OnPrepareDC(&aDC); Н Получить уточненную начальную точку
// Получить ограничивающий прямоугольник и преобразовать в клиентские координаты CRect aRect=((CElement*)pHint)->GetBoundRect();
aDC.LPtoDP(aRect);
aRect .NonnalizeRect();
InvalidateRect(aRect);// Инициировать перерисовку области
} else
InvalidateRect(0); И Объявить недействительной всю клиентскую область }
Это должно сработать для программы в том виде, в котором она есть. Если вы заново соберете Sketcher, то получите работающую прокрутку и поддержку множественных представлений. Следует только не забыть заново нормализовать любой прямоугольник, преобразованный в координаты устройства, для последующего использования в InvalidateRect (). Это также касается любых обратных преобразований.
Удаление и перемещение фигур
Возможность удаления фигур — фундаментальное требование к программе рисования. Единственный вопрос, связанный с этим: как вы собираетесь выбирать элемент, подлежащий удалению. Конечно, после того, как вы решите, как выбрать элемент, это в равной мере можно использовать и в случае, если понадобится перемещать элемент, так что вы можете трактовать перемещение и удаление элементов как взаимосвязанные проблемы. Однако сначала посмотрим, как добавить в программу операции перемещения и удаления.
Изящный способ выполнения функций перемещения и удаления предполагает появление всплывающего контекстного меню в позиции курсора при щелчке правой кнопкой мыши. Затем вы можете поместить в это меню элементы 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> (это сокращение для пункта Viewc>Other Windows1^Properties Window (Вид1^ Другие окна^Окно
свойств) главного меню). Затем можете отредактировать ID ресурса в окне Properties, щелкнув на этом значении ID. Имеет смысл изменить его в правой колонке на нечто более осмысленное вроде IDR_CURSOR_MENU. Обратите внимание, что имя ресурса меню должно начинаться с IDR. Нажатие клавиши <Enter> сохраняет новое имя.
Теперь можете создать два новых элемента в панели меню. Они могут иметь любые старые заголовки, поскольку не будут видимы пользователю, а будут представлять два контекстных меню, которые вы отобразите в Sketcher, так что вы можете назвать их element и no element — в соответствие с ситуациями, в которых контекстное меню будет использоваться. После этого вы можете добавить к всплывающему меню пункты Move и Delete. Идентификаторы ID_ELEMENT_MOVE и ID_ELEMENT_DELETE вполне подойдут, но при желании вы можете изменить их в окне Properties — каждый по отдельности. На рис. 15.12 показано, как выглядят новое меню element.
Sketcher. rc (ЛХ..ОК_МЕ1Ш - Menu)4▼ X
element no element | Гуре Hen
Move Delete
Puc. 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 (Красный) разделяют одну и ту же горячую клавишу). Замена &Red на Re&d исправит это, и неплохо было бы для целей согласованности также изменить IDRSketcherTYPE. Вы делаете это, редактируя свойство Caption (Заголовок) пункта меню. Готовое меню должно выглядеть, как показано на рис. 15.13.
Sketcher.rc (IDR...SOR_MENU - Menu) ▼ X eIerrent i no elerne.it ' Tj e_ ire |
V Line Rectangle Circle Curve
V Black Red Green Blue
Pac. 15.13. Новое меню no 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 = menu.GetSubMenu(0);M Получить первое меню
ASSERT (pPopup 1= NULL);/ / Убедиться, что оно есть
// Отобразить всплывающее меню pPopup->TrackPopupMenu(TPM_LEFTALIGN |
TPM-RIGHTBUTTON, point.х, point.у, this);
}
Пока обработчик произвольно отображает первое из двух контекстных меню. Теперь необходимо найти способ определения того, находится ли курсор над одним из нарисованных элементов, чтобы решить, какое меню отображать, но мы обратимся к этому немного позже. Вызов метода LoadMenu () объекта menu загружает ресурс меню, соответствующий указанному в аргументе идентификатору, и прикрепляет его к объекту меню CMenu. Функция GetSubMenu () возвращает указатель на всплывающее меню, которое соответствует целочисленному аргументу, указывающему позицию всплывающего меню, причем 0 — первое всплывающее меню, 1 — второе и так далее. После того, как вы убедитесь, что возвращенный GetSubMenu () указатель не равен NULL, всплывающее меню отображается вызовом TrackPopupMenu ().
Первый аргумент функции TrackPopupMenu () состоит из двух флагов, объединенных операцией ИЛИ. Один из них специфицирует, как всплывающее меню должно позиционироваться, и может принимать значения, перечисленные в табл. 15.4.
Таблица 15.4. Значения первого аргумента функции TrackPopupMenu ()
ФлагОписание
ТРМ_
CENTERALIGN
Центрирует всплывающее меню по горизонтали, относительно координаты х, указанной в качестве второго аргумента функции.
ТРМ_
LEFTALIGN
Позиционирует всплывающее меню так, что левая сторона его выравнивается по координате х, переданной во втором аргументе функции.
ТРМ_
_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 () отображает только первое контекстное всплывающее меню — независимо от того, где именно в представлении был выполнен щелчок. Это не то, что нужно. Первое контекстное меню применимо к элементу, в то время как второе — к представлению в целом. Необходимо отображать первое меню, если под курсором находится элемент, а в противном случае должно отображаться второе меню.
Рис. 15.14. Создание нового обработчика событий для пункта меню Move
Чтобы добиться этого, понадобятся две вещи. Во-первых, нужен механизм для нахождения элемента (если он есть), находящегося в позиции курсора, и, во-вторых — где-то сохранять адрес этого элемента, чтобы можно было его использовать в обработчике OnContextMenu (). Сначала разберемся с сохранением адреса элемента, поскольку это немного проще.
После нахождения элемента под курсором его адрес сохраняется в переменной- члене m pSelected вашего класса представления. Это доступно обработчику правой кнопки мыши, поскольку он находится в том же классе. Вы можете добавить объявление переменной в раздел protected класса CSketcherView:
class CSketcherView: public CScrollView {
H Остальная часть класса — как раньше.. .
protected:
CPoint m_FirstPoint;// Первая точка, записанная для элемента
CPoint m_SecondPoint;// Вторая точка, записанная для элемента
CElement* m_pTempElement; // Указатель на временный элемент CElement* m_jDSelected; // Текущий выбранный элемент
II Остальная часть класса — как раньше. . .
};
Вы можете добавить показанный код вручную или, в качестве альтернативы, щелкнуть правой кнопкой мыши на имени класса и выбрать пункт Add1^ Add Variable (Добавить1^ Добавить переменную) из контекстного меню, чтобы открыть диалог добавления члена данных. Если вы добавите m pSelected вручную, вам также понадобится инициализировать этот элемент в конструкторе класса с помощью следующего кода:
CSketcherView: : CSketcherView ()
: m_FirstPoint(CPoint(0,0))
, m_SecondPoint(CPoint(0,0))
, m_pTempElement(NULL)
, 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) ( H Определить объект контекста устройства для текущего представления 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-X3etNext(aPos) ; aRect = pElement->GetBoundRect() ; aDC.LPtoDP(aRect) ;// Преобразование к координатам устройства
aRect.Normal!zeRect() ;// Нормализовать прямоугольник повторно
if(aRect.PtlnRect(point))// Есть ли элемент под курсором?
< mjpSelected = pElement; break;
} }
aDC.SetR0P2(R2_N0TX0RPEN); H Установить режим рисования if((nFlags&MK_LBUTTON) && (this==GetCapture ())) {
aDC.DPtoLP(&point); H Преобразовать указатель к логическим координатам m_SecondPoint = point; Н Сохранить текущую позицию курсора if(m_pTempElement) {
if(CURVE == GetDocument()->GetElementType()) И Это кривая?
{ // Рисуем кривую, поэтому добавить
// сегмент к существующей кривой
static_cast<CCurve*>(m_pTempElement)->AddSegment(m_SecondPoint); m_pTempElement->Draw(&aDC);II Теперь нарисовать ее
return;// Готово
}
aDC.SetROP2(R2_NOTXORPEN);// Установить режим рисования
// Перерисовать старый элемент, чтобы он исчез из представления m_pTempElement->Draw(&aDC);
delete m_pTempElement;// Удалить старый элемент
m_pTempElement = 0;/ / Сбросить указатель в 0
}
// Создать временный элемент для типа и цвета,
// записанных в объекте документа и нарисовать его
m_pTempElement = CreateElement();// Создать новый элемент
m_pTempElement->Draw(&aDC);// Нарисовать элемент
Новый код выглядит объемным, тем не менее, он очень прост. Его задача — проверить, что курсор лежит внутри ограничивающего прямоугольника каждого элемента по очереди, и сохраняет адрес первого элемента, для которого это так, в m_pSelected. Если курсор не лежит внутри ни одного из прямоугольников, элемент m_pSelected содержит 0. Обратите внимание на то, как преобразуется каждый ограничивающий прямоугольник в координаты устройства и повторно нормализуется перед тем, как выполнить проверку. Без этого прямоугольник окажется в неправильном месте, поскольку установлен режим отображения MM_LOENGLISH.
После всего этого код готов к тестированию контекстных меню.
Испытание всплывающих меню
Вы добавили весь код, необходимый для работы всплывающих меню, так что можете выполнить сборку и запустить программу Sketcher, дабы испытать их. Если под курсором нет элементов, появляется второе всплывающее меню, позволяя изменить тип и цвет элемента. Эти опции работают, поскольку они генерируют точно те же сообщения, что и соответствующие пункты главного меню, а также потому, что вы уже написали обработчики для них.
Если под курсором есть элемент, появляется первое контекстное меню с пунктами Move и Delete в нем. Пока они ничего не делают, потому что вам еще только предстоит реализовать обработчики для генерируемых ими сообщений. Попробуйте пощелкать правой кнопкой мыши вне окна представления. Сообщения об этом не передаются в окно представления документа вашего приложения, поэтому всплывающие меню не отображаются.
Обратите внимание, что контекстные меню для выбора элемента и цвета работают не совсем правильно — они устанавливают правильный тип и цвет в классе, но не меняют меток во всплывающем меню. Класс документа обрабатывает сообщения от
меню, но сообщения 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 и значение цвета в операторе, чтобы соответствующим образом установить флаги для каждого из прочих пунктов меню цвета.
Пометка пунктов меню элементов, по сути, выполняется так же. Для пометки пункта меню 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 (mjpSelected = 0)
{
// Пометить пункты меню цвета
COLORREF Color = GetDocument () ->GetElementColor () ;
menu. CheckMenuItem (ID_COLOR_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__COLOR_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. CheckMenuItem (ID__ELEMENT_LINE,
(LINE=ElementType?MF_CHECKED:MF_UNCHECKED) |MF_BYCOMMAND) ;
menu. CheckMenuItem (ID__ELEMENT_RECTANGLE,
(RECTANGLE—ElementType ?MF_CHECKED:MF_UNCHECKED) |MF_BYCOMMAND) ;
menu. CheckMenuItem (ID_ELEMENT_CIRCLE,
(CIRCLE—ElementType?MF__CHECKED:MF__UNCHECKED) |MFJBYCOMMAND) ;
menu. CheckMenuItem (ID__ELEMENT_CURVE,
(CURVE=ElementType?MF_CHECKED:MF_UNCHECKED) |MF_BYCOMMAND) ;
}
CMenu* pPopup = menu. Ge tSubMenu (m_jDSelected = 0 ? 1 : 0) ;
ASSERT (pPopup != NULL);
pPopup->TrackPopupMenu(TPM_LEFTALIGN |
TPM_RIGHTBUTTON, point.x, point.у, this);
}
После этого изменения пункты контекстного меню должны помечаться корректно, когда вы заново выполните сборку и запустите программу Sketcher.
Подсветка элементов
В идеале пользователь захочет знать, какой элемент находится под курсором, перед тем, как выполнять щелчок правой кнопкой мыши для вызова контекстного меню. Когда нужно удалить элемент, вы должны знать, какой именно элемент будет удален. Аналогично, когда необходимо использовать второе контекстное меню, например, чтобы изменить цвет, нужно быть уверенным, что под курсором нет элемента. Чтобы точно показать, какой элемент находится под курсором, его следует каким-то образом выделить перед нажатием правой кнопки мыши.
Это можно сделать в функции-члене элемента Draw (). Все, что для этого понадобится — передать аргумент функции Draw (), чтобы указать, когда элемент должен
быть подсвечен. Если передать функции 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); H Для предотвращения вызова конструктора по умолчанию };
Изменение состоит в добавлении второго параметра к виртуальной функции 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; H Конечная точка линии
CLine(void); // Конструктор по умолчанию — не должен использоваться };
Реализация каждой функции Draw () для класса-наследника CElement должна быть расширена таким же способом. Вот как будет выглядеть эта функция в классе CLine:
void CLine::Draw(CDC* pDC, CElement* pElement)
I
// Создать перо для этого объекта и инициализировать
Н его цветом и шириной линии в 1 пиксель
СРеп аРеп;
COLORREF aColor = m__Color; if (this = pElement) aColor = SELECT J2OLOR;
/ / Инициализировать цветом элемента // Элемент выбран?
// Цвет подсветки
if(!aPen.CreatePen(PS_SOLID, m_Pen, aColor)) {
И Создание пера не удалось. Прервать программу
AfxMessageBox(_Т("Не удалось создать перо для рисования линии"), МВ_ОК); AfxAbort ();
}
CPen* pOldPen = pDC->SelectObject(&aPen); II Выбрать перо
// Нарисовать линию
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); II Контекст устройства для этого представления OnPrepareDC(&aDC);// Получить уточненную начальную точку
aDC.SetROP2(R2_NOTXORPEN); // Установить режим рисования
if((nFlags&MK_LBUTTON) && (this==GetCapture ())) (
aDC.DPtoLP(&point);// Преобразовать точку в логические координаты
m SecondPoint = point; // Сохранить текущую позицию курсора
if(m_pTempElement) {
if(CURVE == GetDocument()->GetElementType()) H Это кривая?
{ H Рисуем кривую, поэтому добавить
/ / сегмент к существующей кривой
static_cast<CCurve*>(m_pTempElement)->AddSegment(m_SecondPoint); m_pTempElement->Draw(&aDC); H Теперь нарисовать ее return; H Готово }
aDC.SetROP2(R2_NOTXORPEN);// Установить режим рисования
// Перерисовать старый элемент, чтобы он исчез из представления m_pTempElement->Draw(&aDC);
delete m_pTempElement;H Удалить старый элемент
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->GetBoundRect () ;
aDC.LPtoDP(aRect);
aRect.NormalizeRect();
// Выбрать первый элемент, находящийся под курсором if (aRect. PtlnRect (point))
{
mjpSelected = pElement; break;
} }
if (mjpSelected = pOldSelection) // Если новый выбор совпадает со старым, return;// завершаем
// Снять старую подсветку, если она была
if(pOldSelection != 0)// Проверить, была ли
<
aRect = pOldSelection->GetBoundRect() ;
aDC.LPtoDP(aRect) ;// Преобразовать в координаты устройства
aRect. NormalizeRect () ;// Нормализовать
InvalidateRect(aRect, FALSE); // Сделать область недействительной } // Подсветить новый выбор, если он есть if (mjpSelected != 0) // Проверить, есть ли <
aRect = mjpSelected->GetBoundRect () ;
aDC.LPtoDP(aRect) ; // Преобразовать в координаты устройства
aRect. Normal!zeRect () ;/ / Нормализовать
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;H Хранилище указателя на элемент
while (aPos)И Выполнять цикл, пока aPos — не null
{
pElement = pDoc->GetNext(aPos); И Получить указатель на текущий элемент // Если элемент видимый...
if(pDC->RectVisible(pElement->GetBoundRect())) pElement->Draw(pDC , m_j?Selected) ;// . . .нарисовать его
}
}
Вы должны изменить всего одну строку. Функция Draw () элемента теперь принимает второй аргумент, добавленный для передачи адреса подсвеченного элемента.
Испытание подсветки
Вот и все, что необходимо для того, чтобы подсветка работала все время. Это было не тривиально, но с другой стороны, не так уж и страшно. Вы можете выполнить сборку и запустить программу Sketcher, чтобы испытать ее. Всякий раз, когда под курсором находится элемент, он рисуется в цвете magenta. Это наглядно показывает, к какому элементу будет относиться контекстное меню, прежде чем вы щелкнете правой кнопкой мыши, и означает, что вы заранее знаете, какое именно контекстное меню будет отображено.
Обработка сообщений меню
Следующий шаг состоит в том, чтобы предусмотреть код в теле обработчиков для добавленных ранее пунктов меню Move и Delete. Сначала вы можете добавить код для Delete, поскольку он проще.
Удаление элемента
Код, который необходим в обработчике OnElementDelete () класса CSketcherView для удаления текущего выбранного элемента, достаточно прост:
void CSketcherView: :OnElementDelete () {
if(m_pSelected)
{
CSketcherDoc* pDoc = GetDocument() ;// Получить указатель на документ
pDoc->DeleteElement(m_pSelected); II Удалить элемент pDoc->UpdateAHViews (0) ;// Перерисовать все представления
m_pSelected = 0;// Сбросить указатель на выбранный элемент
}
}
Код удаления элемента выполняется, только если m pSelected содержит правильный адрес, а это говорит о том, что существует элемент, подлежащий удалению. Вы получаете указатель на документ и вызываете функцию DeleteElement () для объекта документа; эту функцию-член мы добавим в класс CSketcherDoc чуть позже. Когда элемент удален из документа, вызывается UpdateAllViews (), чтобы заставить все представления перерисовываться без удаленного элемента. И, наконец, вы устанавливаете m ^Selected в ноль, чтобы указать, что теперь выделенного элемента нет.
Добавим объявление DeleteElement () как public-член класса CSketcherDoc:
class CSketcherDoc : public CDocument
{
protected: // Создавать только сериализацией
CSketcherDoc () ;
DECLARE_DYNCREATE (CSketcherDoc)
// Атрибуты
public:
// Операции
public:
void DeleteElement (CElement* pElement) ; // Удалить элемент
unsigned int GetElementType()// Получить тип элемента
{ return m_Element; }
// Остальная часть класса — как раньше...
Она принимает указатель на элемент, подлежащий удалению, в качестве аргумента и не возвращает ничего. Вы можете реализовать ее в SketcherDoc. срр следующим образом:
void CSketcherDoc::DeleteElement(CElement* pElement)
{
if(pElement)
II Если указатель на элемент правильный,
// найти его в списке и удалить
POSITION aPosition = m_ElementList.Find(pElement);
m_ElementList.RemoveAt(aPosition);
delete pElement; // Удалить элемент из кучи
}
}
У вас не должно возникать проблем с пониманием работы этой функции. После того, как вы убедились, что указатель ненулевой, вы находите значение POSITION указателя в списке, используя функцию-член Find () объекта списка. Затем вы применяете его с функцией-членом RemoveAt (), чтобы удалить указатель из списка, после чего удаляете элемент, на который установлен указатель inElement, из кучи.
Это все, что необходимо сделать для удаления элементов. Теперь вы имеете программу 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;// Флаг перемещения элемента
CPoint m^CursorPos;// Позиция курсора
CPoint m^FirstPos;// Исходная позиция в перемещении
// Остальная часть класса - как раньше...
);
Новые члены также должны быть инициализированы в конструкторе 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))
<
// TODO: добавить сюда код конструирования
}
Процесс перемещения элемента начинается, когда выбирается пункт Move из контекстного меню. Теперь вы можете добавить код обработчика сообщения от пункта меню Move, чтобы установить условия, необходимые для выполнения операции.
void CSketcherView: :OnElementMove () {
CClientDC aDC(this);
OnPrepareDC(&aDC);H Установить контекст устройства
GetCursorPos (&m_CursorPos) ; // Получить позицию курсора в экранных координатах ScreenToClient(&m_CursorPos);// Преобразовать в клиентские координаты aDC.DPtoLP(&m_CursorPos);// Преобразовать в логические координаты
m_FirstPos = m^CursorPos; П Запомнись первую позицию m_MoveMode = TRUE;// Запустить режим перемещения
}
В обработчике происходят четыре вещи, которые перечислены ниже.
Получение координат текущей позиции курсора, поскольку операция перемещения начинается с этой точки.
Преобразование позиции курсора в логические координаты, поскольку ваши элементы определены в логических координатах.
Запоминание начальной позиции курсора на случай, если пользователь позднее захочет прервать перемещение.
Установка режима перемещения как флага для OnMouseMove ().
Функция Windows API по имени GetCursorPosition () сохраняет текущую позицию курсора в m CursorPos. Обратите внимание, что этой функции вы передаете указатель. Позиция курсора измеряется в экранных координатах (то есть, координатах относительно левого верхнего угла экрана). Все операции с курсором осуществляются в экранных координатах. Вам нужна позиция в логических координатах, поэтому вы должны выполнить преобразование за два шага. Функция ScreentoClient () (унаследованный член класса представления) преобразует экранные координаты в клиентские, а затем к результату преобразования применяется функция DPtoLP () объекта aDC, чтобы осуществить преобразование в логические координаты.
Имея установленный флаг перемещения, можно обновить обработчик сообщения о движении мыши, чтобы обрабатывать перемещение элемента.
Модификация обработчика wm__mousemove
Перемещение элемента происходит, когда включен режим перемещения и курсор мыши движется. Таким образом, все, что вам необходимо сделать в OnMouseMove () — это добавить код для обработки движения элемента в блоке, который выполняется только при условии, что m MoveMode равно TRUE. Ниже показан новый код для выполнения этого.
void CSketcherView::OnMouseMove(UINT nFlags, CPoint point) {
CClientDC aDC(this); II Контекст устройства для этого представления OnPrepareDC(&aDC); I/ Получить уточненную начальную точку // Если включен режим перемещения, передвинуть выбранный элемент II и вернуть управление i f (m_MoveMode) {
aDC. DPtoLP (&point) ;// Преобразовать в логические координаты
MoveElement(aDC, point) ;// Переместить элемент
return;
}
// Остальная часть обработчика движения мыши — как раньше...
}
Это дополнение не требует пояснений, не правда ли? Оператор if проверяет, что вы находитесь в режиме перемещения, и затем вызывает функцию MoveElement (), которая делает все необходимое для перемещения элемента. Все, что остается — реализовать эту функцию.
Добавьте объявление MoveElement () как protected-член класса CSketcherView, добавив следующий фрагмент в соответствующую точку определения класса:
void MoveElement(CClientDC& aDC, CPoint& point); II Перемещение элемента
Как всегда, если хотите, можете для этого выполнить щелчок правой кнопкой мыши на имени класса в Class View. Функции необходим доступ к объекту, инкапсулирующему контекст устройства для представления, aDC, и текущая позиция курсора, point, поэтому оба они передаются как ссылочные параметры. Реализация функции в файле SketcherView.cpp выглядит следующим образом.
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 (&aDCf m_pSelected) ; H Нарисовать передвинутый элемент }
}
Расстояние для перемещения текущего выбранного элемента сохраняется локально в форме объекта Distance типа CSize. Класс CSize специально предназначен для представления относительной координатной позиции и включает в себя два члена данных, сх и су, который соответствуют приращениями х и у. Они вычисляются как разница между текущим положением курсора, записанным в point, и предыдущей по-
зицией курсора, сохраненной в m_CursorPos. Для этого применяется операция вычитания, перегруженная в классе С Point. Используемая здесь версия возвращает объект CSize, но существует также версия, возвращающая объект CPoint. Обычно вы можете оперировать комбинацией объектов CSize и CPoint. Вы сохраняете текущую позицию курсора в m_CursorPos для использования при следующем вызове функции, который происходит, когда поступает новое сообщение о движении мыши в процессе текущей операции перемещения.
Вы будете реализовывать перемещение элемента в представлении в режиме рисования R2 NOTXORPEN, поскольку это легко и быстро. Это то же самое, что вы использовали при создании элемента. Вы перерисовываете выбранный элемент в текущем цвете (выбранном), чтобы сбросить его в цвет фона, а затем вызываете функцию Move () для перемещения элемента на расстояние, заданное Distance. Очень скоро мы добавим эту функцию к классу элемента. Когда элемент перемещает себя, вы просто используете функцию Draw () еще раз, чтобы отобразить его с подсветкой в новой позиции. Цвет элемента вернется к нормальному по завершении операции перемещения, когда обработчик OnLButtonUp () нормально перерисует все окно, обратившись к UpdateAllViews ().
Как заставить элементы перемещать себя
Добавим функцию Move () как виртуальный член базового класса СЕ 1 ement. Модифицируйте определение класса следующим образом:
class CElement:public CObject
protected:COLORREF m_Color;CRect m_EnclosingRect; int m Pen;protected:COLORREF m_Color;CRect m_EnclosingRect; int m Pen;{
protected:
COLORREF m_Color;
CRect m_EnclosingRect; int m Pen;
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 (CSizeS 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;H Переместить начальную точку
m_EndPoint += aSize;//и конечную
m_EnclosingRect += aSize; // Переместить описанный прямоугольник
Это просто, благодаря перегруженным операциям += в классах 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; H Переместить прямоугольник, определяющий окружность }
Перемещение объекта CCurve несколько сложнее, так как он определен произвольным числом точек. Вы можете реализовать функцию следующим образом:
void CCurve::Move(CSize& aSize) {
m_EnclosingRect += aSize; // Переместить прямоугольник
H Получить позицию первого элемента
POSITION aPosition = m_PointList.GetHeadPosition();
while(aPosition)
m_PointList.GetNext(aPosition) += aSize; // Переместить каждую точку / / в списке
}
Здесь тоже работы не так уж много. В начале описанный прямоугольник, хранящийся в m_EnclosingRect, перемещается с применением для этого перегруженной операции += для объектов CRect. Затем выполняется итерация по всем точкам, определяющим кривую, с передвижением каждой из них с помощью перегруженной операции += класса CPoint.
Сброс элемента
Все, что теперь осталось — это сбросить элемент в новую позицию по завершении перемещения его пользователем или же полностью отменить перемещение. Чтобы сбросить элемент в новую позицию, пользователь щелкает левой кнопкой мыши, так что вы можете реализовать эту операцию в обработчике OnLBut ton Down (). Чтобы прервать операцию, пользователь щелкает правой кнопкой мыши, поэтому вы можете добавить обработчик OnRButtonDown (), чтобы справиться с этим.
Сначала позаботимся о левой кнопке мыши. Для этого нужно будет предпринять специальное действие, когда включен режим перемещения. В следующем коде необходимые изменения выделены полужирным.
void CSketcherView:-.OnLButtonDown (UINT nFlags, CPoint point) {
CClientDC aDC(this); // Создать контекст устройства
OnPrepareDC(&aDC);// Исправить начальную точку
aDC.DPtoLP(&point);// Преобразовать точку в логические координаты
i f (m_MoveM6de) {
//В режиме перемещения, поэтому сбросить элемент
m_MoveMode = FALSE;// Прекратить режим перемещения
m_pSelected = 0;// Снять выбор с элемента
GetDocuxnent () ->UpdateAll Views (0) ; // Перерисовать представления
} else < m_FirstPoint = point; // Записать текущую позицию SetCapture () ;// Перехватывать последующие сообщения мыши
} }
Как видите, код замечательно прост. Сначала вы убеждаетесь, что режим перемещения включен. Если это так, вы просто возвращаете флаг режима в FALSE и снимаете выделение с элемента. Это все, что требуется, поскольку положение элемента отслеживалось вместе с мышью, так что он уже находится в правильном месте. И, наконец, чтобы привести в порядок все представления документа, вызывается функция документа UpdateAllViews (), что приводит к перерисовке всех представлений.
Добавьте в класс CSketcherView обработчик для сообщений WM RBUTTONDOWN, используя окно свойств этого класса. Его реализация должна решать две задачи: перемещать элемент в исходное положение и отключать режим перемещения. Ниже показан необходимый для этого код.
void CSketcherView::OnRButtonDown(UINT nFlags, CPoint point)
if(m_MoveMode)
{
// В режиме перемещения, поэтому отбросить элемент в исходную позицию CClientDC aDC(this);
OnPrepareDC(&aDC);// Исправить начальную точку
MoveElement(aDC, m_FirstPos); II Переместить элемент в исходную позицию m_MoveMode = FALSE;// Отменить режим перемещения
m_pSelected = 0;// Снять выделение элемента
GetDocument()->UpdateAllViews(0);// Перерисовать все представления
return;// Завершено
Сначала вы создаете объект CClientDC для использования в функции MoveElement (). Затем вы можете вызвать функцию MoveElement () для перемещения текущего выбранного элемента на расстояние от текущей позиции курсора в его исходную позицию, сохраненную в m_FirstPos. После того, как элемент возвращен обратно, вы просто отключаете режим перемещения, снимаете выделение с элемента и инициируете перерисовку всех представлений.
Испытание приложения
Теперь все готово для работы контекстного меню. Если вы снова соберете и запустите программу Sketcher, то сможете выбирать тип и цвет элемента в контекстном меню, или же, если курсор находится над элементом — сможете двигать или удалять элемент, пользуясь вторым контекстным меню.
Работа с маскированными элементами
Остается еще одно ограничение, которое имеет смысл ликвидировать. Если элемент, который вы хотите переместить или удалить, закрыт прямоугольником другого элемента, нарисованного позже данного, то вы не сможете выделить его, поскольку Sketcher всегда находит первым самый верхний элемент. Этот верхний элемент полностью маскирует элементы, которые он покрывает. Это результат последовательного размещения элементов в списке. Вы можете исправить это, добавив в контекстное меню пункт Send to Back (На задний план), чтобы можно было переместить элемент в начало списка.
Добавьте разделитель и новый пункт в ресурс меню IDR CURSOR MENU, как показано на рис. 15.15.
Sketcher.rc (ID...OR_MEUU - Menu)*| ▼ X element no element
Move
Delete
Send to Back
Puc. 15.15. Добавление пункта Send to Back (На задний план)
Обработчик этого пункта меню можно добавить в класс представления через окно Properties класса CSketcherView. Лучше обработать его в представлении, поскольку именно там запоминается выбранный элемент. Выберите кнопку Messages (Сообщения) панели инструментов в окне Properties класса и дважды щелкните на идентификаторе сообщения ID_SEGMENT_SENDTOBACK. После этого вы сможете выбрать ниже COMMAND и <Add> OnElementSendtoback в правой колонке. Обработчик можно реализовать так, как показано ниже.
void CSketcherView:: OnElementSendtoback () {
Ge tDocument ()->SendToBack (injoSelected) ; // Передвинуть элемент в списке
}
Вы заставляете документ выполнить эту работу, передавая указатель на текущий выбранный элемент общедоступной (public) функции SendToBack(), которую вы реализуете в классе CSketcherDoc. Добавьте ее в определение класса с типом возврата void и параметром типа CElement*. Эту функцию можно реализовать следующим образом.
void CSketcherDoc: : SendToBack (CElement* pElement) {
if(pElement)
{
// Если указатель элемента правильный, / / найти его и исключить из списка POSITION aPosition = m_ElementList.Find(pElement); m_ElementList.RemoveAt(aPosition);
m_ElementList.AddTail(pElement); // Поместить его обратно в конец списка
}
Получив значение POSITION, соответствующее элементу, вы удаляете элемент из списка вызовом RemoveAt (). Конечно, это не удалит элемент из памяти, это удалит лишь указатель из списка. Затем вы вставляете указатель на элемент в конец списка, применив для этого функцию AddTail ().
Когда элемент передвинут в конец списка, он не может маскировать никакой другой элемент, потому что поиск всегда начинается с начала. Вы всегда найдете сначала какой-то другой элемент, если соответствующий ограничивающий прямоугольник включит в себя текущую позицию курсора. Пункт меню Send to Back всегда позволит решить любую проблему с маскированием элементов в представлении.
Резюме
В этой главе было показано, как применять классы коллекций MFC к решению проблем управления наборами объектов и указателей на объекты. Коллекции — истинное сокровище в программировании для Windows, потому что данные приложения, которые вы сохраняете в документе, часто поступают в неструктурированном и непредсказуемом виде, и вам необходимо иметь возможность проходить по данным всякий раз, когда представление требует обновления.
Вы также узнали, как создавать данные документа и управлять ими в списке указателей документа, а в контексте приложения Sketcher — как взаимодействуют между собой документ и его представления.
Возможности представлений в программе Sketcher были усовершенствованы в нескольких отношениях. Была добавлена прокрутка с использованием класса MFC по имени CScr oil View, было введено всплывающее меню в месте нахождения курсора для перемещения и удаления элементов. Вы также реализовали средство подсветки элемента, чтобы помочь пользователю увидеть элемент, выбранный для перемещения или удаления.
В этой главе было затронуто немало фундаментальных тем, и особого внимания заслуживают следующие важные моменты.
Если вам нужен класс коллекции для управления объектами или указателями, то лучшим выбором будет один из шаблонных классов коллекций, поскольку в большинстве случаев они обеспечивают безопасные к типам операции с данными.
Когда вы рисуете в контексте устройства, координаты считаются в логических единицах измерения, зависящих от установленного режима отображения. Точки в окне, поступающие от Windows вместе с сообщениями мыши, представлены клиентскими координатами. Обычно упомянутые две координатных системы не совпадают.
Координаты, определяющие положение курсора, относятся к экранным координатам, измеряемым в пикселях относительно левого верхнего угла экрана.
Функции преобразования между клиентскими и логическими координатами доступны в классе CDC.
Windows запрашивает перерисовку представления, отправляя сообщение WM PAINT вашему приложению. Это инициирует вызов функции-члена представления OnDraw().
Все постоянное рисование документа должно выполняться в функции OnDraw () класса представления. Это гарантирует, что окно будет правильно нарисовано, когда того потребует Windows.
Вы можете сделать свою реализацию функции OnDraw () более эффективной, вызывая функцию-член класса CDC по имени RectVisible (), чтобы проверить, какая именно часть сущности должна быть нарисована.
Чтобы обеспечить синхронное обновление множества представлений при обновлении документа, вы можете вызвать функцию-член объекта документа UpdateAllViews (). Это вызовет член OnUpdate () каждого представления.
Вы можете передать информацию функции UpdateAllViews (), указывающую, какая часть представления должна быть перерисована. Это ускоряет перерисовку представлений.
Можно отобразить контекстное меню в позиции курсора в ответ на щелчок правой кнопкой мыши. Это меню создается как обычное всплывающее меню.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Реализуйте класс CCurve так, чтобы указатели добавлялись в начало списка, а не в конец.
Реализуйте класс CCurve в программе Sketcher, используя для представления кривой линии типизированный список указателей вместо списка объектов.
Почитайте о шаблонном классе коллекций САггау в справочной системе и воспользуйтесь им для хранения точек класса CCurve программы Sketcher.
16
Работа с диалогамии элементами управления
Диалоги и элементы управления — это основные инструменты взаимодействия пользователя со средой Windows. В настоящей главе вы изучите реализацию диалогов и элементов управления, применяя их для расширения программы Sketcher.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
Диалоги и создание диалоговых ресурсов.
Элементы управления и способы добавления их к диалогам.
Базовые варианты доступных элементов управления.
Создание класса диалога для управления диалогом.
Как запрограммировать создание диалогового окна и как получить информацию от его элементов управления.
Модальные и немодальные диалоги.
Как реализовать и использовать прямой обмен данными и верификацию в элементах управления.
Как добавить к приложению панель состояния.
Понятие диалогов
Конечно, диалоговые окна не являются для вас чем-то новым. Большинство Windows-программ широко используют диалоги для управления вводом данных. Вы щелкаете на элементе меню, и “всплывает” диалоговое окно с различными элементами управления, которые служат для ввода информации. Почти все, что появляется в диалоговом окне, является элементами управления. Диалог — это в действительности окно, и фактически каждый элемент управления в диалоговом окне также является
специализированным окном. Подумайте о том, что большинство вещей, которые вы видите на экране под Windows, являются окнами.
Хотя элементы управления имеют определенную ассоциацию с диалоговыми окнами, вы также можете при желании создавать и использовать их в других окнах. Типичное диалоговое окно показано на рис. 16.1.
Комбинированный списокКомбинированный списокОкна спискаОкна спискаКнопкиКнопкиКнопки панели инструментовКнопки панели инструментовОкно списка КнопкиОкно списка КнопкиРис. 16.1. Типичное диалоговое окноРис. 16.1. Типичное диалоговое окно
Комбинированный список
Комбинированный список
Окна списка
Окна списка
Кнопки
Кнопки
Кнопки панели инструментов
Кнопки панели инструментов
Окно списка Кнопки
Окно списка Кнопки
Рис. 16.1. Типичное диалоговое окно
Рис. 16.1. Типичное диалоговое окно
Это диалоговое окно, которое вызывается в Visual C++ 2005 при выборе пункта меню File^Open (Файл ^Открыть). Аннотации показывают разнообразные элементы управления, из которых собирается интуитивно понятный интерфейс для выбора файла, который требуется открыть. Это делает диалог легким в использовании, даже несмотря на очень широкий диапазон его возможностей.
Для создания и отображения диалогового окна в программе MFC требуются две вещи: физическое появление диалогового окна, которое определено в ресурсном файле, и объект класса диалога, используемый для управления операциями диалога и его элементов управления. MFC предоставляет вам класс по имени CDialog для использования после того, как вы определяете ваш ресурс диалога.
Что такое элементы управления?
В Windows вам доступно множество различных элементов управления, и в большинстве случаев система обеспечивает высокую степень гибкости в отношении их внешнего вида и поведения. Большинство из этих элементов попадает в одну из шести категорий, перечисленных в табл. 16.1.
Таблица 16.1. Категории элементов правления
Тип элемента управления
Что делает
Статические элементы управления
Эти элементы используются для отображения заголовков или описательной информации.
Кнопочные элементы управления
Кнопки представляют механизм ввода “одним щелчком”.
Существуют три разновидности кнопочных элементов управления: простые нажимаемые кнопки, кнопки-переключатели, из группы которых в каждый отдельный момент времени может быть выбрана только одна, и кнопки-флажки, которые могут пребывать в выбранном состоянии, независимо от соседних флажков в той же группе.
Линейки прокрутки
Линейки прокрутки обычно используются для прокрутки текста или графических изображений, горизонтально или вертикально, внутри другого элемента управления.
Окна списков
Предоставляют списки выбора, в которых одновременно можно выделить один или более вариантов.
Элементы редактирования
Элементы редактирования, принимающие текстовый ввод или позволяющие редактировать отображаемый текст.
Комбинированные списки
Комбинированные списки предоставляют списки выбора, из которых можно выбирать один вариант, комбинированные с возможностью самостоятельного ввода текста.
На рис. 16.2 показаны некоторые примеры различных типов элементов управления.
Статические элементы управления предоставляют статическую информацию, такую как заголовки или инструкции, либо просто представляют декорации в диалоге в форме пиктограммы или закрашенного прямоугольникаСтатические элементы управления предоставляют статическую информацию, такую как заголовки или инструкции, либо просто представляют декорации в диалоге в форме пиктограммы или закрашенного прямоугольникаОкно списка представляет предопределенный список элементов, из которого вы можете осуществлять выбор. Линейка прокрутки необходима, когда окно не умещает все элементы сразу Список также может иметь несколько колонок и прокручиваться в горизонтальном направлении. Доступна также версия окна списка, отображающая пиктограммы наряду с текстом.
Статические элементы управления предоставляют статическую информацию, такую как заголовки или инструкции, либо просто представляют декорации в диалоге в форме пиктограммы или закрашенного прямоугольника
Статические элементы управления предоставляют статическую информацию, такую как заголовки или инструкции, либо просто представляют декорации в диалоге в форме пиктограммы или закрашенного прямоугольника
Это текстовое окно представляет собой простейшую форму редактирующего элемента, который позволяет вводить и/или редактировать одну строку текста. Более сложные элементы редактирования могут отображать множество строк текста и поддерживать прокрутку текста.Это текстовое окно представляет собой простейшую форму редактирующего элемента, который позволяет вводить и/или редактировать одну строку текста. Более сложные элементы редактирования могут отображать множество строк текста и поддерживать прокрутку текста.Переключатели обычно группируются так, что только один из них может быть помечен (нажат) в каждый момент времени, а другие — нет.
Это текстовое окно представляет собой простейшую форму редактирующего элемента, который позволяет вводить и/или редактировать одну строку текста. Более сложные элементы редактирования могут отображать множество строк текста и поддерживать прокрутку текста.
Это текстовое окно представляет собой простейшую форму редактирующего элемента, который позволяет вводить и/или редактировать одну строку текста. Более сложные элементы редактирования могут отображать множество строк текста и поддерживать прокрутку текста.
Флажки помечаются индивидуально, и в каждый момент времени может быть помечено более одного флажка группы.
Вы уже видели линейки прокрутки, прикрепленные к клиентской области окна Sketcher. Линейки прокрутки также могут размещаться свободноВы уже видели линейки прокрутки, прикрепленные к клиентской области окна Sketcher. Линейки прокрутки также могут размещаться свободноКнопки могут иметь метки на своей поверхности, равно как и пиктограммы.
Вы уже видели линейки прокрутки, прикрепленные к клиентской области окна Sketcher. Линейки прокрутки также могут размещаться свободно
Вы уже видели линейки прокрутки, прикрепленные к клиентской области окна Sketcher. Линейки прокрутки также могут размещаться свободно
Комбинированные списки сочетают возможности выпадающего списка, из которого вы можете выбирать, с возможностью самостоятельного ввода текста. Диалог Save As... (Сохранить как) использует комбинированный список, чтобы дать возможность ввести имя файла
Рис. 16.2. Примеры различных типов элементов управления
Элемент управления может быть или не быть ассоциирован с объектом класса. Статические элементы управления ничего не делают непосредственно, поэтому в ассоциированном классе могут показаться излишними; однако в 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 и выбрав пункт
Properties (Свойства) из контекстного меню. Вы можете также отобразить свойства диалога, щелкнув правой кнопкой мыши в панели редактора диалогов и выбрав соответствующий пункт из контекстного меню. Измените ID на нечто, имеющее отношение к назначению диалога, например, IDD_PENWIDTH_DLG. В то же время вы можете также изменить свойство Caption (Заголовок) на Set Pen Width (Установка ширины пера).
Добавление элементов управления в диалоговое окно
Чтобы предоставить механизм для ввода ширины пера, вы можете добавлять элементы управления к базовому диалогу, отображенному изначально, пока он не примет вид, показанный на рис. 16.3.
Рис. 16.3. Окончательный вид диалога установки ширины пера
На рис. 16.3 можно видеть сетку (grid), которая помогает позиционировать элементы управления. Если сетка не отображается, можно выбрать соответствующую кнопку в панели инструментов для ее отображения — эта кнопка включает и отключает сетку. В качестве альтернативы можно отобразить линейки сбоку и сверху диалога, которые можно применить для создания направляющих, как показано на рис. 16.4.
Рис. 16.4. Использование линеек для создания направляющих
Горизонтальная сетка создается щелчком на соответствующей линейке. Вы можете позиционировать направляющую, перетаскивая стрелку по линейке, при этом для позиционирования элемента управления можно применять одну или более направляющих.
Диалог содержит шесть переключателей, обеспечивающих выбор ширины пера. Они заключены в рамку группы под заголовком Pen Widths (Ширины пера). Рамка группы заключает в себе переключатели и заставляет их вести себя как взаимосвязанная группа, в которой выбор (пометка) одного элемента отменяет выбор всех прочих — в каждый момент времени выбран только один член группы. Каждый переключатель снабжен соответствующей меткой, идентифицирующей ширину пера, которую он устанавливает в случае выбора. Есть также кнопка по умолчанию ОК и кнопка Cancel, закрывающие диалоговое окно. Каждый из элементов управления в диалоге имеет свой собственный набор свойств, к которым вы можете обратиться и модифицировать таким же способом, как и свойства самого диалогового окна.
Следующий шаг — добавление рамки группы. Как уже говорилось, рамка группы служит средством ассоциирования переключателей с группой с точки зрения функционирования, и предусматривает заголовок и границы для группы кнопок. Когда присутствует более одного набор переключателей, группы необходимы для их правильной работы. Вы можете щелчком выбрать кнопку, соответствующую рамке группы из палитры общих элементов управления; затем щелкните в предположительной позиции в диалоговом окне, где должен располагаться центр рамки группы. В результате в диалог будет помещена рамка группы с размерами по умолчанию. Вы можете затем перетаскивать грани этой рамки таким образом, чтобы в ней уместились шесть добавленных ранее переключателей. Чтобы установить заголовок рамки группы, наберите нужный заголовок (в данном случае — Pen Widths).
Последний шаг состоит в добавлении переключателей. Выберите элемент управления — переключатель, щелкнув на нем и затем щелкнув на позиции в диалоговом окне, куда хотите его поместить в пределах рамки группы. Повторите то же самое со всеми шестью переключателями. Каждый переключатель можно выбрать щелчком на нем, после этого введите необходимый заголовок. Вы можете также при необходимости перетаскивать границы кнопки, изменяя ее размер. Чтобы отобразить окно свойств элемента управления, щелкните на нем правой кнопкой мыши и выберите пункт Properties из контекстного меню. Вы можете изменить ID каждого переключателя в окне свойств для элемента управления на нечто, соответствующее его назначению: IDC_PENWIDTH0 — для пера шириной в 1 пиксель, IDC_PENWIDTH1 — для пера шириной 0,01 дюйма, IDC_PENWIDTH2 — для пера шириной в 0,02 дюйма и так далее.
Вы можете позиционировать индивидуальные элементы управления, перетаскивая их мышкой. Вы также можете выбрать группу элементов управления, последовательно щелкая на элементах с нажатой клавишей <Shift> либо перетаскивая курсор с нажатой левой кнопкой мыши для создания охватывающей их прямоугольной области. Чтобы выровнять группу элементов управления, выберите соответствующую кнопку в панели инструментов редактора диалогов (Dialog Editor), которая показана на рис. 16.5.
Рис. 16.5. Панель инструментов редактора диалогов
Панель инструментов показана здесь в нестыкованном состоянии, то есть, перенесенной за пределы области панели инструментов окна. Если требуемая панель инструментов не видна, ее можно отобразить, щелкнув правой кнопкой мыши в области панелей инструментов и выбрав ее из отображенного списка панелей инструментов. Вы также можете выровнять элементы управления в диалоге с помощью меню Format (Формат).
Тестирование диалога
Ресурс диалога готов. Вы можете протестировать его, выбрав кнопку, которая находится в левой части панели инструментов, показанной на рис. 16.5, либо нажав <Ctrl+T>. Это отобразит диалоговое окно с доступными базовыми операциями над элементами управления, так что вы сможете пощелкать на созданных переключателях. Когда у вас есть группа переключателей, только один из них может быть выбранным. Выбирая один, вы сбрасываете (отменяете выбор) любого другого, который был выбран ранее. Щелчок на кнопках ОК или Cancel либо даже на пиктограмме Close (Закрыть) в панели заголовка диалога завершает его тестирование. После сохранения диалогового ресурса вы готовы добавить к нему некоторый код поддержки.
Программирование для диалога
Программирование для диалога преследует две цели: заставить его отображаться и обработать взаимодействие пользователя с его элементами управления. Прежде чем вы отобразите диалог, соответствующий созданному вами ресурсу, вы должны сначала определить для него класс диалога. В этом поможет интерактивный мастер создания классов.
Добавление класса диалога
Выполните щелчок правой кнопкой мыши на диалоговом окне, созданном в редакторе диалогов, и выберите пункт Add Class (Добавить класс) из контекстного меню, после чего на экране появится диалоговое окно MFC Class Wizard (Мастер создания классов MFC). Вы должны определить новый класс диалога, унаследованный от MFC- класса CDialog, поэтому выберите имя этого класса из выпадающего окна списка Base Class: (Базовый класс:). Введите имя класса CPenDialog в поле редактирования Class name: (Имя класса:). Диалоговое окно мастера MFC Class Wizard показано на рис. 16.6.
FC Class Wizard - Sketcher? .
Щелкните на кнопке Finish (Готово) для создания нового класса.
Класс CDialog — это класс окна (унаследованный от класса MFC CWnd), специально предназначенный для отображения и управления диалогами. Ресурс диалога, который вы создали, автоматически ассоциируется с объектом типа CPenDialog, поскольку член класса IDD инициализируется идентификатором диалогового ресурса:
class CPenDialog : public CDialog
DECLARE_DYNAMIC(CPenDialog)
public:
CPenDialog(CWnd* pParent = NULL); // Стандартный конструктор virtual ~CPenDialog();
II Данные диалога
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 () для объекта диалога.
Отображение диалога
Куда поместить код отображения диалога в вашей программе — зависит от приложения. В программе Sketcher удобно добавить пункт меню, при выборе которого будет отображаться диалоговое окно установки ширины пера. Вы поместите это в меню IDR_SketcherTYPE. Поскольку и ширина, и цвет ассоциируются с пером, вы можете переименовать меню Color (Цвет) в Реп (Перо). Это делается двойным щелчком на пункте меню Color в панели Resource View и открытием окна Properties, в котором значение свойства Caption меняется на &Реп. После закрытия окна изменения вступают в силу.
Когда вы добавляете пункт Width (Ширина) в меню Реп, то должны отделить его от цветов. Для этого потребуется добавить разделитель после последнего пункта меню, относящегося к цвету, щелкнув правой кнопкой мыши на пустом пункте меню и выбрав пункт Insert Separator (Вставить разделитель) из контекстного меню. Новый пункт меню Width добавляется непосредственно после разделителя. Пункт меню заканчивается многоточием, чтобы указать, что он отобразит диалог; это — стандартное соглашение Windows. Двойной щелчок на меню отображает свойства меню для модификации, как показано на рис. 16.7.
Укажите ID PENWIDTH в качестве идентификатора пункта меню, как показано на рис. 16.7. Вы можете также добавить сообщение для панели состояния, и поскольку вы также добавляете кнопку панели инструментов, то можете включить текст всплывающей подсказки. Помните, что текст всплывающей подсказки нужно помещать сразу за текстом сообщения для панели состояния, отделяя его \п. Вот как будет выглядеть значение свойства Prompt: Change pen widthXnShow pen width options (Изменить ширину пера\пПоказать опции ширины пера). Меню будет выглядеть, как показано на рис. 16.8.
Sketcher.rc (IDR...tcherTYPE - Menu) v X
File Edit View Element Pen Window Help | Type Here |v* Black
Red
Green
Blue
Width...
Puc. 16.8. Окончательный вид меню Pen
Чтобы добавить кнопку в панель инструментов, откройте ресурс панели инструментов, раскрыв папку Toolbar (Панель инструментов) в Resource View и дважды щелкнув на IDR_MAINFRAME. Создайте изображение для кнопки панели инструментов, представляющей ширину пера, как показано на рис. 16.9.
Sketcher.rc (IDR. .HFRAME - Toolbar)т х "I
dHb| «1»|е| еш \|п|оЫ -Н-Н ^J
ГТ пт1
Й
Рис. 16.9. Изображение для кнопки панели инструмен-тов, представляющей ширину пера
Чтобы ассоциировать новую кнопку с добавленным пунктом меню, откройте окно свойств кнопки и укажите в качестве ее идентификатора ID_PENWIDTH — то же значение, что и у соответствующего пункта меню.
Код отображения диалога
Код отображения диалога будет находиться в обработчике пункта меню Ре n^ Width (Перо1^ Ширина), но в каком классе следует реализовать этот обработчик? Класс представления — подходящий кандидат на то, чтобы иметь дело с шириной пера, но следуя предыдущей логике с цветами и элементами имело бы смысл поместить выбор текущей ширины пера в документ, поэтому обработчик должен находиться в классе CSketcherDoc. Щелкните правой кнопкой мыши на пункте меню Width в панели Resource View для меню !D_SketcherTYPE и выберите пункт Add Event Handler (добавить обработчик событий) из контекстного меню. Затем создайте в классе CSketcherDoc функцию для обработчика сообщений COMMAND, соответствующего ID PENWIDTH. Отредактируйте этот обработчик, введя следующий код:
// Обработчик пункта меню установки ширины пера
void CSketcherDoc::OnPenwidth()
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 (), для обнаружения случая закрытия диалога в результате щелчка на кнопке ОК.
Как уже говорилось, вы можете переопределить функцию 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_PeriWidth; // Запись ширины пера
// Плюс остальная часть реализации класса... .
};
Если вы используете контекстное меню для добавления в класс m_PenWidth, не забудьте добавить комментарий к определению класса. Это полезная привычка, даже если имена членов кажутся достаточно очевидными.
Член данных m_PenWidth используется для установки переключателя, соответствующего текущей ширине пера в проверяемом документе. Потребуется также обеспечить сохранение в этом члене ширины пера, выбранной в диалоге, чтобы ее можно было извлечь оттуда после закрытия диалога. Пока вы можете инициализировать m PenWidth значением 0 в конструкторе класса.
Инициализация элементов управления
Инициализировать переключатели можно, переопределив функцию OnlnitDialog (), определенную в базовом классе CDialog. Эта функция вызывается в ответ на сообщение WM INITDIALOG, которое отправляется во время выполнения DoModal (), непосредственно перед отображением диалога на экране. Вы можете добавить эту функцию в класс CPenDialog, выбрав OnlnitDialog в списке переопределений окна свойств Properties класса CPenDialog, как показано на рис. 16.10.
Properties▼ Д X
CPenDialog VCCode Class
В1! U / ' ♦
get_acdValueА
GetlnteHaceHook
GetScrollBarCtrl
HtrnlHelp
IsInvokeAllowed
OnAmbientFropert)
OnCancel
OnChildllotify
OnCmdMsg
OnCommand
OnCreateAggregates
OnFinalRelease
OnlnitDiaOnlnitDialog *
Onr^otify
OnOK
OnToolhitTest
OnWndMsg
PostNcDestroy
F re CreateWi ndow
PrelnitDialog
Pre SubclassWi ndow
PreTranslate Message
Serialize
WiiidowProc
WinHelo ^
OnlnitDialog
Override to augment dialog-box initialization
Рис. 16.10. Выбор функции OnlnitDialog в спискепереопределений окна свойств Properties
Реализация новой версии OnlnitDialog () показана ниже.
BOOL CPenDialog::OnlnitDialog() {
CDialog::OnlnitDialog();
// Пометить переключатель, соответствующий текущей ширине пера switch(m_PenWidth)
{
CheckDlgButton(IDC_PENWIDTHl,l);
break;
CheckDlgButton(IDC_PENWIDTH2,1);
break;
CheckDlgButton(IDC_PENWIDTH3,1);
break;
CheckDlgButton(IDC_PENWIDTH4,l); break;
CheckDlgButton(IDC_PENWIDTH5,l) ; break;
default:
CheckDlgButton(IDC_PENWIDTHO,l);
}
return TRUE; //возвратить TRUE, пока не установлен фокус на элементе управления // ИСКЛЮЧЕНИЕ: страницы свойств OCX должны возвращать FALSE
Вы должны оставить здесь вызов функции базового класса, поскольку она осуществляет некоторые важные установки для диалога. Оператор switch помечает один из переключателей в зависимости от значения, установленного в переменной m_PenWidth. Это подразумевает установку m PenWidth в подходящее значение перед выполнением DoModal (), поскольку функция DoModal () приводит к отправке сообщения WM_INITDIALOG, в результате чего вызывается ваша версия OnlnitDialog ().
Функция CheckDlgButton () унаследована непосредственно от CWnd через С Di a log. Если второй аргумент равен 1, она помечает кнопку, соответствующую идентификатору, специфицированному в первом аргументе. Если второй аргумент равен 0, пометка с кнопки снимается. Это работает как с флажками, так и с переключателями.
Обработка сообщений переключателей
После отображения диалога при каждом щелчке на одном из переключателей сообщение генерируется и отправляется приложению. Чтобы работать с этими сообщениями, вы можете добавить обработчики в класс CPenDialog. Щелкните правой кнопкой мыши на каждом из переключателей и выберите пункт Add Event Handler (Добавить обработчик событий) из контекстного меню, чтобы создать обработчик для сообщения BN_CLICKED. На рис. 16.11 показано диалоговое окно обработчика события для переключателя, имеющего в качестве идентификатора IDC PENWIDTH. Обратите внимание, что имя обработчика отредактировано, поскольку имя по умолчанию выглядит несколько неуклюже.
Реализации обработчиков событий BN_CLICKED для всех этих переключателей похожи, поскольку каждый из них просто устанавливает ширину пера в диалоговом объекте. В качестве примера ниже показан обработчик для IDC PENWIDTHO.
Рис. 16.11. Диалоговое окно обработчика события для переключателя IDC_PENWIDTH
void CPenDialog: -.OnPenwidthO ()
{
m_PenWidth = 0;
}
Вы должны добавить код для всех шести обработчиков реализации класса CPenDialog, устанавливая значение m_PenWidth равным 1 в OnPenWidthl (), 2 — в OnPenWidth2 () и так далее.
Завершение операций диалога
Теперь вы должны модифицировать обработчик OnPenwidth () в CSketcherDoc, чтобы заставить работать диалог. Добавьте в функцию следующий код:
И Обработчик пункта меню установки ширины пера
void CSketcherDoc: :OnPenwidth () {
CPenDialog aDlg; И Создать локальный диалоговый объект
Н Установить ширину пера в диалоге в значение, сохраненное в документе aDlg.m_PenWidth = m_PenWidth;
// Отобразить диалог в модальном режиме.
/ / При закрытии по щелчку на кнопке ОК получить ширину пера if(aDlg.DoModal() == IDOK) m_PenWidth = aDlg.m_PenWidth;
}
Член 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 в класс документа, а также функцию GetPenWidth (), чтобы обеспечить доступ извне к сохраненному в ней значению. Для этого потребуется добавить к определению класса CSketcherDoc следующие операторы, выделенные полужирным:
class CSketcherDoc : public CDocument {
// Остальной код — как раньше. . .
protected:
// Остальной код — как раньше...
int m_PenWidth; / / Текущая ширина пера
// Операции
public:
// Остальной код — как раньше. . .
int GetPenWidth () / / Получить текущую ширину пера
{ return m_PenWidth; }
// Остальной код — как раньше. . .
};
Поскольку это тривиально, вы можете определить функцию GetPenWidth () прямо в определении класса и воспользоваться преимуществами того, что она будет неявно встроенной. Вы по-прежнему нуждаетесь в добавлении инициализации m PenWidth в конструктор CSketcherDoc, поэтому модифицируйте конструктор в CSketcherDoc. срр, добавив строку, выделенную полужирным:
CSketcherDoc: : CSketcherDoc ()
: m_Element (LINE) , m_Color(BLACK)
,m_PenWidth(0) // Перо шириной 1 пиксель
{
// TODO: добавить сюда код одноразового конструирования объекта
}
Добавление ширины пера к элементам
В классе CElement и унаследованных от него классах форм придется сделать немного больше. У вас уже есть член m_Pen в классе CElement, хранящий ширину, используемую при рисовании элемента, и вы должны расширить каждый конструктор элементов для приема ширины пера в качестве аргумента, устанавливая член класса соответственно. Функция GetBoundRect () в CElement должна быть изменена так, чтобы иметь дело с нулевой шириной пера. Сначала вы можете заняться классом CElement. Новая версия функции GetBoundRect () класса CElement будет такой:
/ / Получить ограничивающий прямоугольник элемента
CRect CElement::GetBoundRect()
CRect BoundingRect; //Объект для сохранения ограничивающего прямоугольника BoundingRect = m_EnclosingRect; //Инициализировать описанным прямоугольником
// Увеличить ограничивающий прямоугольник на ширину пера
int Offset = m_Pen = 0? l:m_Pen; // Ширина должна быть не меньше 1 BoundingRect.InflateRect(Offset, Offset);
return BoundingRect;
}
Вы используете локальную переменную Offset, чтобы гарантировать передачу функции InterSect О значения 1, если ширина пера равна нулю (перо шириной 0 всегда рисует линию шириной в один пиксель), и действительной ширину пера во всех остальных случаях.
Каждый из конструкторов CLine, CRectangle, CCirsle и CCurve должен быть модифицирован для приема ширины пера в качестве аргумента и сохранения его в унаследованном члене m_Pen класса. Объявление конструктора в каждом определении класса также должно быть модифицировано добавлением дополнительного параметра. Например, в классе CLine объявление конструктора станет таким, как показано ниже.
CLine (CPoint Start, CPoint End, COLORREF aColor, int PenWidth) ;
Реализация конструктора должна быть модифицирована следующим образом:
CLine::CLine (CPoint Start, CPoint End, COLORREF aColor, int PenWidth) :m_EndPoint(CPoint(0,0))
{
m_StartPoint = Start;// Установить начальную точку линии
m_EndPoint = End; // Установить конечную точку линии mjColor = aColor; // Установить цвет линии
m Pen = PenWidth; // Установить ширину линии
// Определить включающий прямоугольник
m_EnclosingRect = CRect(Start, End) ;
m_EnclosingRect.NormalizeRect();
}
Точно так же вы можете модифицировать каждое из определений классов и конструкторы для всех остальных фигур, чтобы каждый из них инициализировал m Pen значением, переданным в последнем аргументе.
Создание элементов в представлении
Последнее изменение вы должны внести в член CreateElement () класса CSketcherView. Поскольку была добавлена ширина пера как аргумент конструктора каждой из фигур, вы должны обновить и все вызовы этих конструкторов. Измените определение CSketcherView: :CreateElement () следующим образом:
CElement* CSketcherView: :CreateElement () (
II Получить указатель на документ в этом представлении
CSketcherDoc* pDoc = GetDocumentO;
ASSERT_VALID(pDoc); // Проверить указатель
// Выбрать элемент, используя тип, сохраненный в документе switch(pDoc->GetElementType()) {
case RECTANGLE:
return new CRectangle (m_FirstPoint, mJSecondPoint, pDoc->GetElementColor(), pDoc->GetPenWidth());
case CIRCLE:
return new CCircle(m_FirstPoint, mJSecondPoint, pDoc->GetElementColor () , pDoc~>GetPenWidth ()) ;
case CURVE:
return new CCurve(m_FirstPoint, mJSecondPoint, pDoc->GetElementColor(), pDoc->GetPenWidth());
case LINE: //По умолчанию — всегда линия return new CLine (m_FirstPoint, mJSecondPoint, pDoc->GetElementColor(), pDoc->GetPenWidth());
default:// Что-то не так
AfxMessageBox("Недопустимый код элемента", МВ_ОК) ; AfxAbort() ;
}
}
Каждый вызов конструктора теперь передает в аргументе ширину пера. Она извлекается из документа с помощью функции GetPenWidth (), которую вы добавили в класс документа.
Испытание диалога
Теперь вы можете собрать и запустить последнюю версию Sketcher, чтобы посмотреть, как работает диалог установки ширины пера. Выбрав пункт меню Pen^Width
(Перо1^ Ширина) или нажав ассоциированную с ним кнопку панели инструментов, вы отобразите окно диалога, в котором можно будет выбрать ширину пера. Экран на рис. 16.12 показывает, что вы увидите при выполнении программы Sketcher.
Рис. 16.12. Работа программы Sketcher после добавления диалога изменения ширины пера
Обратите внимание, что диалог представляет собой совершенно отдельное окно. Вы можете перетаскивать его в любое место экрана. Вы можете даже перетащить его за пределы окна приложения Sketcher.
Использование кнопки счетчика
Теперь посмотрим, как может пригодиться приложению Sketcher кнопка счетчика (spin button). Кнопки счетчика особенно удобны, когда вы хотите ограничить ввод определенным целочисленным диапазоном. Обычно они применяются в ассоциации с другим элементом управления, называемым дружественным элементом управления (buddy control), который отображает значение, модифицируемое кнопкой счетчика. Ассоциированным элементом управления обычно служит редактирующий элемент, хотя это и не обязательно.
В приложении Sketcher было бы неплохо иметь возможность рисовать в различном масштабе. Если будет возможность изменять масштаб рисования, вы сможете увеличивать рисунок в тех местах, где требуется уточнить мелкие детали вашего шедевра, и возвращаться к исходному масштабу, работая с полной перспективой. Вы можете применять кнопку счетчика для управления масштабированием представления документа. Масштаб рисования должен быть свойством, специфичным для представления, а функции рисования должны принимать во внимание текущий масштаб этого представления. Изменение существующего кода для работы с масштабированием требует относительно большую работу, нежели настройка элемента управления, поэтому сначала мы посмотрим, как можно создать кнопку счетчика и заставить ее работать.
Добавление пункта меню и кнопки панели инструментов для функции масштабирования
Начнем с обеспечения средств для отображения диалога масштабирования. Перейдите к Resource View и откройте меню !DR_SketcherTYPE. Вам потребуется добавить пункт меню Scale (Масштаб) в конец меню View (Вид). Введите заголовок для неиспользованного пункта меню — as Scale.... Этот пункт вызовет диалог масштабирования, поэтому пункт завершается многоточием, указывая на то, что в результате его выбора откроется диалог. Затем можно добавить разделитель, предшествующий новому пункту меню, щелкнув на нем правой кнопкой мыши и выбрав пункт Insert Separator (Вставить разделитель) из контекстного меню. Сверьте установленные свойства пункта меню с теми, что показаны на рис. 16.13.
1 Prooerties
▼ - X
Menu Editor IMenuEd
КA |
В Appearance
Caption
S&cale...
Checked
False
Enabled
True
Grayed
False
Popup
False
El Behavior
Break
None
Right Justify
False
Right Order
False
В Мкс
a me1
Menu Editor
Help
False
ID
ID_VIEW_SCALE
Prompt
Separator
False
ID
Specifies the identifier of the menu item or menu
resource.
Рис. 16.13. Свойства пункта меню Scale
Вы можете также добавить кнопку в панель инструментов для этого пункта меню. Делая это, убедитесь, что идентификатор кнопки будет установлен таким же, как и у пункта меню — ID_VIEW_SCALE.
Создание кнопки счетчика
Итак, пункт меню у вас есть, теперь нужен диалог, который будет им вызываться. Находясь в панели Resource View, добавьте новый диалог щелчком правой кнопкой мыши на папке Dialog (Диалог) и выбором пункта Insert Dialog (Вставить диалог) из контекстного меню. Измените идентификатор диалога на IDD SCALE DLG, а заголовок— на Set Drawing Scale (Установка масштаба рисования).
Щелкните на элементе управления счетчика в палитре, после чего щелкните в позиции диалога, куда вы хотите его поместить. Затем щелкните правой кнопкой мыши на этом элементе, чтобы отобразить его свойства. Измените идентификатор по умолчанию на что-то более осмысленное вроде IDC_SPIN_SCALE. После этого взгляните на свойства кнопки счетчика. Они показаны на рис. 16.14.
Меню должно выглядеть так, как показано на рис. 16.15.
Рис. 16.14. Свойства кнопки счетчика IDC_SPIN_SCALE
Sketcher.rc (IDR...herTYPE - Menu)*,▼ X
File Edit View Element Pen Window Help | Type Here |
Toolbar
Status Bar
' I Scale..|
| Type Here ~]
Puc. 16.15. Новое меню View (Вид)
Свойство Arrow Keys (Клавиши со стрелками) всегда установлено в True, что позволяет вам оперировать кнопкой счетчика клавишами со стрелками на клавиатуре. Вы также должны установить в True значение Set buddy integer (Установка дружественного элемента управления как целого числа), специфицирующее значение дружественного элемента управления как целое число. Значение свойства Auto buddy (Автоматический выбор дружественного элемента управления), предназначенное для автоматического выбора дружественного элемента управления, также следует установить в True. В результате этого элементом управления, выбранным в качестве дружественного, автоматически станет предыдущий элемент, определенный в диалоге. В данный момент это кнопка Cancel, что не то, что нужно, но очень скоро вы увидите, как это исправить. Свойство Alignment (Выравнивание) определяет, как кнопка счетчика будет отображаться относительно ее “друга”. Вы должны установить ее в Right Align (Выравнивание вправо), чтобы кнопка счетчика была присоединена к правому краю ее дружественного элемента управления.
Затем добавьте элемент редактирования рядом с кнопкой счетчика, выбрав его из списка в панели инструментов и щелкнув в месте диалога для его размещения. Измените идентификатор этого элемента редактирования на I DC SCALE.
Чтобы сделать содержимое элемента редактирования достаточно ясным, вы должны добавить статический элемент управления непосредственно слева от элемента редактирования в палитре и ввести View Scale: (Масштаб просмотра:) в качестве его заголовка. Вы можете выбрать три элемента управления, щелкая на них при нажатой клавише <Shift>. Нажатие функциональной клавиши <F9> аккуратно выровняет эти элементы, либо для этого можно воспользоваться меню Format (Формат).
Последовательность табуляции элементов управления
Элементы управления в диалоге имеют последовательность обхода при нажатии клавиши табуляции. Это последовательность, в которой перемещается фокус от одного элемента к другому, изначально определяемая последовательностью добавления элементов в диалог. Вы можете увидеть последовательность табуляции, выбрав пункт Format^Tab Order (Формат^Порядок обхода по клавише табуляции) из главного меню, либо нажав <Ctrl+D>; диалог будет аннотирован, как показано на рис. 16.16.
nset Drawing Scale
1 i ок
? Scale: sample ed 2
I Cancel
Puc. 16.16. Отображение номеров в последо-вательности обхода по клавише табуляции
Порядок табуляции показан на рис. 16.16 последовательностью цифр. Поскольку кнопка Cancel непосредственно предшествует кнопке счетчика в этой последовательности, свойство Auto Buddy кнопки счетчика выбирает ее как дружественный элемент управления. Но на самом деле вы хотите, чтобы элемент редактирования предшествовал кнопке счетчика в последовательности табуляции, поэтому вы должны выбрать эти элементы, щелкнув на них в такой последовательности: кнопка ОК,
кнопка 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
После щелчка на кнопке Finish мастер позаботится о генерации кода, необходимого для поддержки вашей новой переменной элемента управления. После добавления мастером нового члена определение класса будет выглядеть так:
class CScaleDialog : public CDialog
{
DECLARE_DYNAMIC(CScaleDialog)
public:
CScaleDialog(CWnd* pParent = NULL); // Стандартный конструктор virtual ~CScaleDialog();
/ / Данные диалога
enum { IDD = IDD_SCALE_DLG } ;
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);
1
Эта функция вызывается каркасом для обслуживания обмена данными между переменными диалога и его элементами управления. Этот механизм называется обменом данными диалога — Dialog Data Exchange (DDX). Это мощный механизм, в большинстве случаев обеспечивающий автоматическую передачу информации между диалогом и его элементами управления, избавляя вас от необходимости самостоятельного программирования обмена данных, как вы делали это с переключателями в диалоге установки ширины пера.
В диалоге масштабирования DDX обрабатывает обмен данными между элементом редактирования и переменной m_Scale класса CScaleDialog. Переменная pDX, переданная функции DoDataExchange (), управляет направлением передачи данных. После вызова функции DoDataExchange () базового класса вызывается функция DDX Text (), которая действительно перемещает данные между переменной m_Scale и элементом редактирования.
Вызов функции DDV MinMaxInt () контролирует нахождение переданного значения в указанных пределах. Этот механизм называется верификацией данных диало-
га — Dialog Data Validation (DDV). Функция DoDataExchange () вызывается автоматически перед отображением диалога для передачи значения, сохраненного в m Scale, в элемент редактирования. Вам потребуется лишь обеспечить сохранение правильного значения в m Scale перед отображением диалогового окна и получить результат при закрытии диалога.
Инициализация диалога
Вы используете функцию On In it Dialog () для инициализации диалога — так же, как вы делали это в диалоге установки ширину пера. На этот раз вы используете ее для установки кнопки счетчика. Вы инициализируете переменную-член m_Scale чуть позже, когда создадите диалог в обработчике пункта меню Scale (Масштаб), поскольку она должна быть установлена в значение масштаба, сохраненного в представлении. А пока добавьте в класс CScaleDialog переопределение функции OnlnitDialog (), используя тот же механизм, что и в предыдущем диалоге, и добавьте код инициализации элемента счетчика, как показано ниже:
BOOL CScaleDialog::OnlnitDialog() {
CDialog: .-OnlnitDialog () ;
/ / Сначала получить указатель на элемент счетчика CSpinButtonCtrl* pSpin;
pSpin = (CSpinButtonCtrl*)GetDlgltern(IDC_SPIN_SCALE) ;
I / Если вы не помечаете опцию 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(GetDlgltem(IDC_SCALE));
Отображение кнопки счетчика
Диалог должен отображаться, когда выбрана опция меню Scale (или ассоциированная с ним кнопка панели инструментов), поэтому вам придется добавить обработчик событий COMMAND в класс CSketcherView, соответствующий событию IDVIEWSCALE. Для этого применяется окно 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 выполнять большую часть работы. К сожалению, для этого недостаточно просто сменить режим отображения, поскольку ни один из этих режимов не поддерживается CScrollView. Однако если это удастся обойти, вы будете в целости и сохранности.
Вы используете 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 дюйма, с началом координат в левом верхнем углу клиентской области и положительным направлением оси у снизу вверх. Они используются функциями рисования контекста устройства.
Координаты устройства (также известные, как клиентские координаты в окне) измеряются в пикселях в случае окна, с началом в верхнем левом углу клиентской области и положительным направлением оси у сверху вниз. Они используются вне контекста устройства, например, для определения позиции курсора в обработчике событий мыши.
Экранные координаты измеряются в пикселях и имеют начало в верхнем левом углу экрана с положительным направлением оси у сверху вниз. Они используются при установке или получении позиции курсора.
Преобразование логических координат Windows в координаты устройства выражается следующими формулами:
xDevice = (xLogical - xWindowOrg) *(xViewportExt/xWindowExt)+ xViewportOrg yDevice = (yLogical - yWindowOrg) * (yViewportExt/yWindowExt) + yViewportOrg
В координатных системах, отличных от тех, что предоставляются режимами отображения MM_ISOTROPIC и ММ-ANISOTROPIC, величина окна и величина области просмотра фиксированы режимом отображения, и вы не можете их изменить. Вызов функций SetWindowExt () или SetViewportExt () в объекте CDC для их изменения не дает никакого эффекта, хотя вы все-таки можете переместить позицию (0, 0) в ваше логическое обрамляющее окно вызовом SetWindowOrg () или SetViewportOrg (). Однако для данного размера документа, выраженного размером окна в единицах логических координат, вы можете просто настроить масштаб отображения элементов, установив соответствующим образом размер области отображения. Используя и устанавливая размеры окна и области отображения, вы можете получить автоматическое масштабирование.
Установка размера документа
Вы должны поддерживать размер документа в логических единицах в объекте документа. Вы можете добавить в определении класса CSketcherDoc член данных protected по имени 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))
{
// TODO: добавить сюда одноразовый код конструирования
}
Вы используете национальные координаты MM LOENGLISH, поэтому можете трактовать логические единицы как 0,01 дюйма, и такая установка даст вам область для рисования в 30 квадратных дюймов.
Установка режима отображения
Установить режим отображения MM_ANISOTROPIC можно в переопределенной функции OnPrepareDC () класса CSketcherView. Эта функция всегда вызывается для любого сообщения WM PAINT, и вы будете обращаться к ней при рисовании временных объектов в обработчике сообщений мыши; однако потребуется предпринять нечто большее, чем просто установить режим отображения. Вы должны создать
переопределенную функцию с СSketcherView, прежде чем добавлять код. Просто откройте окно свойств класса СSketcherview и щелкните на кнопке Overrides (Переопределения) в панели инструментов. Затем вы можете добавить переопределение, выбрав OnPrepareDC из списка и щелкнув на <Add> OnPrepareDC в соседней колонке. Затем вы сможете ввести код непосредственно в панели редактирования. Ниже показана реализация OnPrepareDC ().
void CSketcherView: : OnPrepareDC (CDC* pDC, CPrintlnfo* plnfo) {
CScrollView::OnPrepareDC(pDC, plnfo);
CSketcherDoc* pDoc = GetDocument () ;
pDC->SetMapMode (MM_ANISOTROPIC) ;/ / Установить режим отображения
CSize DocSize = pDoc->GetDocSize () ;// Получить размер документа
//Размер у должен быть отрицательным, поскольку хотим использовать 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, дает размер документа в дюймах. Если его увеличить на количество логических пикселей на дюйм для устройства, получится эквивалентное количество пикселей на размер. Если затем исполь-
зовать это значение в качестве размера области отображения, получатся элементы, отображенные в масштабе 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(MMJTEXT, DocSize) ; И Настроить линейки прокрутки
)
После создания локального объекта CClientDC для представления вызывается OnPrepareDC () с целью установки режима отображения MM_ANISOTROPIC. Поскольку здесь учитывается масштабирование, член LPtoDP () объекта aDC преобразует размер документа, сохраненный в локальной переменной DocSize, в корректное число пикселей для текущего логического размера документа и текущего масштаба. Общий размер документа в пикселях определяет, насколько большими должны быть линейки прокрутки в режиме ММ_ТЕХТ, а вы должны вспомнить, что логические
координаты ММ ТЕХТ измеряются в пикселях. Затем вы можете воспользоваться 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)r m_CursorPos(CPoint(0, 0)), m_FirstPos(CPoint(0, 0) ), m_Scale(l){: m_FirstPoint(CPoint(0, 0)), m_SecondPoint(CPoint(0, 0) ), m_pTempElement(NULL), m_pSelected(NULL), m_MoveMode(FALSE)r m_CursorPos(CPoint(0, 0)), m_FirstPos(CPoint(0, 0) ), m_Scale(l){// Установить 1-ю записанную точку в 0,0
: m_FirstPoint(CPoint(0, 0))
, m_SecondPoint(CPoint(0, 0) )
, m_pTempElement(NULL)
, m_pSelected(NULL)
, m_MoveMode(FALSE)
r m_CursorPos(CPoint(0, 0))
, m_FirstPos(CPoint(0, 0) )
, m_Scale(l)
{
: m_FirstPoint(CPoint(0, 0))
, m_SecondPoint(CPoint(0, 0) )
, m_pTempElement(NULL)
, m_pSelected(NULL)
, m_MoveMode(FALSE)
r m_CursorPos(CPoint(0, 0))
, m_FirstPos(CPoint(0, 0) )
, m_Scale(l)
{
II Установить 2-ю записанную точку в 0,0
// Установить указатель на временный элемент в 0
// Изначально нет выбранного элемента
// Отключить режим перемещения
// Инициализировать нулем
// Инициализировать нулем
// Установить масштаб 1:1
SetScrollSizes(ММ__ТЕХТ,SetScrollSizes(ММ__ТЕХТ,CSize(0,0)); // Установить произвольные значения
SetScrollSizes(ММ__ТЕХТ,
SetScrollSizes(ММ__ТЕХТ,
Дополнительный оператор просто вызывает SetScrollSizes () с произвольным размером, чтобы инициализировать линейки прокрутки перед рисованием представления. Когда представление рисуется впервые, вызов функции ResetScrollSizes () в OnlnitialUpdate () правильно устанавливает линейки прокрутки.
Конечно, при каждом изменении масштаба представления вы должны обновлять линейки прокрутки перед перерисовкой представления. Вы можете делать это в обработчике OnViewScale(), в классе CSketcherView:
void CSketcherView: :OnViewScale() {
CScaleDialog aDlg;// Создать объект диалога
aDlg.m_Scale = m_Scale; // Передать масштаб представления диалогу if(aDlg.DoModal () == IDOK)
m_Scale = aDlg.m_Scale; // Получить новый масштаб
ResetScrollSizesO ; //Исправить прокрутку в соответствии с новым масштабом 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__pMainWnd; pFrame->SetPaneText(0, "Прощай, жестокий мир") ;
Этот фрагмент кода получает указатель на главное окно приложения и выводит текстовую строку, которую вы видите в крайней левой области панели состояния. Все прекрасно, но главное окно приложения — это не то место, где нужно видеть масштаб
отображения. В 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"), -1, textRect, DT SINGLELINE|DT CALCRECT);
// Настроить размер части панели состояния, достаточную для принятия текста 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. Описанный прямоугольник для текста сохраняется здесь в логических координатах.
Один или более флагов, управляющих работой функции.
Мы специфицировали комбинацию из двух флагов: DT_S INGLE LINE указывает на то, что текст должен размещаться в одной строке, a DT CALCRECT — что необходима функция для вычисления размера прямоугольника, необходимого для отображения строки и сохранения ее в прямоугольнике, на который указывает третий аргумент. Функция DrawText () обычно используется для действительного рисования строки. Существует множество других флагов, которые вы можете использовать в этой функции; за подробностями обращайтесь в справочную систему.
Следующий оператор настраивает части панели состояния:
m_StatusBar.GetStatusBarCtrlО.SetParts(l, Swidth);
Выражение m StatusBar. GetStatusBarCtrl () возвращает ссылку на объект CStatusBarCtrl, относящийся к m_StatusBar. Возвращенная ссылка используется для вызова функции SetParts () объекта. Ее первый аргумент определяет количество частей для панели состояния, в данном случае — 1. Второй аргумент — обычно адрес массива типа int, содержащего координаты х правой грани каждой части в клиентских координатах. Массив имеет по одному элементу для каждой части панели состояния. Поскольку у нас только одна часть, мы передаем адрес отдельной переменной — width, содержащей ширину прямоугольника, сохраненного в textRect. Это клиентские координаты, поскольку контекст устройства использует по умолчанию ММ_ТЕХТ.
И, наконец, мы устанавливаем начальный текст в панели состояния вызовом функции SetText () — члена CStatusBarCtrl. Первый аргумент — выводимая текстовая строка, второй — индекс позиции части, содержащей текстовую строку, а третий специфицирует внешний вид части экрана. Третий аргумент может принимать значения, перечисленные в табл. 16.3.
Таблица 16.3. Стили панели состояния
Код стиля
Внешний вид
0
Текст имеет границу, которая выглядит “утопленной” в панели состояния.
SBT_NOBORDERS
Текст отображается без границ.
SBT_OWNERDRAM
Текст отображается родительским окном.
SBT_POPOUT
Текст имеет границу, выглядящую “выступающей” над поверхностью панели состояния.
В коде вы специфицируете текст с границей, “утопленной” в панели состояния. Можете попробовать другие опции, чтобы посмотреть, как они выглядят.
Обновление панели состояния
Если теперь вы соберете и запустите код, то панели состояния появятся, но они будут показывать значение масштаба 1, независимо от реально установленного фактора масштабирования, что не особенно полезно. Поэтому нужно куда-то добавить код, изменяющий текст при каждом выборе нового масштаба. Это означает модификацию обработчика OnViewScale () в классе CSketcherView для изменения панели состояния обрамляющего окна. Потребуется всего четыре дополнительных строки кода:
void CSketcherView: :OnViewScale () {
CScaleDialog aDlg;// Создать объект диалога
aDl^.m_Scale = m_Scale; // Передать диалогу масштаб представления
if(aDlg.DoModal () == IDOK) {
m_Scale = aDlg.m_Scale; // Получить новый масштаб
/ / Получить обрамляющее окно для данного представления
CChildFrame* viewFrame = static__cast<CChildFrame*>(GetParentFrame());
/ / Построить строку сообщения
CString StatusMsg ("Масштаб отображения:") ;
StatusMsg += static__cast<char> (* 0 * + 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, может поначалу показаться несколько запутанным, поэтому опыт подобного рода вам пригодится.
Удаление диалога масштаба
Сначала вы должны удалить определение и реализацию С Seal eDialog из нового проекта 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 (Сборка1^ Очистить решение) для удаления промежуточных файлов из проекта, которые могут иметь ссылки на CScaleDialog. Когда это будет сделано, вы можете пересоздать диалоговый ресурс для ввода значения масштаба.
Создание элемента управления — окна списка
Щелкните правой кнопкой мыши на Dialog в Resource View и добавьте новый диалог с подходящим идентификатором и заголовком. Вы можете использовать тот же идентификатор, что и ранее — IDD_SCALE_DLG.
Выберите кнопку окна списка в списке элементов управления и щелкните в месте диалогового окна, куда хотите поместить это окно списка. Вы можете увеличить окно списка и уточнить его положение в диалоговом окне, перетащив мышью, куда нужно. Щелкните правой кнопкой на окне списка и выберите Properties из контекстного меню. Вы можете установить значение ID в нечто более подходящее, например, IDC SCALELIST, как показано на рис. 16.18.
Свойство Sort по умолчанию примет значение True, поэтому не забудьте изменить его на False. Это означает, что строки, которые вы добавляете в окно списка, не будут автоматически сортироваться. Вместо этого они будут просто добавляться в конец списка и отображаться в том порядке, в котором были введены вами. Поскольку вы используете позицию выбранного элемента в списке для указания масштаба, важно не менять последовательность. Окно списка по умолчанию снабжено вертикальной линейкой прокрутки, и вы можете принять значения по умолчанию также и для других свойств. Если вы хотите исследовать эффект от других свойств, то можете щелкнуть на каждом из них по очереди, чтобы отобразить текст в нижней части окна Properties, описывающий, что именно делает данное свойство.
Теперь, когда диалог готов, сохраните его и приступайте к созданию класса для этого диалога.
Properties
-^ X
ILT SCALEUSI (L fctbox Control) UBEditor
Notify
TrueA
Right Align Te| contr ol Events|8
Right To Left Reading Or False
Static Edge
False
Transparent
False
Vertical Scrollbar
В Behavior
True
Accept Files
False
Disabled
False
Has Strings
False
Help ID
False
Multicolumn
False
No Data
False
Owner Draw
No
Selection
Single
Sort
Falsev
Use Tabstops
False
Visible
True
Want Key Input
В Mise
False
^lELIST ustb
Group
False
ID
IDC_SCALELIST
Tabstop
True
1 V
Sort
Automatically sorts strings added to the list box.
Рис. 16.18. Доступные свойства окна списка
Создание класса диалога
Щелкните на диалоге правой кнопкой мыши и выберите пункт Add Class (Добавить класс) из контекстного меню. Откроется диалог создания нового класса. Присвойте классу подходящее имя, вроде того, что использовали ранее — CScaleDialog, и выберите CDialog в качестве базового класса. Если после щелчка на кнопке Finish вы получите окно сообщения, говорящее о том, что ScaleDialog.cpp уже существует, значит, вы забыли удалить файлы . h и . срр. Вернитесь и сделайте это сейчас. После этого все должно заработать. Теперь остается добавить в класс public-переменную элемента управления по имени m_Scale, соответствующего идентификатору окна списка — IDC_SCALELIST. Ее типом должен быть int, а предельными значениями — О и 7. Не забудьте установить в качестве Category вариант Value, иначе вы не сможете ввести эти ограничения. Поскольку m_Scale создана как управляющая переменная, для нее реализуется DDX, и вы используете эту переменную для сохранения начинающегося с нуля индекса одного из восьми вхождений окна списка.
Окно списка должно быть инициализировано в обработчике On I nit Di a log () класса CScaleDialog, поэтому добавьте переопределение этой функции, используя окно свойств класса. Соответствующий код показан ниже.
BOOL CScaleDialog::OnInitDialog() {
CDialog::OnInitDialog();
CListBox* pListBox = static_cast<CListBox*>(GetDlgItem(IDC_SCALELIST)); pListBox->AddString(_T("Scale 1"));
pListBox->AddString(_T("Scale 2 "));
pListBox->AddString(_T("Scale 3"));
pListBox->AddStiring ( T ( " Scale 4 ")) ;
pListBox->AddString(_Т("Scale 5")) ;
pListBox->AddString(_Т("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* — указателю на класс МЕС, определяющий окно списка.
Используя указатель на объект CListBox диалога, вы затем многократно вызываете функцию-член AddString () для добавления строк, определяющих список значений масштаба. Они появляются в окне списка в том порядке, в котором вы вводите их, поэтому диалог отображается так, как показано на рис. 16.19.
Рис. 16.19. Диалог с окном списка выбора масштаба
Каждое вхождение списка ассоциировано со значением начинающегося с нуля значения индекса, которое автоматически сохраняется в члене m_Scale класса CScaleDialog через механизм DDX. Таким образом, если вы выберете третий элемент в списке, в m_Scale будет установлено значение 2.
Отображение диалога
Диалог отображается обработчиком OnViewScale () , который вы добавили к CSketcherView в предыдущей версии Sketcher. Вам нужно внести лишь небольшие исправления, чтобы иметь дело с новым диалогом, использующим окно списка. Необходимый для этого код показан ниже.
void CSketcherView: :OnViewScale ()
{
CScaleDialog aDlg;// Создать объект диалога
aDlg.m_Scale = m_Scale; // Передать масштаб представления диалогу if (aDlg. DoModal () == IDOK)
m_Scale = 14- aDlg.m_Scale; // Получить новый масштаб
/ / Получить фрейм-обертку данного представления
CChildFrame* childFrame = static_cast<CChildFrame*>(GetParentFrame());
// Собрать строку сообщения
CString StatusMsg("View Scale:”);
StatusMsg += static_cast<char>(’1 * + m_Scale - 1) ;
// Установить панель состояния
childFrame->m_StatusBar.GetStatusBarCtrl().SetText(StatusMsg, 0, 0);
ResetScrollSizes (); //Уточнить прокрутку в соответствии с новым масштабом InvalidateRect(0); //Пометить все окно как недействительное
}
}
Поскольку значения индекса для выбранного из списка вхождения начинаются с нуля, вам нужно просто прибавить к нему единицу, чтобы получить реальное значение масштаба, которое нужно сохранить в представлении. Код для отображения этого значения в панели состояния представления остается таким же, как раньше. Остальная часть кода для обработки факторов масштабирования уже готова и не требует изменений. После добавления директивы #include для ScaleDialog.h можно заново собрать и выполнить новую версию Sketcher, чтобы увидеть окно списка в действии.
Использование элемента
управления — поля редактирования
Поле редактирования может служить для добавления аннотаций в Sketcher. Вам понадобится новый тип элемента — CText, соответствующий текстовой строке, а также дополнительный пункт меню для установки режима TEXT (Текст) для создания элементов.
Поскольку текстовому элементу нужна только одна точка ссылки, вы можете создать его в обработчике OnLButtonDown () класса представления. Вам также понадобится новый пункт в меню Element для установки режима TEXT. Эта возможность добавляется в Sketcher в описанной ниже последовательности.
Создать ресурс окна диалога и ассоциированный с ним класс.
Добавить новый пункт в меню.
Добавить код открытия диалога для создания элемента.
Добавить поддержку класса CText.
Создание ресурса поля редактирования
Находясь в Resource View, создайте новый диалог щелчком правой кнопкой мыши на папке Dialog и выбором Insert Dialog (Вставить диалог) из контекстного меню. Измените идентификатор (ID) нового диалога на IDD TEXT DLG, а его заголовок (Caption) — на Enter Text (Введите текст).
Чтобы добавить поле редактирования, выберите соответствующую пиктограмму из палитры списка элементов управления, затем щелкните в позиции внутри диалога, куда хотите поместить его. Вы можете настроить размер поля редактирования, перетаскивая его границы, а позицию — перетаскивая его целиком. Отобразите свойства поля редактирования, щелкнув на нем правой кнопкой мыши и выбрав из контекстного меню пункт Properties. Для начала измените ID на IDC_EDITTEXT, как показано на рис. 16.20.
Некоторые из свойств этого элемента управления интересуют в первую очередь. Во-первых, выберите свойство Multiline (Многострочное). Установка его значения в True создает многострочное поле редактирования, в котором вводимый вами текст может распространяться на более чем одну строку. Это позволяет вводить длинные строки текста, которые остаются видимыми целиком в пределах поля редактирования.
Свойство Align text (Выравнивать текст) определяет то, как текст будет позиционироваться в пределах многострочного поля редактирования. Здесь подойдет значение Left (Влево), поскольку вы все равно будете отображать текст как одну строку, тем не менее, есть также выбор установить Center (По центру) или Right (Вправо).
Если вы хотите изменить значение свойства Want return (Необходим возврат каретки) в True, то нажатие клавиши <Enter> в процессе ввода текста в элементе управления вставит символ возврата каретки в текстовую строку. Это позволит вам самостоятельно разбивать текст для отображения во множестве строк. Если вам не нужен этот эффект, оставьте в качестве значения этого свойства False. В этом состоянии нажатие <Enter> даст эффект выбора элемента управления по умолчанию (которым является кнопка ОК), поэтому нажатие <Enter> закроет диалог.
Если вы установите значение свойства Auto HScroll (Автоматическая горизонтальная прокрутка) в False, то при достижении правой границы во время ввода текста автоматический переход на новую строку не произойдет. Однако это нужно лишь для видимости текста в пределах поля редактирования — оно никак не отображается на содержимом строки. Вы можете также установить в True свойство Auto VScroll (Автоматическая вертикальная прокрутка), чтобы позволить тексту продолжаться за пределы количества строк, видимых в элементе управления.
Properties
▼ ^ X
ILrCEDITTEXT (Edit Control) lEdBoxEditor
@2 |yjiy ^
Name1
IDC_EU1I 1ЕЛ1 Edit |Д
Accept Files
False
Align Text
Left
Auto HScroll
False
Auto VScroll
Truev
Border
True
Client Edge
False
Disabled
False
Group
False
Help ID
False
Horizontal Scroll
False
ID
IDC_EDTTTExT
Loft Scrollbar
False
Lowercase
False
Modal Frame
False
Multiline
True
No Hide Selection
False
Number
False
OEM Convert
False
Read Only
False
Right Align Text
False
Right To Left Reading Or False
Static Edge
False
Tabstoo_
Truev
Auto VScroll
Automatically scrolls text up when the user presses
ENTER on the last line.
Рис. 16.20. Установка ID в IDC_EDITTEXT
Завершив настройку свойств поля редактирования, закройте окно Properties. Убедитесь, что поле редактирование является первым в порядке обхода по клавише табуляции, выбрав пункт меню Format^Tab Order (Формат^Порядок обхода по клавише табуляции) либо нажав <Ctrl+D>. Вы можете затем протестировать диалог, выбрав пункт меню Test Dialog (Протестировать диалог) либо нажав <Ctrl+T>. Диалог показан на рис. 16.21.
Рис. 16.21. Диалог Enter Text (Введите текст)
В тестовом режиме можно даже вводить текст в диалоге, чтобы посмотреть, как это работает. Щелкните на кнопке ОК или Cancel, чтобы закрыть диалог.
Создание класса диалога
После сохранения ресурса диалога вы можете создать подходящий класс диалога, соответствующий этому ресурсу, который можно назвать CTextDialog. Щелкните правой кнопкой мыши на диалоге в Resource View и выберите Add Class (добавить класс) из контекстного меню. Базовым классом должен быть С Dia log. Затем вы можете добавить переменную элемента управления, щелкнув правой кнопкой в Class View и выбрав Add^Add Variable (Добавить1^ Добавить переменную) из контекстного меню. Выберите IDC_EDITTEXT в качестве идентификатора элемента управления и Value — в качестве категории. Назовите новую переменную m_TextString и оставьте ее типом CString — мы рассмотрим этот класс после того, как завершим с классом диалога. Можно также специфицировать максимальную длину в поле редактирования Max chars: (Максимальное количество символов), как показано на рис. 16.22.
Рис. 16.22. Создание новой переменной m_TextString в классе CTextDialog
Длина в 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)
// делать что-то...
<
Проверяет, меньше ли одна строка, чем другая.
Проверяет, меньше или равна одна строка другой.
Проверяет, больше ли одна строка, чем другая.
>=
Проверяет, больше или равна одна строка другой.
Переменные StrlnStr2B приведенной выше таблице являются объектами CString. Объекты CString автоматически растут по мере необходимости, например, когда вы добавляете дополнительную строку в конец существующего объекта. Рассмотрим небольшой фрагмент кода.
CString Str = "Строка начинается здесь... ";
Str += "а продолжается здесь.";
Первый оператор объявляет и инициализирует объект Str. Второй оператор добавляет дополнительную строку к Str, поэтому длина Str автоматически увеличивается.
Обычно, насколько это возможно, лучше избегать создания объектов CString в куче. Управление памятью, необходимое для их роста, означает, что операция будет медленной.
Добавление пункта меню Text
Добавление нового пункта меню теперь должно быть легким. Нужно только двойным щелчком в Resource View открыть ресурс меню с идентификатором, равным IDR_SketcherTYPE, и добавить в меню Elements новый пункт — Text (Текст). Идентификатором по умолчанию будет ID_ELEMENT_TEXT, который появится в окне Properties; его можно не изменять. Вы можете добавить сообщение для отображения в панели состояния, соответствующее пункту меню, и поскольку вы также захотите добавить дополнительную кнопку в панель инструментов, соответствующую этому новому пункту меню, можете добавить также всплывающую подсказку для этой кнопки в конец строки сообщения, используя символ \п в качестве разделителя.
Не забудьте о контекстном меню. Вы можете скопировать пункт меню из IDR SketcherTYPE. Щелкните правой кнопкой мыши на пункте меню Text и выберите пункт Сору (Копировать) из контекстного меню. Откройте меню IDR_CURSOR_MENY, разверните меню no elements, щелкните правой кнопкой мыши на пустом элементе внизу и выберите пункт Paste (Вставить) из контекстного меню. Все, что останется сделать — это перетащить элемент в соответствующую позицию над разделителем и сохранить ресурсный файл.
Добавьте кнопку в панель инструментов IDR_MA IN FRAME и установите ее идентификатор (ID) таким же, как у пункта меню, то есть ID ELEMENT TEXT. Вы можете перетащить новую кнопку так, чтобы позиционировать ее в конце блока, определяющего прочие типы элементов. Сохранив ресурс, добавьте обработчик событий для нового пункта меню.
Находясь в панели Class View, щелкните правой кнопкой мыши на CSketcherDoc и отобразите его окно свойств Properties. Добавьте обработчик COMMAND для ID ELEMENT TEXT и поместите в него следующий код:
void CSketcherDoc: :OnElementText()
{
m_Element = TEXT;
}
Для установки типа элемента TEXT в документе необходима только одна строка кода.
Также потребуется добавить функцию для отметки пункта меню текущего режима, поэтому добавьте обработчик UPDATE COMMAND UI, соответствующий ID_ELEMENT_ TEXT, и реализуйте его код следующим образом:
void CSketcherDoc::OnUpdateElementText(CCmdUI* pCmdUI) {
// Установить метку, если текущий элемент - TEXT pCmdUI->SetCheck(m_Element = TEXT) ;
}
Это работает точно так же, как и другие пункты всплывающего меню Element.
Вы также должны добавить следующую строку в заголовочный файл OurConstants .h:
const unsigned int TEXT = 105U;
Можете добавить этот оператор в заголовочный файл, в конец списка определений других типов элементов. Следующий шаг — определение класса С Text для объекта типа TEXT.
Определение текстового элемента
Вы можете унаследовать класс 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. срр.
Конструктор CText
Конструктор объекта CText необходим для инициализации класса и членов его базового класса:
CText::CText(CPoint Start, CPoint End, CString aString, COLORREF aColor) {
m_Pen = 1;II Установить ширину пера
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; // Переместить прямоугольник
Все, что потребуется сделать — изменить точку, определяющую позицию строки, а также член данных, определяющий описывающий прямоугольник, на расстояние, указанное параметром 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); И Перерисовать все представления } else
{
CSketcherDoc* pDoc = GetDocument () ; / / Получить указатель на документ if (pDoc->GetElementType () = TEXT) {
CTextDialog aDlg;
if (aDlg.DoModal () = IDOK) {
// Выход по щелчку на 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 ()) ; H Добавить элемент к документу
pDoc->AddElement(pTextElement);
/ / Обновить все представления
pDoc->UpdateAllViews(0,0,pTextElement);
} return; } m^FirstPoint = point; // Запомнить текущую позицию SetCapture();// Перехватить последующие сообщения мыши
}
Добавленный код выделен полужирным. Он создает объект CTextDialog, а затем открывает диалог с использованием функции DoModal (). Затем члену m TextString объекта aDlg автоматически присваивается строка, введенная в поле редактирования, так что вы можете использовать этот член данных для передачи введенной стро-
ки обратно в конструктор CText, если для закрытия диалога был выполнен щелчок на кнопке ОК. Цвет и ширина пера получаются из документа с помощью функций- членов GetElementColor () и GetPenWidth (), которые вы уже применяли ранее. Положение текста определяется значением point, хранящим текущую позицию курсора, переданную обработчику.
Вам также потребуется вычислить противоположный угол прямоугольника, ограничивающего текст. Поскольку размер прямоугольника для блока текста зависит от шрифта, используемого контекстом устройства, вы используете функцию GetTextExtent () объекта CClientDC по имени aDC, чтобы инициализировать объект CSize по имени TextExtent шириной и высотой текстовой строки в логических координатах.
Подобное вычисление прямоугольника, ограничивающего текст, не совсем “честно” и может вызвать проблемы при сохранении документа в файле, поскольку существует возможность чтения документа в среде, где шрифт контекста устройства по умолчанию окажется больше, чем был на момент вычисления прямоугольника. Это не должно происходить часто, поэтому мы не станем пока об этом беспокоиться, но в качестве подсказки отметим, что вы можете применять объект класса CFont в определении CText для указания конкретного используемого шрифта. Затем вы сможете использовать характеристики шрифта для вычисления ограничивающего прямоугольника для текстовой строки.
Вы также можете применять CPoint для изменения размера шрифта, чтобы текст также изменялся в размере при увеличении масштаба; однако вы также должны предусмотреть способ вычисления ограничивающего прямоугольника на основе размера текущего шрифта, который будет меняться вместе с масштабом представления.
Объект CText создается в куче, поскольку список элементов документа поддерживает только указатели на элементы. Вы добавляете новый элемент в документ вызовом функции AddElement () — члена CSketcherDoc, с указателем на новый текстовый элемент в качестве аргумента. И, наконец, вызывается UpdateAllViews () с первым аргументом равным 0, что говорит о том, что все представления должны быть обновлены.
Для успешной компиляции программы вы должны добавить директиву #include для TextDialog.h в файл SketcherView.срр. Теперь вы сможете создавать аннотированные эскизы, используя множество масштабируемых и прокручиваемых представлений, как показано на рис. 16.23.
Резюме
В этой главе вы познакомились с тремя различными диалогами, использующими разнообразные элементы управления. Хотя вы не создавали диалогов, включающих много разных элементов управления сразу, механизм их обработки будет таким же, как было показано здесь, поскольку каждым элементом управления вы можете оперировать независимо от других.
Ниже перечислены наиболее важные моменты, с которыми вы ознакомились в этой главе.
Диалог включает два компонента: ресурс, определяющий диалоговое окно с его элементами управления, и класс, используемый для отображения и управления диалогом.
Информация может быть извлечена из элементов управления диалога с помощью механизма DDX. Данные могут верифицироваться с помощью механизма
DDV. Для применения DDX/DDV нужно использовать опцию Control Variable (Переменная элемента управления) в интерактивном мастере Add Member Variable Wizard, чтобы определить переменные в классе диалога, ассоциированные с его элементами управления.
Модальный диалог удерживает фокус в приложении до тех пор, пока окно диалога не будет закрыто. Пока отображается модальный диалог, все прочие окна приложения не активны.
Немодальный диалог позволяет переключать фокус с диалогового окна на другие окна приложения и обратно. Немодальный диалог при необходимости может оставаться на экране до тех пор, пока выполняется приложение.
Общие элементы управления (Common Controls) — это набор стандартных элементов управления Windows, которые поддерживаются MFC и средствами редактирования ресурсов Developer Studio.
Хотя элементы управления обычно ассоциированы с диалогом, тем не менее, вы можете добавлять их в любые окна.
Рис. 16.23. Версия программы Sketcher с возможностью ввода текста
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Реализуйте диалог масштабирования с использованием переключателей.
Реализуйте диалог установки ширины пера с использованием окна списка.
, 3. Реализуйте диалог установки ширины пера как комбинированный список с выпадающим списком типов, выбранным на вкладке Styles (Стили) окна свойств (этот выпадающий список позволяет выбирать из фиксированного перечня, но не помещать альтернативные вхождения в список).
17
Сохранение и печатьдокументов
С тем, что вы до сих пор реализовали в программе Sketcher, можно создавать достаточно развитые документы с представлениями в разных масштабах, но информация этих документов временна, поскольку нет средств их сохранения. В этой главе речь пойдет о том, как можно сохранять документы на диске. Кроме того, вы узнаете, как выводить документ на принтер.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
Что такое сериализация и как она работает.
Как сделать объекты класса сериализуемыми.
Роль объекта CArchive в сериализации.
Как реализовать сериализацию в ваших собственных классах.
Как реализовать сериализацию в приложении Sketcher.
Как работает печать в MFC.
Как можно использовать функции класса представления для подцержки печати.
Как реализовать многостраничную печать в приложении Sketcher.
Что такое сериализация?
Документ основанной на MFC программы — не простая сущность, это объект класса, который может оказаться очень сложным. Обычно он содержит широкое разнообразие объектов, каждый из которых может содержать другие объекты, а те, в свою очередь, могут содержать в себе еще объекты... и такая структура может распространяться на множество уровней.
Вам нужно иметь возможность сохранять документ в файле, но запись объекта класса в файл представляет собой некоторую проблему, поскольку это не то же самое, что базовый элемент данных вроде целого числа или символьной строки. Базовый элемент данных состоит из известного количества байт, поэтому чтобы записать его в файл, необходимо записать это определенное количество байт. И наоборот, если вы знаете, что в файл было записано значение типа int, то чтобы получить его обратно, необходимо просто прочесть соответствующее количество байт.
С записью объекта дела обстоят иначе. Даже если вы запишете все данные-члены объекта, этого недостаточно для того, чтобы получить обратно исходный объект. Объекты класса содержат функции-члены наряду с данными-членами, и все члены — как данные, так и функции — имеют спецификаторы доступа; таким образом, чтобы сохранить объекты во внешнем файле, информация, записываемая в этот файл, должна содержать полные спецификации всех участвующих структур классов. Процесс чтения также должен быть достаточно интеллектуальным, чтобы полностью синтезировать исходные объекты на основе данных файла. Библиотека MFC поддерживает механизм, называемый сериализацией, который помогает реализовать ввод с диска и вывод на диск объектов ваших классов с минимальными затратами времени и усилий.
Основная идея, лежащая в основе сериализации, заключается в том, что любой сериализуемый класс самостоятельно заботится о сохранении и восстановлении себя. Это значит, что для того, чтобы ваши классы были сериализуемыми — в случае приложения Sketcher речь идет о классе класс CElement и классах фигур, унаследованных от него — они должны уметь записывать себя в файл. Это подразумевает, что для того, чтобы класс был сериализуемым, все типы данных, используемые в его объявлении, также должны быть сериализуемыми.
Сериализация документа
Все это звучит довольно замысловато, но основная возможность сериализации вашего документа изначально встраивается в приложение мастером Application Wizard. Обработчики для пунктов меню File^Save (Файл1^ Сохранить), File^Open (Файл^Открыть) предполагают, что будет реализована сериализация ваших документов, и уже содержат код, необходимый для ее поддержки. Взгляните на части определения и реализации CSketcherDoc, относящиеся к созданию документов с использованием сериализации.
Сериализация в определении класса документа
Код для определения CSketcherDoc, обеспечивающий сериализацию объекта документа, в следующем фрагменте выделен полужирным.
class CSketcherDoc : public CDocument
{
protected: // Создается только сериализацией
CSketcherDoc() ;
DECLAREJDYNCREATE(CSketcherDoc)
// Остальная часть класса...
// Переопределения
public:
virtual BOOL OnNewDocument();
virtual void Serialize (CArchivefi ar) ;
H Остальная часть класса.. .
Здесь представлены три вещи, имеющие отношение к сериализации объекта документа.
Макрос DECLARE_DYNCREATE ().
Функция-член Serialize ().
Конструктор класса по умолчанию.
DECLARE_DYNCREATE () — это макрос, позволяющий динамически создавать объекты класса CSketcherDoc каркасом приложения во время сериализационного процесса ввода. В реализации класса его дополняет другой макрос — IMPLEMENT_DYNCREATE (). Этот макрос применяется только к классам, унаследованным от CObject, но как вы вскоре увидите, это не единственная пара макросов, которые могут использоваться в этом контексте. Для любого класса, который вы хотите сериализовать, CObject должен быть прямым или непрямым базовым классом, поскольку он добавляет функциональность, которая позволяет работать сериализации. Вот почему класс СЕ lenient унаследован от CObject. Почти все классы MFC унаследованы от CObject, и как таковые, являются сериализуемыми.
В иерархической диаграмме в справочнике Microsoft Foundation Class Reference для Visual C++ 2005 показаны me классы, которые не унаследованы от CObject. Обратите внимание, что С Ar chive входят в этот список.
Определение класса также включает объявление виртуальной функции Serialize (). Каждый сериализуемый класс должен содержать эту функцию. Она вызывается для выполнения операций сериализации — как ввода, так и вывода — с членами данных класса. Объект типа CArchive, передаваемый в качестве аргумента этой функции, определяет, какая операция выполняется — ввода или вывода. Вы узнаете об этом подробнее, когда будете рассматривать реализацию сериализации класса документа.
Обратите внимание, что класс явно определяет конструктор по умолчанию. Это также существенно для правильной работы сериализации, поскольку конструктор по умолчанию используется каркасом для синтеза объекта при чтении из файла, и синтезируемый объект затем заполняется данными из файла, устанавливая значения дан- ных-членов объекта.
Сериализация в реализации класса документа
Есть две части файла, содержащие реализацию CSketcherDoc, которые касаются сериализации. Первая — это макрос IMPLEMENT_DYNCRETE (), дополняющий макрос DECLARE_DYNCREATE ():
// SketcherDoc.cpp : реализация класса CSketcherDoc //
#include "stdafx.h"
#include "Sketcher.h"
#include "PenDialog.h”
#include#includeSketcherDoc.hSketcherDoc.h
#include
#include
SketcherDoc.h
SketcherDoc.h
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// CSketcherDoc
IMPLEMENT-DYNCREATE (CSketcherDoc, CDocument) // Карты сообщений и остальная часть файла...
Все, что делает макрос — это определение класса С Do cume nt в качестве базового для CSketcherDoc. Это требуется для правильного динамического создания объекта CSketcherDoc, включая члены, унаследованные от базового класса.
Функция Serialize ()
Реализация класса также включает определение функции Serialize ():
void CSketcherDoc:: Serialize (CArchive& ar) {
if (ar.IsStoring()) (
// TODO: добавить сюда код сохранения
1 else (
// TODO: добавить сюда код загрузки 1
}
Функция сериализует данные-члены класса. Аргумент, передаваемый функции — это ссылка на объект аг класса 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 объектными и примитивными типами.
Таблица 17.1. Типы, с которыми работают операции вставки и извлечения, перегруженные в классе CArchive
Тип
Определение
bool
Булевские значения — true или false.
float
Стандартный тип с плавающей точкой одинарной точности.
double
Стандартный тип с плавающей точкой двойной точности.
BYTE
8-битовое целое без знака.
char
8-битовый символ.
wchar_t
16-битовый символ.
intИ short
16-битовое целое со знаком.
LONG И long
32-битовое целое со знаком.
LONGLONG
64-битовое целое со знаком.
ULONGLONG
64-битовое целое без знака.
WORD И unsigned int
16-битовое целое без знака.
DWORD и unsigned int
CObject *
32-битовое целое без знака.
Указатель на CObject.
CString
Объект cstring, определяющий строку.
SIZE ИCSize
Объект, определяющий размер, как пару сх, су.
POINT ИCPoint
Объект, определяющий точку, как пару сх, су.
RECT ИCRect
Объект, определяющий прямоугольник по его левому верхнему и правому нижнему углам.
CObject *
Указатель на CObject.
Для базовых типов данных в ваших объектах вы используете операции вставки и извлечения для сериализации данных. Чтобы прочесть или записать объект сериализуемого класса, который вы наследуете от CObject, вы можете либо вызвать функцию Serialize () для объекта, либо воспользоваться операциями извлечения или вставки. Какой бы путь вы ни выбрали, следует обеспечить согласованность между вводом и выводом, поэтому вы не должны выводить объект с использованием операции вставки, а затем читать его обратно функцией Serialize (), или наоборот.
Когда вам не известен тип объекта при чтении, как, например, в случае указателей в списке фигур документа, вы должны использовать только функцию Serialize(). Это вводит в действие механизм виртуальных функций, поэтому каждый раз вызывается функция Serialize (), соответствующая типу объекта, с которым она вызвана, что определяется во время выполнения.
Объект CArchive конструируется либо для сохранения объектов, либо для их извлечения. Функция IsStoring () класса CArchive возвращает TRUE, если объект предназначен для вывода, и FALSE — если для ввода. Вы видели это в операторе if в тексте функции Serialize () класса CSketcherDoc.
Существует множество других функций-членов класса CArchive, которые касаются детальных механизмов процесса сериализации, но обычно для применения сериализации в своих программах знать их не обязательно.
Функциональность классов, базирующихся на CObject
Есть три уровня функциональности, доступных вашим классам, если они наследуются от класса MFC CObject. Конкретный уровень, который вы получаете в вашем классе, определяется тем, какой из трех макросов вы используете в определении вашего класса (табл. 17.2).
Таблица 17.2. Макросы, определяющие уровень функциональности
МакросФункциональность
DECLARE,
DYNAMIC()
Поддержка информации о классе во время выполнения.
DECLARE-
_DYNCREATE()
Поддержка информации о классе во время выполнения и динамическое создание объектов.
DECLARE-
_SERIAL ()
Поддержка информации о классе во время выполнения и динамическое создание объектов и сериализации объектов.
Каждый из этих макросов требует дополняющего макроса, именованного с префиксом IMPLEMENT— вместо DECLARE—, помещенного в файл, содержащий реализацию класса. Как следует из табл. 17.2, эти макросы обеспечивает нарастающий объем функциональности, поэтому сосредоточим внимание на третьем из них — DECLARE— SERIAL (), поскольку он обеспечивает все то, что предшествующие ему, и даже более. Этот макрос следует применять, чтобы обеспечить возможность сериализации ваших собственных классов. Он требует добавления макроса IMPLEMENT—SERIAL () к файлу, содержащему реализацию класса.
Возможно, вас удивит — почему класс документа использует DECLARE—DYNCREATE (), а не DECLARE—SERIAL (). Макрос DECLARE—DYNCREATE () обеспечивает возможность сериализации класса плюс динамическое создание объектов класса, таким образом, заключая в себе эффект от DECLARE—DYNCREATE (). Ваш класс документа не нуждается в сериализации, поскольку каркас должен только синтезировать объект документа и затем восстанавливать значения данных-членов; однако данные-члены документа должны быть сериализуемыми, поскольку этот процесс применяется для их сохранения и извлечения.
Макрос, добавляющий сериализацию к классу
С макросом DECLARE—SERIAL () в определении вашего основанного на CObject класса, вы получаете доступ к поддержке сериализации, обеспечиваемой CObject. Это включает специальные операции new и delete, отвечающие за обнаружение утечек памяти в отладочном режиме. Вам не нужно делать ничего особенного, чтобы использовать это, поскольку все оно работает автоматически.
Макрос требует спецификации имени класса в качестве аргумента, поэтому для сериализации класса СЕ lenient понадобится добавить следующую строку к определению класса:
DECLARE_SERIAL(CElement)
Здесь точка с запятой не требуется, поскольку это - вызов макрос, а не оператор C++.
Неважно, куда именно вы поместите макрос в пределах определения класса, но если вы всегда будете помещать его в первой строке, то всегда сможете убедиться в его присутствии, даже когда определение класса состоит из множества строк кода.
Макрос IMPLEMENT_SERIAL (), который вы помещаете в файл реализации класса, принимает три аргумента. Первый аргумент — имя класса, второй — имя непосредственного базового класса, а третий аргумент — беззнаковое 32-битное целое, идентифицирующее номер схемы, или номер версии вашей программы. Этот номер схемы позволяет защитить процесс сериализации от проблем, которые могут возникнуть, если вы пишете объекты в одной версии программы, а читаете в другой, в которой классы могут отличаться.
Например, вы можете добавить в реализацию класса СЕ 1 ement такую строку кода:
IMPLEMENT—SERIAL(CElement, CObject, 1)
Если вы последовательно модифицируете определение класса, то должны при этом изменять номер схемы на что-то другое, например, на 2. Если программа попытается прочесть данные, которые были записаны с номером схемы, отличным от номера текущей активной программы, возбуждается исключение. Лучше всего поместить этот макрос в первой строке, следующей за директивами #include и всеми начальными комментариями в файле .срр.
Когда CObject является непрямым базовым классом, как, например, в случае CLine, для корректной работы сериализации каждый класс в иерархии должен иметь добавленный макрос сериализации в классе верхнего уровня. Чтобы работала сериализация CLine, макрос также должен быть добавлен к CElement.
Как работает сериализация
Общий процесс сериализации документа в упрощенном виде показан на рис. 17.1.
Вывод документа с применением сериализации
Рис. 17.1. Общий процесс сериализации документа
Функция Serialize () в объекте документа вызывает функцию Serialize () (или использует перегруженную операцию вставки) для каждого из членов данных. Когда член является объектом класса, функция Serialize () для этого объекта сериализи- рует каждый из членов данных по очереди, пока в конечном счете все базовые типы данных будут записаны в файл. Поскольку большинство классов MFC в конечном итоге унаследованы от CObject, они содержат поддержку сериализации, так что вы можете почти всегда сериализовать объекты классов MFC.
Когда вы наследуете класс CObject через множество уровней, функция Serialize () вашего класса и объект документа приложения в каждом случае должна вызывать член Serialize () его непосредственного предка. Обратите внимание, что сериализация не поддерживает множественное наследование, поэтому должен существовать только один базовый класс для каждого класса, определенного в иерархии.
Как реализовать сериализацию класса
На основе предшествующей дискуссии можно подытожить шаги, которые необходимо предпринять для добавления в класс возможностей сериализации.
Убедитесь, что класс унаследован прямо или непрямо от CObject.
Добавьте макрос DECLARE_SERIAL () в определение класса (и в непосредственный базовый класс, если это не CObject).
Реализуйте в своем классе функцию Serialize ().
Теперь посмотрим, как можно реализовать сериализацию документов в программе Sketcher.
Применение сериализации
Чтобы реализовать сериализацию в приложении Sketcher, вы должны реализовать функцию Serialize () в CSketcherDoc, так чтобы она обрабатывала все данные- члены класса. Вам понадобится добавить сериализацию к каждому классу, который специфицирует объекты, включаемые в документ. Прежде чем вы начнете добавлять сериализацию к классам приложения, нужно будет внести некоторые небольшие изменения в программу, чтобы фиксировать факт внесения изменений в документ- эскиз. Это не является абсолютно необходимым, но весьма желательно, поскольку позволяет программе предотвращать закрытие документа без сохранения изменений.
Запись изменений в документе
У нас уже есть механизм для фиксации факта изменения документа, он использует унаследованный член CSketcherDoc — SetModif iedFlag (). Вызывая эту функцию согласованно при каждом изменении документа, вы фиксируете факт такого изменения в члене данных объекта класса документа. Это вызывает автоматический запрос при попытке выхода из приложения без сохранения модифицированного документа. Аргумент функции SetModif iedFlag () имеет тип BOOL и значение по умолчанию TRUE. Если вы имеете возможность отметить, что документ не был изменен, то можете вызвать эту функцию с аргументом FALSE, хотя такая необходимость может возникнуть редко.
Существуют только три случая, когда вы изменяете объект документа.
При вызове члена 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() ;// Установить флаг модификации
}
// Остальная часть функции — как раньше.. .
Вы вызываете унаследованный член GetDocument () класса представления, чтобы получить доступ к указателю на объект документа, и затем используете этот указатель для вызова функции SetModif iedFlag (). Таким образом, покрываются все места, где документ может изменяться.
Если вы соберете и запустите проект Sketcher и затем модифицируете документ или станете добавлять к нему элементы, то перед выходом из программы получите приглашение на сохранение документа. Конечно, опция меню File^Save (Файл1^ Сохранить) пока ничего не делает, за исключением очистки флага модификации и сохранения пустого файла на диск. Вы должны реализовать сериализацию для правильной записи документа на диск, и это будет следующим шагом.
Сериализация документа
Первый шаг предусматривает реализацию функции 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())
// Сохранить текущий цвет, И текущий тип элемента, И текущую ширину пера //и текущий размер документа// Сохранить текущий цвет, И текущий тип элемента, И текущую ширину пера //и текущий размер документа// Восстановить текущий цвет, // текущий тип элемента, / / текущую ширину пера //и текущий размер документа// Восстановить текущий цвет, // текущий тип элемента, / / текущую ширину пера //и текущий размер документа(
// Сохранить текущий цвет, И текущий тип элемента, И текущую ширину пера //и текущий размер документа
// Сохранить текущий цвет, И текущий тип элемента, И текущую ширину пера //и текущий размер документа
// Восстановить текущий цвет, // текущий тип элемента, / / текущую ширину пера //и текущий размер документа
// Восстановить текущий цвет, // текущий тип элемента, / / текущую ширину пера //и текущий размер документа
ar « m_Color « m_Element « m_PenWidth « m_JDocSize;
}
else
{
ar » m_Color » m_Element » m_PenWidth » m_DocSize;
}
Для четырех из данных-членов вы просто используете операции извлечения и вставки, которые в классе CArchive перегружены. Это работает с членом m_Color, даже несмотря на то, что его типом является COLORREF, поскольку тип COLORREF — это тоже самое, что тип long. Вы не можете использовать операции извлечения и вставки для m_E lenient List, поскольку его тип не поддерживается операциями, но до тех пор, пока 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 Ш—Color;// Цвет элемента
CRect m_EnclosingRect;// Прямоугольник, описывающий элемент
int m_Pen;// Ширина пера
public: virtual -CElement (){}H Виртуальный деструктор
И Виртуальная операция рисования virtual void Draw(CDC* pDC, CElement* pElement=O) {} virtual void Move(CSize& aSize) {} // Перемещение элемента CRect GetBoundRect () ;// Получить описывающий элемент прямоугольник
virtual void Serialize (CArchiveS ar) ; И Функция сериализации класса protected:
CElement (void) ;// Чтобы предотвратить вызов
};
Здесь добавлен макрос DECLARE_SERIAL (), а также объявление виртуальной функции Serialize().
У вас уже есть конструктор по умолчанию, созданный мастером Application Wizard. Вы изменяете его на protected, хотя его спецификатор доступа не имеет значения до тех пор, пока он не появляется явно в определении класса. Он может быть public, protected или private, и сериализация все равно будет работать. Однако если вы забудете включить конструктор по умолчанию, то получите сообщение об ошибке при компиляции макроса IMPLEMENT_SERIAL ().
Макрос 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)
IMPLEMENT^SERIAL(CCircle, CElement, VERSION—NUMBER)
IMPLEMENT-SERIAL(CCurve, CElement, VERSION—NUMBER)
IMPLEMENT-SERIAL(CText, CElement, VERSION—NUMBER)
Функции Serialize () классов фигур
Теперь вы можете реализовать функцию-член Serialize () для каждого из классов фигур. Начните с класса CElement:
void. CElement::Serialize(CArchiveS ar)
CObject::Serialize(ar) ; if (ar.IsStoring()) {ar « m_Color« m_EnclosingRect« m_Pen;} else (ar » m_Color» m_EnclosingRect » m_Pen;}CObject::Serialize(ar) ; if (ar.IsStoring()) {ar « m_Color« m_EnclosingRect« m_Pen;} else (ar » m_Color» m_EnclosingRect » m_Pen;}// Вызвать функцию базового класса// Вызвать функцию базового класса// Сохранить цвет,// описывающий прямоугольник, И и ширину пера// Сохранить цвет,// описывающий прямоугольник, И и ширину пера// Извлечь цвет,// описывающий прямоугольник, Н и ширину пера// Извлечь цвет,// описывающий прямоугольник, Н и ширину пера
CObject::Serialize(ar) ; if (ar.IsStoring()) {
ar « m_Color
« m_EnclosingRect
« m_Pen;
} else (
ar » m_Color
» m_EnclosingRect » m_Pen;
}
CObject::Serialize(ar) ; if (ar.IsStoring()) {
ar « m_Color
« m_EnclosingRect
« m_Pen;
} else (
ar » m_Color
» m_EnclosingRect » m_Pen;
}
// Вызвать функцию базового класса
// Вызвать функцию базового класса
// Сохранить цвет,
// описывающий прямоугольник, И и ширину пера
// Сохранить цвет,
// описывающий прямоугольник, И и ширину пера
// Извлечь цвет,
// описывающий прямоугольник, Н и ширину пера
// Извлечь цвет,
// описывающий прямоугольник, Н и ширину пера
Эта функция имеет ту же форму, что и ранее добавленная в класс CSketcherDoc. Все данные-члены, определенные в CElement, поддерживаются перегруженными операциями вставки и извлечения, и потому все необходимое делается этими one-
рациями. Обратите внимание, что вы должны вызвать член Serialize () для класса CObject, чтобы гарантировать сериализацию унаследованных данных-членов.
Для класса CLine эту функцию можно закодировать следующим образом:
void CLine::Serialize(CArchive& ar) {
CElement::Serialize(ar);// Вызов функции базового класса
if (ar.IsStoring())
ar « m_StartPoint « m EndPoint;ar « m_StartPoint « m EndPoint;// Сохранить начальную точку линии // и ее конечную точку
ar « m_StartPoint « m EndPoint;
ar « m_StartPoint « m EndPoint;
else
ar » m_StartPoint» m EndPoint;ar » m_StartPoint» m EndPoint;// Извлечь начальную точку линии // и ее конечную точку
ar » m_StartPoint» m EndPoint;
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(CArchiveS ar) {
CElement::Serialize(ar); II Вызов функции базового класса
}
Для класса 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:
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. Автоматический запрос на сохранение модифицированного документа
Запрос работает благодаря добавленным вами вызовам SetModif iedFlag () везде, где вы обновляете документ. Если вы щелкнете на кнопке Yes (Да) на экране, показанном на рис. 17.2, то увидите диалоговое окно FiJe^Save As (Файл ^Сохранить как), показанное на рис. 17.3.
Это стандартное диалоговое окно для упомянутого пункта меню в Windows. Оно полностью функционально и поддерживается кодом, предоставленным каркасом. Имя файла для документа генерируется по назначенному ему при первом открытии документа, а расширение файла автоматически определяется как . ske. Теперь приложение обладает полной поддержкой файловых операций с документом. Просто, не правда ли?
Перемещение текста
Теперь самое время вернуться немного назад и решить проблему, возникшую в предыдущей главе. Вспомните, что когда вы пытались перемещать текстовый элемент, он оставлял за собой след до тех пор, пока текст не был позиционирован на новом месте в документе. Это вызвано тем, что мы полагались на рисование ROP в функции-члене представления MoveElement ():
void CSketcherView: :MoveElement (CClientDC& aDC, CPoint& point) {
CSize Distance = point - m_CursorPos; II Получить расстояние перемещения m_CursorPos = point; // Установить текущую точку как 1-ю для следующего раза
// Если есть выбранный элемент, переместить его if(m_pSelected) {
aDC.SetROP2(R2_NOTXORPEN);
m_pSelected->Draw(&aDC,m_pSelected); II Нарисовать элемент, чтобы I/ стереть его
m_pSelected->Move(Distance);// Переместить элемент
m_pSelected->Draw(&aDC,m_pSelected); // Нарисовать перемещенный элемент }
1
Как упоминалось ранее, установка режима рисования контекста устройства в R2 NOTXORPEN не удаляет след, оставляемый текстом при перемещении. Вы можете обойти это, используя метод пометки недействительных прямоугольников, которые затронуты перемещаемыми элементами, так чтобы они перерисовывали себя. Однако это может привести к некоторому нежелательному мерцанию при быстром перемещении элемента. Лучшее решение состоит в использовании метода пометки недействительными только текстовых элементов и исходного метода ROP для всех прочих элементов, но как узнать, к какому классу относится выбранный элемент? Это неожиданно просто: можно воспользоваться оператором if, как показано ниже:
if (m_pSelected->IsKindOf(RUNTIME_CLASS(CText)))
(
И Помещенный сюда код выполняется только в случае,
// если выбран элемент класса CText
1
Здесь применяется макрос RUNTIME_CLASS для получения указателя на объект типа CRuntimeClass, затем указатель this передается функции IsKindOf () — члену mjpSelected. Она возвращает ненулевой результат, если mjpSelected относится к классу CText, и ноль — в противоположном случае. Единственное условие — чтобы проверяемый класс был объявлен с использованием макроса DECLARE DYNCREATE или DECLARE__SERIAL, чем объясняется то, что решение проблемы откладывалось до этого момента.
Существует и другой способ определения типа класса, использующий средство, встроенное в ANSI/ISO C++. Операция type id () возвращает ссылку на объект типа type_inf о, который инкапсулирует указатель на имя типа объекта времени выполнения, либо выражение, которое помещается в скобках. Поскольку вы можете сравнивать объекты type_info, используя операцию == (или !=), всегда можно проверить, относится ли m_pSelected к типу CText:
if (typeid(m_pSelected) == typid(CText))
(
// Помещенный сюда код выполняется только в случае,
/ / если выбран элемент класса CText
1
Если вы хотите использовать операцию typeid (), то должны добавить директиву #include для заголовочного файла ISO/ANSI C++ <typeinfo>. Конечно, в этом случае не обязательно, чтобы типы были объявлены с макросами МЕС, используемыми в предыдущем методе, хотя вы должны использовать опцию компилятора /GR, которая включает информацию о типе времени выполнения.
Ниже показан окончательный код MoveElement () с использованием макроса МЕС RUNTIME_CLASS.
void CSketcherView: :MoveElement (CClientDC& aDC, CPoint& point) {
CSize Distance = point - m_CursorPos; // Получить расстояние перемещения m_CursorPos = point; // Установить текущую точку как 1-ю для следующего раза
// Если есть выбранный элемент, переместить его if(m_pSelected)
// Если элементом является текст, использовать этот метод...
if(m_pSelected->IsKindOf(RUNTIME_CLASS(CText))) {
CRect 01dRect=m_pSelected->GetBoundRect();// Получить старый
// граничный прямоугольник m_pSelected->Move(Distance);// Переместить элемент
CRect NewRect=m_pSelected->GetBoundRect();// Получить новый
// граничный прямоугольник
OldRect.UnionRect(&OldRect,SNewRect); //Комбинировать прямоугольники 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 для ctypeinf о> вSketcherView.срр.
Печать документа
Теперь обратимся к печати документа. У вас уже есть базовые возможности печати, реализованные в программе Sketcher, благодаря предусмотрительности мастера Application Wizard и каркасу. Пункты меню File^Print (Файл^Печать), File^Print Setup (Файл^Параметры страницы) и File^Print Preview (Файл=>Предварительный просмотр) уже работают. Выбор пункта File^ Print Preview отображает окно, показывающее текущий документ Sketcher на странице (рис. 17.4).
Все, что есть в текущем документе, помещается на один лист бумаги в текущем масштабе представления. Если размеры документа выходят за пределы бумажного листа, то часть документа, выходящая за границы листа, не печатается. Если вы щелкнете на кнопке Print (Печать), данная страница будет отправлена на ваш принтер.
В качестве базовой возможности, которую вы получаете бесплатно, это достаточно впечатляет, но не подходит для большинства случаев. Типичный документ нашей программы не будет помещаться на одну страницу, поэтому вы должны будете либо изменить масштаб, чтобы все уместилось, либо при необходимости печатать документ на нескольких страницах. Вы можете добавить собственный код обработки печати, чтобы расширить возможности, предоставленные каркасом, но сначала вы должны разобраться, как печать реализована в MFC.
Рис. 17.4. Окно предварительного просмотра документа
Процесс печати
Печать документа управляется текущим представлением. Этот процесс неизбежно несколько запутан, поскольку печать — по природе своей запутанный процесс, который потенциально вовлекает вас в реализацию собственных версий множества унаследованных функций вашего класса представления.
На рис. 17.5 показана логика процесса и участвующие в нем функции.
о S & V
Члены представления
— 1 ->
OnPreparePrintingO
Ци*
CDC::StartDoc()
<-3- раницы
<—5 —
<—7 —
«-8-
-2-*
OnBeginPrintingO
сл, пока остаются ст
CDC::StartPageO
CDC::EndPage()
CDC::EndDoc()
— 4—>
OnPrepareDCQ
-6->
OnPrintO
— 9 —>
OnEndPrintingO
• Вычисление количества страниц
• Вызов DoPreparePrintingO
• Выделение ресурсов GDI
• Смена начала координат области отображения
• Установка атрибутов DC
• Печать нижних и верхних колонтитулов
• Печать текущей страницы
• Возврат выделенных ресурсов GDI
На рис. 17.5 видно, как последовательность событий управляется каркасом, и как печать документа включает вызов пяти унаследованных методов вашего класса представления, которые вы должны переопределить. Функции-члены С DC, показанные в левой части диаграммы, взаимодействуют с драйвером устройства печати и вызываются каркасом автоматически.
Типичная роль каждой функции в текущем представлении в процессе операции печати специфицирована в примечаниях, расположенных рядом. Последовательность их вызовов указана числами на стрелках. На практике вам не обязательно реализовывать все эти функции, а только те, которые нужны для обеспечения ваших конкретных требований к печати. Обычно вам понадобится реализовать, как минимум, собственные версии OnPreparePrinting (), OnPrepareDC () и OnPrint (). Далее в этой главе вы увидите пример реализации этих функций в контексте программы Sketcher.
Вывод данных на принтер выполняется таким же образом, как и на дисплей — через контекст устройства. Вызовы GDI, которые вы используете для вывода текста или графики, независимы от устройства, поэтому они работают одинаково хорошо как с принтером, так и с дисплеем. Единственное отличие ~ в применяемом объекте CDC.
Функции CDC на рис. 17.5 взаимодействуют с драйвером принтера. Если документ, подлежащий печати, требует для своего вывода более одной страницы, процесс циклически повторяет вызовы функции OnPrepareDC () для каждой следующей новой страницы, как определено функцией EndPage ().
Все функции в вашем классе представления, участвующие в процессе печати, получают в качестве аргумента указатель на объект типа CPrintlnfо. Этот объект обеспечивает связь между всеми функциями, управляющими процессом печати, поэтому рассмотрим класс CPrintlnfо более подробно.
Класс CPrintlnfo
Объект CPrintlnfo играет основополагающую роль в процессе печати, поскольку сохраняет информацию о задании печати и подробности о его состоянии в любой момент времени. Также он обеспечивает функции доступа и манипуляции данными. Этот объект — средство передачи информации от одной функции представления к другой во время печати, а также между каркасом и вашими функциями представления.
Объект класса CPrintlnfo создается всякий раз, когда вы выбираете пункты меню File1^ Print и File1^ Print Preview. После использования каждой из функций текущего представления, которая участвует в процессе печати, он автоматически удаляется по окончании операции печати.
Все данные-члены класса CPrintlnfo общедоступны (public). Их перечень приведен в табл. 17.3. Объект CPrintlnfo имеет общедоступные функции-члены, перечисленные в табл. 17.4.
При печати документа, состоящего из нескольких страниц, вы должны определить, сколько печатных страниц займет документ, и сохранить эту информацию в объекте CPrintlnfo, чтобы сделать ее доступной каркасу. Это можно сделать в вашей версии функции-члена текущего представления OnPreparePrinting ().
Для установки номера первой страницы документа потребуется вызвать функцию SetMinPage () в объекте CPrintlnfo, которая принимает номер страницы как аргумент типа UINT. Возвращаемое значение отсутствует. Чтобы установить номер последней страницы документа, вы вызываете функцию SetMaxPage (), которая также принимает номер страницы как аргумент типа UINT и не возвращает значения. Если вы позднее пожелаете извлечь эти значения, то можете вызвать функции GetMinPage () и GetMaxPage () с объектом CPrintlnfo.
Таблица 17.3. Данные-члены класса CPrintlnfo
Член
Использование
m_pPD
Указатель на объект CPrintDialog, отображающий диалоговое окно печати.
m_bDirect
Устанавливается каркасом в true в операции печати, чтобы пропустить диалоговое окно печати; в противном случае равно false.
m_bPreview
Член типа bool, имеющий значение true при выборе File1^ Print Preview; в противном случае равно false.
m_bContinuePrinting
Значение типа bool. Если установлено в true, каркас продолжает цикл печати, показанный на диаграмме. При установке в false цикл печати завершается. Вы должны установить эту переменную, только если не передаете счетчик страниц для операции печати объекту CPrintlnfo (используя функцию-член SetMaxPage ()). В этом случае вы отвечаете за своевременное извещение об окончании печати установкой этой переменной в false.
m_nCurPage
Значение типа uint, хранящее номер текущей страницы. Обычно страницы нумеруются, начиная с 1.
m_nNumPreviewPages
Значение типа uint, специфицирующее количество страниц, отображенных в окне предварительного просмотра печати. Может быть 1 или 2.
m_lpUserData
Значение типа lpvoid, хранящее указатель на объект, который вы создаете. Это позволяет вам создать объект для хранения дополнительной информации об операциях печати и ассоциировать его с объектом CPrintlnfo.
m_rectDraw
Объект CRect, определяющий используемую область страницы в логических координатах.
m_strPageDesc
Объект cstring, содержащий форматную строку, используемую каркасом для отображения номеров страниц во время предварительного просмотра печати.
Таблица 17.4. Функции-члены класса CPrintlnfo
Функция
Описание
SetMinPage(UINT nMinPage)
Аргумент специфицирует номер первой страницы документа. Типа возврата нет.
SetMaxPage (UINT nMaxPage)
Аргумент специфицирует номер последней страницы документа. Типа возврата нет.
GetMinPage() const
Возвращает номер первой страницы документа, как тип uint.
GetMaxPageO const
Возвращает номер последней страницы документа, как тип uint.
GetFromPage() const
Возвращает номер первой страницы документа, подлежащей печати, как тип uint. Это значение устанавливается в диалоге печати.
GetToPage() const
Возвращает номер последней страницы документа, подлежащей печати, как тип uint. Это значение устанавливается в диалоге печати.
Задаваемые вами номера страниц сохраняются в объекте CPrintDialog, на который указывает член CPrintlnfo m_pPD, и отображается в диалоговом окне, всплывающем при выборе пункта меню File1^Print. Пользователь затем получает возможность специфицировать номера первой и последней печатаемых страниц, которые вы можете получить вызовом GetFromPage () и GetToPage () объекта CPrintlnfo. В каждом случае возвращается значение типа UINT. Диалог автоматически верифицирует
вхождение номеров первой и последней печатаемых страниц в диапазон, указанный минимальной и максимальной страницей документа.
Теперь вам известно, какие функции вы можете реализовать для себя в классе представления с целью управления печатью, оставив большую часть работы каркасу. Вы также знаете, какая информация доступна через объект CPrintlnfo, передаваемый функциям, занятым в процессе печати. Вы получите гораздо более ясное понимание подробностей механизма печати, если реализуете базовую возможность многостраничной печати документов Sketcher.
Реализация многостраничной печати
Вы используете режим отображения MM LOENGLISH в программе Sketcher для установки всех вещей и затем переключаетесь в MM ANISOTROPIC. Это означает, что фигуры и размеры представления измеряются в сотых долях дюйма. Конечно, с фиксированными физическими единицами измерения в идеале вы хотите печатать объекты в их действительном размере.
При размере документа, специфицированном как 3000 на 3000 единиц вы можете создавать документы площадью до 30 квадратных дюймов, которые распространяются на несколько листов бумаги, если заполнить всю их область. Вычисление количества страниц, необходимых для печати эскиза, требует немного больше усилий, нежели при печати типичного текстового документа, поскольку в большинстве случаев для печати полного документа-эскиза понадобится двумерный массив страниц.
Чтобы избежать чрезмерного усложнения, предположим, что вы печатаете на нормальный лист бумаги (формата А4 или размером 8/2 на 11 дюймов) в портретной ориентации (то есть длинная сторона располагается по вертикали). При любом размере листа вы печатаете в центральной его части размером 6 на 9 дюймов. При этих предположениях вы не должны заботиться о реальном размере бумаги; вам нужно только поделить документ на куски размером 600 на 900 единиц. Документ, занимающий более одной страницы, придется разбить, как показано в примере на рис. 17.6.
Как видите, страницы будут нумероваться построчно, поэтому в данном случае страницы от 1 до 4 находятся в первой строке, а страницы с 5 по 8 — во второй.
Рис. 17.6. Печать многостраничного документа
Получение полного размера документа
Чтобы узнать, сколько страниц занимает конкретный документ, вам нужно знать, насколько велик эскиз, а для этого нужно вычислить прямоугольник, который включает в себя весь документ. Это легко сделать, добавив функцию 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;
}
Вы можете добавить определение этой функции в файл SketcherDoc . срр или просто добавить ее код, применив средство Add^Add Function (Добавить1^Добавить функцию) из контекстного меню в Class View. Процесс проходит в цикле по каждому элементу в документе, используя переменную aPosition для выполнения шагов по списку и получения ограничивающего прямоугольника для каждого элемента. Функция UnionRect (), член класса CRect, вычисляет минимальный прямоугольник, содержащий два других прямоугольника, переданных ей в качестве аргументов, и помещает его значение в объект CRect, для которого данная функция вызвана. Таким образом, DocExtent продолжает расти в размере, пока все элементы не поместятся в него. Обратите внимание, что вы инициализируете DocExtent размером (0, 0, 1, 1), поскольку функция UnionRect () не работает правильно с прямоугольниками нулевой длины или ширины.
Сохранение данных печати
Функция OnPreparePrinting () в классе представления вызывается каркасом приложения для того, чтобы позволить вам инициализировать процесс печати документа. Требуемая базовая инициализация предназначена для того, чтобы представить информацию о страницах, требуемых вашим документом, которую вы могли бы использовать позже, в других функциях представления, участвующих в процессе печати. Вы создаете ее в функции-члене OnPreparePrinting () класса представления, сохраняете в объекте вашего собственного класса, который определяете специально для этой цели, и сохраняете указатель на объект в объекте CPrintlnfo, доступ к которому обеспечивает каркас. Такой подход предназначен преимущественно для демонстрации того, как работает данный механизм; в большинстве случаев вы обнаружите,
что проще сохранить данные в вашем объекте представления — в основном потому, что это обеспечит более простой способ обращения к данным.
Вы должны сохранить количество страниц, приходящихся на ширину документа, m nWidths, и количество строк страниц, приходящихся на длину документа, m_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, и затем выбрав пункт Add^New Item (Добавить1^ Новый элемент) из всплывающего меню. После этого вы можете ввести определение класса в новый файл.
Вам не понадобится отдельный файл реализации этого класса. Конструктор по умолчанию (генерируемый автоматически) здесь вполне адекватен. Поскольку объект этого класса существует и используется кратковременно, вам не нужно применять CObject в качестве базового класса, как не нужны и другие усложнения.
Процесс печати начинается с вызова функции-члена класса представления OnPreparePrinting(), так что разберем, как вы должны реализовать ее.
Подготовка к печати
Мастер Application Wizard добавляет версии OnPreparePr int ing ( ) , OnBeginPrinting () и OnEndPrinting () к CSketcherView с самого начала. Базовый код OnPreparePrinting () вызывает DoPreparePrinting() в операторе return, как видно в следующем коде:
BOOL CSketcherView: :OnPreparePrinting (CPrintlnfо* plnfo) {
// Подготовка по умолчанию
return DoPreparePrinting(plnfo);
}
Функция DoPreparePrinting () отображает диалоговое окно Print, используя информацию о количестве страниц, подлежащих печати, которая определена в объекте CPrintlnfo. Всегда, когда это возможно, вы должны вычислять количество страниц, подлежащих печати, и сохранять его в объекте CPrintlnfo перед этим вызовом. Конечно, во многих ситуациях вам может понадобиться информация для принтера из контекста устройства прежде, чем вы сможете сделать это, например, когда печатается документ, в котором количество страниц зависит от размера используемого шрифта; в таких случаях может оказаться невозможным получить счетчик страниц в
члене 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 и номер
// последней страницы — равным общему количеству страниц pInfo->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
Вам следует использовать этот подход для всех ссылок на члены объекта 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*>(p!nfo->m_lpUserData);
}
Это все, что необходимо добавить в эту функцию в программе Sketcher, но в некоторых случаях понадобится сделать несколько больше. Вся окончательная очистка должна быть помещена сюда. Не забудьте удалить знаки комментария (/* */), окружающие имя второго параметра; в противном случае функция не скомпилируется. В реализации по умолчанию имена параметров закомментированы, поскольку вам может и не понадобится обращаться к ним в коде. Поскольку вы использовали параметр plnfo, то должны убрать комментарий с него; в противном случае компилятор сообщит, что он не определен.
Вам ничего не нужно добавлять к функции OnBeginPrintingO в программе Sketcher, но вы должны добавить код для выделения ресурсов GDI, таких как перья, если они понадобятся в процессе печати. Затем вы должны удалить все это в процессе очистки в OnEndPrinting ().
Подготовка контекста устройства
Пока что программа Sketcher вызывает функцию OnPrepareDC (), которая устанавливает в качестве режима отображения MM_ANISOTROPIC, дабы учесть фактор масштабирования. Вы должны внести некоторые дополнительные изменения, чтобы контекст устройства был правильно подготовлен в случае печати:
void CSketcherView: :OnPrepareDC (CDC* pDC, CPrintlnfo* plnfo) (
int Scale = m Scale; // Сохранить масштаб локально
if(pDC->IsPrinting())
Scale = 1;// Если выполняется печать, установить масштаб 1
CScrollView::OnPrepareDC(pDC, plnfo);
CSketcherDoc* pDoc - GetDocument () ;
pDC->SetMapMode(MM_ANISOTROPIC); И Установить режим отображения CSize DocSize = pDoc->GetDocSize();// Получить размер документа
//Значение у должно быть отрицательным, поскольку необходим режим MM_LOENGLISH DocSize.су = -DocSize.су;// Изменить знак у
pDC->SetWindowExt(DocSize) ;// Установить размер окна
// Получить количество пикселей на дюйм для х и у int xLogPixels = pDC->GetDeviceCaps(LOGPIXELSX); int yLogPixels = pDC->GetDeviceCaps(LOGPIXELSY);
// Вычислить размер области отображения по х и у
long xExtent = static_cast<long>(DocSize.ex)*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, означают количество логических точек на дюйм в направлениях х и у для вашего принтера — в случае печати либо эквивалентные значения для вашего дисплея при рисовании на экране, потому это автоматически адаптирует размер области отображения для соответствующего устройства, когда вы посылаете на него вывод.
Печать документа
Записывать данные в контекст устройства принтера можно в функции OnPrint (). Она вызывается один раз для каждой печатаемой страницы. Вы должны добавить переопределение этой функции к CSketcherView, используя окно свойств класса. Выберите OnPrint () из списка переопределений и щелкните на <Add> OnPrint в правой колонке.
Вы можете получить номер текущей страницы из члена m_nCurPage объекта CPrintlnfo и использовать это значение для нахождения координат положения документа, соответствующих левому верхнему углу текущей страницы. Как это делается, лучше всего пояснить на примере, поэтому предположим, что выполняется печать страницы под номером семь в восьмистраничном документе, как показано на рис. 17.7.
600*((m_nCurPage-1 )%m_nWidths)
600*(6%4) = 600*2= 1200
Рис. 17.7. Печать страницы под номером семь в восьмистраничном документе
Вы можете получить индекс горизонтальной позиции страницы, уменьшив номер страницы на 1 и взяв остаток от деления на количество ширин страниц, приходящихся на ширину печатаемой области документа. Умножение результата на 600 дает координату х верхнего левого утла страницы относительно верхнего левого угла прямоугольника, описывающего все элементы документа. Аналогично вы можете определить индекс вертикальной позиции в документе, разделив номер текущей страницы, уменьшенный на 1, на количество ширин страниц, приходящихся на длину всего документа. Умножив остаток на 900, вы получаете относительную координату у верхнего левого угла страницы. Эти два оператора можно выразить следующим образом:
int xOrg = (static_cast<CPrintData*>(p!nfo->m_lpUserData))-> m_DocRefPoint.x + 600*((pInfo->m_nCurPage - 1)% ((static_cast <CPrintData*>(p!nfo->m_lpUserData))->m_nWidths));
int yOrg = (static_cast<CPrintData*>(pInfo->m_lpUserData))-> m_DocRefPoint.у - 900*((plnfo->m_nCurPage - 1) /
((static_cast <CPrintData*>(p!nfo->m_lpUserData))~>m_nWidths));
Эти операторы кажутся сложными, но это в основном из-за необходимости обращения к информации, хранящейся в объекте CPrintData через указатель в объекте CPrintlnfo.
Было бы неплохо напечатать имя файла документа на вершине каждой страницы, но вы хотите убедиться, что не напечатаете данные документа поверх имени файла. Вы также хотите отцентрировать на странице печатную область. Это можно сделать, переместив начало системы координат в контексте устройства печати после того, как будет напечатано имя файла. Все это проиллюстрировано на рис. 17.8.
На рис. 17.8 показано соответствие между печатной областью в контексте устройства и печатаемой страницей в ссылаемой рамке данных документа. Помните, что все это представлено в логических координатах — эквивалент MM LOENGLISH в Sketcher — поэтому у имеет возрастающее отрицательное значение в направлении сверху вниз. Страница показывает выражения смещений от начала координат страницы для области 600 на 900, куда мы собираемся печатать страницу. Вы хотите печа-
тать информацию документа в заштрихованную область, показанную на странице, поэтому вам следует отобразить точку xOrg, yOrg в документе на позицию, показанную на печатаемой странице, которая отображается со смещением xOf fset и yOf fset от начала координат страницы.
По умолчанию начало системы координат, которую вы используете для определения элементов документа, отображается на начало координат контекста устройства, однако вы можете это изменить. В объекте С DC для этой цели предусмотрена функция SetWindowOrg (). Это позволяет определить точку в системе логических координат, которую вы хотите поставить в соответствие началу координат контекста устройства. Важно сохранить исходную начальную точку, возвращенную функцией SetWindowOrg (), как объект CPoint. Вы должны восстановить старую начальную точку по завершении рисования текущей страницы; в противном случае, когда вы приступите к печати следующей страницы, член m_rectDraw объекта CPrintlnfo будет установлен неправильно.
Точка документа, которую вы хотите отобразить на начало координат страницы, имеет координаты xOrg-xOffset, yOrg+yOffset. Это может быть нелегко визуализировать, но вспомните, что установкой начальной точки окна вы определяете точку, ото-
бражающую начало координат представления. Если вы подумаете об этом, то увидите, что точка xOrg, yOrg в документе — это та точка, которая вам нужна на странице.
Ниже показан полный код печати страницы документа.
// Напечатать страницу документа
void CSketcherView: :OnPrint (CDC* pDC, CPrintlnfo* 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) ;// Выравнивать текст влево
// Вычислить начальную точку текущей страницы int xOrg =
(static_cast<CPrintData*> (pInfo->m_lpUserData)) ->m_DocRefPoint. x + 600* ((p!nfo->m_nCurPage - 1) %
((static_cast<CPrintData*>(pInfo->m_lpUserData)) ->m_nWidths)) ;
int yOrg =
(static__cast<CPrintData*>(pInfo->m_lpUserData)) ->m_DocRefPoint. у - 900* ((p!nfo->m_nCurPage - 1) /
((static_cast<CPrintData*> (pInfo->m_lpUserData)) ->m_nWidths)) ;
// Вычислить смещения центра области рисования как положительные значения int xOffset = (plnf o->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) ;// Нарисовать весь документ
pDC->SelectClipRgn (NULL) ;// Удалить отсекающий прямоугольник
pDC->SetWindowOrg (OldOrg); И Восстановить старое начало окна
}
Первый шаг — вывод имени файла, которое вы сохранили в объекте CPrintlnfo. Функция-член SetTextAllign () объекта CDC позволяет определить выравнивание последующего текстового вывода по отношению к точке привязки текстовой строки в функции TextOut (). Выравнивание определяется константой, передаваемой в аргументе функции. Доступны три варианта указания выравнивания текста, перечисленные в табл. 17.5.
Таблица 17.5. Выравнивание текста по горизонтали
Константа
Выравнивание
TA_LEFT
Точка находится слева от ограничивающего текст прямоугольника, так что текст располагается справа от указанной точки. Это — выравнивание по умолчанию.
TA_RIGHT
Точка находится справа от ограничивающего текст прямоугольника, так что текст располагается слева от указанной точки.
TA_CENTER
Точка находится в центре ограничивающего текст прямоугольника.
Вы определяете координату х имени файла на странице как половину ее ширины, а координату у — как 20 единиц, что составляет 0,2 дюйма, от вершины страницы. После вывода имени файла документа в виде центрированного текста выравнивание
текста сбрасывается в значение по умолчанию, TA_LEFT, для последующего вывода текста в документе.
Функция SetTextAlign () также позволяет изменить позицию текста по вертикали, комбинируя с помощью операции ИЛИ второй флаг с флагом выравнивания. Второй флаг может быть любым из перечисленных в табл. 17.6.
Таблица 17.6. Выравнивание текста по вертикали
Константа
Выравнивание
ТА_ТОР
Выровнять вершину прямоугольника, ограничивающего текст, по точке, определяющей положение текста. Установлено по умолчанию.
ТА_ВОТТОМ
Выровнять низ прямоугольника, ограничивающего текст, по точке, определяющей положение текста.
TA_BASELINE
Выровнять базовую линию шрифта, используемого для текста по точке, определяющей позицию текста.
Следующее действие в OnPrint () использует метод, который обсуждался ранее, для отображения области документа на текущую страницу. Вы получаете документ, нарисованный на странице, вызывая функцию OnDraw(), которая используется для отображения документа в представлении. Это потенциально нарисует весь документ, но вы можете ограничить то, что появляется на странице, определив отсекающий прямоугольник (clip rectangle). Вывод вне этого прямоугольника подавляется. Также для этого можно определить и области неправильной формы.
Начальная область отсечения по умолчанию определена в контексте устройства печати по границам страницы. Вы определяете отсекающий прямоугольник, соответствующий области 600 на 900 в центре страницы. Это гарантирует, что будет нарисована только эта область, а имя файла не будет перекрыто.
После рисования текущей страницы вы вызываете SetClipRgn () с аргументом NULL для удаления отсекающего прямоугольника. Если этого не сделать, то вывод заголовка документа будет подавлен на всех страницах, следующих за первой, поскольку он лежит вне этого прямоугольника, который останется в силе на протяжении всего процесса печати до тех пор, пока не произойдет следующий вызов IntersectClipRect().
Последнее действие состоит в еще одном вызове SetWindowOrg (), необходимом для восстановления начальной точки в ее исходном положении, как было сказано ранее в настоящей главе.
Получение печатного вывода документа
Чтобы получить первый печатный документ Sketcher, нужно только собрать проект и выполнить программу (исправив все возможные опечатки). Выбор пункта меню File^Print Preview (Файл^предварительный просмотр) должен привести к отображению окна, подобного показанному на рис. 17.9.
То есть совершенно бесплатно вы получаете полную функциональность предварительного просмотра печати. Каркас использует ваш код для нормальной операции многостраничной печати, чтобы создать образы страниц в окне предварительного просмотра. То, что вы видите в окне предварительного просмотра печати, должно полностью соответствовать тому, что вы получите на печатной странице.
Рис. 17.9. Окно предварительного просмотра печатного документа
Резюме
В настоящей главе вы узнали, как получить документ для сохранения на диске, в форме, позволяющей прочитать и реконструировать составляющие его объекты с помощью процесса сериализации, поддерживаемого MFC. Чтобы реализовать сериализацию для классов, определяющих данные документа, потребуется выполнить перечисленные ниже действия.
Определить класс как прямой или непрямой наследник CObject.
Специфицировать макрос DECLARE_SERIAL () в определении класса.
Специфицировать макрос IMPLEMENT_SERIAL () в реализации класса.
Реализовать конструктор по умолчанию в классе.
Объявить в классе функцию Serialize ().
Реализовать в классе функцию Serialize () для сериализации всех данных- членов.
Процесс сериализации использует объект С Ar chive для выполнения ввода и вывода. Вы используете объект CArchive, переданный функции Serialize () для сериализации данных-членов вашего класса.
Также вы познакомились с поддержкой MFC процесса вывода на печать. Для добавления базовых возможностей печати, предоставляемых по умолчанию, вы можете реализовать свои собственные версии функций класса представления, участвующие в процессе печати документа. В табл. 17.7 описаны основные роли каждой из этих функций.
Таблица 17.7. Основные роли функций, связанный с печатью
Функция
Роль
OnPreparePrinting()
Определяет количество страниц в документе и вызывает член представления DoPreparePrinting ().
OnBeginPrinting()
Выделяет ресурсы, необходимые контексту устройства печати на протяжении процесса печати, а также определяет количество страниц в документе, зависящее от информации из контекста устройства.
OnPrepareDC()
Устанавливает атрибуты контекста устройства печати при необходимости.
OnPrint()
Печатает документ.
OnEndPrinting()
Удаляет все ресурсы GDI, созданные в OnBeginPrinting (), а также выполняет всю прочую необходимую очистку.
Информация, касающаяся процесса печати, сохраняется в объекте типа CPrintlnfo, который создается каркасом. Вы можете сохранить дополнительную информацию в своем представлении либо в другом вашем собственном объекте. Если вы используете объект вашего собственного класса, то можете отслеживать его, сохраняя его указатель в объекте CPrintlnfo.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Добавьте некоторый код в функцию OnPrint (), чтобы внизу каждой страницы документа печатался номер страницы в форме “Page тГ. Если вы используете средства класса CString, то сможете сделать это всего тремя дополнительными строками кода!
В качестве дальнейшего усовершенствования класса CText измените реализацию так, чтобы правильно работало масштабирование. (Подсказка: обратитесь к описанию функции CreatePointFont () в оперативной справочной системе.)
18
Написание собственныхDLL-библиотек
В главе 9 было описано, как библиотеки классов C++/CLI сохраняются в файле .dll. Динамически подключаемые библиотеки также интенсивно используются приложениями на “родном” C++. Полное описание DLL-библиотек в приложениях на “родном” C++ выходит за рамки книги для начинающих, тем не менее, вам предлагается глава ознакомительного характера, посвященная этим библиотекам.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
Библиотеки DLL и их работа.
Когда следует рассматривать возможность реализации DLL.
Какие существуют вариации DLL и для чего они используются.
Как можно расширить МЕС с помощью DLL.
Как обращаться к содержимому DLL из ваших программ.
Что такое DLL-библиотека?
Почти все языки программирования поддерживают библиотеки или стандартные модули кода, включающие часто используемые функции. В “родном” C++ вы используете множество функций, хранящихся в стандартных библиотеках, таких как функция cell (), которую вы применяли в предыдущей главе, объявленная в заголовочном файле <cmath>. Код этой функции сохраняется в библиотечном файле с расширением . lib, и когда создается исполняемый модуль программы Sketcher, компоновщик извлекает код этой стандартной функции из библиотечного файла и интегрирует его копию в файл . ехе программы Sketcher. Если вы пишете другую программу и используете ту же функцию, она также получает собственную копию функции cell (). Функция cell () является статически связываемой с каждым приложением и представляет собой составную часть каждого исполняемого модуля, как показано на рис. 18.1.
Статическая библиотека
Рис. 18.1. Статическая компоновка
Хотя это очень удобный способ использования стандартной функции с минимальными усилиями с вашей стороны, с ним связаны и недостатки, поскольку он ведет к тому, что несколько параллельно выполняющихся программ применяют несколько копий одной и той же функции в среде Windows. Статически связываемые стандартные функции, используемые более чем одной программой, дублируются в памяти для каждой использующей их программы. Это может показаться и несущественным для функции cell (), но некоторые функции, например, ввода и вывода, неизбежно являются общими для большинства программ, и весьма вероятно, что они займут значительные объемы памяти. Статическая компоновка таких функций была бы чрезвычайно неэффективной.
Другой аргумент против статической компоновки заключается в том, что стандартная функция из статической библиотеки может быть скомпонована в сотни программ в вашей системе, поэтому идентичные копии их кода будут занимать дисковое пространство в файле . ехе каждой программы. По этой причине Windows поддерживает дополнительную возможность работы с библиотеками стандартных функций. Она называется динамически подключаемыми или динамически компонуемыми библиотеками (dynamic link library — DLL). Это позволяет разделять доступ к одной
копии функции между несколькими параллельно выполняющимися программами, избегая необходимости помещения копии кода библиотечных функций в исполняемый модуль использующих их программ.
Как работают 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.
Наличие функций, хранящихся в DLL, дает возможность изменения этих функций, не затрагивая использующих их программ. До тех пор, пока интерфейс функции в DLL остается неизменным, программа может работать с новой версией функции вполне успешно, без необходимости какой-либо перекомпиляции или перекомпоновки. К сожалению, это также имеет и свою отрицательную сторону: очень легко в конечном итоге использовать с программой неправильную версию DLL. Это может представить проблему для программ, устанавливающим DLL в системную (System) папку Windows. Некоторые коммерческие приложения произвольным образом пишут DLL, ассоциированные с программой, в эту папку, несмотря на вероятность того, что могут перезаписать существующие там одноименные DLL. Это может негативно сказаться на других приложениях, которые инсталлированы ранее, и в худшем случае, нарушить их работоспособность.
4. Программа В загружена4. Программа В загружена2. Библиотека .dll загружена2. Библиотека .dll загруженаLibrary.dllProgramC.exeLibrary.dllProgramC.exe
4. Программа В загружена
4. Программа В загружена
2. Библиотека .dll загружена
2. Библиотека .dll загружена
Library.dll
ProgramC.exe
Library.dll
ProgramC.exe
ProgramB.exe
ProgramA.exe
Программа А загружена
6. Программа С загружена
Программа АПрограмма ВПрограмма С5. Связывание с3. Связывание сфункцией из DLLПрограмма АПрограмма ВПрограмма С5. Связывание с3. Связывание сфункцией из DLL7. Связывание с функцией из DLL7. Связывание с функцией из DLLLibrary.dllLibrary.dllФункцияФункцияй из DLLй из DLL
Программа А
Программа В
Программа С
5. Связывание с
3. Связывание с
функцией из DLL
Программа А
Программа В
Программа С
5. Связывание с
3. Связывание с
функцией из DLL
7. Связывание с функцией из DLL
7. Связывание с функцией из DLL
Library.dll
Library.dll
Функция
Функция
й из DLL
й из DLL
Память компьютера
Динамическое связывание во время выполнения
Библиотеки DLL, которые вы будете создавать в этой главе, автоматически загружаются в память, когда в память для выполнения попадает использующая их программа. Это известно как динамическое связывание во время загрузки, или раннее связывание, поскольку связи с используемыми функциями устанавливаются, как только программа и DLL-библиотека загружаются в память. Операция такого рода проиллюстрирована на рис. 18.2; однако это не единственно возможный вариант выбора. Также допускается вызвать загрузку DLL-библиотеки после начала выполнения программы. Это называется динамическим связыванием во время выполнения, или поздним связыванием. Последовательность операций, осуществляемых при этом, показана на рис. 18.3.
1. Программа загружается, но DLL-библиотеки не загружаются. Программа может использовать любую из трех DLL.1. Программа загружается, но DLL-библиотеки не загружаются. Программа может использовать любую из трех DLL.Library1.dllLibrary2.dllLibrary3.dllLibrary1.dllLibrary2.dllLibrary3.dllk^Program. exe^Jk^Program. exe^J3. Библиотека Library2.dll загружается по запросу программы.3. Библиотека Library2.dll загружается по запросу программы.
1. Программа загружается, но DLL-библиотеки не загружаются. Программа может использовать любую из трех DLL.
1. Программа загружается, но DLL-библиотеки не загружаются. Программа может использовать любую из трех DLL.
Library1.dll
Library2.dll
Library3.dll
Library1.dll
Library2.dll
Library3.dll
k^Program. exe^J
k^Program. exe^J
3. Библиотека Library2.dll загружается по запросу программы.
3. Библиотека Library2.dll загружается по запросу программы.
V—
Программа
Library2.dll
2. В этой точке программа определяет,
что ей необходима Library2.dll
—
и инициирует ее загрузку.
>
Функция
——
4. Программа получает адрес
функции из DLL и использует
его для вызова функции
—
Память компьютера
Рис. 183. Позднее связывание
Динамическое связывание во время выполнения позволяет программе отложить связывание с DLL-библиотекой до момента, когда потребуется определенная функция из этой DLL. Это позволяет писать программу, которая может самостоятельно определять, когда ей необходимо загружать одну или более DLL-библиотек, на основе полученного ею ввода, так что в память будут загружены только те функции, которые действительно нужны. В некоторых случаях это может радикально снизить объем памяти, необходимой для выполнения программы.
Программа, реализованная для использования динамического связывания во время выполнения, вызывает библиотечную функцию Windows LoadLibraryO, чтобы загрузить DLL-библиотеку в случае необходимости. Адрес функции внутри DLL может быть получен с помощью функции GetProcAddress (). Когда программе уже не нужна DLL-библиотека, она может отключиться от нее вызовом функции FreeLibrary (). Если никакая другая программа не использует ту же самую DLL, она будет удалена из памяти. Подробности работы этого механизма в настоящей книге не рассматриваются.
Содержимое DLL-библиотеки
Динамически подключаемая библиотека не ограничена хранением кода функций. Вы можете также размещать в ней ресурсы, включая битовые графические изображения и шрифты. Игра Solitaire (Пасьянс), поставляемая вместе с Windows, использует динамическую библиотеку Cards .dll, содержащую все битовые изображения карт, наряду с функциями манипуляции ими. Если вы захотите написать собственную карточную игру, то можете с успехом использовать эту DLL в качестве базы и не заботиться о создании собственных графических изображений, необходимых для представления карт на экране. Конечно, потребуется точно знать, какие функции и ресурсы включены в DLL.
Вы также можете определить в DLL статические глобальные переменные, включая объекты классов C++, так что они станут доступными программам, использующим ее. Конструкторы глобальных статических объектов класса вызываются автоматически при создании этих объектов. Вы должны заметить, что каждая программа, использующая DLL, получает собственную копию любого статического глобального объекта, определенного в DLL, даже если он не обязательно применятся в программе. Для глобальных объектов классов это включает накладные расходы на вызов конструктора каждого из них. Поэтому вы должны избегать включения таких объектов в DLL, если только это не является абсолютно необходимым.
Интерфейс DLL-библиотеки
Вы не можете просто получить доступ ко всему, что содержится в DLL. Внешнему миру видимы только элементы, специально идентифицированные как экспортированные. Функции, классы, глобальные статические переменные и ресурсы — все они могут экспортироваться из DLL, и вместе они образуют ее интерфейс. Все, что не экспортировано, остается недоступным извне. Далее в этой главе вы увидите, как можно экспортировать элементы из DLL.
Функция DllMain ()
Даже несмотря на то, что DLL-библиотека не является исполняемым модулем, будучи независимой программой, она содержит специальную разновидность функции main () по имени DllMain (). Эта функция вызывается Windows, когда DLL впервые загружается в память, чтобы позволить ей выполнить всю необходимую инициали-
зацию перед использованием ее содержимого. Windows также вызывает DllMain () непосредственно перед удалением DLL из памяти, чтобы дать ей возможность выполнить за собой необходимую очистку. Существуют и другие ситуации, когда вызывается DllMain (), но они выходят за пределы тем, рассматриваемых в настоящей книге.
Вариации DLL-библиотек
Существуют три разновидности DLL-библиотек, которые вы можете собирать в Visual C++ 2005 с использованием MFC: DLL расширения МЕС, обычную DLL со статически связанной МЕС, а также обычную DLL с динамически связанной МЕС.
DLL-библиотека расширения MFC
Сборка DLL этого типа осуществляется, когда в нее планируется включить классы, унаследованные от МЕС. Ваши классы-наследники в DLL, по сути, расширяют МЕС. В среде, где будет применяться ваша DLL, МЕС должна присутствовать, поэтому все классы МЕС должны быть доступны вместе с вашими производными классами — отсюда и название “DLL расширения МЕС”. Однако наследование ваших классов от МЕС — не единственная причина использования DLL такого типа. Если вы пишете DLL, которая содержит функции, передающие указатели на объекты классов МЕС функциям программы, использующей ее, или же принимающие такие параметры от функций программы, то вы должны создавать ее как DLL расширения МЕС.
Доступ к классам в МЕС через DLL расширения всегда разрешается динамически путем связывания с разделяемой версией МЕС, которая сама реализована в виде набора DLL. DLL расширения создаются с использованием разделяемой DLL-версии МЕС, поэтому когда вы применяете DLL расширения, должна быть доступной и разделяемая версия MFC. DLL расширения МЕС может использоваться обычным приложением, сгенерированным мастером Application Wizard. Оно требует выбора опции Use MFC as Shared Dll (Использовать MFC как разделяемую DLL) в наборе свойств General (Общие), который доступен через пункт меню Projects Properties (Проект1^Свойства). Это — выбор по умолчанию для программ, сгенерированных мастером Application Wizard. По причине фундаментальной природы разделяемой версии МЕС в DLL расширениях МЕС такие библиотеки не могут использоваться программами, которые статически связаны с МЕС.
Обычные DLL, статически связанные с MFC
Это DLL-библиотеки, которые используют классы МЕС, связанные статически. Применение DLL не требует необходимой доступности МЕС в среде во время выполнения, поскольку код всех используемых классов включен в DLL. Это увеличивает размер DLL, но в то же время дает то значительное преимущество, что подобные DLL могут использоваться любыми программами Win32, независимо от того, применяют ли они МЕС.
Обычные DLL, динамически связанные с MFC
Это DLL-библиотеки, использующие динамически связанные классы МЕС, но не добавляющие своих собственных классов. С DLL подобного рода могут работать любые программы Win32, независимо от того, используют ли они сами по себе МЕС, однако их применение требует доступности МЕС в среде выполнения.
Для сборки всех трех типов DLL, использующих МЕС, можно применять мастер Application Wizard. Вы также можете создать проект для DLL, который вообще не-
зависим от 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 расширения
Вы можете создать DLL расширения МЕС, которая будет содержать классы фигур для приложения Sketcher. Хотя это не предоставит особых преимуществ данному приложению, все же позволит продемонстрировать способ написания DLL расширения без необходимости написания значительного объема нового кода.
Начальной точкой будет мастер Application Wizard, поэтому создайте новый проект, нажав <Ctrl+Shift+N> и выбрав в качестве типа проекта MFC, а в качестве шаблона— MFC DLL, как показано на рис. 18.4.
Такой выбор идентифицирует, что вы создаете проект для основанной на МЕС динамической библиотеки по имени ExtDLLExample. Щелкните на кнопке ОК и выберите Application Settings (Настройки приложения) в следующем окне. Это окно должно выглядеть, как показано на рис. 18.5.
Здесь находятся три переключателя, соответствующие трем типам МГС-ориенти- рованных DLL, о которых говорилось ранее. Вы должны выбрать третью опцию, как показано на рисунке.
Два флажка ниже группы переключателей позволяют включить в DLL код поддержки автоматизации (Automation) и сокетов Windows (Windows sockets). Это дополнительные возможности Windows-программ, которые здесь вам не понадобятся. Автоматизация обеспечивает возможность размещения объектов, созданных и управляемых одним приложением, внутри другого. Сокеты Windows предоставляют классы и функции, позволяющие вашей программе взаимодействовать с другими по сети, но пока вам не понадобится это, к тому же оно выходит за рамки тематики книги. Щелкните на кнопке Finish (Готово) и завершите создание проекта.
Рис. 18.4. Создание нового проекта для DLL расширения MFC
Рис. 18.5. Окно Application Settings
После того, как мастер MFC DLL завершит свою работу, можете заглянуть в сгенерированный им код. Если вы посмотрите на содержимое проекта в панели Solution Explorer (Проводник решений), то увидите, что мастер MFC DLL сгенерировал несколько файлов, включая файл . txt, содержащий описание остальных файлов. Вы можете все прочитать в этом файле . txt, однако два из сгенерированных файлов реализации DLL представляют непосредственный интерес (табл. 18.1).
Таблица 18.1. Некоторые файлы, сгенерированные мастером MFC DLL
Имя файлаСодержимое
ExtDLLExample. срр Этот файл содержит функцию DllMain () и является первичным исходным файлом DLL.
ExtDLLExample. def Информация в этом файле используется во время компиляции. Он содержит имя DLL-библиотеки, и вы можете также добавить в него определения элементов DLL, которые должны быть доступны программе, которая будет использовать эту DLL. В приведенном далее примере применяется альтернативный и в некоторой степени более простой способ идентификации таких элементов.
Когда ваша DLL будет загружена в память, то первое, что произойдет — запуск функции DllMain (), поэтому рассмотрим сначала эту функцию.
Функция DllMain ()
Если просмотреть содержимое ExtDLLExample. срр, там можно увидеть, что мастер MFC DLL сгенерировал версию DllMain (), которая выглядит следующим образом:
extern "С" int APIENTRY
DllMain(HINSTANCE hlnstance, DWORD dwReason, LPVOID IpReserved)
{
// Удалите это, если используется IpReserved UNREFERENCED-PARAMETER(IpReserved);
if (dwReason == DLL_PROCESS_ATTACH) {
TRACED("EXTDLLEXAMPLE.DLL Initializing’\n");
// Однократная инициализация DLL расширения
if (JAfxInitExtensionModule(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. Убедитесь, что вы перемещаете файлы, а не копи-
руете их. Поскольку классы фигур для 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 (Проект^Добавить существующий элемент) и укажите файл Elements. срр в окне списка диалогового окна, как показано на рис. 18.6.
Рис. 18.6. Добавление файла Elements. срр в проект DLL расширения
Проект также должен включать файлы, содержащие определения классов форм и ваши константы, поэтому повторите процесс для файлов Elements . h и OurConstants. h, добавив их в проект. Вы можете добавить множество файлов за один прием, удерживая нажатой клавишу <Ctrl> при выборе файлов из списка в диалоговом окне Add Existing Item (Добавление существующего элемента). В конечном итоге вы должны увидеть все файлы в панели Solution Explorer и все классы — в панели Class View проекта.
Экспорт классов из DLL расширения
Имена классов, определенных в DLL, которые должны быть доступны в использующей ее программе, должны быть некоторым образом идентифицированы, чтобы между программой и DLL могли устанавливаться соответствующие связи. Как было показано ранее, одним из способов сделать это является добавление информации к файлу . de f для DLL. Это включает добавление того, что называется декорированными именами (decorated names) к DLL и ассоциацию декорированного имени с уникальным идентифицирующим числовым значением, называемым порядковым (ordinal). Декорированное имя объекта — это имя, сгенерированное компилятором, который добавляет дополнительную строку к имени, которое вы присваиваете объекту. Эта дополнительная строка представляет информацию о типе объекта, или — в случае функции — информацию о типах параметров функции. Помимо прочего, это гарантирует уникальность идентификаторов и позволяет компоновщику отличать перегруженные функции друг от друга.
Получение декорированных имен и присвоение порядковых значений для экспорта элементов из DLL требует большого объема работы и является не лучшим и не самым простым подходом в Windows. Намного легче идентифицировать классы, которые вы хотите экспортировать из DLL, модифицируя их определения в Elements .h так, чтобы они включали ключевое слово AFX_EXT__CLASS для каждого имени класса, как показано в следующем фрагменте для класса CLine:
// Класс, определяющий объект - линию
class AFX_EXT_CLASS CLine: public CElement {
DECLARE_SERIAL(CLine)
public:
virtual void Draw(CDC* pDC, CElement* pElement=0) ; // Функция отображения H lanasroi
virtual void Move (CSize& aSize) ;// Функция перемещения элемента
// Конструктор объекта линии
CLine (CPoint Start, CPoint End, COLORREF aColor, int PenWidth);
virtual void Serialize (CArchive& ar) ; // Функция сериализации CLine protected:
CPoint mjS tartPoint; // Начальная точка линии
CPoint m_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 (), будут недоступными.
И последняя необходимая модификация — добавление директивы:
#include <afxtempl.h>
в файл stdaf х . h проекта DLL, чтобы сделать доступным определение класса CList.
Теперь сделано все необходимое для добавления классов фигур к DLL. Все, что остается — скомпилировать и выполнить сборку проекта, создав DLL.
Сборка DLL-библиотеки
Сборка DLL выполняется точно таким же образом, как и любого другого проекта— с использованием пункта меню Builds 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. срр, Sketcher Do с. h и SketcherView.cpp). Вы также должны добавить файл Dlllmports .h к проекту, щелкнув правой кнопкой мыши на папке Header Files в панели Solution Explorer и выбрав Add^ Existing Item (Добавить1^Существующий элемент) из контекстного меню.
Когда вы будете перестраивать приложение Sketcher, компоновщику понадобится указать зависимость проекта от файла ExtDLLExample .lib, поскольку этот файл содержит информацию о содержимом DLL. Щелкните правой кнопкой мыши на Sketcher в панели Solution Explorer и выберите пункт Properties (Свойства) из контекстного меню. Затем введите имя файла . lib как дополнительную зависимость, что показано на рис. 18.7.
Conrjgu ratio n: | Acth e(Detug)Conrjgu ratio n: | Acth e(Detug)Plamrm. Achw(Win32)Plamrm. Achw(Win32)* [ Configuration Manager... |* [ Configuration Manager... |® Conor .or. Properties E Corrtiguration ProportionGeneral Debugging • c/c+t В linkerGeneralInpu antfeCt HieDebuggingSystem□ptrmizationEmbeddec IDLAdvicedCommand Line^ Manifest Tool•■ Resourcesf XML Document Geneim Browse Info rmot "inm Build Events@ Custom Build Step v® Conor .or. Properties E Corrtiguration ProportionGeneral Debugging • c/c+t В linkerGeneralInpu antfeCt HieDebuggingSystem□ptrmizationEmbeddec IDLAdvicedCommand Line^ Manifest Tool•■ Resourcesf XML Document Geneim Browse Info rmot "inm Build Events@ Custom Build Step vAdditional Ue jendencies Ignore di Default Libraries Ignore Specif c Library &'odu e Definition File Add Modul e to Assembly Embed Managed Resource File Force Symbol ЙйТв"впс£ь Delay Leaded DLLs Assembly Link ResourceAdditional Ue jendencies Ignore di Default Libraries Ignore Specif c Library &'odu e Definition File Add Modul e to Assembly Embed Managed Resource File Force Symbol ЙйТв"впс£ь Delay Leaded DLLs Assembly Link ResourcesYExtDLLExample\debijg\ExtDLLiExample'to f ■ NosYExtDLLExample\debijg\ExtDLLiExample'to f ■ NoAdditiona Depende aciesSpecifies additional rtemj to add to the link line (ex kernelJ2Jib}; cnnfiguratran specificiAdditiona Depende aciesSpecifies additional rtemj to add to the link line (ex kernelJ2Jib}; cnnfiguratran specifici
Conrjgu ratio n: | Acth e(Detug)
Conrjgu ratio n: | Acth e(Detug)
Plamrm. Achw(Win32)
Plamrm. Achw(Win32)
* [ Configuration Manager... |
* [ Configuration Manager... |
® Conor .or. Properties E Corrtiguration Proportion
General Debugging • c/c+t В linker
General
Inpu antfeCt Hie
DebuggingSystem□ptrmizationEmbeddec IDLAdvicedCommand Line^ Manifest Tool•■ Resourcesf XML Document Geneim Browse Info rmot "inm Build Events@ Custom Build Step v
® Conor .or. Properties E Corrtiguration Proportion
General Debugging • c/c+t В linker
General
Inpu antfeCt Hie
DebuggingSystem□ptrmizationEmbeddec IDLAdvicedCommand Line^ Manifest Tool•■ Resourcesf XML Document Geneim Browse Info rmot "inm Build Events@ Custom Build Step v
Additional Ue jendencies Ignore di Default Libraries Ignore Specif c Library &'odu e Definition File Add Modul e to Assembly Embed Managed Resource File Force Symbol ЙйТв"впс£ь Delay Leaded DLLs Assembly Link Resource
Additional Ue jendencies Ignore di Default Libraries Ignore Specif c Library &'odu e Definition File Add Modul e to Assembly Embed Managed Resource File Force Symbol ЙйТв"впс£ь Delay Leaded DLLs Assembly Link Resource
sYExtDLLExample\debijg\ExtDLLiExample'to f ■ No
sYExtDLLExample\debijg\ExtDLLiExample'to f ■ No
Additiona Depende acies
Specifies additional rtemj to add to the link line (ex kernelJ2Jib}; cnnfiguratran specific
i
Additiona Depende acies
Specifies additional rtemj to add to the link line (ex kernelJ2Jib}; cnnfiguratran specific
i
Ske'cher Pro er Pa^es
0< Cancel
Puc. 18.7. Добавление к проекту дополнительной зависимости
На рис. 18.7 показано вхождение для отладочной версии Sketcher. Файл . lib для DLL расположен в папке Debug внутри папки проекта DLL. Если вы создаете версию релиза Sketcher, то вам также понадобится рабочая версия DLL с соответствующим файлом . lib, поэтому вы должны ввести полностью квалифицированное имя файла . lib для рабочей версии DLL, соответствующее рабочей версии Sketcher. Файл, к которому применяются свойства, выбирается в выпадающем окне списка Configuration (Конфигурация) в окне Properties (Свойства). У вас есть только одна внешняя зависимость, но при необходимости вы можете ввести их несколько, щелкнув на кнопке справа от текстового поля ввода. Поскольку здесь вводится полный путь файла .lib, компоновщик будет знать не только о внешней зависимости от ExtDLLExample. lib, но и о его местоположении.
Имейте в виду, что если полный путь к файлу . lib содержит пробелы (как в приведенном примере), то его придется заключить в двойные кавычки, чтобы компоновщик смог правильно его распознать.
Теперь выполните сборку приложения Sketcher еще раз и все должно компилироваться и компоноваться, как обычно. Однако если вы попытаетесь запустить программу, то увидите окно сообщения, показанное на рис. 18.8.
Sketcher.exe - Unable То Locate Component
This application has failed to start because E4DLLE sample.dll was not found. Re-installing the application may fix this problem.
Puc. 18.8. Сообщение о невозможности нахождения компонента
Это одно из наименее загадочных сообщений об ошибке — здесь совершенно ясно указано, что именно идет не так. Чтобы позволить 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.
.lib
Определяет элементы, экспортированные 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 с программой, собранной для использования старой версии, не будет вынужден повторно компилировать свои приложения.
Вы должны использовать атрибут 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, включаемом в файлы . срр программы, которые обращаются к этим символам.
Реализация экспорта символов из 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;
H И Ш Ш Ш H H H H Ш ! Ш И Ш H
/ / Значения цветов для рисования
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);
И H H ! H H i ! H H ! H H ! ! H ! ! ! ! ! ! HI
Добавьте все это в начало Elements. срр, сразу после директив #include. Сначала вы определяете символ DllExport для упрощения спецификаций экспортируемых переменных, как уже видели ранее. Затем вы присваиваете атрибут DllExport каждому типу элемента и цвету.
Обратите внимание, что спецификатор extern также добавляется к определениям этих переменных. Причина этого — в эффекте от модификатора const, который указывает компилятору, что эти значения являются константами и не должны модифицироваться в программе, что вам и требовалось. Однако по умолчанию ключевое слово const также указывает на то, что переменные имеют внутреннее связывание, поэтому они локальны по отношению к файлу, в котором они появляются. Вы хотите экспортировать эти переменные в другую программу, поэтому должны добавить модификатор extern, чтобы переопределить спецификацию компоновки по умолчанию относительно модификатора const и гарантировать их внешнее связывание. Символы, которым назначено внешнее связывание, являются глобальными, а потому могут экспортироваться. Конечно, если переменные не имеют модификатора const, то вам не понадобится и extern, поскольку они являются глобальными автоматически, если появляются в глобальном контексте.
Теперь файл OurConstants. h будет содержать только одно определение:
/ / Определения констант
#pragma once
Н Определение номера версии программы для применения в сериализации UINT VERSION NUMBER = 1;
Конечно, это по-прежнему необходимо, поскольку используется в макросе IMPLEMENT_SERIAL () в Elements. срр. Теперь вы сможете заново выполнить сборку DLL, так что она будет готова для использования программой Sketcher. Не забудьте скопировать последнюю версию файла .dll в папку Debug проекта Sketcher.
Использование экспортированных символов
Чтобы сделать экспортированные из DLL символы доступными в программе Sketcher, вы должны специфицировать их как импортируемые из DLL. Это делается добавлением идентификации импортированных символов в файл Dll Import. h, который содержит определения импортируемых классов. Таким образом, вы получаете один файл, специфицирующий все элементы, импортированные из DLL. Операторы, появляющиеся в этом файле, выглядят так:
// Переменные фигур, определенные в DLL ExtDLLExample.dll
#pragma once
#define Dlllmport declspec( dlllmport )
/ / Импортировать объявления типов элементов
// Каждое значение типа должно быть уникальным
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;
1Ш1ШНШНН1НШНШШШ1
/ / Импортировать значения цвета для рисования
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.срр.
Может показаться, что теперь вы сделали все необходимое для использования новой версии DLL в приложении Sketcher, однако это не так. Если вы попытаетесь перекомпилировать Sketcher, то получите сообщения об ошибках для оператора switch в члене CreateElement () класса CSketcherView.
Значения в этих операторах case должны быть константами, но хотя вы и присвоили переменным типов фигур атрибут const, компилятор не имеет доступа к их значениям, поскольку они определены в DLL, а не в самой программе Sketcher. Поэтому компилятор не может определить значения констант для конструкций case и генерирует ошибку. Простейший способ обойти эту проблему — заменить оператор switch в функции CreateElement () серией операторов if, как показано ниже:
// Создать элемент текущего типа
CElement* CSketcherView: :CreateElement () {
// Получить указатель на документ для данного представления CSketcherDoc* pDoc = GetDocument();
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, m_SecondPoint, ElementColor, PenWidth) ; if(ElementType = CURVE)
return new CCurve(m^FirstPoint, m_SecondPoint, ElementColor, PeriWidth) ; else
// Всегда по умолчанию - линия
return new CLine (m_FirstPoint, m_SecondPoint, ElementColor, PenWidth);
}
Вы добавили локальные переменные для хранения текущего типа элемента, цвета и ширины пера, которые извлекаются из объекта документа. Тип элемента сравнивается с типами элементов, импортированных из DLL, в серии операторов if. Они решают ту же задачу, что и оператор switch, но не требуют точной информации о значениях констант типов элементов. Если вы соберете программу Sketcher с приведенными изменениями, она будет выполняться с использованием DLL, применяя экспортированные символы наряду с экспортированными классами фигур.
Резюме
В настоящей главе вы изучили основы конструирования и применения динамически подключаемых библиотек. Ниже перечислены наиболее важные моменты в этом контексте.
Динамически подключаемые библиотеки предоставляют средства динамического связывания стандартных функций при выполнении программы, вместо включения их в исполняемый модуль программы.
Программы, сгенерированные мастером Application Wizard, по умолчанию связываются с версией МЕС, сохраненной в DLL.
Единственная копия DLL в памяти может использоваться несколькими параллельно выполняющимися программами.
DLL расширения так называется потому, что расширяет набор классов MFC. DLL расширения должна использоваться тогда, когда вы хотите экспортировать из DLL основанные на МЕС классы или объекты классов MFC. DLL расширения также может экспортировать обычные функции и глобальные переменные.
Обычная DLL может использоваться, если вы хотите экспортировать только обычные функции или глобальные переменные, не являющиеся экземплярами классов МЕС.
Вы можете экспортировать классы из DLL расширения с помощью ключевого слова AFX_EXT_CLASS, предваряя им имя класса в DLL.
Вы можете экспортировать обычные функции и глобальные переменные из DLL, присваивая им атрибут dll export с ключевым словом _declspec.
Вы можете импортировать классы, экспортированные DLL расширения, применяя включение файла . h из DLL, содержащего определения классов с использованием ключевого слова AFX EXT CLASS.
Вы можете импортировать обычные функции и глобальные переменные, экспортированные из DLL, присваивая атрибут dll import к их объявлениям в вашей программе, используя ключевое слово deci spec.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
. Это последний шанс для усовершенствования этой версии программы Sketcher, так что воспользуйтесь им. Применяя созданную нами DLL, реализуйте браузер документов Sketcher — другими словами, программу, которая просто открывает документ, созданный программой Sketcher, и отображает его в окне целиком. Вам не нужно беспокоиться о редактировании, прокрутке и печати, но потребуется обрабатывать масштабирование, которое необходимо для помещения большой картинки в маленьком окне!
19
Подключениек источникам данных
В этой главе будет показано, как можно взаимодействовать с базой данных, используя Visual C++ и MFC для доступа к данным. Это, конечно, не стоит рассматривать как совершенно исчерпывающую и всестороннюю дискуссию обо всех существующих возможностях, поскольку полное описание разработки приложений баз данных с применением Visual C++ само по себе заняло бы достаточно объемную книгу. Однако в этой главе вы узнаете, как читать данные из базы, а в следующей главе будут изложены основы обновления данных в базе. Конечно, вы можете обращаться к источникам данных в приложениях CLR, и об этом речь пойдет в главе 22.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
Язык SQL и его применение.
Как извлекать данные оператором SQL SELECT.
Какие службы баз данных поддерживает MFC.
Что собой представляет объект набора записей и как он связывается с таблицей реляционной базы данных.
Как объект набора записей может извлекать информацию из базы.
Как представление записи может отображать информацию из набора записей.
Как создать проект для приложения баз данных.
Q Как добавить наборы записей к вашей программе.
Как обрабатывать многострочные представления записей.
Основы баз данных
Это не место для детальной диссертации по технологиям баз данных, но предварительно следует убедиться, что вы обладаете общим пониманием терминологии баз
данных. Существует широкое разнообразие баз данных, но в наши дни наибольшее распространение получили реляционные базы данных. Именно о них и пойдет речь в настоящей главе.
В реляционной базе ваши данные организованы в одну или более таблиц. Вы можете воспринимать таблицу базы как электронную таблицу, состоящую из строк и столбцов. Каждая строка содержит информацию о единственном элементе (записи), а каждый столбец — информацию о некоторых характеристиках каждого элемента.
Запись базы эквивалентна строке электронной таблицы. Каждая запись состоит из элементов данных, составляющих ее. Эти элементы данных известны как поля. Поле — это ячейка таблицы, идентифицируемая заголовком столбца. Термин поле также может означать целый столбец.
Вы можете лучше представить структуру таблицы, взглянув на диаграмму, показанную на рис. 19.1.
Здесь видно, что данная таблица служит для хранения информации о линейке продуктов. Поэтому не удивительно, что таблица называется таблицей продуктов. Каждая запись в таблице, представленная строкой на диаграмме, содержит данные об одном продукте. Описание продуктов разделено на поля таблицы, причем каждое поле хранит информацию об одном отдельном аспекте продукта: наименование продукта (Product Name), цена за единицу (Unit Price) и так далее.
Хотя поля этой таблицы хранят только относительно простую информацию (символьные строки или числовые значения), тип данных, который вы выберете для конкретного поля, может представлять почти все, что вы пожелаете. Вы можете хранить в базе времена, даты, изображения или даже двоичные объекты.
Таблица Products (Продукты)Таблица Products (Продукты)2222Каждый столбец таблицы идентифицирует поле строкиКаждый столбец таблицы идентифицирует поле строки10002кофе1.5010002хлеб0.5010003пирожное0.3010004чай1.2010005апельсины0.0510006яблоки0.1510007молоко0.3010002кофе1.5010002хлеб0.5010003пирожное0.3010004чай1.2010005апельсины0.0510006яблоки0.1510007молоко0.30Каждая строка определяет отношение, состоящее из набора связанных полейКаждая строка определяет отношение, состоящее из набора связанных полей113333
Таблица Products (Продукты)
Таблица Products (Продукты)
2
2
2
2
Каждый столбец таблицы идентифицирует поле строки
Каждый столбец таблицы идентифицирует поле строки
10002
кофе
1.50
10002
хлеб
0.50
10003
пирожное
0.30
10004
чай
1.20
10005
апельсины
0.05
10006
яблоки
0.15
10007
молоко
0.30
10002
кофе
1.50
10002
хлеб
0.50
10003
пирожное
0.30
10004
чай
1.20
10005
апельсины
0.05
10006
яблоки
0.15
10007
молоко
0.30
Каждая строка определяет отношение, состоящее из набора связанных полей
Каждая строка определяет отношение, состоящее из набора связанных полей
1
1
3
3
3
3
Обычно таблица имеет, по крайней мере, одно поле, которое может использоваться для уникальной идентификации каждой записи, и в приведенном выше примере таким полем, вероятно, может служить идентификатор продукта. Поле таблицы, служащее для идентификации каждой записи внутри таблицы, называется ключом; ключ, уникально идентифицирующий каждую запись таблицы, называется первичным ключом. В некоторых случаях могут использоваться два или более ключевых поля — в этом случае первичный ключ представляет комбинация этих полей.
Реляционный аспект базы данных и важность ключей проявляются, когда вы сохраняете реляционную информацию в отдельных таблицах. Вы определяете отношения между таблицами, используя ключи, и применяете эти отношения для нахождения ассоциированной информации, хранящейся в вашей базе данных. Обратите внимание, что таблицы сами по себе ничего не знают об отношениях, поскольку никак не интерпретируют биты данных, хранящиеся в них. Это задача программы, обращающейся к данным, которая может использовать информацию из таблиц для совместного извлечения взаимосвязанных данных — будь то Access, SQL Server или ваша собственная программа, написанная на C++. Все вместе это именуется системами управления реляционными базами данных, или СУРБД (relational database management system — RDBMS).
Реальные, хорошо спроектированные реляционные базы данных обычно состоят из большого количества таблиц. Каждая таблица обычно имеет только несколько полей и множество записей. Причина того, что таблицы ограничиваются лишь несколькими полями, связана с необходимостью повышения производительности. Не вдаваясь в подробности оптимизации баз данных, скажу лишь, что гораздо быстрее выполняются запросы ко многим таблицам с небольшим количеством полей, чем запросы к единственной таблице с множеством полей.
Давайте расширим представленный выше пример для иллюстрации реляционной базы данных с двумя таблицами: Products (Продукты) и Categories (Категории) из базы данных Northwind, как показано на рис. 19.2.
Таблица Products (Продукты)Таблица Products (Продукты)Таблица Categories (Категории)Таблица Categories (Категории)10001кофе1.50110002хлеб0.50210003пирожное0.30210004чай1.20110005апельсины0.05310006яблоки0.15310007молоко0.30110001кофе1.50110002хлеб0.50210003пирожное0.30210004чай1.20110005апельсины0.05310006яблоки0.15310007молоко0.301ЛЛ1напитки2штучный товар3фрукты1напитки2штучный товар3фрукты
Таблица Products (Продукты)
Таблица Products (Продукты)
Таблица Categories (Категории)
Таблица Categories (Категории)
10001
кофе
1.50
1
10002
хлеб
0.50
2
10003
пирожное
0.30
2
10004
чай
1.20
1
10005
апельсины
0.05
3
10006
яблоки
0.15
3
10007
молоко
0.30
1
10001
кофе
1.50
1
10002
хлеб
0.50
2
10003
пирожное
0.30
2
10004
чай
1.20
1
10005
апельсины
0.05
3
10006
яблоки
0.15
3
10007
молоко
0.30
1
Л
Л
1
напитки
2
штучный товар
3
фрукты
1
напитки
2
штучный товар
3
фрукты
Данные в этом поле могут использоваться для получения имени категории из таблицы Categories
Как видно на этой диаграмме, поле Category ID служит для связи информации, хранящейся в двух таблицах. Category ID уникально идентифицирует запись о категории в таблице Categories, поэтому оно является первичным ключом для этой таблицы. В таблице Products поле Category ID используется для связи записи о продукте с категорией, поэтому упомянутое поле здесь называется внешним ключом; внешние ключи не обязаны быть уникальными и часто таковыми не являются.
Реляционные базы данных могут создаваться и управляться различными способами. На рынке присутствует огромное количество СУРБД, которые предоставляют широкий набор возможностей для создания и манипуляции информацией баз данных. Очевидно, что вы можете добавлять и удалять записи в таблице базы, а также обновлять значения полей в записи, хотя обычно в СУРБД присутствуют средства, ограничивающие такую деятельность на основе уровня авторизации пользователя. Наряду с доступом к информации отдельной таблицы базы данных вы можете комбинировать записи из двух и более таблиц в новой таблице, основанной на их отношениях, а также извлекать информацию из них. Подобное комбинирование таблиц называется соединением таблиц (table join). Чтобы запрограммировать все эти операции в реляционной базе данных, вы можете использовать язык, известный как SQL и поддерживаемый большинством СУРБД и сред программирования.
Немного об SQL
SQL означает “Structured Query Language” (язык структурированных запросов). Это относительно простой язык, предназначенный специально для доступа и модификации информации в реляционных базах данных. Изначально он был разработан в компании IBM для сред универсальных вычислительных машин (мэйнфреймов), но в настоящее время используется в компьютерном мире повсеместно. SQL сам по себе не существует в виде программного пакета — обычно он “живет” в некоторой среде, будь то СУРБД либо библиотека, реализованная для языка программирования, такого как COBOL, С или C++. Среда, включающая в себя SQL, обеспечивает такие глобальные вещи, как обычный ввод-вывод и общение с операционной системой, в то время как SQL применяется только для запросов к базе данных.
Поддержка баз данных МЕС использует SQL для спецификации запросов и других операций над таблицами баз данных. Эти операции предоставляются набором специализированных классов. В примере, который мы разберем далее в настоящей главе, вы увидите, как используются некоторые из них.
Язык SQL включает в себя операторы для извлечения, сортировки и обновления записей таблицы, для добавления и удаления записей и полей, для соединения таблиц и вычисления результатов, наряду со многими другими возможностями создания и манипулирования таблицами базы. Я не стану погружаться в перечисление всех возможных опций программирования, доступных в SQL, но опишу ряд деталей, достаточных для того, чтобы вы могли понимать то, что происходит в примерах, которые вы напишете, даже если ранее вы никогда не имели дело с SQL.
Когда вы используете SQL в программах на основе МЕС, большей частью вам не придется писать полные операторы SQL, поскольку каркас МЕС позаботится о сборке полного оператора и применении его к используемому вами механизму базы данных. Тем не менее, я объясню, как пишутся типичные операторы SQL во всей их полноте, чтобы вы почувствовали, как они структурированы.
Операторы SQL обычно (хотя и не обязательно) пишутся с ограничителем — точкой с запятой (как и операторы C++), и по соглашению ключевые слова языка запи-
сываются заглавными буквами. Рассмотрим несколько примеров 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]. Позднее, когда в этой главе мы напишем программу, работающую с базой данных, вы увидите эту нотацию в действии.
Выбор записей
В отличие от полей, записи таблицы не имеют имен. Единственный способ выбрать определенные записи — это применить некоторое условие или ограничение содержимого одного или более полей в записи, чтобы выбраны были только записи, удовлетворяющие этому условию. Это делается добавлением конструкции 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 (Продукты)Таблица Products (Продукты)Таблица Orders (Заказы)Таблица Orders (Заказы)
Таблица Products (Продукты)
Таблица Products (Продукты)
Таблица Orders (Заказы)
Таблица Orders (Заказы)
10001
кофе
1.50
20001
10002
VEAD
50
10002
хлеб
0.50
20002
10003
TOMS
40
10003
пирожное
0.30
20003
10002
VEAD
30
Рис. 193. Таблицы Products и Orders
Здесь вы имеете ограниченный набор продуктов в таблице Products, включающий только кофе, хлеб и пирожное, и имеете три заказа, содержащиеся в таблице Orders, но ни один из них не оформлен на кофе.
Вы можете соединить эти таблицы с помощью следующего оператора SELECT:
SELECT * FROM Products, Orders;
Этот оператор создает результирующий набор, используя записи из двух указанных таблиц. Набор записей содержит семь полей — три из таблицы Products и четыре из таблицы Orders, но сколько записей он будет иметь? Ответ можно найти на рис. 19.4.
Таблица Products (Продукты)Таблица Orders (Заказы)Таблица Products (Продукты)Таблица Orders (Заказы)10001кофе1.5010002хлеб0.5010003пирожное0.3010001кофе1.5010002хлеб0.5010003пирожное0.302000110002VEAD502000210003TOMS402000310002VEAD302000110002VEAD502000210003TOMS402000310002VEAD30
Таблица Products (Продукты)Таблица Orders (Заказы)
Таблица Products (Продукты)Таблица Orders (Заказы)
10001
кофе
1.50
10002
хлеб
0.50
10003
пирожное
0.30
10001
кофе
1.50
10002
хлеб
0.50
10003
пирожное
0.30
20001
10002
VEAD
50
20002
10003
TOMS
40
20003
10002
VEAD
30
20001
10002
VEAD
50
20002
10003
TOMS
40
20003
10002
VEAD
30
10001
кофе
1.50
20001
10002
VEAD
50
10001
кофе
1.50
20002
10003
TOMS
40
10001
кофе
1.50
20003
10002
VEAD
30
10002
хлеб
0.50
20001
10002
VEAD
50
10002
хлеб
0.50
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, поэтому в него включены все возможные комбинации. Это может быть не совсем тем, что вы ожидали. Произвольное включение всех комбинаций записей из одной таблицы с записями другой таблицы должно как-то ограничиваться. Смысл записи, содержащей подробности о хлебе с заказом на пирожное, понять трудно. Вы также можете столкнуться с невероятно большим объемом таблиц в такой ситуации.
Если вы скомбинируете таблицу, содержащую 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 (Продукты)Таблица Products (Продукты)Таблица Orders (Заказы)Таблица Orders (Заказы)10001кофе1.5010002хлеб0.5010003пирожное0.3010001кофе1.5010002хлеб0.5010003пирожное0.302000110002VEAD502000210003TOMS402000310002VEAD302000110002VEAD502000210003TOMS402000310002VEAD3010001хлеб0.502000110002VEAD5010002хлеб0.502000310002VEAD3010003пирожное0.302000210003TOMS4010001хлеб0.502000110002VEAD5010002хлеб0.502000310002VEAD3010003пирожное0.302000210003TOMS40Результат операции соединения со следующим условием: Products."Product ID" = Orders."Product ID"Результат операции соединения со следующим условием: Products."Product ID" = Orders."Product ID"
Таблица Products (Продукты)
Таблица Products (Продукты)
Таблица Orders (Заказы)
Таблица Orders (Заказы)
10001
кофе
1.50
10002
хлеб
0.50
10003
пирожное
0.30
10001
кофе
1.50
10002
хлеб
0.50
10003
пирожное
0.30
20001
10002
VEAD
50
20002
10003
TOMS
40
20003
10002
VEAD
30
20001
10002
VEAD
50
20002
10003
TOMS
40
20003
10002
VEAD
30
10001
хлеб
0.50
20001
10002
VEAD
50
10002
хлеб
0.50
20003
10002
VEAD
30
10003
пирожное
0.30
20002
10003
TOMS
40
10001
хлеб
0.50
20001
10002
VEAD
50
10002
хлеб
0.50
20003
10002
VEAD
30
10003
пирожное
0.30
20002
10003
TOMS
40
Результат операции соединения со следующим условием: Products."Product ID" = Orders."Product ID"
Результат операции соединения со следующим условием: Products."Product ID" = Orders."Product ID"
Конечно, всего этого может оказаться недостаточно, поскольку вы получите два поля, содержащих идентификатор продукта, но от лишнего легко избавиться, если в операторе 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
При написании приложений баз данных с применением МЕС у вас есть выбор, поскольку здесь поддерживается два принципиальных подхода, описанные в табл. 19.1.
Таблица 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 (динамически подключаемую библиотеку), содержащую драйвер для работы с форматом файлов приложения 6213 данных. Назначение драйвера — служить интерфейсом для стандартного набора системно-независимых вызовов для операций с базами данных, которые будут использованы в вашей программе в соответствии со спецификой конкретной реализации базы.
Классы MFC для поддержки ODBC
Поддержка ODBC со стороны МЕС реализована в пяти классах, описанных в табл. 19.2.
Таблица 19.2. Классы MFC, реализующие поддержку ODBC
Класс
Описание
CDatabase
Объект этого класса представляет соединение с вашей базой данных. Это соединение должно существовать до того, как вы попытаетесь выполнить любую операцию с базой данных.
CRecordset
Объект класса, производного от этого, представляет результат выполнения SQL- оператора select, то есть набор записей. Этот объект делает доступной по одной записи за раз и предлагает функции для перемещения по набору вперед и назад.
CRecordView
Объект класса, производного от этого, используется для отображения текущей информации из ассоциированного объекта набора записей. Представление — это, по сути, диалоговое окно.
CFieldExchange
Этот класс обеспечивает обмен данных между базой и объектом набора записей. Вы должны использовать этот класс непосредственно только в том случае, если реализуете обмен данными пользовательских типов.
CDBException
Объекты этого класса представляют исключения, возникающие внутри операций с базами данных ODBC.
Чтобы лучше понять, как работают операции баз данных с 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.
ODBC Data Soiree Administrator
User DSN System DSN Rie DSN Drivers Tracing | Connection Pooling About
User Data Sources:
NameDriver
JBASE Files'
dBase Ries - Word Excel Ries
FoxPro Ries - Word
MQIS
MS Access Database 0chnical_librarySQ L Visual FoxPro Database Visual FoxPro Tables
Microsoft d Ease Driver ’.dbt, Microsoft dBase VFP Driver fdbf) Microsoft Excel Driver (Cxls) Microsoft FoxPro VFP Dover ' dbf SQL ServerMicrosoft Access Driver f .mdb) SQL ServerMicrosoft Visual FoxPro Driver Microsoft Visual FoxPro DriverMicrosoft d Ease Driver ’.dbt, Microsoft dBase VFP Driver fdbf) Microsoft Excel Driver (Cxls) Microsoft FoxPro VFP Dover ' dbf SQL ServerMicrosoft Access Driver f .mdb) SQL ServerMicrosoft Visual FoxPro Driver Microsoft Visual FoxPro DriverAn ODBC User data source stores information about how to connect to the indicated data provider. A User data source is only visible to you. and can only be used on the current machine.
Microsoft d Ease Driver ’.dbt, Microsoft dBase VFP Driver fdbf) Microsoft Excel Driver (Cxls) Microsoft FoxPro VFP Dover ' dbf SQL Server
Microsoft Access Driver f .mdb) SQL Server
Microsoft Visual FoxPro Driver Microsoft Visual FoxPro Driver
Microsoft d Ease Driver ’.dbt, Microsoft dBase VFP Driver fdbf) Microsoft Excel Driver (Cxls) Microsoft FoxPro VFP Dover ' dbf SQL Server
Microsoft Access Driver f .mdb) SQL Server
Microsoft Visual FoxPro Driver Microsoft Visual FoxPro Driver
ок
■Cancel
£г Ju
Help
Puc. 19.6. Диалоговое окно управления источниками данных ODBC
С eate ew Data SoireeС eate ew Data SoireeSelect a driver for which you want to set up a data source.Select a driver for which you want to set up a data source.NameMrasi^ Access Dri vet .n db‘ Microsoft Access-Treiber Cmdb) Microsoft dBase Driver (*.dbf) Microsoft dBase VFP Driver f* dbi; Microsoft dBase-Treiber (*.dbP Microsoft Excel Driver (*jds) Microsoft Excel-TreiberfCxls) Microsoft FoxPro VFP Driver f.dbf Microsoft ODBC for OracleNameMrasi^ Access Dri vet .n db‘ Microsoft Access-Treiber Cmdb) Microsoft dBase Driver (*.dbf) Microsoft dBase VFP Driver f* dbi; Microsoft dBase-Treiber (*.dbP Microsoft Excel Driver (*jds) Microsoft Excel-TreiberfCxls) Microsoft FoxPro VFP Driver f.dbf Microsoft ODBC for OracleFinishFinishCancelCancelPuc. 19.7. Диалоговое окно создания нового источника данныхЗдесь потребуется выбрать из списка драйверов ODBC тот, который вы собираетесь использовать, в данном случае — Microsoft Access Driver (*.mdb) (или, если вы работаете с SQL Server, то его драйвер). Эти драйверы должны быть инсталлированы автоматически при типичной установке Windows ХР. Если вы не видите в списке нужный драйвер, вам следует вернуться к установке Windows и инсталлировать его. Выбрав драйвер, щелкните на кнопке Finish (Готово). Это откроет еще одно диалоговое окно, показанное на рис. 19.8.Puc. 19.7. Диалоговое окно создания нового источника данныхЗдесь потребуется выбрать из списка драйверов ODBC тот, который вы собираетесь использовать, в данном случае — Microsoft Access Driver (*.mdb) (или, если вы работаете с SQL Server, то его драйвер). Эти драйверы должны быть инсталлированы автоматически при типичной установке Windows ХР. Если вы не видите в списке нужный драйвер, вам следует вернуться к установке Windows и инсталлировать его. Выбрав драйвер, щелкните на кнопке Finish (Готово). Это откроет еще одно диалоговое окно, показанное на рис. 19.8.Щелкнув на кнопке Add (Добавить), вы увидите диалоговое окно Create New Data Source (Создание нового источника данных), показанное на рис. 19.7.
С eate ew Data Soiree
С eate ew Data Soiree
Select a driver for which you want to set up a data source.
Select a driver for which you want to set up a data source.
Name
Mrasi^ Access Dri vet .n db‘ Microsoft Access-Treiber Cmdb) Microsoft dBase Driver (*.dbf) Microsoft dBase VFP Driver f* dbi; Microsoft dBase-Treiber (*.dbP Microsoft Excel Driver (*jds) Microsoft Excel-TreiberfCxls) Microsoft FoxPro VFP Driver f.dbf Microsoft ODBC for Oracle
Name
Mrasi^ Access Dri vet .n db‘ Microsoft Access-Treiber Cmdb) Microsoft dBase Driver (*.dbf) Microsoft dBase VFP Driver f* dbi; Microsoft dBase-Treiber (*.dbP Microsoft Excel Driver (*jds) Microsoft Excel-TreiberfCxls) Microsoft FoxPro VFP Driver f.dbf Microsoft ODBC for Oracle
Finish
Finish
Cancel
Cancel
Puc. 19.7. Диалоговое окно создания нового источника данных
Здесь потребуется выбрать из списка драйверов ODBC тот, который вы собираетесь использовать, в данном случае — Microsoft Access Driver (*.mdb) (или, если вы работаете с SQL Server, то его драйвер). Эти драйверы должны быть инсталлированы автоматически при типичной установке Windows ХР. Если вы не видите в списке нужный драйвер, вам следует вернуться к установке Windows и инсталлировать его. Выбрав драйвер, щелкните на кнопке Finish (Готово). Это откроет еще одно диалоговое окно, показанное на рис. 19.8.
Puc. 19.7. Диалоговое окно создания нового источника данных
Здесь потребуется выбрать из списка драйверов ODBC тот, который вы собираетесь использовать, в данном случае — Microsoft Access Driver (*.mdb) (или, если вы работаете с SQL Server, то его драйвер). Эти драйверы должны быть инсталлированы автоматически при типичной установке Windows ХР. Если вы не видите в списке нужный драйвер, вам следует вернуться к установке Windows и инсталлировать его. Выбрав драйвер, щелкните на кнопке Finish (Готово). Это откроет еще одно диалоговое окно, показанное на рис. 19.8.
ODBC Microsoft Access SetupODBC Microsoft Access SetupРис. 19.8. Диалоговое окно настройки драйвера Microsoft AccessРис. 19.8. Диалоговое окно настройки драйвера Microsoft AccessВведите имя файла базы данных в поле Data Source Name (Имя источника данных), которым в этом случае должно быть Northwind. Вы используете это имя для идентификации базы данных при генерации приложения с помощью мастера Application Wizard. Затем вы должны щелкнуть на кнопке Select (Выбрать), чтобы перейти к финальному диалоговому окну, которым должно быть окно Select Database (Выбор базы данных), где вы сможете выбрать файл в том каталоге, где он хранится. Последнее диалоговое окно для выбора базы данных показано на рис. 19.9.
ODBC Microsoft Access Setup
ODBC Microsoft Access Setup
Рис. 19.8. Диалоговое окно настройки драйвера Microsoft Access
Рис. 19.8. Диалоговое окно настройки драйвера Microsoft Access
Select DatabaseDatabase NameSelect DatabaseDatabase Name‘.Norths sampdata.mdb technical_librarv.mdb technical_library_V7.mt‘.Norths sampdata.mdb technical_librarv.mdb technical_library_V7.mtList files of Type:List files of Type:Directories
Select Database
Database Name
Select Database
Database Name
‘.Norths sampdata.mdb technical_librarv.mdb technical_library_V7.mt
‘.Norths sampdata.mdb technical_librarv.mdb technical_library_V7.mt
List files of Type:
List files of Type:
d:\...Xmodel access db— —
Cancel &d:\
P^ Beginning Visual Cto Model Access DE
Г Read Only
Exclusive
Drives:
PSTData- ▼ Network... |
Puc. 19.9. Диалоговое окно выбора базы данных
В конце последовательно щелкните на трех кнопках ОК, в результате чего ваша база данных будет зарегистрирована. Если что-то будет выглядеть не так, нужно будет обратиться к справочной системе или поэкспериментировать с опцией ODBC в панели управления. Истина где-то рядом.
Когда все получится, вы сможете двигаться дальше в разработке приложения базы данных и, как всегда, начальной точкой будет выбор пункта меню File^New^Project (Файл^Создать^Проект) в Visual C++ 2005 либо просто нажатие комбинации клавиш <Ctrl+Shift+N>.
Генерация программы MFC ODBC
Обычным образом создайте новый проект МЕС на основе шаблона 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.
Рис. 19.11. Диалоговое окно Select Data SourceРис. 19.11. Диалоговое окно Select Data Source
Рис. 19.11. Диалоговое окно Select Data Source
Рис. 19.11. Диалоговое окно Select Data Source
После выбора базы данных и щелчка на кнопке ОК отображается диалоговое окно Login (Вход) для этой базы данных. В нем необходимо ввести регистрационное имя и пароль, чтобы открыть базу данных. Когда вы щелкнете на кнопке ОК, то увидите диалоговое окно Select Database Object (Выбор объекта базы данных), показанное на рис. 19.12, в котором потребуется выбрать объекты базы данных, доступ к которым нужен.
OKOKCancelCancelSelect Database Object
OK
OK
Cancel
Cancel
- Tables
01 Categories Ц Customers
§ Employees О Order Details О Orders
products?
Q Shippers
s Suppliers
* B® Views
Разверните узел 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 (Готово) и сгенерируйте проект.
* *FC Application Wizard - DBSample
Рис. 19.13. Классы, сгенерированные мастером для приложения
Структура программы
Базовая структура программы остается такой же, как вы видели ранее — с классом приложения CSBSampleApp, классом обрамляющего окна CMainFrame, классом документа 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)
// Плюс прочая часть определения класса...
/ / Переопределения
/ / Переопределения сгенерированных мастером виртуальных функции public:
virtual CString GetDefaultConnect () ; / / Строка подключения по умолчанию virtual CString GetDefaultSQL() ; //SQL-код по умолчанию для набора записей virtual void DoFieldExchange(CFieldExchange* pFX);// Поддержка RFX
// Плюс некоторая другая стандартная часть };
Этот класс имеет в качестве базового CRecordset и обеспечивает функциональность извлечения данных из базы. Конструктор класса принимает указатель на объект CDatabase, по умолчанию установленный в NULL. Параметр конструктора позволяет объекту CProductset создаваться для уже существующего объекта CDatabase, который позволяет использовать существующее соединение с базой данных. Открытие соединения с базой данных — длительная операция, поэтому когда это возможно, выгодно повторно использовать существующие соединения.
Если конструктору не передается никакой указатель, как в случае члена m_DB Sample Set класса документа CDBSampleDoc, то каркас автоматически создает для вас объект CDatabase и вызывает функцию-член CProductset GetDefaultConnect () для определения соединения. Мастер Application Wizard предлагает следующую реализацию этой функции:
CString CProductset::GetDefaultConnect() {
return _T(
”DSN=Northwind;
DBQ=D:\\Beg Visual C++ 2005\\Model Access DBWNorthwind.mdb; Driverld=25;
FIL=MS Access;
MaxBufferSize=2048;
PageTimeout=5;
UID=admin;");
}
Функция GetDefaultConnect () — пустая виртуальная функция базового класса CRecordset, а потому должна всегда быть реализована в классе-наследнике набора записей. Возвращенное функцией значение — это одиночная строка, заключенная в двойные кавычки, но я показал ее разбитой на несколько строк, чтобы сделать ее содержимое более ясным. Реализация, предоставленная мастером Application Wizard, возвращает показанную строку каркасу. Она идентифицирует базу данных с ее именем и путем, а также значениями прочих параметров, которые вы можете видеть, и позволяет каркасу создавать объект CDatabase, устанавливающий соединение с базой данных автоматически. Описание аргументов строки соединения приведено в табл. 19.3.
На практике обычно для доступа к базе данных также необходимо указывать пароль наряду с идентификатором пользователя, однако неразумно помещать пароль в код в открытой форме. По этой причине мастер Application Wizard вставляет следующую строку, предшествующую определению функции GetDefaultConnect ():
#error Безопасность: строка соединения может содержать пароль
При наличии этой директивы в коде компиляция завершается ошибкой, поэтому вы должны закомментировать ее либо удалить, чтобы можно было успешно скомпилировать программу.
Таблица 19.3. Аргументы строки соединения
Аргумент
Описание
DSN
Имя источника данных.
DBQ
Квалификатор базы данных, в данном случае — путь к файлу базы данных Access.
DriveriD
Идентификатор драйвера ODBC для базы данных.
FIL
Тип файла базы данных.
MaxBufferSize
Максимальный размер буфера, используемого для обмена данными с базой.
PageTimeout
Длительность времени ожидания соединения с базой в секундах. Важно установить это значение в адекватную величину, чтобы избежать сбоев соединения при доступе к удаленной базе.
UID
Идентификатор пользователя для доступа к базе данных.
Вы можете заставить каркас вывести всплывающее диалоговое окно для пользователя, чтобы он мог выбрать имя базы данных из списка зарегистрированных источников данных, переписав оператор return в функции GetDefaultConnection () следующим образом:
return _T(”ODBC;'*);
Также при попытке доступа к базе данных вам будет предложено ввести идентификатор пользователя и пароль.
Выполнение запроса к базе данных
Класс CProductset включает член данных для каждого поля таблицы Products. Мастер Application Wizard получает имена полей из базы данных и использует их для именования соответствующих данных-членов класса. Они появляются в блоке кода, следующем за комментарием Данные полей/параметров в определении класса CProductset.
class CProductSet : public CRecordset
(
public:
CProductset(CDatabase* pDatabase = NULL);
DECLARE_DYNAMIC(CProductset)
/ / Данные полей/параметров
U Строковые типы ниже (если есть) отражают фактические типы данных
И позгеи. базы - CStringA для типов данных ANSI и CStringW для типов
// данных Unicode. Это необходимо для того, чтобы предотвратить выполнение
// драйвером ODBC потенциально ненужных преобразований.
// Если хотите, можете изменить типы этих членов на CString
//и тогда драйвер ODBC будет выполнять все необходимые
// преобразования.
/ / (Примечание: для поддержки этих, а также Uni code-преобразований необходимо
// использовать драйвер ODBC версии 3.5 и новее.)
long m_ProductID; // Номер, автоматически присваиваемый каждому продукту. CStringW m__ProductName;
long m_SupplierID; И Некоторое вхождение в таблицу Suppliers.
long m__CategoryID; // Некоторое вхождение в таблицу Categories.
CStringW m_QuantityPerUnit; // (например, случай с упаковкой из 24
// литровых бутылок) .
double m UnitPrice;
int m_UnitsInStock;
int m__UnitsOnOrder ;
int m_ReorderLevel; // Минимальный запас на складе.
BOOL m_Discon tinned; // Да означает, что элемент уже недоступен.
/ / Переопределения
// Переопределения сгенерированных мастером виртуальных функций
public:
virtual CString GetDefanltConnect () ; / / Строка соединения по умолчанию virtual CString GetDefaultSQL (); //SQL-код по умолчанию для набора записей virtual void DoFieldExchange (CFieldExchange* pFX) ; И Поддержка RFX
/ / Реализация
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& de) const;
#endif
Тип каждого члена данных устанавливается согласно типу соответствующего поля в таблице Products. На практике вам могут и не понадобиться все эти поля, но, хотите или нет, вы не должны удалять их из определения класса. Как вы вскоре убедитесь, ссылки на них появляются и в ряде других мест, так что в этом случае вам придется удалять их вручную и там. Еще одно предупреждение заключается в том, что вы не должны удалять первичные ключи. Если это сделать, то набор записей не будет работать, так что вы должны выяснить, какие поля служат первичными ключами, прежде чем нарушать то, что нарушать не желательно.
Обратите внимание, что два поля имеют тип CStringW. Вы еще не сталкивались с ним ранее, поэтому знайте, что класс CStringW инкапсулирует в себе строку Unicode вместо строки ASCII. Вам будет удобнее обращаться к полям, используя тип CString, поэтому измените тип членов m_ProductName и m_QuantityPerUnit на CString. Это позволит в данном примере обрабатывать строки как ASCII. Ясно, что если вы собираетесь интернационализировать приложения баз данных, то вам придется поддерживать поля CStringW, поскольку они могут содержать символы, не входящие в набор символов ASCII.
Операция SQL, применяемая к набору записей для наполнения этих членов данных, специфицирована в функции GetDefaultSQL (). Реализация, предложенная мастером Application Wizard, выглядит так:
CString CProductSet::GetDefaultSQL()
{
return _T(”[Products]");
}
Возвращаемая строка, очевидно, создана на основе имени выбранной таблицы во время создания проекта. Квадратные скобки включены для обеспечения возможности применения пробелов в имени таблицы. Если вы выбираете несколько таблиц в процессе создания проекта, они должны вставляться здесь и разделяться запятыми, причем имя каждой таблицы заключается в квадратные скобки.
Функция GetDef aultSQL () вызывается каркасом MFC, когда он конструирует оператор SQL, применяемый к набору записей. Каркас вставляет строку, возвращенную этой функцией, в скелетный оператор SQL следующей формы:
SELECT * FROM < Строка, возвращенная GetDefaultSQL() >;
Это выглядит упрощением, и в самом деле таковым является, но, как вы вскоре увидите, позднее к операции можно добавить конструкции WHERE и ORDER BY.
Передача данных между базой данных и набором записей
Передача данных от базы к набору записей и обратно обеспечивается членом класса CProductset — функцией DoFileExchange (). Ее реализация выглядит показана ниже.
void CProductset::DoFieldExchange(CFieldExchange* pFX) {
pFX->SetFieldType(CFieldExchange::outputcolumn);
Н Такие макросы, как RFX_Text() и RFX_Int(), зависят от типа // переменной-члена, а не от типа поля таблицы базы данных.
// ODBC попытается автоматически преобразовать значение столбца II к запрошенному типу.
RFX_Long(pFX, _Т(”[ProductID] ”), m_ProductID);
RFX_Text(pFX, _T(”[ProductName]”), m_ProductName);
RFX_Long(pFX, _T(”[SupplierlD]”), m_SupplierID);
RFX_Long(pFX, _T(”[CategorylD]”), m_CategoryID);
RFX_Text(pFX, _T(”[QuantityPerUnit]”), m_QuantityPerUnit);
RFX_Double(pFX, _T("[Unitprice]”), m_UnitPrice);‘ RFX_Int(pFX, _T (" [UnitsInStock]’’) , m_Units!nStock); 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)
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(CDumpContext& de) const;
#endif
protected:
// Сгенерированные функции карты сообщений protected:
DECLARE_MESSAGE_MAP()
};
Класс представления для набора записей всегда должен наследоваться, поскольку он должен подстраиваться для отображения конкретных полей из требуемого набора. Базовый класс CRecordView включает всю функциональность, необходимую для управления взаимодействием с наборами записей. Все, что вам нужно сделать — это подогнать класс представления записи под нужды вашего приложения. Вскоре мы вернемся к этому вопросу.
Обратите внимание, что конструктор является protected. Это связано с тем, что объекты данного класса создаются только через сериализацию, что является предпосылкой по умолчанию для классов представления записи. Когда вы добавляете новые классы представления записей к приложению, вам необходимо изменить доступ к их конструкторам по умолчанию на public, поскольку вы создадите представления самостоятельно.
В первом блоке public этого класса перечисление добавляет идентификатор IDD_DBSAMPLE_FORM в качестве члена класса. Это идентификатор пустого диалога, который мастер Application Wizard должен включить в программу. Вы добавите элементы управления к этому диалогу для отображения полей базы данных из таблицы Products, которые хотите отобразить. Идентификатор диалога передается базовому классу CRecordView в списке инициализации конструктора класса представления:
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 показано, как данные из базы в конечном итоге отображаются в представлении.
1111Таблица базы данныхТаблица базы данныхОбъект представленияОбъект представления
1
1
1
1
Таблица базы данных
Таблица базы данных
Объект представления
Объект представления
Объект набора записейОбъект набора записейDoFieldExchange() — член объекта CRecordset, передающий данные между базой и набором записейDoFieldExchange() — член объекта CRecordset, передающий данные между базой и набором записейРис. 19.14. Отображение данных в представлении записейРис. 19.14. Отображение данных в представлении записей
Объект набора записей
Объект набора записей
DoFieldExchange() — член объекта CRecordset, передающий данные между базой и набором записей
DoFieldExchange() — член объекта CRecordset, передающий данные между базой и набором записей
Рис. 19.14. Отображение данных в представлении записей
Рис. 19.14. Отображение данных в представлении записей
DoDataExchange() — член объектапредставления, передающийданные между наборомзаписей и представлением
Передача данных между данными-членами в объекте CProductset, которые соответствуют полям таблицы Products, и элементами управления диалогового окна, ассоциированного с объектом CProductView, управляется членом DoDataExchange () класса CProductView. Код этой функции пока отсутствует, поскольку сначала потребуется добавить элементы управления к диалогу, которые должны отображать данные, и затем связать элементы управления с членами данных набора записей.
Создание диалога представления
Первый шаг состоит в размещении элементов управления в диалоговом окне, поэтому обратитесь в панель Resource View, разверните список диалоговых ресурсов и дважды щелкните на Idd_Dbsample_Form. Вы можете удалить из диалога объект статического текста с сообщением TODO. Если выполнить щелчок правой кнопкой мыши на диалоговом окне, вы увидите его свойства, как показано на рис. 19.15.
Если вы пройдетесь вниз по списку свойств, то увидите, что свойство Style (Стиль) установлено в Child (Дочерний), поскольку диалог должен быть дочерним окном и заполнять клиентскую область. Свойство Border (Рамка) установлено в None (Нет), поскольку если диалог заполняет клиентскую область, он не нуждается в рамке.
Вы должны добавить статический текстовый элемент управления для идентификации каждого поля из набора записей, которое вы хотите отобразить, а также редактирующий элемент управления для его отображения.
Можете при необходимости увеличить диалог, перетаскивая его границы. Затем поместите элементы управления на его поверхность, как показано на рис. 19.16.
Рис. 19.15. Окно свойств диалога
Idd_Dbsampl e_Form
Рис. 19.16. Размещение элементов управления в диалогеРис. 19.16. Размещение элементов управления в диалоге
Рис. 19.16. Размещение элементов управления в диалоге
Рис. 19.16. Размещение элементов управления в диалоге
4 х4 хВы можете добавить текст к каждому статическому элементу управления, просто набирая его на клавиатуре сразу после помещения этого элемента в диалог. Я ввел текст для каждого статического элемента, чтобы он соответствовал имени каждого поля базы данных. Хорошей идеей будет присвоить каждому элементу осмысленные и различные идентификаторы, поэтому щелкните правой кнопкой мыши на каждом из них и модифицируйте их свойства. На рис. 19.17 показаны свойства элемента, соответствующего долю Product ID.
4 х
4 х
Client Edge
False
Disabled
False
Group
False
Help ID
False
orizuntal Scroll
ID
IDC_PRODUCTID
Left Scrollbar
False
Lowercase
False
Modal Frame
False
Multiline
False
No Hide Selection
False
Number
False
OEM Convert
False
Password
False
Read Only
True
Right Align Text
False
Right To Left Reading ( False
Static Edge
False
Tabstop
True
Transparent
False
Uppercase
False
Tical Scr
Visible
True
Want Return
False
Read Only
Prevents user from t/ping or editing text in the edit control.
Properties▼
IDC PRODGCTID (Edit Control) IHdBoxEditor
Puc. 19.17. Окно свойств элемента,соответствующего тюлю Product ID
Полезно использовать имя поля в качестве части идентификатора (ID) элемента, поскольку это указывает, что должен отображать данный элемент. На рис. 19.17 показан идентификатор первого редактирующего элемента в заголовке окна свойств по-
еле модификации. Вы можете изменить идентификаторы остальных редактирующих элементов аналогичным образом. Поскольку обновление базы данных в настоящем примере не планируется, необходимо обеспечить, чтобы данные, отображаемые в каждом поле редактирования, не могли изменяться с клавиатуры. Вы можете обеспечить это, установив свойство Read Only (Только для чтения) для каждого их элементов редактирования в значение True. Фон таких полей редактирования будет иметь другой цвет, указывая на то, что текст в них не может быть изменен (рис. 19.18).
При желании можно добавить в диалоговое окно и другие поля. Наиболее важное из них для последующего развития нашего примера — это Product ID, поэтому включите его. Сохраните диалоговое окно и затем перейдите к последнему шагу: связыванию элементов управления с переменными в классе набора записей.
Рис. 19.18. Редактирующие элементы управления, предназначенные только для чтения
Связывание элементов управления с набором записей
Как вы видели ранее на рис. 19.14, получение данных из набора записей для отображения соответствующим элементом управления — это работа для функции DoDataExchange () из класса CProductView. Член m pSet обеспечивает средства доступа к членам объекта CProductset, содержащего поля, извлеченные из базы данных, поэтому связать элементы управления с CProductset достаточно просто. В МЕС определена группа функций DDX Field в глобальном контексте, специально предназначенных для передачи данных между представлением и набором записей. В частности, функция DDX_FieldText () имеет перегруженные версии, передающие широкое разнообразие типов данных между полем набора записей и полем редактирования в объекте CRecordView. Ниже перечислены типы, которые могут быть переданы функцией DDX_FieldText().
short int
UINT
long
DWORD
float
double
CString
COleDateTime
COleCurrency
При вызове функции 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, DDX_FieldText(pDX, DDX_FieldText(pDX, DDX_FieldText(pDX, DDXFieldText (pDX, DDX^FieldText (pDX,DDX_FieldText (pDX, DDX_FieldText(pDX, DDX_FieldText(pDX, DDX_FieldText(pDX, DDXFieldText (pDX, DDX^FieldText (pDX,IDCFRODUCTID, mFSet->mFroductID, mjpSet) ;
DDX_FieldText (pDX, DDX_FieldText(pDX, DDX_FieldText(pDX, DDX_FieldText(pDX, DDXFieldText (pDX, DDX^FieldText (pDX,
DDX_FieldText (pDX, DDX_FieldText(pDX, DDX_FieldText(pDX, DDX_FieldText(pDX, DDXFieldText (pDX, DDX^FieldText (pDX,
IDC^PRODUCTNAME, m_pSet->mFroductNaine, m^pSet) ;
IDC__UNITPRICE, mFSet->mFnitPrice f mjpSet) ;
IDCJUNITSINSTOCK, mjpSet->m_UnitsInStock, mjpSet) ;
IDC__CATEGORYID, mjpSet->m_CetegoryID, mjpSet) ;
IDCJUNITSONORDER, mFSet~>mFnitsOnOrder, m_pSet) ;
Программный механизм для передачи данных между базой и диалоговым окном, которым владеет объект CProductView, проиллюстрирован на рис. 19.19.
Класс набора записей и класс представления записей кооперируются для обеспечения передачи данных между базой и элементами управления диалогового окна. Класс CProductset обрабатывает передачу данных между базой и своими данными-члена- ми, a CProductView имеет дело с передачей между данными-членами CProductset и элементами управления диалога.
Тестирование примера
Верите или нет, но вы уже можете запустить этот пример. Для этого просто соберите его обычным образом и затем выполните. Приложение должно отобразить окно, подобное тому, что показано на рис. 19.20.
Базовый класс CRecordView автоматически реализует кнопки панели инструментов, которые позволят передвигаться от одной записи в наборе к следующей или предыдущей. Есть также кнопки панели для перемещения непосредственно к первой или последней записи в наборе. Конечно, продукты отображаются в последовательности по умолчанию. Было бы хорошо получать их отсортированными по категориям и по идентификаторам в рамках каждой категории. Ниже вы увидите, как это можно сделать.
в DoFieldExchangeO
s о38 2 CDs о38 2 CDФ О) С со i S со О о оФ О) С со i S со О о о
s о
3
8 2 CD
s о
3
8 2 CD
Ф О) С со i S со О о о
Ф О) С со i S со О о о
Рис. 19.19. Передача данных между базой и диалоговым окном
Сортировка набора записей
Как было показано ранее, данные, извлекаются из базы набором записей посредством SQL-оператора SELECT, сгенерированного каркасом с использованием члена GetDefaultSQL (). Вы можете добавить конструкцию ORDER BY к сгенерированному оператору, установив значение члена m_strSort класса CProductset, унаследованного от CRecordSet. Это отсортирует выходную таблицу запроса на основе строки, хранящейся в strSort. Вам достаточно только установить правильное значение члена str Sort, указав в нем имя поля либо имена полей, по которым требуется выполнить сортировку, каркас добавит необходимые ключевые слова ORDER BY. В случае указания нескольких имен полей разделяйте их запятыми. Но куда должен быть добавлен код?
Передача данных между базой и набором записей происходит при вызове члена Open () класса набора записей. В вашей программе функция-член Open () объекта набора записей вызывается в члене OnlnitialUpdate () базового класса представления — CRecordView (). Таким образом, вы можете поместить код для установки спецификации сортировки в член OnlnitialUpdate () класса CProductView, как показано ниже:
void CProductView::OnlnitialUpdate () {
mjpSet = &GetDocument()->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 = "[CategorylD],[ProductID]”; //Установить поля сортировки CRecordView::OnlnitialUpdate();
/ / Установить заголовок окна в имя таблицы
if (m_pSet~>IsOpen()) М Убедиться, что набор записей открыт {
CString strTitle = __Т("Table Name”); И Установить базовую строку заголовка CString str Table = m_pSet->GetTableName () ;
if(!strTable.IsEmpty())// Убедиться, что мы имеем имя таблицы
strTitle += _Т(”: ”) + strTable; // и добавить к базовой таблице
GetDocument() ->SetTitie (strTitle) ; // Установить заголовок документа }
}
После проверки того, что набор записей действительно открыт, вы инициализируете локальный объект CString базовой строкой заголовка. Затем вы получаете имя таблицы от объекта набора записей вызовом функции-члена GetTableName (). При этом могут возникнуть различные ситуации, которые помешают установить имя таблицы. Например, в наборе записей может присутствовать более одной таблицы. После добавления к базовому заголовку в strTitle двоеточия, за которым следует имя извлекаемой таблицы, вы устанавливаете результат как заголовок документа, вызывая функцию-член документа SetTitle ().
Если вы заново соберете приложение и вновь запустите его, оно будет работать, как и раньше, но с новым заголовком окна (рис. 19.21). Значение Product ID следуют в убывающем порядке в пределах каждого значения Category ID, причем Category ID также упорядочены.
Table апле:[Products - DBSample U
Рис. 19.21. Добавление заголовка, отображающего имя таблицы
Использование второго объекта набора записей
Теперь, когда вы можете видеть все продукты в базе данных, разумным усовершенствованием программы будет добавление возможности видеть заказы по каждому конкретному продукту. Для этого потребуется добавить еще один класс набора записей, чтобы обработать информацию об адресах, и дополнительный класс представления для отображения некоторых полей из этого набора. Кроме того, вы добавите кнопку к диалогу Products, позволяющую переключаться на диалог Orders, когда нужно просмотреть заказы текущего продукта. Это позволит работать в окружении, показанном на рис. 19.22.
Диалоговое окно Products — начальная точка, от которой вы можете двигаться вперед и назад по всем доступным продуктам. Щелчок на кнопке Show Orders (Показать заказы) переключает на диалог, в котором вы можете видеть заказы текущего продукта. Вы можете вернуться к диалоговому окну Products, щелкнув на кнопке Show Products (Показать продукты).
Рис. 19.22. Усовершенствование приложения просмотра базы данных
Добавление класса набора записей
Начать следует с добавления класса набора записей для заказов; щелкните правой кнопкой мыши на DBS ample в Class View и выберите Add1^ Class (Добавить*^ Класс) из контекстного меню. Выберите 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.
Рис. 19.23. Мастер потребителей MFC ODBC
Рис. 19.24. Выбор таблиц, в диалоговом окне Select Database Object
Теперь нужно выбрать две таблицы для ассоциирования с новым классом набора записей, который вы собираетесь создать, поэтому удерживайте нажатой клавишу <Ctrl> и щелкайте на именах таблиц Orders и Order Details. Затем щелкните на кнопке ОК для завершения процесса выбора. Это вернет вас в диалоговое окно мастера MFC ODBC Consumer Wizard, где вы увидите имя класса и введенные имена файлов. Можете изменить имя класса на COrderset и, соответственно, имена файлов, как показано на рис. 19.25. Щелчок на кнопке Finish завершит процесс и вызовет генерацию класса COrderset.
Как вы видели в классе CProductset, который создавали как часть начального проекта, реализация функции GetDefaultConnect () класса COrderset предварена директивой #еггог, предотвращающей компиляцию, так что закомментируйте ее.
Член данных в классе COrderset создается для каждого поля каждой таблицы. Когда для определенного набора записей вы выбираете две или более таблиц, то всегда возможно, и даже вероятно, что имена полей будут дублироваться; поле Order ID, например, присутствует в обеих таблицах. Чтобы гарантировать отличие имен членов данных, соответствующих полям, они снабжаются префиксами — именами таблиц. Если вам не нужны все эти поля, можете удалить или закомментировать любые из них, но как уже говорилось ранее, вы не должны удалять переменные, составляющие первичные ключи. Удалив член данных, отвечающий за поле таблицы, вы также должны удалить его инициализацию в конструкторе класса и его вызов RFX_ () в функции-члене DoFieldExchange (). Вы также должны изменить начальное значение члена m_nFields в конструкторе COrderset, чтобы он отражал количество полей, оставшихся в классе. Данные-члены, которые вам понадобятся в данном примере: m_OrdersOrderID, m_OrderDetailsOrderID, m_OrderDetailsProductID, m_OrderDetailsQuantity и m_OrdersCustomerID. Если вы оставите только их, то должны изменить значение m nFields на 5. Измените типы всех членов с CstringW на CString.
Чтобы связать новый набор записей с документом, вам понадобится добавить член данных к определению класса С 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 {
// Остальная часть определения класса }
Добавление класса представления для набора записей
На этом этапе вы можете ожидать добавления класса, унаследованного от CRecordView, с использованием пункта Add1^Class (Добавить1^Класс) из контекстного меню для отображения данных из объекта COrderset. Это было верно для ранних версий Visual C++, но, к сожалению, Visual C++ 2005 не предоставляет такой возможности. Диалоговое окно для добавления нового класса вообще не позволяет выбирать CRecordView в качестве базового класса, так что вам всегда придется вручную создавать классы, унаследованные от CRecordView.
Прежде чем создавать класс представления, вы должны создать еще один ресурс диалога, чтобы иметь идентификатор ресурса, который можно использовать в определении класса представления.
Создание ресурса диалога
Переключившись на вкладку 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) редактирующих элементов управления, чтобы они отражали назначение этих элементов. Вы можете использовать имена полей таблицы с префиксом — именем
таблицы для установки соответствия имен данных-членов. И, наконец, вы должны сделать редактирующие элементы управления доступными только для чтения, установив свойство Read Only в True. Альтернативно вы можете установить их доступными только для чтения в один прием, выбрав их все при нажатой клавише <Ctrl> и затем установив свойство Read Only в True.
Элемент управления — кнопка, помеченная как Show Products (Показать продукты), используется для возврата к представлению таблицы Products, поэтому измените ID этой кнопки на IDC_PRODUCTS. Когда вы упорядочите все по своему усмотрению, сохраните ресурс диалога.
Рис. 19.26. Элементы управления для класса COrder set
Создание класса представления записей
Создайте файл 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(CDumpContext& de) const;
#endif
Этот код основан на сгенерированном CProductView. Макрос DECLARE_DYNCREATE позволяет каркасу MFC создавать объекты класса во время выполнения. В обычном документе MFC, в представлении и в классе обрамляющего окна должен присутствовать этот макрос. Дополняющий его макрос IMPLEMENT DYNCREATE вы добавите в файл . срр позднее. Я пропустил отладочную версию Get Do cume nt (), поскольку класс 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 () связывает элементы управления в диалоге с полями набора записей. Определение этой функции выглядит следующим образом:
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, первый аргумент определяет, передаются ли данные в элемент управления либо из него. Последний аргумент просто идентифицирует набор записей, участвующий в процессе.
Две функции должны быть определены для участия в процессе извлечения набора записей. Вы вызовете функцию Get Re cordSet () для получения указателя на объект COrderset, инкапсулирующий набор записей. Ее можно реализовать следующим образом:
COrderSet* COrderView::GetRecordset()
{
ASSERT(m_pSet !=NULL);
return m_pSet;
}
Член m_pSet содержит указатель на набор записей. Макрос МЕС по имени ASSERT здесь прерывает программу с сообщением, если выражение между скобками дает в результате вычисления 0. То есть, он просто проверяет, что указатель на COrderset не равен NULL. Макрос ASSERT обладает тем преимуществом, что работает только в отладочной версии приложения. В рабочей версии он ничего не делает.
Функция OnGetRecordset () — чистая виртуальная функция базового класса, поэтому вы обязаны определить ее здесь. Реализовать ее можно так:
CRecordset* COrderView::OnGetRecordset()
{
return m_pSet;
}
В данном случае она просто возвратит адрес, содержащийся в m pSet. Очевидно, что в ситуациях, когда вам необходимо пересоздавать набор записей, код будет более сложным.
Однако на этом работа над классом представления не закончена. Следующий шаг — необходимо более точно определить, что должен содержать набор записей о заказах.
Настройка набора записей
SQL-оператор SELECT для объекта COrderset порождает таблицу, которая будет содержать все комбинации записей из двух участвующих таблиц. Этих записей получится великое множество, поэтому вы должны добавить к запросу эквивалент конструкции WHERE, чтобы ограничить выбираемые записи только теми, что имеют смысл. Но существует и еще одна проблема: когда вы переключаетесь с отображения таблицы
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 ProductlD класса CProductset, который у нас относится к типу long, и вы можете специфицировать его имя как m_ProductIDparam.
Также вы можете оставить его 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;II То же, что Order ID в таблице Orders.
long m_OrderDetailsProductID;// То же, что 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);
II VCT^aobWBb тип поля как параметр pFX->SetFieldType(CFieldExchange::param) ; RFX__Long(pFX,_T (” Product IDParam”) , m_ProductIDparam) ;
}
Мастер 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->m_pDatabase = pDoc->m_DBSampleSet.m_pDatabase;
// Установить текущий 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 () в конце функции.
Первое, что делает код — это установка члена m_pDatabase объекта COrderset в то же значение, что и у объекта С Product set. Если вам не нужно это делать, то каркас заново открывает базу данных при открытии набора записей. Поскольку база данных уже открыта для набора записей продуктов, это приводит к напрасной трате времени.
Далее вы устанавливаете значение переменной параметра m_ProductIDparam в текущее значение, хранящееся в члене m_ProductID набора записей продуктов. Это значение заменяет вопросительный знак в фильтре при открытии набора записей, выбирая тем самым те записи, которые вам нужны; затем устанавливается фильтр для набора записей заказов в строку, которую вы видели ранее.
Доступ к многотабличным представлениям
Поскольку вы реализовали программу в однодокументном интерфейсе, ваше приложение имеет только один документ и одно представление. Доступность единственного представления может показаться проблемой, но на практике это не так. Вы можете заставить объект обрамляющего окна программы создать экземпляр класса COrderView и переключать текущее окно для отображения набора записей заказов.
Вам придется отслеживать текущее окно; это можно делать, присваивая уникальный идентификатор каждому окну представления записи в приложении. Пока у нас есть только два представления: представление продуктов и представление заказов. Чтобы определить идентификаторы для них, создайте новый файл по имени OurConstants.h и добавьте в него следующий код:
II Определения констант
#pragma once
// Произвольные константы для идентификации представлений записей
const unsigned int PRODUCT_VIEW = 1;
const unsigned int ORDER_VIEW = 2;
После этого вы можете использовать одну из этих констант для идентификации каждого представления и записывать идентификатор текущего представления в объекте обрамляющего окна. Для записи идентификатора текущего представления добавьте public-член данных типа unsigned int в класс CMainFrame и назовите его m_CurrentViewID. Сделав это, вы сможете инициализировать его в конструкторе класса СМаinFrame, добавив для этого следующий код:
CMainFrame::CMainFrame()
: m__Cur г entViewID (PRODUCT-VIEW)
{
}
Изначально приложение стартует с представления продуктов, поэтому вы инициализируете m_CurrentViewID соответствующим образом. Добавьте директиву # include для Our Constants . h в начало MainFrm. срр, чтобы это определение PRODUCT_VIEW было доступно в исходном файле.
Переключение представлений
Чтобы активизировать механизм переключения представлений, вы добавляете общедоступную функцию-член в класс CMainFrame, именуемую SELECTView (), с параметром, указывающим новый идентификатор. Эта функция выполняет переключение текущего представления на любое, специфицированное переданным в аргументе идентификатором.
Щелкните правой кнопкой мыши на СМаinFrame и выберите Add^Add Function (Добавить1^Добавить функцию) из контекстного меню, чтобы добавить новый член к классу. Укажите в качестве типа возврата 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, 0L, CFrameWnd::rectDefault,
this, ViewID, &context);
pNewActiveView->OnInitialUpdate();
}
SetActiveView(pNewActiveView);// Активизировать новое представление
p01dActiveView->ShowWindow(SW_HIDE); // Скрыть старое представление pNewActiveView->ShowWindow(SW_SHOW); I/ Показать новое представление pOldActiveView->SetDlgCtrlID(m_CurrentViewID); И Установить ID
// старого представления pNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
m_CurrentViewID = ViewID;// Сохранить ID нового представления
RecalcLayout();
}
Операции этой функции делятся на три части.
Получение указателей на текущее представление и новое представление.
Создание нового представления, если оно еще не существует.
Помещение нового представления на место текущего.
Адрес текущего активного представления предоставляется членом GetActiveView () объекта CMainFrame. Чтобы получить указатель на новое представление, вы вызываете член GetDlgltem () объекта обрамляющего окна. Если представление с идентификатором, указанным в аргументе функции, существует, возвращается адрес представления, в противном случае возвращается NULL, и вам нужно будет создать новое представление.
После создания объекта представления вы определяете объект context типа CCreateContext. Объект CCreateContext необходим только тогда, когда вы создаете окно для представления, которое должно быть подключено к документу. Объект CCreateContext содержит данные-члены, которые могут быть связаны с документом, обрамляющим окном и представлением, а для приложений MDI — также и с шаблоном документа. Когда вы переключаетесь между представлениями, вы создаете новое окно для нового представления, подлежащего отображению. Всякий раз, когда вы создаете окно нового представления, вы используете объект CCreateContext для установки соединения между представлением и объектом документа. Вы должны сохранить указатель на объект документа только в члене m pCurrentDoc на context. Вообще вам может понадобиться сохранить дополнительные данные в объекте CCreateContext, прежде чем вы создадите представление; это зависит от конкретных условий и вида создаваемого окна.
В вызове члена Create () объекта представления, который создает окно для нового представления, в качестве аргумента передается объект context. Это устанавливает правильное отношение между вашим документом и верифицирует указатель документа. Аргумент this в вызове Create () специфицирует текущее обрамляющее окно в качестве родительского окна, а аргумент ViewID — идентификатор окна. Этот идентификатор позволяет получить адрес окна при последующих вызовах члена GetDlgltemO родительского окна.
Чтобы сделать новое представление активным, вы вызываете член SetActiveView () класса СМаinFrame. Затем новое представление заменяет текущее активное представление. Для удаления окна старого представления вызывается член Showwindow () представления с аргументом SWHIDE, используя указатель на старое представление. Чтобы отобразить окно нового представления, вызывается та же функция с аргументом SW SHOW, используя указатель на новое представление.
SetActiveView(pNewActiveView); Н Активизировать новое представление p01dActiveView->ShowWindow(SW_HIDE); II Скрыть старое представление pNewActiveView->ShowWindow(SW_SHOW); // Показать новое представление p01dActiveView->SetDlgCtrlID(m_CurrentViewID);// Установить ID старого // представления
pNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
m_CurrentViewID = ViewID;// Сохранить ID нового представления
Вы восстанавливаете идентификатор старого активного представления в значение идентификатора, которое вы определили для него в члене m CurrentViewID класса СМа in Frame, добавленного ранее. Вы также устанавливаете идентификатор нового представления в AFX_IDW_PANE_FIRST для идентификации его в качестве первого окна приложения. Это необходимо, потому что приложение имеет только одно представление, поэтому первое представление — оно же и единственное. Позднее вы сохраняете идентификатор нового окна в члене m__CurrentViewID, так чтобы можно было следующий раз заменить текущее представление. Вызов RecalculateLayout () приводит к перерисовке при выборе нового представления.
Вам следует добавить директиву #include для Orderview, h в начало файла MainFrm.срр, чтобы в нем было доступно определение класса COrderView. После сохранения MainFrm.срр можно перейти к добавлению кнопки к диалогу Products для вызова диалога Orders. Затем вы сможете добавить обработчики для этой кнопки и ее аналога в диалоге Orders для вызова члена SELECTView () класса CMainFrame.
Обеспечение операции переключения
Чтобы реализовать механизм переключения представлений, вернитесь на вкладку Resource View и откройте IDD_DBSAMPLE_FORM. Вы должны добавить кнопочный элемент управления в диалоговое окно, как показано на рис. 19.27.
Рис. 19.27. Добавление кнопки Show Orders (Показать заказы)
Установите ID этой кнопки в IDC_ORDERS, в соответствии с другими элементами управления в диалоговом окне.
После сохранения ресурса вы можете создать обработчик для кнопки, щелкнув на ней правой кнопкой мыши и выбрав Add Event Handler (Добавить обработчик событий) из контекстного меню. Воспользуйтесь мастером добавления обработчиков событий (Event Handler Wizard), чтобы добавить в класс CProductView функцию OnOrders () для сообщения BN_CLICKED; этот обработчик вызывается при щелчке на кнопке. Для завершения этого обработчика потребуется добавить всего одну строку кода:
void CProductView::OnOrdersО
{
static_cast<GMainFrame*>(GetParentFrame()) ->SelectView (ORDER__VIEW) ;
}
Член GetParentFrame () объекта представления наследован от CWnd — непрямого базового класса СМа in Frame. Функция возвращает указатель на родительское обрамляющее окно, и вы используете его для вызова функции SELECTView (), которую только что добавили в класс CMainFrame. Значение аргумента ORDER_VIEW заставляет обрамляющее окно переключиться на диалоговое окно Orders. Если это происходит первый раз, то при этом создается объект представления и соответствующее окно. При втором и последующих вызова выполняется переключение на выбранное окно к выбранному представлению заказов, используя повторно уже существующее представление.
Вы должны добавить следующие директивы #include в Productview.срр:
#include "OurConstants.h”
#include "MainFrm.h"
Следующая задача — добавить обработчик для кнопки, которую вы ранее поместили в диалог IDD_ORDERS_FORM. В окне редактора, показывающем этот диалог, воспользуйтесь тем же процессом для добавления обработчика On Products () в класс COrderView и добавьте в его реализацию единственную строку кода:
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_VTEW)
{
if (!m_pSet->IsOpen())// Проверить, что набор записей открыт
return;
/ / Установить текущий Product ID в качестве параметра mjpSet->m_ProductIDparam = pDoc->m_DBSampleSet.m_ProductID; mjpSet->Requery () ;/ / Получить данные из БД
// Если мы достигли EOF, записей больше нет if (m_pSet->IsEOF ())
AfxMessageBox(L”No orders for the current product ID”) ;
)
П Установить заголовок окна
CString strTitle = _T("Table Name: ”);
CString strTable = mjpSet ~>Ge tTableName () ;
if(!strTable.IsEmpty())
strTitle += strTable;
else
strTitle += _T("Orders - Multiple Tables") ;
pDoc->SetTitie(strTitle);
CRecordView: -.OnlnitialUpdate () ; / / Обновить значения в диалоге }
CRecordView: .‘OnActivateView(bActivate, pActivateView, pDeactiveView);
}
Вы выполняете этот код, только если представление активизировано, и в этом случае аргумент 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 striable = m_pSet-X3etTableName ();
strTitle +=_!(": ") + striable; 1
GetDocument()->SetTitle(strTitle);
}
CRecordView:: OnActivateView (bActivate, pActivateView, pDeactiveView);
}
В данном случае все, что нужно делать для активизации представления — это обновить заголовок окна. Поскольку представление продуктов — ведущее представление для остальной части приложения, вы всегда захотите вернуть представление в его состояние перед тем, как деактивизировать. Если вы не делаете ничего помимо обновления заголовка окна, представление отображается в своем предыдущем состоянии.
Просмотр заказов для продукта
Теперь вы готовы к тому, чтобы собрать исполняемый модуль новой версии примера. Когда вы запустите пример, то должны будете иметь возможность просматривать заказы для любого продукта простым щелчком на кнопке Show Orders (Показать заказы) в диалоговом окне продуктов. Типичное представление заказа можно видеть на рис. 19.28.
Щелчок на кнопке Show Products (Показать продукты) возвращает в диалоговое окно продуктов, так что вы сможете продолжить просмотр списка продуктов. В этом диалоге вы можете использовать кнопки панели инструментов для просмотра всех заказов текущего продукта. Идентификатор Customer ID выглядит несколько загадочно. Давайте добавим еще одно представление для отображения подробностей имени и адреса заказчика. Это будет не особенно сложно, поскольку механизм для переключения между представлениями уже создан.
Рис. 19.28. Типичное представление заказа
Просмотр подробностей о заказчике
Базовый механизм, который вы добавите, будет работать через другой кнопочный элемент управления в диалоге заказа, который позволит переключиться к новому диалогу с данными заказчика. Наряду с элементами управления, предназначенными для отображения данных заказчика, вы добавите сюда еще две кнопки: одну для возврата в представление заказов, и еще одну — для возврата в представление продуктов. Вам понадобится другой идентификатор, соответствующий представлению заказчиков, и его можно добавить с помощью следующей строки кода в файле OurConstants. h:
const unsigned int CUSTOMER_VIEW = 3;
Теперь добавив набор записей для подробностей о заказчике.
Добавление набора записей заказчиков
Процесс точно такой же, как и тот, что вы проходили в классе COrderset. Используйте пункт контекстного меню Add1^ Class (Добавить^Класс) в Class View и выберите шаблон MFC ODBC Consumer (Потребитель MFC ODBC) для определения класса CCustomerSet, с классом 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;
Не забудьте закомментировать директиву #еггог в файле 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.
Рис. 19.29. Диалог отображения деталей о заказчике
Две кнопки позволяют переключаться либо на диалог заказов, из которого вы попадаете в данный диалог, либо непосредственно на диалог продуктов. Размер окна приложения определяется размером первого отображенного диалогового окна, поэтому поскольку диалог заказчика немного больше, увеличьте размер диалога Products по меньшей мере до его размера.
Специфицируйте идентификаторы элементов управления, используя имена полей в качестве основы. В этом может помочь отображение списка членов CCustomerSet в Class View. Присвойте идентификаторам кнопок значения IDC ORDERS и IDC_PRODUCTS. После сохранения диалогового ресурса вы готовы к созданию класса представления для набора записей.
Создание класса представления заказчиков
Класс представления для набора записей заказчиков создается вручную — так же, как это делалось для класса COrderView. Добавьте в проект файлы Customerview.h и Customerview, срр и вставьте следующий код определения класса в файл Customerview.h:
// Представление записей 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);
II Реализация
protected:
virtual -CCustomerView(){}
#ifdef _DEBUG
virtual void AssertValid() 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_joSet(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
Как видите, это такой же стандартный код, что и в классе COrderView, и в нем определены такие же функции. Первая директива #include предназначена для предварительно скомпилированных заголовков, а вторая представляет определения идентификаторы ресурсов.
Обработка щелчков на кнопочных элементах управления осуществляется в диалоговом окне IDD CUSTOMER FORM таким же способом, как это делалось ранее при добавлении функций OnOrders () и OnProducts() в класс CCustomerView.
void CCustomerView::OnOrders()
{
static__cast<CMainFrame*> (GetParentFrame ()) ->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.
Параметр фильтра будет определен в члене 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::OnInitialUpdate(); // Перерисовать диалог
// Проверить, не пустой ли набор записей 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 для отображения диалога.
Добавьте следующие операторы #include в начало файла Customerview, срр:
#include "Productset.h”
#include "OrderSet.h"
#include "CustomerSet.h"
#include "DBSampleDoc.h"
#include "OurConstants.h"
#include "MainFrm.h"
Необходимость в первых трех операторах объясняется тем, что в них определены классы, использованные в определении класса документа. 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"n))
{
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);
}
Я пропустил строки комментариев в начале функции, чтобы сэкономить место. После установки режима param вызовом члена SetFieldType () объекта pFX вы вызываете функцию RFX_Text () для передачи значения параметра для подстановки в фильтре. Вы используете здесь RFX_Text () потому, что переменная параметра имеет тип CString. Существуют различные функции RFX_ (), поддерживающие диапазон типов параметров.
После завершения этой модификации сохраните файл CustomerSet.срр.
Связывание диалога заказов с диалогом информации о заказчике
Чтобы дать возможность переключаться на диалог информации о заказчике, потребуется кнопочный элемент управления в диалоге IDD_ORDERS_FORM, поэтому откройте его в Resource View и добавьте дополнительную кнопку Show Customer (Показать заказчика), как показано на рис. 19.30.
Order IDSample edCustomer ID Sample ed
Product IDSample edQuantity Sample ed
Shov.' Products |I Show Customer
Puc. 19.30. Добавление кнопки Show Customer
Я слегка изменил исходное расположение элементов управления. Вы можете расположить их по-своему. Определите идентификатор для нового кнопочного элемента управления как I DC CUSTOMER. После сохранения диалога добавьте обработчик кнопки щелчком правой кнопкой мыши на ней и выбором пункта Add Event Handler (Добавить обработчик событий) из контекстного меню. Обработчик требует только одной строки кода:
void COrderView::OnCustomer()
{
s tatic__cast<CMainFr ante *> (Get ParentFrame ()) ->Selectview (CUSTOMER_VIEW) ;
}
Это получает адрес обрамляющего окна и использует его для вызова члена SELECTView () класса CMainFrame для переключения на представление заказчиков. Предпоследний шаг, необходимый для завершения программы — добавление кода к функции SELECTView (), который имеет дело с представлением CUSTOMER_VIEW, переданным ему. Это потребует только трех строк кода, как показано ниже.
void CMainFrame::SelectView(UINT ViewID) {
CView* pOldActiveView = GetActiveView(); // Получить текущее представление
// Получить указатель на новое представление, если оно существует.
// Если оно не существует, указатель будет равен null.
CView* pNewActiveView = static_cast<CView*>(GetDlgltem(ViewID));
// Если это первое обращение к новому представлению, // оно еще не существует, поэтому его нужно создать if (pNewActiveView == NULL) {
switch(ViewID) {
case ORDER_VIEW:И Создать представление заказов
pNewActiveView = new COrderView;
break;
case CUSTOMER_VIEW:// Создать представление заказчика
pNewActiveView = new CCustomerView;
break;
default:
AfxMessageBox ("Invalid View ID"); return;
}
/ / Переключение представлении
/ / Получить контекст текущего представления для применения / / в новом представлении
CCreateContext context;
context.m_pCurrentDoc = pOldActiveView->GetDocument () ;
pNewActiveView-XZreate (NULL, NULL, OL, CFrameWnd: :rectDefault, this, ViewID, & con text) ;
pNewActiveView-X)nInitialUpdate () ;
}
SetActiveView (pNewActiveView) ; / / Активизировать новое представление pOldActiveView->ShowWindow(SW—HIDE) ; // Скрыть старое представление pNewActiveView->ShowWindow(SW—SHOW); / / Показать новое представление pOldActiveView->SetDlgCtrlID (nt—CurrentViewID) ;// Установить ID старого // представления
pNewActiveView->SetDlgCtrlID (AFX_IDW_PANE_FIRST) ;
Ш—CurrentViewID = ViewID; // Сохранить ID нового представления
RecalcLayout () ;
}
Единственное изменение, необходимое вдобавок к оператору case в switch — создать объект CCustomerView, когда он не существует. Каждый объект представления будет использован повторно при следующем обращении, поэтому они создаются только один раз. Код переключения между представлениями работает с любым их количеством, поэтому если вы хотите, чтобы эта функция справлялась с большим числом представлений, вы просто должны добавить дополнительные case в оператор switch для каждого нового представления, которое вам понадобится. Хотя объекты представлений создаются здесь динамически, вам не нужно заботиться об их удалении. Поскольку они ассоциированы с объектом документа, каркас их удалит при закрытии приложения.
Поскольку вы обращаетесь к классу CCustomerView в функции SELECTView (), нужно добавить оператор #include для файла Customerview.h в начальный блок MainFrm.срр.
Чтобы завершить приложение, добавьте реализацию функции DoDataExchange () в класс CCustomerView в файле Customerview, срр:
void CCustomerView::DoDataExchange(CDataExchange* pDX)
I
CRecordView: ‘.DoDataExchange (pDX) ;
DDX_FieldText(pDX, IDC_ADDRESS, m_pSet->m_Address, m_pSet);
DDXJFieldText(pDX, IDC_CITY, m_pSet->m_City, 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.
Рис. 19.31. Работа программы просмотра базы данных
Эти две кнопки позволяют переключаться к представлениям заказов и продуктов соответственно.
Резюме
Теперь вы должны чувствовать себя уверенно в отношении того, как МЕС связывается с базами данных через ODBC.
Ниже перечислены ключевые моменты, с которыми вы познакомились в настоящей главе.
Библиотека МЕС обеспечивает поддержку OLE DB и ODBC для доступа к базам данных.
Чтобы использовать базу данных через ODBC, она должна быть зарегистрирована.
Подключение к базе представлено объектами CDatabase и CDaoDatabase.
Объект набора записей представляет SQL-оператор SELECT, примененный к определенному набору таблиц. Где необходимо, каркас автоматически создает объект базы данных, представляющий подключение к базе, когда создается объект набора записей.
Конструкция WHERE может быть добавлена к объекту — набору записей через член данных m_st г Filter.
Конструкция ORDER BY может быть добавлена к объекту — набору записей через член данных m_strSort.
Объект представления записей служит для отображения содержимого объекта набора записей.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Вновь используя таблицу Products, добавьте к приложению диалоговое окно управления запасами. Оно должно быть доступно через кнопку в диалоговом окне продуктов и должно само содержать кнопку возврата к диалогу продуктов. Поля, которые оно должно отображать, идентификатор продукта, наименование продукта, нижний уровень запасов, при котором их следует пополнять, цену за единицу и количество единиц на складе. Пока не беспокойтесь о фильтрации или сортировке — просто добейтесь работоспособности базового механизма.
Усовершенствуйте предыдущий проект так, чтобы диалог управления запасами автоматически отображал информацию о продуктах, которые были показаны в диалоговом окне продуктов на момент щелчка на кнопке.
Реализуйте систему, которая предупреждала бы пользователя базы данных в окне управления заказами о том, что текущий запас ниже или близок к границе, когда его следует пополнить. Не извещайте пользователя об этом, если установлена нулевая граница минимальных запасов для некоторых продуктов.
20
Обновлениеисточников данных
В этой главе, на основе знаний о доступе к базам данных через интерфейс ODBC, полученных в предыдущей главе, вы получите возможность испытать свои силы в обновлении базы данных Northwind Traders.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
Транзакции базы данных.
Способы обновления базы данных с помощью объектов набора записей.
Передача данных из набора записей в базу данных в ходе выполнения операции обновления.
Методика обновления существующей строки в таблице.
Методика добавления новой строки в таблицу.
Операции обновления
При написании кода, обеспечивающего только просмотр информации базы данных, единственный возникающий при этом вопрос — наличие полномочий для получения доступа к данным. До тех пор, пока база данных обладает соответствующей защитой доступа, данные в ней находятся в безопасности. Однако, как только дело доходит до написания кода обновления базы данных, ситуация существенно меняется. Поскольку код изменяет содержимое базы данных, это может привести к нарушению ее целостности и превратить содержимое таблицы в бессмыслицу или даже сделать ее непригодной к использованию. Прежде чем применять код в реальных приложениях, всегда необходимо тщательно его проверять на тестовой базе данных.
Как правило, обновление базы данных предполагает изменение одного или нескольких полей в строке существующей таблицы (например, изменение объема заказа) или добавление новой строки — например, строки нового заказа в базе данных
Northwind. Мы разработаем примеры кода для выполнения обеих этих задач, но вначале рассмотрим связанные с этим проблемы.
Большинство осложнений, которые могут возникать при выполнении операций обновления базы данных, становятся очевидными при рассмотрении многопользовательских баз данных. При отсутствии соответствующего контроля за процессом обновления параллельный доступ со стороны нескольких пользователей может создавать проблемы двух типов. Первый вид проблем возникает в тех случаях, когда пользователю разрешено извлекать запись во время выполнения обновления этой же самой записи. Пользователь, который выполняет только чтение данных, теоретически может получить устаревшие данные, предшествовавшие обновлению, или даже смешанные данные, когда ряд полей будет содержать старые данные, а ряд — новые. Второй тип проблемы возникает при одновременном обновлении данных несколькими пользователями, когда один пользователь начинает обновлять запись во время ее обновления другим пользователем. Если обновление затрагивает только одну запись таблицы, существует вероятность того, что обновление будет утеряно. Если обновление затрагивает записи из нескольких таблиц, данные в базе могут оказаться несогласованными. Прежде чем приступить к ознакомлению со способами решения этих проблем, рассмотрим работу основных операций обновления набора записей.
Операции обновления CRecordset
В предыдущей главе рассматривалось извлечение данных из выбранных полей таблицы или таблиц базы и их передача членам объекта набора записей с помощью функций RFX_ () члена — объекта набора записей DoFieldExchange (). Эти же функции используются для обновления полей в таблице базы данных или для добавления новой строки.
Класс CRecordset содержит пять функций-членов, которые поддерживают операции обновления, как показано в табл. 20.1.
Таблица 20.1. Функции-члены класса CRecordset
функцияОписание
Edit ()Эту функцию вызывают, чтобы начать обновление существующей записи. Она возбуж
дает исключение CDBException, если обновление таблицы невозможно, и исключение CMemoryException — при возникновении ситуации переполнения памяти.
AddNew ()Эту функцию вызывают, чтобы начать добавление совершенно новой записи. Она
возбуждает исключение CDBException, если запись новой записи в конец существующей таблицы невозможна.
Update ()Эта функция служит для завершения обновления существующей записи или добав
ления новой. Она возбуждает исключение CDBException, если ни одна запись не была обновлена, или в случае возникновения ошибки.
Delete ()Удаляет текущую запись, создавая и выполняя SQL-запрос delete. При возник
новении ошибки — например, если база данных доступна только для чтения — функция возбуждает исключение CDBException. После выполнения операции Delete () всем данным-членам набора записей будут присвоены нулевые значения — что эквивалентно набору нулевых значений. Прежде чем выполнять любую другую операцию по отношению к объекту набора записей, необходимо перейти к другой записи.
Cancelupdate () Эта функция отменяет любую отложенную операцию обновления существующей записи или добавления новой.
Ни одна из перечисленных в табл. 20.1 функций не имеет параметров. Первые четыре функции могут вызывать исключения, поэтому во избежание внезапного завершения программы при возникновении ошибок следует поместить вызов этих функций в блок try и добавить в программу блок catch.
Для удаления текущей записи из объекта набора записей достаточно вызывать его член Delete (). Затем, прежде чем предпринимать попытку использования любой из вышеприведенных функций, в наборе записей потребуется выполнить переход к новой позиции, поскольку после вызова функции Delete () значения данных-членов этого объекта будут неправильными.
Общая последовательность событий обновления существующей записи или добавления новой показана на рис. 20.1.
Добавление новой записиДобавление новой записиОбновление существующей записи
Добавление новой записи
Добавление новой записи
Вызов функции EDIT() применительно к объекту набора записей:- Сохранение текущих значений данных-членов полей набора записей в буфере.Установка новых значений данных-членов полей.Вызов функции Update() применительнок объекту набора записей:Проверка наличия измененных полей посредством сравнения с сохраненными значениями.Создание и выполнение SQL-запроса INSERT для обновления измененных полей базы данных.Освобождение буфера, содержащего старые сохраненные значения полей.Вызов функции EDIT() применительно к объекту набора записей:- Сохранение текущих значений данных-членов полей набора записей в буфере.Установка новых значений данных-членов полей.Вызов функции Update() применительнок объекту набора записей:Проверка наличия измененных полей посредством сравнения с сохраненными значениями.Создание и выполнение SQL-запроса INSERT для обновления измененных полей базы данных.Освобождение буфера, содержащего старые сохраненные значения полей.Вызов функции AddNew() применительно к объекту набора записей:Сохранение текущих значений данных-членов полей набора записей в буфере.Установка текущих значений данных-членов полей набора записей равным PSEUDO_NULL.Установка новых значений данных-членов полей.Вызов функции Update() применительно к объекту набора записей:Проверка на наличие полей, отличных от NULL.Создание и выполнение SQL-запроса INSERT для обновления полей базы данных, отличных от NULL.Восстановление старых сохраненных значений из буфера.Вызов функции AddNew() применительно к объекту набора записей:Сохранение текущих значений данных-членов полей набора записей в буфере.Установка текущих значений данных-членов полей набора записей равным PSEUDO_NULL.Установка новых значений данных-членов полей.Вызов функции Update() применительно к объекту набора записей:Проверка на наличие полей, отличных от NULL.Создание и выполнение SQL-запроса INSERT для обновления полей базы данных, отличных от NULL.Восстановление старых сохраненных значений из буфера.
Вызов функции EDIT() применительно к объекту набора записей:
- Сохранение текущих значений данных-членов полей набора записей в буфере.
Установка новых значений данных-членов полей.
Вызов функции Update() применительно
к объекту набора записей:
Проверка наличия измененных полей посредством сравнения с сохраненными значениями.
Создание и выполнение SQL-запроса INSERT для обновления измененных полей базы данных.
Освобождение буфера, содержащего старые сохраненные значения полей.
Вызов функции EDIT() применительно к объекту набора записей:
- Сохранение текущих значений данных-членов полей набора записей в буфере.
Установка новых значений данных-членов полей.
Вызов функции Update() применительно
к объекту набора записей:
Проверка наличия измененных полей посредством сравнения с сохраненными значениями.
Создание и выполнение SQL-запроса INSERT для обновления измененных полей базы данных.
Освобождение буфера, содержащего старые сохраненные значения полей.
Вызов функции AddNew() применительно к объекту набора записей:
Сохранение текущих значений данных-членов полей набора записей в буфере.
Установка текущих значений данных-членов полей набора записей равным PSEUDO_NULL.
Установка новых значений данных-членов полей.
Вызов функции Update() применительно к объекту набора записей:
Проверка на наличие полей, отличных от NULL.
Создание и выполнение SQL-запроса INSERT для обновления полей базы данных, отличных от NULL.
Восстановление старых сохраненных значений из буфера.
Вызов функции AddNew() применительно к объекту набора записей:
Сохранение текущих значений данных-членов полей набора записей в буфере.
Установка текущих значений данных-членов полей набора записей равным PSEUDO_NULL.
Установка новых значений данных-членов полей.
Вызов функции Update() применительно к объекту набора записей:
Проверка на наличие полей, отличных от NULL.
Создание и выполнение SQL-запроса INSERT для обновления полей базы данных, отличных от NULL.
Восстановление старых сохраненных значений из буфера.
Рис. 20. L Общая последовательность событий обновления существующей записи или добавления новой
При вызове функции AddNew () по отношению к набору записей, чтобы начать добавление новой записи в таблицу, функция сохраняет текущие значения всех данных- членов объекта набора записей, которые соответствуют значениям полей в буфере, а затем устанавливает значения данных-членов равными PSEUDO_NULL. Это значение не является нулем или значением null, как имеет место в случае указателя. Оно представляет собой значение, которое указывает на то, что значение члена данных не установлено. При вызове функции Update () для завершения добавления записи восстанавливаются исходные значения данных-членов набора записей, предшествовавшие вызову функции AddNew (). Если требуется, чтобы набор записей содержал значения новой записи, необходимо вызвать функцию-член Requery () объекта набора записей. В случае успешного выполнения операции эта функция возвращает значение TRUE (значение MFC-типа BOOL). Функцию Requery () вызывают также в тех случаях, когда необходимо получить другое представление данных, извлечение данных из которого будет выполняться с помощью другой SQL-команды или другого фильтра записей.
Передача данных между данными-членами набора записей и базой данных всегда выполняется с помощью члена DoFieldExchange () объекта набора записей. Следовательно, функции RFX_ () обеспечивают две возможности — как запись в базу данных, так и чтение из нее.
Проверка допустимости операций
Всегда целесообразно убедиться, что операция, которую нужно выполнить, допустима для данного объекта набора записей. Слишком легко может оказаться, что набор записей доступен только для чтения — для этого достаточно забыть сбросить атрибут “только для чтения” файла базы данных (например, Northwind.mdb)! При попытке обновления таблицы, доступной только для чтения, возбуждается исключение, которого можно полностью избежать, всего лишь проверяя допустимость операции. Использование исключений для выявления непредвиденных ошибок не эффективно, и в общем случае этого следует избегать. Лучше по возможности заранее проверять допустимость операции, как в данном случае. В результате код обработки исключений будет использоваться в действительно исключительных ситуациях.
Функция-член CanUpdate () класса CRecordset возвращает значение TRUE, если изменение записей в таблице, представленной объектом набора записей, возможно. Если требуется добавить новую запись, вначале для проверки можно вызвать функцию-член CanAppend () класса CRecordset. Эта функция возвращает значение TRUE, если добавление новых записей в таблицу разрешено.
Блокировка записей
Блокировка записей препятствует доступу других пользователей к заблокированной записи во время обновления строки таблицы. Степень блокировки записи во время обновления определяется режимом блокировки, установленным в объекте набора записей. В объекте CRecordset определены два режима блокировки, называемые оптимистическим и пессимистическим (табл. 20.2).
Таблица 20.2. Режимы блокировки записей CRecordset
Режим блокировки
Описание
CRecordset::optimistic
В оптимистическом режиме блокировки запись блокируется только при выполнении функции-члена Update (). Это минимизирует время, в течение которого запись недоступна другим пользователям базы данных. Если время редактирования может занимать длительное время, часто пессимистическая блокировка не подходит, поскольку другим пользователям может требоваться получение доступа к базе данных. Стандартным подходом является использование оптимистической блокировки и применение какого-то механизма разрешения конфликтов.
CRecorset::pessimistic
В пессимистическом режиме блокировки запись блокируется немедленно после вызова функции Edit () и остается заблокированной и, следовательно, недоступной для других пользователей, до завершения выполнения функции Update () или до отмены операции обновления. Понятно, что такой подход может существенно сказаться на производительности в тех случаях, когда обновления подготавливаются интерактивно. Однако во многих ситуациях этот режим просто незаменим для поддержания целостности данных.
По умолчанию для объекта набора записей применяется оптимистический режим блокировки, поэтому его установка требуется только при необходимости установки пессимистического режима. Для этого необходимо вызывать функцию-член SetLockingMode () объекта набора записей с аргументом CRecordset: :pessimistic. Естественно этот режим можно переустановить, вызывая функцию с аргументом CRecordset::optimistic.
Транзакции
Основная идея применения транзакций в контексте базы данных — обеспечение возможность безопасной отмены операций в случае необходимости. Транзакция объединяет строго определенные последовательности одного или более изменений базы данных в одну операцию, что позволяет в случае возникновения ошибки вернуть все элементы данных в исходное состояние (выполнить откат) в любой момент до завершения транзакции. Понятно, что сбой обновления в тот момент, когда оно выполнено частично, например, из-за возникновения аппаратного сбоя, мог бы оказать крайне негативное влияние на целостность базы данных. Транзакции не сводятся к обновлению только одной таблицы. Они могут включать в себя очень сложные операции с базой данных, требующие выполнения ряда изменений множества таблиц, и их выполнение может занимать значительное время. В подобных ситуациях поддержка транзакций совершенно необходима для обеспечения целостности базы данных.
При выполнении операций, ориентированных на транзакции, система базы данных управляет обработкой транзакции и записывает информацию, необходимую для восстановления данных, чтобы в случае возникновения какой-то проблемы в ходе этого процесса можно было отменить любые действия, выполняемые транзакций по отношению к данным. Выполнение операций с базой данных на основе транзакций позволяет защитить базу данных от ошибок, которые могут возникать во время обработки. Как правило, обработка с применением транзакций по мере необходимости блокирует записи, а также обеспечивает, чтобы другие пользователи базы данных, обращающиеся к измененным транзакцией данным, немедленно видели все изменения.
Транзакции поддерживаются большинством коммерческих систем баз данных, установленных на мэйнфреймах, но в системах баз данных, работающих на ПК, это не всегда так. Тем не менее, класс CDatabase в библиотеке МЕС поддерживает транзакции. Это же относится и к поддержке Microsoft ODBC баз данных Access. Поэтому при желании можно поэкспериментировать с обработкой транзакций применительно к базе данных Sample Data.
Операции транзакций класса CDatabase
Управление транзакциями осуществляется с помощью членов объекта класса CDatabase, который обеспечивает подключение к базе данных. Для выяснения того, поддерживаются ли транзакции для данного подключения, необходимо вызвать функцию-член CanTransact () объекта CDatabase. Эта функция возвращает значение TRUE, если транзакции поддерживаются. Кроме того, функция-член CanUpdate () класса CDatabase возвращает значение FALSE, если источник данных доступен только для чтения.
При обработке транзакций используются три функции-члена класса CDatabase, описанные в табл. 20.3.
Последовательность событий в ходе выполнения транзакции очень проста.
Вызов функции BeginTrans () для запуска транзакции.
Вызов соответствующих функций Edit (), Update (), AddNew (), необходимых для обработки набора записей.
Вызов функции CommitTrans () для завершения транзакции.
Таблица 20.3. Функции-члены класса CDatabase для обработки транзакций
Функция
Описание
BeginTrans()
Запускает транзакцию в базе данных. Все последующие операции с набором записей являются частью транзакции до тех пор, пока не будет вызвана функция CommitTrans () либо Rollback (). Функция возвращает значение TRUE, если запуск транзакции был успешным.
CommitTrans()
Фиксирует транзакцию, что приводит к быстрому завершению всех операций с набором записей, являющихся частью транзакции. При ошибке функция возвращает значение false, и в этом случае состояние источника данных оказывается неопределенным.
Rollback()
Выполняет откат всех операций с набором записей, выполненных с момента вызова функции 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 (). Поэтому, прежде чем предпринимать попытку применения транзакций в приложении, необходимо уяснить для себя, как предполагается использовать конкретные драйверы.
Простой пример обновления
Пора рассмотреть выполнение операций обновления на практике, начиная с общего примера. В этом примере вначале мы опустим большинство уже описанных нюансов, но впоследствии он будет усложнен за счет добавления некоторых из описанных возможностей. Приложение обновления таблицы базы данных можно создать ценой минимальных усилий, используя мастер MFC Application Wizard, который мы уже применяли в предыдущей главе. Мы создадим программу, которая позволяет обновлять определенные поля в таблице Order Details (Сведения о заказе).
Используя шаблон MFC Application (Приложение MFC), создайте проект DBS imp le Up date. Выберите переключатель Database view without file support (Представление баз данных без файловой поддержки) и укажите ODBC в качестве параметра Client type (Тип клиента), как это делалось в предыдущей главе. Как и ранее, мы будем использовать базу данных Northwind через драйвер ODBC, но на этот раз в качестве типа набора записей необходимо выбрать Dynaset (Динамический набор). В многопользовательской среде динамический набор автоматически обновляется любыми изменениями, выполняемыми в записи при обращении к ней программы. Это обеспечивает постоянную актуальность всех данных в приложении. Для выполнения действий по изменению существующей записи или добавлению новых записей в качестве типа набора записей должен быть выбран Dynaset.
Поскольку мы планируем обновлять базу данных, набор записей необходимо преобразовать в одну таблицу базы данных. Классы базы данных MFC не поддерживают обновление наборов записей, предполагающие соединение двух или более таблиц. В качестве набора записей, используемого по умолчанию, выберите таблицу Order Details (Сведения о заказе), как показано на рис. 20.2.
В случае выбора нескольких таблиц обновление набора записей подавляется, поскольку набор записей автоматически определяется в качестве доступного только для чтения. Для объединений нескольких таблиц классы базы данных поддерживают доступ только для чтения, но не для обновления.
Представление и класс набора записей и связанные с ними имена файлов можно изменить в соответствии с обрабатываемой таблицей, как показано на рис. 20.3.
Рис. 20.3. Сгенерированные мастером классы
Настройка приложения
Таблица Order Details содержит пять столбцов — Order ID (Идентификатор заказа), Product ID (Идентификатор продукта), Unit Price (Цена единицы продукта), Quantity (Количество) и Discount (Скидка). Если отобразить вкладку Class View (Представление класса) и взглянуть на члены объекта COrderDetailsSet, можно увидеть соответствующие им данные-члены. Диалоговое окно представления набора записей должно содержать статическое текстовое поле и элемент редактирования, которые вместе соответствуют каждому из этих данных-членов. Они размещены так, как показано на рис. 20.4, но их можно разместить любым удобным образом.
Присвойте элементам редактирования идентификаторы, соответствующие именам полей, как это было сделано в предыдущей главе — например, последнему элементу присвойте идентификатор IDC_DISCOUNT. Стиль, установленный для элементов редактирования по умолчанию, допускает ввод данных с клавиатуры, но, исходя из предположения, что необходимо ограничить количество полей набора записей, которые можно изменять, первые три элемента редактирования следует определить как доступные только для чтения. Для этого необходимо использовать вкладку стилей диалогового окна Properties (Свойства).
Рис. 20.4. Диалог для таблицы Order DetailsРис. 20.4. Диалог для таблицы Order Details
Рис. 20.4. Диалог для таблицы Order Details
Рис. 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, IDC_ORDERID, m_pSet~>m_OrderID, m_pSet);
DDX_FieldText(pDX, IDC_PRODUCTID, m_pSet->m_ProductID, m_pSet);
DDX_FieldText (pDX, IDC_UNITPRICE, m_pSet->m__Unit Price, m_pSet) ;
DDX_FieldText(pDX, IDC_QUANTITY, m_pSet~>m_Quantity, m_pSet);
DDX_FieldText(pDX, IDC_DISCOUNT, m_pSet->m_Discount, m_pSet);
Верите или нет, но создание программы для обновления таблицы Order Details завершено.
Практическое занятиеПрактическое занятиеОбновление базы данныхОбновление базы данных
Практическое занятие
Практическое занятие
Обновление базы данных
Обновление базы данных
Если вы правильно определили все элементы управления и не забыли закомментировать директиву #error, расположенную перед определением функции GetDefaultConnect () в классе COrderDetailsSet, эта программа должна быть полностью готовой к сборке. Указанная директива служит для устранения проблем, которые могут возникать при подключении к базе данных. Во время выполнения программы можно будет перемещаться по строкам таблицы, используя кнопки панели
инструментов. При вводе данных в поля редактирования Quantity или Discount заказа они обновляются при перемещении назад или вперед в наборе записей. Окно приложения показано на рис. 20.5.
Как видите, для продукта с идентификатором Product ID, равным 72 в заказе с идентификатором Order ID, равным 10248, введены некоторые произвольные значения.
Описание полученных результатов
В результате щелчка на одной из кнопок панели инструментов для перехода к другой записи вызывается обработчик OnMove (), предоставленный используемым по умолчанию базовым классом CRecordView. Перед выполнением перехода к новой записи в наборе записей посредством вызова функции Move () класса CRecordset, наследуемой классом COrderDetailsSet, эта функция записывает любые изменения, введенные в набор записей. Помните, что в данном случае существует два уровня обмена данными. Функции RFX_ (), вызываемые в функции-члене DoFieldExchange () класса COrderDetailsSet, выполняют передачу данных между строкой в наборе записей базы данных и данными-членами класса. Функции DDX_ (), вызываемые в функции-члене DoDataExchange () класса COrderDetailsView, осуществляют передачу данных между элементами редактирования и данными-членами класса COrderDetailsSet. При изменении значения в элементе редактирования новые данные передаются соответствующим данным-членам объекта набора записей. При переходе к следующему набору записей с помощью щелчка на кнопке панели инструментов функция DoFieldExchange () записывает новые данные в базу.
Управление процессом обновления
Рассмотренный пример программы достаточно хорош, но запись данных в базу без выполнения каких-либо очевидных действий со стороны пользователя несколько сбивает с толку. Можно было бы вначале определить все элементы редактирования доступными только для чтения, чтобы по умолчанию ввод данных с клавиатуры подавлялся для всех элементов управления. Затем можно было бы добавить в диалоговое окно кнопку Edit Order (Изменить заказ), которая делала бы соответствующие элементы доступными для ввода данных с клавиатуры. Это диалоговое окно показано на рис. 20.6.
В этой программе мы реализуем два примечательных режима: режим “только для чтения”, при котором обновление невозможно, поскольку элементы управления будут доступны только для чтения, и режим правки, в котором ввод данных с клавиатуры возможен для некоторых из элементов управления, что позволяет обновлять набор записей. Идея этого подхода состоит в том, чтобы при щелчке на кнопке Edit Order пользователь активизировал ввод данных с клавиатуры для тех полей редактирования, которые должны быть доступными для обновления, тем самым включая режим редактирования. Добавьте кнопку в диалоговое окно приложения DBSimpleUpdate. Кнопке можно присвоить идентификатор IDC_EDITORDER. Обработчик для кнопки добавляется в класс COrderDetailsView. Для этого потребуется щелкнуть на кнопке правой кнопкой мыши и выбрать пункт Add Event Handler (Добавить обработчик событий) из контекстного меню. Сократите имя функции обработчика до OnEditorder().
Рис. 20.6. Добавление кнопки Edit Order
В идеале в режиме обновления следует подавить использование кнопок панели инструментов или команд меню Record (Запись), предназначенных для перехода к другой строке в таблице, поскольку щелчок на кнопке должен завершать операцию обновления, а не перемещать текущую позицию в наборе записей.
При щелчке на кнопке Edit Order атрибут “только для чтения” полей количества и скидки должен удаляться. Кроме того, диалоговое окно должно содержать кнопку, активизирующую выполнение обновлений. После щелчка на кнопке Edit Order диалоговое окно приложения должно принимать вид, показанный на рис. 20.7.
Теперь поля редактирования Quantity и Discount допускают ввод данных, надпись кнопки Edit Order изменилась на Update (Обновить), а в диалоговом окне появилась новая кнопка Cancel (Отмена), которая позволяет пользователю при необходимо-
сти отменить операцию. Щелчок на кнопке Edit Order должен отключать не только кнопки панели инструментов, но и текущую запись. Это же относится к меню Record. Теперь программа находится в режиме “редактирования”.
4
Older ID:Sampte edi
Product ID:Sample edi
Unit Price:Sample edi
Quantity:Sample ediQuantity:Sample ediCancelCancelD'scount:Sample ediD'scount:Sample ediUpdateUpdate
Quantity:Sample edi
Quantity:Sample edi
Cancel
Cancel
D'scount:Sample edi
D'scount:Sample edi
Update
Update
Puc. 20.7. Редактирование некоторых полей заказа
Кнопку Cancel можно добавить в диалоговое окно, но не желательно, чтобы она отображалась сразу после запуска приложения. Поэтому в качестве значения ее свойства Visible следует установить False.
Для кнопки Cancel также требуется обработчик события, поэтому добавьте обработчик OnCancel () в класс COrderDetailsView, так же, как это делалось для кнопки Edit Order — код этого обработчика будет представлен несколько позже.
Процесс выполнения операции обновления следующий: пользователь вводит данные в доступные для этого поля диалогового окна и щелкает на кнопке Update, чтобы завершить обновление. Затем диалоговое окно возвращается в исходное состояние режима “только для чтения”, в котором все поля редактирования доступны только для чтения. Если пользователь решает отказаться от выполнения операции обновления, он щелкает на кнопке Cancel.
Чтобы приложение вело себя описанным образом, и чтобы процессом обновления можно было эффективно управлять, после щелчка на кнопке Edit Order необходимо выполнить ряд перечисленных ниже действий.
Изменить текст на кнопке Edit Order на Update, чтобы кнопка превратилась в кнопку завершения операции обновления.
Обеспечить отображение кнопки Cancel в диалоговом окне — иначе говоря, сделать ее видимой.
Отразить в классе переход в режим “редактирования”. Это необходимо потому, что одну и ту же кнопку мы будем использовать для выполнения двух задач: Edit Order и Update.
Активизировать ввод данных с клавиатуры для элементов управления, отображающих поля, обновление которых нужно разрешить.
Теперь соберем воедино код, который будет обеспечивать выполнение всех этих действий.
Реализация режима обновления
Начнем с фиксации того, находится ли приложение в режиме обновления. Это можно сделать, добавляя в класс COrder De tai Is View объявление enum и переменную типа enum, отражающую текущий режим. Добавьте следующие две строки в раздел public класса:
enum Mode {READ_ONLY, UPDATE}; И Режимы приложения
Mode m_Mode;// Запись текущего режима
Первоначально приложение находится в режиме READ ONLY, поэтому m_Mode можно инициализировать в конструкторе соответствующим образом:
COrderDetailsView::COrderDetailsView() : CRecordView(COrderDetailsView::IDD)
,m_Mode(READ_ONLY)
{
m_pSet = NULL;
II TODO: сюда необходимо вставить код конструктора }
Переключения надписи кнопки и режима программы можно выполнять в обработчике OnEditorder (), добавленном в класс представления. В принципе, начальная версия функции должна реализовать функциональные возможности, подобные следующим:
void COrderDetailsView::OnEditorder() {
if(m_Mode == UPDATE)
{ //Если во время щелчка на кнопке программа находилась в режиме обновления
// Отключение ввода для полей редактирования
// Изменение текста кнопки Update на Edit Order
// Сокрытие кнопки Cancel
// Активизация команд меню Record и кнопок панели инструментов
// Завершение обновления
m_Mode = READ_ONLY;// Переход в режим "только чтение"
} else
{ //Если во время щелчка на кнопке программа находилась в режиме "только чтение"
// Активизация ввода для полей редактирования
// Изменение текста кнопки Edit Order на Update
// Отображение кнопки Cancel
// Отключение команд меню Record и кнопок панели инструментов
/ / Начало обновления
m_Mode = UPDATE;// Переключение в режим обновления
}
Код переключения режима уже готов. На данный момент функция лишь изменяет значение члена m Mode с READ ONLY на UPDATE и обратно, фиксируя текущий режим. Остальные необходимые действия описаны в виде комментариев. Теперь последовательно рассмотрим, как можно реализовать каждую из этих строк.
Активизация и отключение полей редактирования
Чтобы изменить свойства элемента управления, необходимо вызвать определенную связанную с ним функцию. Следовательно, мы должны располагать доступом к
объекту, который представляет элемент управления. Добавление переменной элемента управления в класс представления не составляет сложности. Достаточно щелкнуть на элементе управления правой кнопкой мыши в панели Resource View (Представление ресурсов) и в контекстном меню выбрать пункт Add Variable (Добавить переменную). Диалоговое окно, отображаемое для элемента редактирования значения скидки, показано на рис. 20.8.
Рис. 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;// Переход в режим "только чтение"
}
else
{ //Если во время щелчка на кнопке программа находилась в режиме "только чтение"
// Активизация ввода для полей редактирования m_QuantityCtrl.SetReadOnly(FALSE);
m_DiscountCtrl.SetReadOnly(FALSE);
// Изменение текста кнопки Edit Order на Update
II Отображение кнопки Cancel
// Отключение команд меню Record и кнопок панели инструментов / / Начало обновления
m_Mode = UPDATE;// Переключение в режим обновления
}
}
Функция-член SetReadOnly () класса CEdit имеет параметр типа BOOL, который по умолчанию получает значение TRUE. Поэтому вызов этой функции без аргументов означает использование значения аргумента, заданного по умолчанию, и установку свойства “только для чтения” элемента управления равным TRUE. Передача функции при ее вызове значения FALSE ведет к установке свойства “только для чтения” в значение FALSE. В данном случае можно было бы уменьшить объем кода функции, удалив из нее обращения к функции SetReadOnly () в операторе if-else и добавив два оператора после оператора if:
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();
И Изменение текста кнопки Update на Edit Order m_EditOrderCtrl.SetWindowText(_T("Edit Order"));
H Сокрытие кнопки Cancel
II Активизация команд меню 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
{ //Если во время щелчка на кнопке программа находилась в режиме "только чтение"
II Активизация ввода для полей редактирования 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. г с этот ресурс меню хранится с идентификатором IDR_MA 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, но ситуации, в которых может оказаться необходимым их отключение, несколько осложняются поведением пунктов меню и кнопок панели инструментов непосредственно перед обращением к ним.
Рис. 20.9. Мастер создания обработчиков событий
Когда текущая запись является первой в наборе записей, по умолчанию программа отключает пункты меню и кнопки панели инструментов, соответствующие идентификаторам ID_RECORD_FIRST и ID_RECORD_PREV. Аналогично, когда текущая запись является последней в наборе записей, программа отключает элементы ID_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.
Обработчики, соответствующие идентификаторам ID_RECORD_NEXT и ID_RECORD_ LAST, также требуют одной и той же строки кода:
void COrderDetailsView::OnUpdateRecordLast(CCmdUI* pCmdUI) {
II Отключение элемента, если значение m_Mode - UPDATE
// Активизация элемента, если значение m_Mode - READ_ONLY, // и запись не является последней
pCmdUI->Enable((m_Mode == READ_ONLY) && 1IsOnLastRecord());
}
Этот обработчик работает аналогично предыдущему.
Фактическое выполнение обновления
Теперь осталось только выполнить действительное обновление по щелчку на кнопке Update. Для обновления записи пользователь вначале щелкает на кнопке Edit Order, потому на этом этапе необходимо вызвать функцию-член Edit () объекта набора записей, чтобы начать процесс изменения набора записей. После щелчка на кнопке Update необходимо вызвать функцию-член Update () объекта набора записей, чтобы сохранить новые данные в записи базы. Описанный алгоритм можно реализовать с помощью функции-члена m_pSet класса представления следующим образом:
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);
I/ Завершение обновления m_pSet->Update();
m_Mode = READ_ONLY; // Переход в режим "только чтение" } 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();
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 ()
// Отмена операции обновления ));// Переключение текста кнопки// Сокрытие кнопки Cancel// Установка состояния поля// редактирования объема заказа// Установка состояния поля// редактирования скидки// Переключение режима// Отмена операции обновления ));// Переключение текста кнопки// Сокрытие кнопки Cancel// Установка состояния поля// редактирования объема заказа// Установка состояния поля// редактирования скидки// Переключение режима{
// Отмена операции обновления ));// Переключение текста кнопки
// Сокрытие кнопки Cancel
// Установка состояния поля
// редактирования объема заказа
// Установка состояния поля
// редактирования скидки
// Переключение режима
// Отмена операции обновления ));// Переключение текста кнопки
// Сокрытие кнопки Cancel
// Установка состояния поля
// редактирования объема заказа
// Установка состояния поля
// редактирования скидки
// Переключение режима
m_pSet->CancelUpdate();
m_EditOrderCtrl.SetWindowText(_T("Edit m_CancelEditCtrl.ShowWindow(SW_HIDE);
m_QuantityCtrl.SetReadOnly(TRUE);
m_DiscountCtrl.SetReadOnly(TRUE);
m_Mode = READ_ONLY;
Функция CancelUpdate () завершает операцию обновления и восстанавливает значения полей объекта набора записей, которые предшествовали вызову функции
Edit (). Поскольку щелчок на кнопке Cancel возможен только в режиме редактирования, обновление кнопок и других элементов управления можно выполнить так же, как в обработчике OnEditorder (). Вот и все. Теперь можно осуществить пробный запуск приложения.
Практическое занятиеПрактическое занятиеИнтерактивное обновлениеИнтерактивное обновление
Практическое занятие
Практическое занятие
Интерактивное обновление
Интерактивное обновление
Если код введен без ошибок, после компиляции и запуска программы она должна работать так, как планировалось. После щелчка на кнопке Edit Order (Изменить заказ) ввод данных возможен только в полях редактирования Quantity (Количество) и Discount (Скидка). Пример выполнения операции обновления показан на рис. 20.10.
Теперь кнопки перехода к новой записи отключены, равно как и пункты меню Record (Запись). Завершение обновления после ввода новых данных выполняется щелчком на кнопке Update (Обновить). Это приводит к записи новых данных в базу и возврату приложения в нормальное состояние, в котором все поля редактирования отключены, а кнопки и меню находятся в своих исходных состояниях.
Рис. 20.10. Обновление информации по заказуРис. 20.10. Обновление информации по заказу
Рис. 20.10. Обновление информации по заказу
Рис. 20.10. Обновление информации по заказу
Добавление строк в таблицу
Расширим рассмотренный пример, чтобы реализовать возможность добавления нового заказа в базу данных Northwind. Это позволит подробнее рассмотреть некоторые из практических проблем и сложностей, которые могут встретиться при выполнении подобной операции.
Сам по себе заказ не является простой записью в таблице базы данных Northwind. Его определяют две таблицы. Основные данные заказа хранятся в таблице Orders (Заказы), которая содержит информацию о клиентах. Каждая запись заказа в таблице Orders связана с одной или более записями в таблице Order Details (Сведения о
заказах) (по одной для каждого продукта, включенного в заказ) по столбцу Order ID (Идентификатор заказа). Отношение между этими таблицами отражено на рис. 20.11.
Однако процесс добавления нового заказа затрагивает не только эти две таблицы. При создании нового заказа необходимо предоставить пользователю средства выбора клиента из таблицы Customers (Клиенты). Таблица Orders содержит поле, определяющее сотрудника, которым должен быть один из сотрудников, записанных в таблице Employees (Сотрудники). Очевидно, что после того, как информация, требующаяся для включения в новую запись таблицы Orders, определена, придется выбрать один или более продуктов из числа определенных в таблице Products (Продукты). Необходимость внесения изменений во все эти таблицы делает задачу достаточно сложной. Ее можно несколько упростить, по умолчанию заполняя поле столбца Employee ID (Идентификатор сотрудника) значением равным, например, 1. Это позволит избежать необходимости внесения изменений в таблицу Employees в ходе выполнения данного примера. Но в любом случае вначале потребуется определить общую логику действий.
Рис. 20.11. Отношение между таблицами Orders и Order Details
Процесс ввода заказа
Кроме уже созданной диалоговой формы просмотра и редактирования информации существующих заказов, мы будем использовать еще две формы. Одна из них обеспечивает выбор клиента, делающего заказ, и ввод требуемой даты доставки заказа, а вторая позволяет вводить информацию о продуктах и их количествах, включаемых в заказ. Диалоговое окно выбора клиента связано с таблицей Customers базы данных, а диалоговое окно выбора продуктов — с таблицей Products. Кнопки диалоговых окон обеспечивают переход из одного окна в другое. Общая логика работы с диалоговыми окнами показана на рис. 20.12.
От создания новой записи в таблице Orders следует воздержаться до ввода первой записи в таблицу Order Details. Это позволит избежать создания заказа, который не содержит ни одного продукта. Теперь соберем воедино все необходимые диалоговые ресурсы, а затем реализуем код, обеспечивающий выполнение операций.
Рис. 20.12. Работа с диалоговыми окнами
Создание ресурсов
В уже существующее окно потребуется добавить дополнительную кнопку, инициализирующую процесс создания нового заказа, поэтому добавьте кнопку с надписью New Order (Новый заказ) и идентификатором IDC_NEWORDER. После этого новую кнопку можно разместить в той же позиции, что и кнопку Cancel, поскольку в каждый конкретный момент времени будет видна только одна из этих кнопок. Кнопка New Order должна быть видима по умолчанию. В то же время, если вы предпочитаете избегать маскирования одного ресурса другим, эти кнопки можно разместить в различных позициях.
Чтобы при размещении кнопки New Order в той же позиции, что и кнопка Cancel, она отображалась поверх нее, необходимо чтобы в таблице перемещения по элементам управления она следовала за ней. Измененная диалоговая форма IDD_SIMPLEUPDATE, в которой кнопка New Order расположена поверх кнопки Cancel и поэтому маскирует ее, показана на рис. 20.13.
Обработчик для кнопки New Order можно добавить в объект COrderDetailsView, щелкая на кнопке правой кнопкой мыши и выбирая пункт Add Event Handler (Добавить обработчик событий) из контекстного меню. При желании определенное по умолчанию имя обработчика событий можете сократить до OnNewOrder (). Чтобы обработчик события был создан именно для новой кнопки, перед щелчком на ней может потребоваться ее перемещение. Код этого обработчика мы добавим несколько позже. Конечно, в данном случае можно было бы использовать кнопку Cancel, изменяя ее надпись и действие обработчика в зависимости от состояния члена m_Mode объекта COrderDetailsView, однако предлагаемый сейчас подход позволит получить представление о возможном способе работы с двумя кнопками.
Nw Orde'Nw Orde'Edit OrderEdit Order
Nw Orde'
Nw Orde'
Edit Order
Edit Order
Рис. 20.13. Измененная диалоговая форма IDDJSIMPLEUPDATE
Необходимые новые диалоговые формы можно создать, щелкая правой кнопкой мыши на папке Dialog (Диалог) в панели Resource View и из контекстного меню выбирая пункт Insert Dialog (Вставить диалог). Присвойте новым диалоговым формам идентификаторы IDD_CUSTOMER_FORM и IDD_PRODUCT_FORM соответственно. Для обоих диалогов необходимо выбрать значения Child (Дочерний) для свойства Style (Стиль) и None (Нет) для свойства Border (Рамка). Кроме того, размеры всех трех диалоговых форм можно определить приблизительно равными и немного превышающими размеры первоначальной формы.
Создание наборов записей
Нам требуются два класса набора записей, соответствующие таблицам Customers и Products базы данных. Добавление каждого из них выполняется одинаково: необходимо щелкнуть правой кнопкой мыши на объекте DBMSimpleUpdate в панели Class View (Представление классов) и в контекстном меню выбрать пункт Add1^ Class (Добавить1^ Класс), а затем в качестве шаблона выбрать MFC ODBC Consumer (Потребитель MFC ODBC). В качестве имен классов можно указать CCustomerSet и CProductSet соответственно для таблиц Customers и Products, а также в обоих случаях выбрать Snapshot (Снимок) для типа набора записей. Измените тип полей
CStringW на CString и не забудьте в каждом классе удалить или закомментировать директиву #еггог, предшествующую определению функции GetDefaultConnect ().
Создание представлений наборов записей
Теперь можно создать классы представлений записей, которые подключаются к новым диалоговым окнам, поэтому добавьте в проект файлы .Ни. срр, содержащие код новых классов 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, хранящий указатель на объект набора записей, можно инициализировать в конструкторе в файле Customerview, срр:
#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
содержит директиву #include файла Resource.h, содержащего определения идентификаторов созданных ресурсов.
Теперь добавим в файл Customerview, срр определения функций, которые должны генерироваться в режиме отладки:
И Диагностика CCustomerView
#ifdef _DEBUG
void CCustomerView::AssertValid() const {
CRecordView::AssertValid();
void CCustomerView::Dump(CDumpContext& 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());
}
В данном случае необходимо выполнить явное преобразование типа, поскольку в иерархии классов преобразование типа выполняется от базового класса к производному. Остальные два переопределения функций будут реализованы несколько позже.
Поскольку объект CCustomeгSet создается в куче (в области динамического распределения памяти), следует позаботиться о его удалении в деструкторе класса CCustomeгView:
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(1strTable.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;
// Операции
public:
CProductset* GetRecordset ();
// Реализация
protected:
#ifdef _DEBUG
virtual void AssertValid() const;
virtual void Dump(CDumpContext& de) const;
#endif
Теперь, применяя метод, использованный для предыдущего класса, в класс CProductView можно добавить переопределения функций OnGetRecordset (),
DoDataExchange () и OnlnitialUpdate (), поместив в начало файла Productview, срр следующие директивы:
#include#include#include#include#include#include#include#include’’stdafx.h” "DBSimpleUpdate.h ”Productview.h" ’’Productset .h"
#include
#include
#include
#include
#include
#include
#include
#include
Конструктор должен инициализировать член 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)H Если набор записей отсутствует
{
m_pSet = new CProductSet(NULL); II его необходимо создать, m_pSet->Open();// а затем открыть
}
return m_pSet;// Возвращение адреса набора записей
}
Реализация функции GetRecordset () в классе CProductView также в основном идентична ее реализации в классе CCustomerView:
CProductSet* CProductView::GetRecordset () {
return static__cast<CProductSet*> (OnGetRecordset ());
}
Можно также добавить определения диагностических функций, которые будут использоваться в режиме отладки:
#ifdef JDEBUG
void CProductView::AssertValid() const
{
CRecordView:;AssertValid();
}
void CProductView::Dump(CDumpContexts de) const
{
CRecordView::Dump(de);
}
#endif //_DEBUG
К функциям, реализация которых в классе CProductView еще не определена, мы вернемся несколько позже в этой главе, а пока можно приступить к заполнению диалогов необходимыми элементами управления.
Добавление элементов управления в ресурсы диалогов
Хотя мы связали диалоговое окно IDD_CUSTOMER_FORM только с таблицей Customers, в процессе добавления новой записи придется предоставлять всю информацию, которая необходима для создания новой записи в таблице Orders. Источники данных для каждого из полей новой записи таблицы Orders показаны на рис. 20.14.
Часть полей заполняется данными, извлеченными из выбранной пользователем записи таблицы Customers. Поскольку мы создаем новый заказ, необходимо синтезировать новый уникальный идентификатор заказа. Для этого можно найти максимальный идентификатор, используемый в данный момент в таблице Orders, и просто увеличить это значение на 1.
Чтобы выбрать клиента, пользователь выполняет прокрутку в наборе записей до отображения требуемого клиента. Затем из набора записей можно извлечь данные, необходимые для создания новой записи в таблице Orders. В диалоговом окне можно отобразить текущую дату в качестве даты оформления заказа и предоставить элемент управления для выбора необходимой даты доставки. Остальным полям можно просто присвоить произвольные значения, дабы излишне не усложнять пример.
Генерируется какПроизвольное Текущая Ввод из Устанавливается Произвольное
Произвольное значение, равное 9.95
Рис. 20.14. Источники данных для полей новой записи таблицы Orders
Естественно, в диалоговом окне не обязательно отображать всю информацию таблицы Customers — для обеспечения выбора достаточно отобразить имя, идентифицирующее клиента. Однако набор записей должен содержать все остальные данные. Элементы управления в диалоге IDD_CUSTOMER_FORM можно разместить, как показано на рис. 20.15.
На рис. 20.15 видно, как будут использоваться элементы управления, и как выполняется присваивание необходимых идентификаторов. Элементы выбора даты позволяют вводить или выбирать значения даты или времени. Выбор даты или времени зависит от значения, установленного в свойстве Format (Формат). Изображенные на этом рисунке элементы управления используют формат Short Date (Короткая дата).
Выбор даты осуществляется щелчком на стрелке вниз и выбором даты из открывающегося окна календаря. Может выполнить эти действия для ознакомления с работой этого элемента управления. Обратите внимание, что элемент управления, отображающий дату создания заказа, не доступен пользователю, поскольку его значение представляет собой всего лишь текущую дату. Такое состояние элемента управления обеспечивается установкой значения свойства Disabled (Отключен) равным True. Обратите также внимание, что свойства Read Only (Только для чтения) полей редактирования идентификатора заказа и имени клиента установлены равными True, дабы предотвратить изменение отображаемых значений.
IIDC_COMPANYNAME -iПодключается к таблице CustomersIIDC_COMPANYNAME -iПодключается к таблице Customersг-IDC_CANCELORDERОтменяет создание нового заказаг-IDC_CANCELORDERОтменяет создание нового заказа
IIDC_COMPANYNAME -i
Подключается к таблице Customers
IIDC_COMPANYNAME -i
Подключается к таблице Customers
г-IDC_CANCELORDER
Отменяет создание нового заказа
г-IDC_CANCELORDER
Отменяет создание нового заказа
IDC_NEWORDERIDУникальный идентификатор, созданный как max(OrderlD)+1 на основе данных из таблицы OrdersIDC_NEWORDERIDУникальный идентификатор, созданный как max(OrderlD)+1 на основе данных из таблицы OrdersIDC_REQUIREDDATEДата выполнения заказаIDC_REQUIREDDATEДата выполнения заказаIDC_SELECTPRODUCTS Выполняет переход к диалогу для выбора продуктов, включаемых в заказIDC_SELECTPRODUCTS Выполняет переход к диалогу для выбора продуктов, включаемых в заказIDC_ORDERDATEТекущая датаIDC_ORDERDATEТекущая датаЭто элементывыбора даты/времениЭто элементывыбора даты/времени
IDC_NEWORDERID
Уникальный идентификатор, созданный как max(OrderlD)+1 на основе данных из таблицы Orders
IDC_NEWORDERID
Уникальный идентификатор, созданный как max(OrderlD)+1 на основе данных из таблицы Orders
IDC_REQUIREDDATE
Дата выполнения заказа
IDC_REQUIREDDATE
Дата выполнения заказа
IDC_SELECTPRODUCTS Выполняет переход к диалогу для выбора продуктов, включаемых в заказ
IDC_SELECTPRODUCTS Выполняет переход к диалогу для выбора продуктов, включаемых в заказ
IDC_ORDERDATE
Текущая дата
IDC_ORDERDATE
Текущая дата
Это элементывыбора даты/времени
Это элементывыбора даты/времени
Required Date:Required Date:02 05/2005▼02 05/2005▼Customer Name:Customer Name:| Sample edit box| Sample edit box
Required Date:
Required Date:
02 05/2005▼
02 05/2005▼
Customer Name:
Customer Name:
| Sample edit box
| Sample edit box
Self ct Customer and Re quire d Ship Date
Order ID:Order ID:Order Date:
Order ID:
Order ID:
CancelCancelSelect Product [
Cancel
Cancel
Puc. 20.15. Элементы управления в диалоге IDDjCUSTC№RwFOPM
В класс CCustomerView можно добавить переменные для хранения значений, полученных из элементов управления выбора даты/времени. Щелкните правой кнопкой мыши на поле даты заказа и в контекстном меню выберите пункт Add Variable (Добавить переменную). Открывающееся при этом диалоговое окно показано на рис. 20.16.
Поскольку нам необходима переменная для хранения значения, полученного из элемента управления, а не для обеспечения доступа к самому элементу, в качестве значения Category (Категория) необходимо выбрать Value (Значение), а в качестве Variable type (Тип переменной) — Ctime. Создаваемому члену можно присвоить имя m OrderDate.
Рис» 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 (Выбрать продукты) в диалоговом окне выбора клиента. Диалоговое окно должно отображать достаточный объем информации для выбора продукта, а также должно
содержать элементы управления для ввода количества единиц продукта и размера скидки. Это диалоговое окно показано на рис. 20.17.
Обратите внимание, что большинство элементов управления этого диалогового окна, за исключением полей ввода значений объема заказа и скидки, доступны только для чтения. Это связано с тем, что пользователь должен вводить только два упомянутых значения. Поле имени продукта можно связать с набором записей, добавляя оператор функции DoDataExchange О в класс CProductView:
void CProductView::DoDataExchange(CDataExchange* pDX)
{
CRecordView::DoDataExchange(pDX);
DDX_FieldText(pDX, IDC_PRODUCTNAME, 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 Завершает создание заказа
Sc oil to s ^ect a product ?nd en er the quantity and discount
Order ID:Order ID:Sample edSample edCustomer:Customer:Sample edit boxSample edit boxProduct Name:Product Name:Sample edit boxSample edit boxQuantity:Quantity:1 Sample ed1 Sample edDiscour,t:Discour,t:Sample edSample ed
Order ID:
Order ID:
Sample ed
Sample ed
Customer:
Customer:
Sample edit box
Sample edit box
Product Name:
Product Name:
Sample edit box
Sample edit box
Quantity:
Quantity:
1 Sample ed
1 Sample ed
Discour,t:
Discour,t:
Sample ed
Sample ed
DoneDoneSelect Product
Done
Done
Рис. 20.1 *7. Диалоговое окно для ввода количества единиц продукта и размера скидки
Щелкните правой кнопкой мыши на каждом из этих элементов управления в диалоговом окне IDD_PRODUCT_FORM и в контекстном меню выберите команду Add Variable. Для каждой из этих переменных должна быть выбрана категория Value, а в качестве значения Acess (Доступ) установлено Public (Общедоступная). Для переменной m_0rderID установите тип long, а для переменной m_CusternerName — тип 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 };
Для записи идентификатора текущего представления класс СМаinFrame должен содержать переменную типа ViewID, поэтому добавьте переменную m_CurrentViewID, щелкнув правой кнопкой мыши на объекте Ста inFrame в панели Class View и выбрав пункт Add Member Variable (Добавить переменную-член) из контекстного меню. Эта переменная должна быть инициализирована, потому измените конструктор класса СМа in Frame следующим образом:
CMainFrame::СМаinFrame() : m_CurrentViewID(ORDER_DETAILS)
{
// TODO: здесь следует вставить код инициализации члена
}
Приведенный код определяет представление, с открытия которого всегда будет запускаться приложение. Добавьте в файл . срр директиву #include для файла
ViewConstants .h, поскольку он содержит определения идентификаторов представлений.
Теперь в класс СМаinFrame можно добавить функцию-член 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:// Создание представления для
H добавления продукта в заказ 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); // Активизация нового представляения p01dActiveView->ShowWindow(SW_HIDE); // Сокрытие старого представления pNewActiveView->ShowWindow(SW_SHOW); H Отображение нового представления pOldActiveView->SetDlgCtrlID(m_CurrentViewID); //Определение идентификатора //старого представления
pNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
m_CurrentViewID - viewID; // Сохранение идентификатора нового представления RecalcLayout();
}
Приведенный код ссылается на классы 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 ():
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О;
m_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);
II Начало обновления
Ш—pSet->Edit();
m_Mode = UPDATE; // Переключение в режим обновления } } catch(CException* рЕх) {
pEx->ReportError ();// Отображение сообщения об ошибке
} } else
AfxMessageBox(_Т("Набор записей недоступен для обновления.")); }
Теперь, в зависимости от того, хранится ли в переменной m_UpdateMode значение READ_ONLY или UPDATE, можно скрывать или отображать кнопку New Order. Потребуется также обеспечить видимость кнопки в обработчике OnCancel ():
void COrderDetailsView::0nCancel()
m_pSet->CancelUpdate ();// Отмена операции обновления
m_EditOrderCtrl.SetWindowText(_Т("Edit")); II Переключение текста кнопки m_CancelEditCtrl.ShowWindow(SW_HIDE);И Сокрытие кнопки Cancel
m_NewOrderCtrl. ShowWindow (SW_SHOW) ;// Отображение кнопки New Order
m_QuantityCtrl.SetReadOnly(TRUE);11 Установка состояния поля
// редактирования объема заказа 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.
Рис. 20.18. Создание переменной для хранения идентификатора нового заказа
По умолчанию типом переменной является CString, поэтому нс забудьте его изменить на long. Функции DDX_Text (), выполняющие передачу данных в элемент редактирования и из него, имеют множество разновидностей, которые соответствуют различным типам данных, представленным в раскрывающемся списке этого диалогового окна.
Создание нового идентификатора заказа
Объект COrderSet относится к объекту документа, поэтому наряду с членом m_DBSimpleUpdateSet, который был создан мастером Application Wizard, добавьте в класс CDBSimpleUPdateDoc член данных по имени m_OrderSet. Для этого, как обычно, щелкните правой кнопкой мыши на имени класса в панели Class View и в контекстном меню выберите пункт Add^Add Variable. Документ COrderSet создается автоматически при создании объекта документа. Когда объект заказа определен в документе, он доступен в любом из классов представлений, нуждающемся в нем.
Для обеспечения генерации уникального идентификатора нового заказа в класс COrderSet следует добавить новую функцию-член. Перейдите в панель Class View и добавьте функцию CreateNewOrderlD() с возвращаемым типом long и без параметров.
Первым делом функция CreateNewOrderlD () должна проверять, открыт ли набор записей:
long COrderSet::CreateNewOrderlD()
{
if (! IsOpen ())
Open(CRecordset::dynaset);
// Остальной код реализации функции...
Функция IsOpen (), вызываемая в операторе if, возвращает значение TRUE, если набор записей открыт, и значение FALSE в противном случае. Для открытия набора записей потребуется вызвать член Open (), унаследованный от класса CRecordSet. Эта функция посылает базе данных SQL-запрос с типом набора записей, который указан в первом аргументе. В данном случае первый аргумент указан в виде CRecordset: : dynaset, что, как легко догадаться, открывает набор записей в качестве динамического набора. В данном случае явное указание типа набора записей не обязательно, поскольку при отсутствии этого аргумента был бы применен используемый по умолчанию тип, заданный при создании класса — dynaset. Однако его явное указание служит дополнительным поводом для описания других его возможных значений (табл. 20.5).
Таблица 20.5. Типы набора записей
Тип
Описание
CRecordset::snapshot
Набор записей открывается как снимок; снимки и динамические наборы рассматривались в предыдущей главе.
CRecordset::forwardonly
Набор записей открывается как доступный только для чтения и в нем можно выполнять прокрутку записей только вперед.
(При открытии набора записей он автоматически позиционируется на первую запись.)
CRecordset::dynamic
Набор записей открывается с возможностью прокрутки в обоих направлениях, а изменения, выполненные другими пользователями, отражаются в его полях.
AFX_DB_USE_DEFAULT_TYPE
Набор записей открывается с типом, указанным по умолчанию, который хранится в унаследованном члене m nDefaultType, инициализируемом в конструкторе.
Функция Open () получает еще два параметра, для которых мы принимаем значения аргументов, используемые по умолчанию. Второй параметр — указатель на строку, которая может быть именем таблицы, SQL-оператором SELECT, вызовом предварительно определенной процедуры запроса или нулевой строкой — последнее значение используется по умолчанию. Если это значение — null, функция использует строку, возвращенную функцией GetDefaultSQL (). Третий параметр — битовая маска, которая может служить для указания множества параметров подключения, в том числе для указания доступа только для чтения, что делает запись в набор записей вообще невозможной, или для разрешения добавления записей только в конец набора, что запрещает редактирование или удаление записей. Подробнее эти параметры описаны в документации по функции.
Когда набор записей открыт, необходимо просмотреть все записи для отыскания наибольшего значения в поле Order ID. Это выполняется с помощью следующего кода:
long COrderSet::CreateNewOrderlD()
if(!IsOpen ())
Open(CRecordset::dynaset);
// Проверка на предмет отсутствия записей в наборе записей long newOrderlD = 0;
if ( ! (IsBOFO && IsEOFO ) )
{// Записи существуют,
MoveFirst ();// поэтому перейти к первой записи
while(!IsEOF())// Сравнение со всеми остальными
(
// Сохранение идентификатора заказа, если он - наибольший if(newOrderlD < m_OrderID)
newOrderlD = m_OrderID;
MoveNextQ;// Переход к следующей записи
}
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 будет содержать максимальный идентификатор заказа, поэтому прежде чем возвратить значение нового идентификатора, достаточно увеличить его на единицу.
Последнее действие, которое потребуется выполнить — передача значения элементу управления, чтобы оно отображалось в диалоговом окне IDD_CUSTOMER_FORM. Этой цели служит вызов функции UpdateData () с аргументом FALSE для объекта представления набора записей. Эта функция наследуется классом представления записей из класса CWnd. Значение аргумента, равное FALSE, вызывает передачу данных из члена класса представления элементам управления диалогового окна. Значение TRUE вызывает извлечение данных из элементов управления и их сохранение в членах. В обоих случаях необходимые операции выполняются посредством обращения к функции-члену DoDataExchange () представления, которое вызывается каркасом приложений.
Инициализация создания идентификатора
Представление клиентов нуждается в доступном идентификаторе нового заказа при первом отображении. Добавьте в класс CCustomerView функцию-член типа public и реализуйте ее следующим образом:
void CCustomerView::SetNewOrderlD(void)
// Получение идентификатора нового заказа из объекта COrderSet в документе m_NewOrderID = static_cast<CDBSimpleUpdateDoc*>
(GetDocument())->m_OrderSet.CreateNewOrderlD();
UpdateData(FALSE); // Передача данных элементам управления }
Типом указателя, возвращаемого унаследованной функцией GetDocument (), является Cdocument. Этот указатель требуется для получения доступа к члену m_OrderSet производного класса, поэтому его тип нужно привести к CDBSimpleUpdateDoc*. Затем применительно к члену m_OrderSet класса документа можно вызвать функцию-член, которая возвращает идентификатор нового заказа, и сохранить результат в члене М_NewOrderlD класса 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 всегда создается первым, поэтому
Н о его создании можно не беспокоиться.
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 = pOldActiveView->GetDocument();
pNewActiveView->Create(NULL, NULL, 0L, 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
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;
/ / Конструктор по умолчанию
COrder():
m_0rderID(0),II Будет устанавливаться объектом CCustomerView
m_EmployeeID(1),// Идентификатору сотрудника присвоено
// произвольное значение
m_ShipVia(3),// Произвольная компания доставки
m_CustomerID(_Т(””)), m_Freight(0.0), m_ShipName(_Т(””)), m_ShipAddress(_Т ("")), m_ShipCity(_Т ("")), m_ShipRegion(_Т (””)), m_ShipPostalCode(_Т ("”)), m_ShipCountry(_Т ("”))
SYSTEMTIME Now;
GetLocalTime(&Now);// Получение текущего времени
m_OrderDate = m_RequiredDate = CTime(Now); // Установка текущего времени
// в качестве значений полей }
};
Вообще говоря, не рекомендуется делать все данные-члены класса общедоступными (public), как в данном случае, но поскольку все классы наборов записей, генерируемые мастером классов, содержат только члены типа public, определение членов в нашем классе как private практически ничего не изменило бы.
Если в класс документа добавить член данных m_Order типа COrder, его можно будет использовать для передачи данных заказа объекту С Product View. Для этого достаточно вынудить объект CCustomerView при щелчке на кнопке Select Products загружать данные-члены, подготовленные для выбора объектом С Pro duct View. Обработчик кнопки в объекте 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;
pDoc-> m_Order.m_ShipRegion = m_pSet->m_Region;
II Установка значений полей на основании значений, / / введенных в диалоговом окне 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. Поэтому можно приступить к процессу выбора продукта.
Выбор продуктов для включения в заказ
Для представления выбора продукта требуются переменные элементов управления, которые обеспечивали бы отображение уже установленных значений идентифи-
катора заказа и имени клиента. Эти значения будут извлекаться из члена Order объекта документа. Для этого в класс CProductView можно добавить соответствующую функцию. Ей можно присвоить имя InitializeView (), а в качестве возвращаемого типа указать void. Эту функцию можно вызывать из члена Select View () объекта СМаinFrame приложения. Тем самым мы обеспечим гарантированную инициализацию элементов управления перед отображением диалогового окна.
Но прежде чем реализовать функцию InitializeViewO, необходимо учесть несколько дополнительных обстоятельств. Добавление новой записи в таблицу 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_CustomerName = pDoc->m_Order.m_ShipName;
m_Quantity = 1; II Обязателен заказ не менее одной единицы продукта 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) {
switch(viewID)
case NEW_ORDER: II Создание представления для добавления нового заказа 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();
else if(viewID == SELECT_PRODUCT)
static_cast<CProductView*>(pNewActiveView)->InitializeView();
p01dActiveView->ShowWindow(SW—HIDE); // Сокрытие старого представления pNewActiveView->ShowWindow(SW—SHOW); II Отображение нового представления pOldActiveView->SetDlgCtrlID(m_CurrentViewID); //Определение идентификатора //старого представления
pNewActiveView->SetDlgCtrlID(AFX_IDW_PANE_FIRST);
m_CurrentViewID = viewID; // Сохранение идентификатора нового представления RecalcLayout();
}
Значение параметра ViewID, равное SELECT_PRODUCT, будет приводить к инициализации переменных класса CProductView, соответствующих полям идентификатора заказа и имени клиента, а также переменной типа bool, управляющей созданием новой записи в таблице Orders.
Добавление нового заказа
Последняя часть создаваемой программы — код добавления нового заказа. Добавление нового заказа всегда выполняется функцией-членом OnSelectproducts () класса CProductView. Эффект от щелчка на кнопке Select Products зависит от значения члена данных ш_О г de г Added. Если оно равно false, функция должна добавить новые записи в таблицы Orders и Order Details. Если значение этой переменной равно true, добавление новой записи должно выполняться только в таблицу Order Details, поскольку речь идет о еще одном продукте в рамках одного и того же заказа. Все значения, требуемые для создания новой записи объекта Orders, хранятся в члене m Order документа. Нужно лишь скопировать их в члены объекта COrderSet, который также является членом документа. Объект документа обладает всем необходимым для выполнения этой задачи, поэтому добавьте в класс CDBS imp 1 eUpdate Do с функцию-член AddOrder () с возвращаемым типом bool и реализуйте ее так, как показано ниже.
bool CDBSimpleUpdateDoc::AddOrder()
try
// Если набор записей не открыт, // его нужно открыть// Если набор записей не открыт, // его нужно открыть{
// Если набор записей не открыт, // его нужно открыть
// Если набор записей не открыт, // его нужно открыть
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_0rder.m_Freight;
m_OrderSet.m_OrderDate = m_0rder.m_OrderDate;
m_OrderSet.m_OrderID = m_0rder.m_0rderID;
m_OrderSet.m_RequiredDate = m_Order.m_RequiredDate;
m_OrderSet.m_ShipAddress = m_Order.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_0rder.m_ShipVia;
// Для поля Shipped Date значение не передается
m_OrderSet.SetFieldNull(&m_OrderSet.m_ShippedDate);
m_OrderSet.Update();// Завершение добавления новой записи
return true;H Возврат признака успешного добавления
} else
AfxMessageBox(_Т("Дополнение таблицы Orders невозможно")); } catch(CException* рЕх)// Перехват любых исключений
pEx->ReportError();// Отображение сообщения об ошибке
}
return false;// Признак неудачи
Ранее в этой главе было показано, что функции добавления и редактирования записей в наборе могут возбуждать исключения. Поэтому во избежание прерывания приложения в этих случаях код помещен в блок try и любые исключения будут перехвачены.
После получения подтверждения открытия набора записей COrderSet с помощью вызова функции CanAppend () мы проверяем возможность добавления в него записей. Добавление новой записи состоит из трех шагов, которые описаны ниже.
Вначале вызывается функция-член AddNew () набора записей. Она начинает процесс и сохраняет текущие значения данных-членов в наборе записей, поскольку впоследствии они будут изменены на значения null. Это значение не имеет ничего общего с нулевым значением указателей и не является нулем — в данном случае нулевое значение означает отсутствие значения переменной.
Значения всех данных-членов полей набора записей устанавливаются равными требуемым в записи. Это достаточно просто. Мы просто копируем значения, сохраненные в членах объекта m_0rder, в члены объекта набора записей. Член m_ShippedDate является нулевым, поскольку его значение еще не установлено.
Далее вызывается функция Update () для выполнения действительной записи и одновременного восстановления первоначальных значений объекта набора
записей. В данном случае это не применяется, но при отображении набора записей, куда выполняется добавление, для вывода значений новой записи пришлось бы вызвать член Requery () объекта набора записей.
Теперь можно приступить к определению общей логики работы обработчика OnSelectproduct () класса CProductView. Для выполнения передачи данных, которые были введены в поля редактирования, данным-членам объекта представления необходимо вызвать функцию Update Dat а () представления. Основной код функции обработчика выглядит следующим образом:
void CProductView::OnSelectproduct() {
UpdateData(TRUE); M Передача данных из элементов управления
// Получение указателя на документ
CDBSimpleUpdateDoc* pDoc = static_cast<CDBSimpleUpdateDoc*>(GetDocument());
if(!m_0rderAdded) // Если заказ не был добавлен, m_OrderAdded = pDoc->AddOrder(); Н попытаться его добавить
if(m_0гdeгAdded)
II Код добавления новой записи в таблицу 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)
{ try { if(!m_DBSimpleUpdateSet.IsOpen())// Если набор записей не открыт,
m_DBSimpleUpdateSet.Open();// его нужно открыть
m_DBSimpleUpdateSet.AddNew();// Начало добавления новой записи
// Установка значений данных-членов для продукта
m_DBSimpleUpdateSet.m_0rderID = m_0rder.m_0rderID;
m_DBSimpleUpdateSet.m_Quantity = quantity; m_DBSimpleUpdateSet.m_Discount = discount; m_DBSimpleUpdateSet.m_ProductID = ID;
m_DBSimpleUpdateSet.m_UnitPrice = price;
m_DBSimpleUpdateSet.Update();H Завершение добавления новой записи
} catch(CException* рЕх)H Перехват любых исключений
{
pEx->ReportError();// Отображение сообщения об ошибке
}
}
Эта функция устанавливает значения членов m_DBSimpleUpdateSet, а затем обновляет таблицу, по сути, так же, как это выполнялось для таблицы Orders. Как и ранее, код должен быть помещен в блок try, чтобы обеспечить перехват любых исключений, которые могут генерироваться функцией AddNew () или Update ().
Эта функция должна вызываться при каждом вызове обработчика кнопки Select Product в классе CProductView, поэтому обработчик можно изменить:
void CProductView::OnSelectproduct()
{
UpdateData(TRUE); // Передача данных из элементов управления И Получение указателя на документ
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.
Рис. 20.19. Добавление очередного заказа
Далее был выполнен щелчок на кнопке Select Product, после чего выбран продукт, количество и скидка, как показано на рис. 20.20.
Рис. 20.20. Выбор продукта и установка для него количества единиц и размера скидки
Щелчок на кнопке Select Product добавляет выбранный продукт к заказу данного клиента, после чего можно выбрать другой продукт. Каждый щелчок на кнопке Select Product добавляет новую запись в таблицу Order Details заказа с текущим идентификатором заказа. Когда заказ сформирован, достаточно щелкнуть на кнопке Done (Готово), чтобы завершить процесс.
В том, что заказ добавлен, легко убедиться, перейдя к последнему заказу в форме просмотра сведений о заказах, как показано на рис. 20.21.
Рис. 20.21. Очередной заказ добавлен
Вы могли обратить внимание, что представления не сбрасываются к началу набора записей по завершении операции ввода заказа. Попытайтесь выполнить первое упражнение, предложенное в конце этой главы, чтобы устранить этот недостаток для набора записей клиентов. Эта задача должна быть не особенно сложной.
Резюме
В этой главе рассматривалась реализация элементарных обновлений с использованием поддержки ODBC из библиотеки MFC.
Ниже перечислены ключевые моменты, с которыми вы познакомились в настоящей главе.
Обновление возможно только в том случае, если набор записей соответствует одной единственной таблице. Наборы записей, соответствующие соединению таблиц, не могут быть обновлены.
Чтобы начать редактирование записи в наборе записей, необходимо вызвать функцию-член Edit () объекта набора записей.
Чтобы начать добавление новой записи в набор, потребуется вызвать функцию-член AddNew () объекта набора записей.
Чтобы завершить изменение существующей записи или добавление новой, необходимо вызвать функцию-член Update () объекта набора записей.
Прежде чем инициализировать обновление набора записей, всегда следует убедиться, что набор записей открыт, и что операция обновления, которую требуется выполнить, является допустимой.
Транзакция объединяет в единое целое последовательности операций обновления базы данных, что позволяет восстановить исходное состояние базы данных в случае ошибки.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Измените разработанное в этой главе приложение обновления так, чтобы диалоговое окно добавления нового заказа всегда отображало клиентов в алфавитном порядке, и чтобы при каждом его открытии оно отображало первого клиента.
Измените пример так, чтобы представление выбора продуктов для включения в заказ (CProductView) отображало общую сумму нового заказа.
Расширьте приведенный в этой главе пример, чтобы сотрудников можно было выбирать из записей таблицы Employees (Сотрудники).
Дополнительно расширьте пример, чтобы поле грузоотправителя можно было выбирать из числа записей таблицы Shippers (Грузоотправители).
21
Приложения,использующие средстваWindows Forms
В этой главе вы научитесь создавать графический интерфейс пользователя для приложений Windows Forms. Вы узнаете, как обеспечить поддержку взаимодействия пользователя с элементами управления интерфейса. Для достижения этой цели в ходе ознакомления с материалом этой главы будет постепенно создаваться единственное приложение, которое к концу главы превратится полностью функциональную программу Windows Forms умеренного объема.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
Используя возможности конструктора форм Form Design, интерактивно строить графический интерфейс пользователя приложения.
Добавлять в форму элементы управления.
Добавлять к элементам управления обработчики событий.
Общее представление о Windows Forms
Windows Forms (Формы Windows) — это набор средств для создания Windows-приложений, выполняющихся в среде CLR (Common Language Runtime — общеязыковая исполняющая среда). Форма — это окно, служащее основой окна приложения или диалогового окна, в которое можно добавлять другие элементы управления, предназначенные для взаимодействия с пользователем. Пакет Visual C++ 2005 поставляется со стандартным набором более 60 элементов управления, которые можно применять в формах. Поскольку количество доступных элементов управления очень велико, в этой книге рассматриваются только наиболее показательные из них, но при этом читатели должны получить достаточно полное представление об их применении, чтобы
самостоятельно продолжить ознакомления с остальными элементами. Многие стандартные элементы управления, такие как 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), создайте новый проект CLR и присвойте ему имя Ех21_01. Панель Editor отобразит графическое представление окна приложения, поскольку построение графического интерфейса пользователя с помощью приложения Windows Forms выполняется графически. Даже двойной щелчок на имени файла Formi. h в панели Solution Explorer (Проводник решений) не приводит к отображению кода. Но его можно открыть, щелкая правой кнопкой мыши в окне Editor (Редактор) и выбирая пункт View Code (Показать код) из контекстного меню.
Код определяет класс Formi, представляющий окно приложения. Прежде всего, следует отметить, что он определен в собственном пространстве имен:
using namespace using namespace using namespace using namespace using namespace using namespaceusing namespace using namespace using namespace using namespace using namespace using namespacenamespace Ех21_01 {
using namespace using namespace using namespace using namespace using namespace using namespace
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
Это пространство имен содержит классы, которые определяют типы данных, используемые во всех приложениях CLR. Оно содержит также классы событий и обработчиков событий, исключения и классы, поддерживающие функции общего применения.
System::ComponentModel
Это пространство имен содержит классы, которые поддерживают работу компонентов графического интерфейса пользователя в приложениях CLR.
System::Collections
Это пространство имен содержит классы коллекций, предназначенные для всевозможной организации данных, в том числе классы определения списков, очередей, словарей (карт) и стеков.
System::Windows::Forms
Это пространство имен содержит классы, которые поддерживают использование в приложении средств Windows Forms.
System::Data
Это пространство имен содержит классы, которые поддерживают набор компонентов ADO.NET, используемый для доступа и обновления источников данных. Подробнее осуществление доступа к источникам данных в приложении CLR описано в следующей главе.
System::Drawing
Это пространство имен определяет классы, которые поддерживают основные графические операции, подобные рисованию на форме или компоненте.
Класс Formi~ производный от класса Form, который определен в пространстве имен System: : Windows : : Forms. Класс Form представляет окно приложения или
диалогового окна, а класс Formi, который определяет окно для пространства имен Ех21_01, наследует все члены класса Form.
Раздел в конце класса Formi содержит определение функции 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 класса Formi унаследован от базового класса, и его задача — отслеживание компонентов, постепенно добавляемых в форму. Первый оператор сохраняет в члене components дескриптор объекта Container, представляющего коллекцию, которая хранит компоненты графического интерфейса пользователя в списке. Каждый новый компонент, добавляемый в форму с помощью средств Form Design, добавляется в этот объект Container.
Изменение свойств формы
Остальные операторы функции InitializeComponent () определяют свойства объекта Formi. Ни одно из этих свойств не следует изменять непосредственно в коде, но их значения можно выбирать посредством окна Properties (Свойства) формы. Поэтому снова обратитесь к вкладке Formi.h[Design] формы в окне редактора и щелкните на ней правой кнопкой мыши, чтобы открыть окно Properties, показанное на рис. 21.1.
Чтобы получить представление о доступных возможностях, имеет смысл просмотреть список свойств формы. Щелчок на любом из них приводит к отображению описания в нижней части окна. На рис. 21.1 приведено окно Properties, ширина которого увеличена для отображения описаний. Чтобы изменить значение свойства, можно выбрать ячейку в правой колонке. Свойства, слева от которых отображен символ +, обладают несколькими значениями, и щелчок на этом символе отображает значения, чтобы их можно было изменять по отдельности. Щелкая на кнопке, можно также упорядочить свойства по алфавиту. Это облегчает поиск свойства с известным именем.
Попробуйте изменить значение Width (Ширина) свойства Size (Размер) на 600, а значение Height (Высота) — на 400. Можно также изменить свойство Text (Текст) (размещенное в категории Appearance (Внешний вид) на "A Winning Application”. Это приведет к изменению текста в строке заголовка окна приложе-
ния. Если вы вернетесь к вкладке Formi. h в окне Editor, то увидите, что код функции InitializeComponent О изменился, отражая внесенные изменения свойств.
Рис. 21.1. Окно Properties объекта Formi
Обратите внимание, что, устанавливая значение свойства Windowstate, окно приложения можно определить так, чтобы при запуске программы оно открывалось с максимальным размером. При используемом по умолчанию значении Normal окно открывается с указанными размерами, но в списке значений этого свойства можно выбирать Maximized (Развернутое) или Minimized (Свернутое).
Запуск приложения
Как всегда, выполнение приложения начинается в функции main (), которая в файле Ех21_01. срр определена следующим образом:
int main(array<System::String A> Aargs)
{
// Активизация визуальных эффектов Windows XP до создания каких-либо
// элементов управления
Application::EnableVisualStyles();
// Создание главного окна и его запуск
Application::Run(gcnew Formi());
return 0;
}
Функция main () вызывает две статические функции, которые определены в классе Application, определенном в пространстве имен System: :Windows: :Forms. Статические функции класса Application образуют основу любого приложения
Windows Forms. Функция EnableVisualStyles (), вызываемая в функции main () первой, активизирует для приложения визуальные стили. Функция Run () запускает для приложения цикл сообщений Windows и делает видимым объект Form, передаваемый в качестве аргумента. Приложение, выполняемое в среде CLR, обязательно остается приложением Windows, поэтому с циклом сообщений оно работает точно так же, как все остальные приложения Windows.
Индивидуальная настройка графического интерфейса пользователя
Процесс индивидуальной настройки графического интерфейса пользователя в соответствии с потребностями конкретного приложения предполагает добавление стандартных элементов управления и/или дополнительных форм, а также настройку их свойств. Нам предстоит преобразовать проект Ех21_01 в программу генерирования выигрышных номеров лотереи. Это позволит приобрести хорошие практические навыки работы со значительным набором элементов управления из числа доступных. Дизайн этой программы будет не идеальным, поскольку она включает ряд элементов управления, без особой необходимости дублирующих некоторые функции, но зато она позволит на практике исследовать различные возможности действующего приложения. В качестве примера были выбраны две лотереи. Если вам больше по нраву какие-то другие лотереи, соответствующее изменение примера не должно вызывать особые сложности.
По поводу ввода лотерейных номеров необходимо сделать одно замечание— вероятно, не стоит выбирать номера ат 1 до 6, поскольку их выигрыш весьма маловероятен. Конечно, в действительности выпадение выигрыша на эти номера столь же вероятно, как и на любые другие, но ни в одной известной мне лотерее номера с 1 по 6 практически никогда не оказывались среди выигрышных. Поэтому можно смело полагать, что выбор любого из них ведет
к проигрышу.
Чтобы отобразить доступные элементы управления, вначале выберите вкладку Form1.h[Design] в окне Editor, а затем нажмите комбинацию клавиш <Ctrl+Alt+X> или выберите пункт меню View^Toolbox (Вид1^Панель инструментов). Откроется окно списка доступных элементов управления, показанное на рис. 21.2.
Первый блок в окне Toolbox озаглавлен как All Windows Forms (Все формы Windows) и он содержит список всех доступных элементов управления, которые можно использовать в форме. Если блок All Windows Forms еще не развернут, щелкните на его символе +. Свернуть этот блок обратно можно, щелкнув на знаке -, расположенном слева от заголовка блока. Как вы убедитесь при этом, элементы управления сгруппированы также по типу, начиная с блока Common Controls (Общие элементы управления). Вероятно, поначалу удобнее использовать расположенную в верхней части списка группу, содержащую все элементы управления. Но после ознакомления с доступными элементами управления, возможно, вы предпочтете свернуть все группы и разворачивать их по одной, по мере необходимости.
Toolbox▼4 x- All Windows Forms^ PointerBackgroundworker d Binding Navigator 3 Bindingsource @ Button 0 CheckBoxChecked ListBox] ColorDialog@ ComboBox ff ContextVenuStripDataGridView^ DataSetDateTimePickerDi recto ryEntryDirector/SearcherDomainlipDown j ErrorProiider ^ EventlogToolbox▼4 x- All Windows Forms^ PointerBackgroundworker d Binding Navigator 3 Bindingsource @ Button 0 CheckBoxChecked ListBox] ColorDialog@ ComboBox ff ContextVenuStripDataGridView^ DataSetDateTimePickerDi recto ryEntryDirector/SearcherDomainlipDown j ErrorProiider ^ EventlogРис. 21.2. Окно Toolbox со списком доступных элементов управРис. 21.2. Окно Toolbox со списком доступных элементов управления
Toolbox▼
4 x
- All Windows Forms
^ Pointer
Backgroundworker d Binding Navigator 3 Bindingsource @ Button 0 CheckBox
Checked ListBox
] ColorDialog
@ ComboBox ff ContextVenuStrip
DataGridView
^ DataSet
DateTimePicker
Di recto ryEntry
Director/Searcher
DomainlipDown j ErrorProiider ^ Eventlog
Toolbox▼
4 x
- All Windows Forms
^ Pointer
Backgroundworker d Binding Navigator 3 Bindingsource @ Button 0 CheckBox
Checked ListBox
] ColorDialog
@ ComboBox ff ContextVenuStrip
DataGridView
^ DataSet
DateTimePicker
Di recto ryEntry
Director/Searcher
DomainlipDown j ErrorProiider ^ Eventlog
Рис. 21.2. Окно Toolbox со списком доступных элементов управ
Рис. 21.2. Окно Toolbox со списком доступных элементов управ
Добавление элементов управления в форму
Добавьте элемент управления MenuStrip в верхнюю часть формы, щелкнув на элементе MenuStrip в окне Toolbox, а затем там, куда его нужно поместить — в верхней части клиентской области окна приложения. Слева над элементом управления отобразится небольшая стрелка. Щелчок на ней приведет к открытию всплывающего окна, показанного на рис. 21.3.
MenuSti ip Tasks
Embed in ToolStripGontainer j
| ГорSjHiddenvh| ГорSjHiddenvhInsert Standard Ее ms
| ГорSj
Hiddenvh
| ГорSj
Hiddenvh
Dock
GripStyle
Edit Items...
Puc. 21.3. Окно MenuStrip Tasks
Первый элемент во всплывающем окне MenuStrip Tasks (Задачи строки меню) внедряет строку меню внутрь элемента управления ToolStripContainer, предоставляющего панели, расположенные вдоль всех четырех сторон формы, и центральную область, в которой можно помещать другой элемент управления. Второй элемент генерирует четыре стандартных элемента меню: File (Файл), Edit (Правка), Tools (Сервис) и Help (Справка), содержащие списки соответствующих пунктов меню. В данном примере эти элементы не требуются, но возможность мгновенного генерирования меню весьма пригодится во многих приложения. В то же время, добавление этого меню предоставляет то преимущество, что меню File будет содержать пункт Exit (Выход), позволяющий закрывать приложение. Элемент RenderMode позволяет выбирать стиль изображения строки меню, и для него можно оставить значение, выбранное по умолчанию. Опция Dock (Стыковка) позволяет выбирать сторону формы, к которой будет пристыкована строка меню, или же оставлять строку меню в отстыкованном состоянии. Опция GripStyle определяет видимость рамки для перетаскивания меню. В Visual C++ 2005 строки меню снабжены видимыми рамками для перемещения. Выбор последней опции Edit Items (Редактировать элементы) приводит к отображению диалогового окна, в котором можно изменять свойства строки меню или любых его элементов.
Добавление меню
Чтобы добавить меню, введите текст меню в поле строки меню. Добавьте в строку меню три элемента, присвоив им имена &Р1ау, SLimits и &Не1р. Символ & предшествует символу, выполняющему роль клавиши быстрого доступа для данного элемента меню — так, например, клавиатурная комбинация <Alt+P> служит комбинацией быстрого доступа к меню Play (Игра). Теперь окно Editor, содержащее форму со строкой меню, должно выглядеть, как показано на рис. 21.4.
Как видите, имя строки меню, menus tripl, отображается под окном формы. Эта информация служит лишь для облегчения выбора — например, для изменения свойств. Свойство (Name) элемента управления — это имя переменной в коде, который ссылается на объект этого элемента управления. Поэтому значение свойства (Name) элемента управления MenuStrip выглядит как menuStripl. Для таких элемен-
тов управления, как MenuStrip, с которыми не предполагается работать программно, можно оставлять любые имена, присвоенные им по умолчанию. Но имя, присвоенное по умолчанию, может оказаться достаточно громоздким для работы с ним в коде. Поэтому его лучше изменить.
Рис. 21.4. Добавление нового пункта меню
Меню 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 (Редактор) можно работать непосредственно со строкой меню.
Рис. 21.5. Диалоговое окно Items Collection Editor
Чтобы добавить элементы в меню, щелкните на нем в строке меню. Затем щелкните на расположенном под ним элементе меню и введите нужный текст. Нам необходимо изменить свойства новых элементов меню, поэтому, прежде всего, щелкая правой кнопкой мыши на элементе меню Upper и выбирая в контекстном меню пункт Properties, откройте его окно свойств. Нам придется обеспечивать обработку событий для этого элемента меню, поэтому заданное по умолчанию имя свойства имеет смысл заменить каким-либо более кратким — например, upperMenuItem. Для этого элемента можно также добавить клавишу быстрого доступа. Для этого потребуется щелкнуть на стрелке вниз в колонке значения свойства Short cut Keys, в результате чего откроется список, показанный на рис. 21.6.
Выберите клавишу или клавиши-модификаторы, установив соответствующие флажки, и выберите клавишу быстрого доступа из раскрывающегося списка. Как видите, для этого элемента меню в качестве клавиш быстрого доступа была выбрана комбинация <Ctrl+Alt+U>. На рис. 21.6 видно также, что в качестве значения свойства ToolTipText был определен текст "Set upper limit for values" (“Установить верхний предел значений”). Помещение курсора мыши над этим элементом меню после короткой задержки будет приводить к отображению подсказки. Установка значения свойства AutoToolTip, расположенного в начальной части списка, равным True приведет к отображению подсказки, текст которой будет совпадать с текстом элемента меню. Если же оставить это значение равным False, в качестве текста подсказки будет использовано значение свойства ToolTipText. Устанавливая значение свойства ShowShortcutKeys, можно также управлять отображением комбинации клавиш быстрого доступа вместе с текстом элемента меню.
Щелчок на элементе меню Lower приведет к отображению его свойств в окне Properties. После этого значение свойства (Name) можно изменить на 1 owe rMenuI tern, значение свойства ShortcutKeys — на Ctrl+Alt+L, а значение свойства ToolTipText — на "Set lower limit for values" (“Установить нижний предел значений”). Точно так же можно изменить свойство (Name) элемента меню Reset на resetMenuItem, значение свойства shortCutKeys — на Ctrl+Alt+R, а текст подсказки — на "Reset limits to original values" (“Сбросить пределы в первоначальные значения”).
4 X4 XProperties
4 X
4 X
upp^rMenuItem System.Windows.Forms.ToolStripMenuIt€ ^
-0-0TextAlign TextDirection TexHmageRelatiori BehaviorAuto Size AutoToolTip CheckOn Click DoubleClickEnabledEnabledT colTipT ext VisibleData(Applicationsettings) DropDown DropDownltems Tagi—Design1(Name) CenerateMember ModifiersLayout MiseShortcutKeys ShowS ho rtcutKeysTextAlign TextDirection TexHmageRelatiori BehaviorAuto Size AutoToolTip CheckOn Click DoubleClickEnabledEnabledT colTipT ext VisibleData(Applicationsettings) DropDown DropDownltems Tagi—Design1(Name) CenerateMember ModifiersLayout MiseShortcutKeys ShowS ho rtcutKeysMiddleCenter Horizontal ImageBeforeTextMiddleCenter Horizontal ImageBeforeTextTrueFalseFalseFalseTrueSet upper limit for valuesTrueTrueFalseFalseFalseTrueSet upper limit for valuesTrue(none)Collection)(none)Collection)Modifiers:Modifiers:0E^ □ Shift Ei Alt Key:0E^ □ Shift Ei Alt Key:Ctrl- Alt+UTrueCtrl- Alt+UTrue
-
0
-
0
TextAlign TextDirection TexHmageRelatiori Behavior
Auto Size AutoToolTip CheckOn Click DoubleClickEnabled
Enabled
T colTipT ext Visible
Data
(Applicationsettings) DropDown DropDownltems Tagi—
Design1
(Name) CenerateMember Modifiers
Layout Mise
ShortcutKeys ShowS ho rtcutKeys
TextAlign TextDirection TexHmageRelatiori Behavior
Auto Size AutoToolTip CheckOn Click DoubleClickEnabled
Enabled
T colTipT ext Visible
Data
(Applicationsettings) DropDown DropDownltems Tagi—
Design1
(Name) CenerateMember Modifiers
Layout Mise
ShortcutKeys ShowS ho rtcutKeys
MiddleCenter Horizontal ImageBeforeText
MiddleCenter Horizontal ImageBeforeText
True
False
False
False
True
Set upper limit for values
True
True
False
False
False
True
Set upper limit for values
True
(none)
Collection)
(none)
Collection)
Modifiers:
Modifiers:
0E^ □ Shift Ei Alt Key:
0E^ □ Shift Ei Alt Key:
Ctrl- Alt+U
True
Ctrl- Alt+U
True
ShortcutKeys
The shortcut key associated with the menu item.
Pac. 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▼ £ X
tabCon troll System.Windows.Forms.TabControl т
0 ItemSize
58 18
Multiline
False
0 Padding
6. 3
ShowTco (Tips
False
Size Mode
Normal
Tabindex
1
TabPages
(Collection)
Tabstop
True
Visible
True
□ Data
0 (ApplicationSetting*
Defines which borders of the control are bound to the container
Puc. 21.7. Свойство Dock в окне Propertiesэлемента управления с вкладками
Элемент управления с вкладками содержит две вкладки, но при необходимости в него можно добавлять новые вкладки, щелкая на кнопке со стрелкой в правом верхнем углу элемента управления и выбирая команду Add Tab (Добавить вкладку) из всплывающего меню (рис. 21.8).
Рис. 21.8. Добавление новой вкладки
Необходимо изменить текст каждой вкладки на что-либо более вразумительное. Это же относится к значениям свойства (Name). Перейдите в окно Properties элемента управления TabControl — нажатие клавиши <F4> при выбранном элементе управления приведет к отображению его окна свойств, если оно еще скрыто. Затем выберите поле значения свойства TabPage и щелкните на появившейся кнопке с символом многоточия. Откроется диалоговое окно Tab Page Collection Editor (Редактор коллекции вкладок), показанное на рис. 21.9.
Рис. 21.9. Диалоговое окно Tab Page Collection Editor
Текст вкладки должен отражать название лотереи, с которой связаны элементы управления этой вкладки, поэтому значение свойства (Name) изменено на lottoTab, а значение свойства Text — на Lotto, что можно видеть на рис. 21.9. Значение свойства (Name) — имя, используемое в классе Forml для переменной, которая ссылается на эту вкладку, а значение свойства Text — текст, отображаемый на вкладке. При желании можете выбрать другие значения.
Вкладка, озаглавленная как tabPage2, предназначена для работы с записями второй лотереи, которая в данном случае носит название Euromillions. Как и ранее, можете выбрать название любой предпочитаемой лотереи. Щелкните на элементе tabPage2 в левой панели, чтобы отобразить его свойства на правой панели. После выбора новой вкладки имя элемента tabPage1 в левой панели должно измениться на lottoTab, а текст первой вкладки в форме должен обновиться. В качестве значения свойства Text элемента управления tabPage2 определен текст Euromillions, а в качестве свойства (Name) — euroTab. Третью вкладку мы будем использовать для отображения вкладки Web Page (Web-страница) для ввода номеров лотереи, поэтому измените значение свойства Text вкладки tabPage3 на Web Раде, а значение свойства имени — на WebTab. После внесения этих изменений можно щелкнуть на кнопке ОК, чтобы закрыть диалоговое окно.
Использование элементов управления GroupBox
- а х- а хЭлемент управления GroupBox (Групповая рамка) можно использовать для объединения других элементов управления в группу. Кроме того, этот элемент управления выравнивает группу относительно ограничивающей рамки и позволяет присваивать ей отличительное название. Запись лотереи Lotto состоит из шести различных номеров от 1 до 49. Добавьте на вкладку Lotto элемент управления GroupBox, щелкнув на элементе управления в окне Toolbox, а затем внутри вкладки Lotto. После этого можно изменить значения свойств Text, (Name) и Dock элемента управления GroupBox на значения, показанные на рис. 21.10.
- а х
- а х
Properties
lottoVa lues Syste m. Wi nciows. Fo rms G ro up Box
Backgroundimage | | (none)
Bad.groundlmageL Tile
00000000CursorFlatstyleFontForeColorRightToLeftTextUseWaitCursorBehaviorDataDesign(Name)GenerateMemberLockedModifiersFocusCausesValidationLayoutAnchorAuto SizeAutoSizeModeDockCursorFlatstyleFontForeColorRightToLeftTextUseWaitCursorBehaviorDataDesign(Name)GenerateMemberLockedModifiersFocusCausesValidationLayoutAnchorAuto SizeAutoSizeModeDockDefaultStandardMicrosoft Sans Serif 8.25ptH ControlTextNoValues 1 to 49FalseDefaultStandardMicrosoft Sans Serif 8.25ptH ControlTextNoValues 1 to 49FalselottoValuesTi ueFalsePrp'atelottoValuesTi ueFalsePrp'ateTrueTrue3 Location3 Martin3 Location3 MartinTop, Left False Gro^Dnly fill 3, 3Top, Left False Gro^Dnly fill 3, 3
0
0
0
0
0
0
0
0
Cursor
Flatstyle
Font
ForeColor
RightToLeft
Text
UseWaitCursor
Behavior
Data
Design
(Name)
GenerateMember
Locked
Modifiers
Focus
CausesValidation
Layout
Anchor
Auto Size
AutoSizeMode
Dock
Cursor
Flatstyle
Font
ForeColor
RightToLeft
Text
UseWaitCursor
Behavior
Data
Design
(Name)
GenerateMember
Locked
Modifiers
Focus
CausesValidation
Layout
Anchor
Auto Size
AutoSizeMode
Dock
Default
Standard
Microsoft Sans Serif 8.25pt
H ControlText
No
Values 1 to 49
False
Default
Standard
Microsoft Sans Serif 8.25pt
H ControlText
No
Values 1 to 49
False
lottoValues
Ti ue
False
Prp'ate
lottoValues
Ti ue
False
Prp'ate
True
True
3 Location
3 Martin
3 Location
3 Martin
Top, Left False Gro^Dnly fill 3, 3
Top, Left False Gro^Dnly fill 3, 3
Dock
Defines which borders of tl ie control are bound to the container.
Puc. 21.10. Установка свойствэлемента управления GroupBox
Значение свойства Text отображает диапазон возможных значений номеров лотереи, отображаемых на этой вкладке. Значение свойства Dock вынуждает элемент управления GroupBox заполнять свой контейнер, которым является вкладка Lotto элемента управления Tab. Значение свойства (Name) определяет имя переменной в классе Formi, идентифицирующей этот элемент управления.
Запись лотереи на вкладке Euromillions предполагает наличие двух групп значений: первой, состоящей из пяти различных значений от 1 до 50, и второй, состоящей из двух различных “звездных” значений из диапазона чисел от 1 до 9. Номера в первой группе могут совпадать с двумя “звездными” значениями. Для реализации такой структуры нам придется использовать еще один контейнер на вкладке Euromillions, поэтому щелкните на элементе Splitcontainer в группе Containers диалогового окна Toolbox, а затем для помещения этого элемента управления на вкладку Euromillions щелкните внутри нее. Чтобы выбрать вкладку, вначале щелкните на надписи вклад-
ки в верхней части элемента управления с вкладками — это приведет к выбору элемента управления с вкладками с выделенной надписью. Затем потребуется щелкнуть внутри области отображения вкладки, чтобы выбрать выделенную вкладку внутри элемента управления. По умолчанию значением свойства Dock элемента управления Splitcontainer является Fill, поэтому элемент управления должен заполнять вкладку. Этот элемент управления содержит две панели, каждая из которых может содержать другие элементы управления. Для изменения относительных размеров панелей можно перетаскивать разделяющую их границу.
Откройте окно Properties элемента управления и определите свойства Orientation и (Name), как показано на рис. 21.11.
UseWaitCursor .—
□ Behavior|Property Pages|
AllowDropFalse
Conte: tMenuStnp (none)
EnabledTrue
ImeModeNo Control
OrientationHorizontal
Tablndex(J
TabstopTrue
VisibleTrue
0 Data
В Design
(Name)
euroEntry
GenerateMember
Locked
Modifiers
В Focus
CausesValidation
True False Private
True
В Layout
Anchor
Dock
Fixed Panel
Top, Left
Fill
None
IsSplitterFixed
Truev
0 Location
3, 3
0 Ma rain
-
Properties
euroEntry Systern.Windows.Forms.SplitContainer I
Un у
IsSpTitterFDeed
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 нижней панели — соответственно "Values 1 to 9" (“Значения от 1 до 9”), euroStars и Fill. Окно редактора должно выглядеть, как показано на рис. 21.12.
Рас. 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.
Рис. 21.13. Расположение кнопок на вкладке Euromillion
Значения для отображения на этих кнопках в качестве текста будут генерироваться, поэтому необходимо изменить свойства каждой из них. Я предлагаю в качестве значений свойства Text пяти верхних кнопок установить 1, 2, 3, 4 и 5, а в качестве значений свойства Text двух нижних — 1 и 2. Свойство (Name) каждой кнопки также можно изменить. Кнопкам на верхней панели можно присвоить имена euroValuel, euroValue2 и так до euroValueS, а двум кнопкам на нижней панели — euroStarl и euroStar2. При желании можно изменить свойство BackColor каждой кнопки, которое определяет цвет фона. Для пяти верхних кнопок выбран цвет Silver (Серебро), а для двух нижних —Gold (Золото). Оба эти цвета были выбраны из палитры цветов Web. После выполнения этих действий окно приложения с выбранной вкладкой Euromillions должно выглядеть так, как показано на рис. 21.14.
Рис. 21.14. Вкладка Euromillions приложения
Теперь можно вернуться на вкладку Lotto, щелкнув на ней в окне Editor. Затем добавьте туда необходимые кнопки. Лотерея Lotto очень проста: потребуется всего лишь выбрать для ввода шесть различных значений в диапазоне от 1 до 49. Поэтому добавьте шесть элементов управления Button в элемент управления GroupBox, уже присутствующий на вкладке Lotto. После размещения кнопок должным образом измените отображаемый на них текст, чтобы по умолчанию они отображали значения от 1 до 6, и измените значения их свойства (Name) соответственно на lottoValuel, lottoValue2 и так до lottoValueG. Можете также изменить значение свойства 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, а значения свойства Uri — на URL-адрес Web-страницы, которую должен отображать элемент управления. Здесь можете ввести URL-адрес местной организации, проводящей лотерею. Убедитесь, что значение свойства Dock равно Fill. Теперь вкладка содержит все, что требуется для отображения Web-страницы. Конечно, страница будет отображаться только в том случае, если во время выполнения программы компьютер будет иметь активное подключение к Internet. После по-
вторной компиляции и запуска программы вкладка Web Раде должна выглядеть подобно показанной на рис. 21.17.
Рис. 21.16. Окно свойств элемента управления WebBrowser
Рис. 21.17. Вкладка Web Page во время выполнения
Если Web-страница не открывается, это может быть обусловлено отсутствием активного подключения к Internet. Обратите внимание, что по умолчанию при необходимости элемент управления WebBrowser отображает полосы прокрутки. Кроме того, он обеспечивает навигацию по Internet путем выбора активных ссылок на Web-странице.
Итак, мы почти (пока не полностью) завершили создание графического интерфейса пользователя для приложения. Теперь пора подробно рассмотреть функционирование приложения.
Работа приложения Winning Application
Щелчок на элементе меню Play открывает все поля ввода записи той вкладки, которая отображается в окне приложения в данный момент времени. Таким образом, элемент Play можно применять к вкладке Lotto или Euromillions. Номера лотереи отображаются в порядке возрастания. Переход на вкладку Web Раде приводит к отображению Web-страницы, на которой можно принять участие в розыгрыше лотереи.
После того как запись создана, может потребоваться изменение того или иного значения — например, поскольку вы испытываете беспричинную неприязнь к определенным числам или считаете, что числа больше 30 не принесут вам удачу. Щелчок на кнопке мог бы приводить к генерации нового числа для замены того, на кнопке которого выполняется щелчок. Естественно, новый номер должен отличаться от уже присутствующих в текущей записи.
Еще одна возможность — выбор конкретного числа, которое, как вы надеетесь, будет счастливым— например, соответствующего дате или месяцу дня рождения или количеству орешков, оставленных на тарелке во время сегодняшнего обеда. Эту возможность можно реализовать путем добавления контекстного меню, открывающегося по щелчку правой кнопкой мыши на конкретной кнопке. Для этого можно было бы использовать пункт Choose (Выбрать) контекстного меню. Обработка события, генерируемого в результате щелчка на элементе контекстного меню, требует выполнения определенных действий, поскольку нам придется обеспечить возможность ввода данных. Кроме того, потребуется также проверить допустимость вводимого числа. Оно должно принадлежать разрешенному диапазону и не должно дублировать уже существующие значения в текущей группе кнопок.
Элементы меню Limits1^ Upper (Пределы1^Верхний) и Limits^Lower (Пределы1^ Нижний) позволяют использовать более ограниченный диапазон значений для генерации записи. При этом также требуется выполнение соответствующих проверок. Диапазон должен быть частью диапазона, допустимого для данной лотереи, и при этом быть достаточно широким, чтобы обеспечивать генерацию требуемого количества различных значений.
И, наконец, элемент меню Help1^About (Справка1^О программе) должен отображать окно сообщения с информацией о приложении.
Первое, что необходимо сделать для реализации описанного функционирования приложения, это добавить контекстное меню для кнопок, отображающих значения номеров лотереи. Это удивительно просто.
Добавление контекстного меню
Контекстное меню в приложении CLR Forms — всего лишь еще один элемент управления ContextMenuStrip, расположенный в группе элементов управления Menus & Toolbars (Меню и панели инструментов) в окне Toolbox. Чтобы добавить его
в приложение, щелкните на элементе управления ContextMenuStrip, а затем в серой области в нижней части окна Editor. Контекстное меню отобразится в форме под уже существующим меню. Щелчок в первом меню позволит вставить имя элемента. В качестве текста элемента меню можно ввести SChoose, в результате чего комбинация клавиш <Alt+C> станет комбинацией быстрого доступа к этому элементу. Контекстному меню желательно присвоить более понятное имя, поэтому откройте окно Properties элемента управления ContextMenuStrip и измените значение его свойства (Name) на buttonContextMenu. Можно также изменить значение свойства (Name) элемента меню Choose на chooseValue.
Теперь можно активизировать элемент управления buttonContextMenu в качестве контекстного меню для каждой из кнопок вкладок ввода номеров обеих лотерей. Для этого измените значение свойства ContextMenuStrip каждой кнопки на имя элемента управления ContextMenuStrip — buttonContextMenu. Это имя отображается в раскрывающемся списке ячейки значения свойства, поэтому для установки значения достаточно щелкнуть на нем.
Создание обработчиков событий
Процесс визуального построения графического интерфейса пользователя с помощью средств Form Design автоматически генерирует код для добавляемых элементов управления. Теперь существует член класса Forml для каждого добавленного в форму элемента управления, а в функцию Initial!zeComponent () помещен код инициализации каждого из этих членов. Если взглянуть на эту функцию, легко заметить, что теперь она содержит значительный объем кода, устанавливающего значения свойств отдельных элементов управления и группирующего элементы в соответствующие контейнеры. Графическое создание графического интерфейса пользователя — очень простая задача. Чтобы получить готовое приложение, достаточно лишь немного подумать.
Чтобы программа работала должным образом, потребуется создать функции обработчиков событий, которые приложение должно распознавать и обрабатывать, и реализовать эти функции, чтобы взаимодействие пользователя с графическим интерфейсом приводило к нужному результату. Хотя автоматическое генерирование кода обработки событий невозможно, среда IDE все же может помочь в этом, генерируя скелет функций обработчиков событий и регистрируя их с помощью делегатов событий.
События элемента управления можно просматривать, щелкая на кнопке событий в окне Properties конкретного элемента управления. В результате отобразятся все возможные события данного элемента управления, и имя функции, которая должна обрабатывать конкретное событие, можно будет установить в ячейке, расположенной справа от имени события. Естественно, идентификация функций обработчиков событий требуется только для тех событий, которые необходимо распознавать, и в большинстве случаев они составляют лишь небольшую часть всех событий, доступных для данного элемента управления. Нам потребуется создать функции обработчиков события Click (Щелчок) для всех элементов меню и для каждой из кнопок на вклад- ках. Существует способ быстрого создания функции обработчика события Click для элемента управления. Для этого достаточно дважды щелкнуть на элементе управления в окне Editor, в результате чего отобразится созданный код, а функция будет зарегистрирована в качестве обработчика объекта события. В результате для элемента управления будет создан уникальный обработчик события, но иногда требуется, чтобы одна и та же функция обрабатывала события данного типа более чем для одного элемента управления. В этом случае следует обратиться в окно Properties.
Обработчики событий для элементов меню
Начнем с создания функции обработчика события для элемента меню Play. Двойной щелчок на этом элементе меню создает следующий код обработчика события:
private: System::Void playMenuItem_Click(System::ObjectA sender, System::EventArgsA e)
{ }
В этом скелете обработчика тело функции не содержит никакого кода. Первый параметр — дескриптор, ссылающийся на элемент управления, выдавший событие, а второй аргумент предоставляет информацию о самом событии. Тип первого аргумента соответствует типу элемента управления, инициировавшего событие — в данном случае это ToolStripMenuItemA, поскольку функция обработчика вызывается при щелчке на элементе меню Play. Дескриптор элемента меню хранится в члене playMenuItem класса Formi, и его тип можно проверить в определении класса. Аналогично, фактический тип второго аргумента обработчика события Click также зависит от типа элемента управления.
Имя функции обработчика, генерируемое по умолчанию — playMenuItem_Click. Не следует изменять имя функции в коде. Собственно, я рекомендую вообще не изменять какие-либо автоматически сгенерированные имена в коде. Это всегда следует делать в окне Properties. Если имя, созданное для функции обработчика события, вас не устраивает, его можно изменить через свойство события Click в окне Properties данного элемента управления.
Регистрация имени функции обработчика объектом события выполняется с помощью следующего оператора в функции InitializeComponent ():
this*->playMenuItem->Click +=
gcr.ew System::EventHandler(this, &Forml: :playMenuItem__Click) ;
Этот оператор добавляет имя функции к делегату Click в объекте playMenuItem, который является членом класса Formi. Делегаты и процесс регистрации функций обработчиков событий рассматривались в главе 9.
Обработчики для элементов меню Limits1^Upper, Limits^Lower и Help^About можно добавить, дважды щелкая на каждом из них. Это же можно выполнить для элемента Choose контекстного меню.
Добавление членов в класс Formi
Прежде чем приступить к реализации обработчиков для элементов меню, необходимо создать несколько дополнительных полей, которые потребуются для хранения данных, связанных с ограничениями значений лотерейных номеров. Вы уже умеете добавлять новое поле в класс Formi. Этот метод мы часто использовали для добавления членов в класс. Перейдите на вкладку Class View (Представление классов), щелкните правой кнопкой мыши на классе Formi и в контекстном меню выберите команду Add^Add Variable (Добавить1^Добавить переменную). Можно также просто добавить код вручную, но в этом случае убедитесь, что вы не затрагиваете раздел определения класса, зарезервированный для использования операциями Form Design. В качестве приватных членов потребуется добавить следующие переменные:
private:
int lottoValuesCount; // Количество значений в записи лотереи Lotto
int euroValuesCount; // Количество значений в записи лотереи Euromillions
int euroStarsCount; // Количество "звездных" значений в записи // лотереи Euromillions
int lottoLowerLimit; // Минимально допустимое значение в лотерее Lotto int lottoUpperLimit; // Максимально допустимое значение в лотерее Lotto int lottoUserMinimum; // Нижнее граничное значение лотереи Lotto,
// введенное пользователем
int lottoUserMaximum; // Верхнее граничное значение лотереи 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, введенное пользователем
В класс Formi необходимо также добавить приватное поле типа Random:
RandomA random; // Генерирует псевдослучайные числа
Объект типа Random может генерировать псевдослучайные значения различных типов. Мы используем его для генерации значений номеров лотереи.
Все эти поля должны быть инициализированы в конструкторе класса. Убедитесь, что определение конструктора Formi выглядит подобно приведенному ниже:
public ref class Formi : public System::Windows::Forms::Form
{
public:
Formi(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;
}
Мы определили все новые члены класса, которые требуются на данном этапе, поэтому вернемся к обработке событий.
Обработка события меню Play
Обработчик playMenuItemClick () должен создавать новый набор значений, отображаемых на кнопках видимой в данный момент вкладки. Вспомните, что ранее в этой главе мы определили значения свойства (Name) для вкладок элемента управления TabControl соответственно как lottoTab и euroTab. Если теперь взглянуть на достаточно обширное определение класса Formi, в нем будут присутствовать две переменные TabPageА с этими именами. Объекты типа TabPage обладают свойством Visible типа bool, значение которого равно true, если страница видна, и false, если она скрыта. Вот и все, что требуется для реализации обработчика элемента меню Play.
Общая логика использования обработчиком значений свойства Visible страниц вкладок такова:
private: System: : Void playMenuItem_Click (System: :ObjectA sender,
System::EventArgsA e)
{
if(lottoTab->Visible) {
// Генерация и установка значений записи лотереи 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 () в качестве приватного члена класса Formi и дополните описание, как показано в следующем фрагменте кода:
void GetValues(array<int>A values, int min, int max) {
values[0] = random->Next(min, max); // Генерация первого случайного значения
// Генерация остальных случайных значений for (int i = 1 ; i<values->Length ; i++)
{ 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; II Переменная для хранения дескриптора массива целых чисел if(lottoTab->Visible) {
/ / Генерация и установка значений записи лотереи Lotto values = gcnew array<int>(lottoValuesCount); // Создание массива GetValues(values, lottoUserMinimum, lottoUserMaximum);// Генерация
// значений SetValues(values, lottoValues);
else if(euroTab->Visible) {
// Генерация и установка значений записи лотереи Euromillions
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 состоит из трех описанных ниже шагов.
Создание массива для хранения значений.
Генерация значений с помощью функции GetValues ().
Установка значений в качестве надписей на кнопках с помощью функции SetValues ().
Для записи лотереи Euromillions эти действия выполняются дважды: один раз для определения пяти значений и второй раз — для определения двух “звездных” значений.
При реализации функции SetValues () можно воспользоваться тем, что кнопки размешены внутри элемента управления GroupBox. Свойство Controls объекта GroupBox возвращает коллекцию всех элементов, добавленных в объект. Коллекция, возвращаемая свойством Controls для самого элемента управления GroupBox, обладает определенным по умолчанию индексированным свойством, обеспечивающим доступ к элементам управления в коллекции. Коллекция представляет собой стек типа “первым вошел, последним вышел”, поэтому индексные значения свойства обеспечивают доступ к элементам управления в обратном порядке по отношению к порядку их добавления в групповую рамку. Функцию SetValues () можно реализовать в виде приватного члена класса Formi следующим образом:
//Установка значений в качестве надписей на кнопках в элементе управления 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].ToString();
}
После сортировки значений массива мы устанавливаем переменную count как значение индекса последнего элемента массива. Затем используем цикл для сохранения в обратном порядке строковых представлений значений в свойстве Text- каждого элемента управления Button. Выражение groupBox->Controls [i] создает дескриптор типа ControlА, который ссылается на элемент управления, соответствующий индексу i в коллекции, и перед обращением к свойству Text для установки его значения мы преобразуем этот тип в ButtonA.
Нам не нужно реализовать возможности, которые функция SetValues () предоставляет посредством объекта GroupBox. Все кнопки являются явными членами класса Formi, поэтому для установки их свойства Text можно использовать простейший подход и обращаться к каждой из них непосредственно. Для этого требуется, по меньшей мере, две функции — одна для записи лотереи Lotto, и одна для лотереи Euromillions, хотя последнюю удобнее разбить на три функции, в результате чего общее количество необходимых функций становится равным трем. Функция установки значений записи лотереи Lotto может иметь следующий вид:
void SetNewValues(array<int>A values)
values [0] .ToStringO values[ 1 ] .ToString() values [2] .ToStringO values [3] .ToStringO values[ 4 ] .ToString() values [5] .ToStringOvalues [0] .ToStringO values[ 1 ] .ToString() values [2] .ToStringO values [3] .ToStringO values[ 4 ] .ToString() values [5] .ToStringOArray::Sort(values); lottoValuel->Text = lottoValue2->Text = lottoValue3->Text = lottoValue4->Text = lottoValue5->Text = lottoValue6->Text =
values [0] .ToStringO values[ 1 ] .ToString() values [2] .ToStringO values [3] .ToStringO values[ 4 ] .ToString() values [5] .ToStringO
values [0] .ToStringO values[ 1 ] .ToString() values [2] .ToStringO values [3] .ToStringO values[ 4 ] .ToString() values [5] .ToStringO
Эта функция использует дескриптор каждой кнопки для установки ее значения Тех^Функции установки свойства Text записи лотереи Euromillions можно реализовать практически так же. Затем придется изменить функцию обработчика playMenu- Item Click О , чтобы она вызывала эти функции для установки значений.
Теперь можно заново скомпилировать Ех21_01, чтобы проверить, все ли работает так, как должно. Если вам удалось ввести весь код без ошибок, программа должна генерировать значения для лотереи Lotto, как показано на рис. 21.18.
Рис. 21.18. Генерация значений для лотереи LottoРис. 21.18. Генерация значений для лотереи Lotto
Рис. 21.18. Генерация значений для лотереи Lotto
Рис. 21.18. Генерация значений для лотереи Lotto
Числа на кнопках отображаются в порядке возрастания, и поэтому форма выглядит четкой и аккуратной. Неупорядоченное отображение чисел на кнопках может свидетельствовать о неупорядоченном добавлении кнопок в элемент управления GroupBox.
Программа создает также значения для лотереи Euromillions, что можно видеть на рис. 21.19.
Теперь, когда основные функциональные возможности реализованы, поря продолжить разработку приложения.
Обработка событий меню Limits
Меню Limits содержит три элемента, и для каждого из них необходим обработчик события Click. Последовательно дважды щелкните на каждом элементе меню, чтобы
сгенерировать функции обработчиков и зарегистрировать каждую из них в качестве обработчика события Click.
Обработчик события Click элемента меню Reset наиболее прост в реализации, поскольку единственная его задача — возврат установленных пользователем предельных значений к предельным значениям, определенным по умолчанию, для отображаемой в текущий момент лотереи. Обработчики событий Click остальных двух элементов этого меню будут выполнять существенно больше действий. Нам придется обеспечить средства для ввода предельных значений, и очевидный способ выполнения этой задачи — отображение диалогового окна после щелчка на элементе меню. Понятно, что следующий шаг по разработке приложения требует уяснения того, как можно создать это диалоговое окно.
Рис. 21.19. Генерация значений для лотереи ЕиготШигнх
Создание диалогового окна
Диалоговое окно Toolbox предлагает несколько стандартных диалогов. Все они предоставляют достаточно большие возможности, но ни одно из них не подходит в настоящем случае. Поскольку данная программа требует выполнения очень специфичных действий, нам придется самостоятельно создать диалоговое окно. Диалоговое окно — это всего лишь форма, значение свойства FormBorderStyle которой установлено равным FixedDialog, поэтому инструмент Form Designer (Конструктор форм) значительно облегчит создание диалогового окна.
Выберите команду главного меню Project^Add New Item (Проекта Добавить новый элемент) или нажмите комбинацию клавиш <Ctrl+Shift+A>, чтобы открыть диалоговое окно Add New Item (Добавление нового элемента), показанное на рис. 21.20.
В списке Categories: (Категории:) в левой панели установите категорию UI (Интерфейс пользователя), а в левой панели Templates: (Шаблоны:) выберите шаблон Windows Form (Форма Windows) и введите имя, как показано на рис. 21.20. Это диалоговое окно, которое будет отображаться при вводе верхнего или нижнего предела лотереи Lotto. Позднее мы создадим диалоговую форму для лотереи Euromillions. Щелчок на кнопке Add (Добавить) приводит к добавлению в проект новой формы и
ее отображению в окне Editor. Тип класса нового диалогового окна — это введенное имя LottoLimitsDialog.
Рис. 21.20. Диалоговое окно Add New Item
Нажмите клавишу <F4>, чтобы открыть окно Properties для новой формы. Значение свойства Text можно изменить на "Set Limits for Lotto Values" ("Установка предельных значений лотереи Lotto") — этот текст отображается в строке заголовка диалогового окна. Ширину окна можно настроить, перетаскивая его правый край до тех пор, пока строка заголовка не будет видна полностью. Можно также установить значение свойства Startposition (Начальная позиция) в группе свойств Layout (Макет) равным Center Parent (По центру родительской формы), чтобы диалоговое окно отображалось в центре отображающей его родительской формы, в данном примере — окна приложения. Поскольку создаваемое окно будет диалоговым окном, а не окном приложения, установите значение свойства FormBorderStyle равным FixedDialog. Во время его отображения диалоговое окно не должно быть доступным для сворачивания или разворачивания на весь экран пользователем, поэтому для удаления из него упомянутых возможностей установите значения свойств MinimizeBox и Maxim!zeBox в группе Window Style равными False. Диалоговое окно должно закрываться кнопками, которые мы создадим для него, поэтому установите значение свойства ControlBox равным False, чтобы удалить из строки заголовка рамки управляющих и системных элементов.
Следующий шаг — добавление двух кнопок в нижнюю часть формы. Они будут кнопками ОК и Cancel (Отмена) диалогового окна. Для левой кнопки в качестве значения свойства Text установите "ОК", а в качестве значения свойства (Name) — lottoOK. Можно также установить равным ОК значение свойства DialogResult в группе Behavior. Значениями для этих же свойств правой кнопки должны быть соответственно "Cancel", lottoCancel и Cancel. Эффект установки значения свойства DialogResult кнопок заключается в установке значения DialogResult диалогового окна соответствующим этому свойству той кнопки, на которой был выполнен щелчок
для закрытия диалогового окна. Это позволяет программно проверять, какая кнопка использовалась для закрытия диалогового окна, и выполнять различный код, в зависимости от того, была ли это кнопка ОК или Cancel.
Теперь, когда мы добавили кнопки в диалоговое окно, можно вернуться к свойствам диалога и установить значения свойств AcceptButton и CancelButton в группе свойств Mise (Разные) равными, соответственно, lottoOK и lottoCancel. В результате при открытом диалоговом окне нажатие клавиши <Enter> клавиатуры будет равносильно щелчку на кнопке ОК, а нажатие клавиши <Esc> — щелчку на кнопке Cancel.
Диалоговое окно содержит несколько элементов управления, которые можно применять для обеспечения ввода предельных значений. Элемент управления ListBox (Список) предоставляет пользователю возможность выбирать нужное значение из списка доступных значений, так что можете воспользоваться им в данном случае. В форму диалогового окна рядом с элементами управления ListBox потребуется добавить два элемента управления Label (Надпись), как показано на рис. 21.21.
Рис. 21.21. Внешний вид тфоекти^емого диалогового окна
Свойствами (Name) верхнего и нижнего списков должны быть соответственно lottoLowerList и lottoUpperList. Как видите, размеры элементов ListBox изменены так, чтобы их высота была равной высоте элементов управления Label, а ширина достаточной для отображения одиночного предельного значения. Кроме того, я изменил значение свойства Size (Размер) шрифта на 10, а значение свойства AcrollAlwaysVisible — на True. Убедитесь, что в качестве значения свойства SelectionMode (Режим выбора) обоих списков выбрано значение One (Один), поскольку необходимо обеспечить так, чтобы одновременно из списка можно было выбирать только один элемент.
Графический интерфейс пользователя диалогового окна готов, но чтобы он выполнял необходимые действия, придется снова заняться созданием кода. Можно начать с кода, который заполняет элементы управления ListBox предельными значениями.
Добавление списка в элемент управления ListBox
Список элементов управления ListBox представляет собой набор объектов, хранящихся в виде дескрипторов типа ObjectA, поэтому в списке можно хранить любой вид объектов. В данном случае в каждом списке потребуется хранить целочисленные
предельные значения, и, в основном, при необходимости преобразования значений типа int в и из объектов типа Int 32 можно полагаться на функции автоматической упаковки и распаковки. Свойство 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->Selected!tem = Int32(selected);
Аргументами функции SetList () являются элемент управления списка, в который должен быть добавлен список, минимальное и максимальное целочисленные значения добавляемого диапазона и целое число, которое должно быть выбрано в списке. Функция добавляет список целые числа из диапазона от min до max включительно, применяя при этом функцию Add () к объекту коллекции, возращенному свойством Items объекта ListBox. Она устанавливает также выбранное значение в качестве первоначально выбранного элемента списка при его отображении, устанавливая это значение в качестве значения свойства Selectedltem элемента управления списка.
Когда пользователь выбирает в списке предельное значение, его необходимо куда-нибудь поместить, чтобы к нему можно было обращаться из функции, принадлежащей объекту 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);
}
}
property int UpperLimit // Свойство доступа к верхнему предельному значению
int get(){ return upperLimit; } void set(int limit) { 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)
{
// Если в текущий момент существует выбранное верхнее предельное
// значение, его нужно сохранить
if(lottoUpperList->Selected!tem != nullptr)
upperLimit = safe_cast<Int32>(lottoUpperList->Selected!tem);
// Если в текущий момент существует выбранное нижнее предельное значение, // его нужно сохранить
if(lottoLowerList->Selected!tem ’= nullptr)
lowerLimit = safe_cast<Int32>(lottoLowerList->Selected!tem);
}
Сначала функция сохраняет верхнее предельное значение из объекта lotto- Upper Li st ListBox в переменной-члене, добавленной для этой цели. Свойство Selectedltem объекта ListBox обеспечивает доступность в виде дескриптора типа ObjectA выбранного в текущий момент элемента, и в качестве меры предосторожности код проверяет, чтобы возвращенный дескриптор не был нулевым. Перед сохранением тип выбранного элемента потребуется привести к действительно используемому типу — Int32. Затем функция автоматической упаковки преобразует объект в целое число. После этого обработчик аналогичным образом сохраняет нижнее предельное значение, извлеченное из другого объекта ListBox. По завершении работы обработчика диалоговое окно автоматически закрывается.
Управление состоянием объектов ListBox
В ответ на события Click обоих элементов меню Limits^Upper и 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: LottoLimitsDialogA lottoLimitsDialog;
В тело конструктора Forml поместите следующий код:
lottoLimitsDialog = gcnew LottoLimitsDialog;
lottoLimitsDialog->SetLowerLimitsList (1, lottoUpperLimit-lottoValuesCount+1, lottoUserMinimum);
lottoLimitsDialog->SetUpperLimitsList(lottoValuesCount, lottoUpperLimit, lottoUserMaximum);
Этот код очень прост. Первый оператор создает объект диалогового окна. Следующие два оператора вызывают функции, которые инициализируют списки в объектах ListBox. Максимальное значение в объекте ListBox, устанавливающее минимальный верхний предел, вычисляется так, чтобы можно было создать нужное количество значений в записи. Например, если максимальное значение равно 49, а количество значений в записи — 6, максимальные значение нижнего предела должно быть равно 44 — в случае использования любого более высокого значения создание шести различных значений было бы невозможно. Аналогичные рассуждения справедливы по отношению к минимальному значению верхнего предела; оно не может быть меньше количества значений в записи. Выбранные элементы в списках — lottoUserMinimum и lottoUserMaximum.
Поскольку конструктор класса Formi ссылается на имя класса LottoLimitsDialog, в файл заголовка Formi. h потребуется добавить директиву #include определения класса:
#include ’’LottoLimitsDialog. h”
Использование диалогового окна
Обращение к диалоговому окну будет выполняться в коде обработчиков событий Click элементов Upper и Lower меню Limits. Для отображения диалогового окна как модального необходимо вызвать функцию ShowDialog () для объекта диалогового окна. При желании функции ShowDialog () в качестве аргумента можно передать дескриптор родительской формы. Функции обработчика события Click можно реализовать следующим образом:
System:: Void lowerMenuItem_Click (System:: ObjectA sender, System::EventArgsA e)
{
if(lottoTab->Visible) { lottoLimitsDialog->SetLowerEnabled(); ::DialogResult result = lottoLimitsDialog->ShowDialog(this);
if(result == ::DialogResult::0K) {
II Обновление вводимых пользователем предельных значений / / из свойств диалогового окна
lottoUserMaximum = lottoLimitsDialog->UpperLimit; lottoUserMinimum = lottoLimitsDialog->LowerLimit;
} }
}
System::Void upperMenuItem_Click (System::ObjectA sender. System::EventArgsA e) {
if(lottoTab->Visible) { lottoLimitsDialog->SetUpperEnabled(); ::DialogResult result = lottoLimitsDialog->ShowDialog(this) ; if(result == ::DialogResult::0K) {
// Обновление вводимых пользователем предельных значений / / из свойств диалогового окна 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 можно было бы обращаться непосредственно, поэтому оператор if можно было бы записать следующим образом:
if(lottoLimitsDialog->DialogResult == ::DialogResult::ОК) {
// Обновление вводимых пользователем предельных значений
/ / из свойств диалогового окна
lottoUserMaximum = lottoLimitsDialog->UpperLimit; lottoUserMinimum = lottoLimitsDialog->LowerLimit;
}
Первая версия более удобна, поскольку в коде видно, что он выполняет проверку значения, возвращенного функцией ShowDialog ().
Полный набор констант, определяемый перечислением DialogResult, приведен ниже.
OKCancel (Отмена)None (Нет)OKCancel (Отмена)None (Нет)Yes (Да)No (Нет)
OKCancel (Отмена)
None (Нет)
OKCancel (Отмена)
None (Нет)
Retry (Повторить) Ignore (Игнорировать)
Значение свойства 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->Selected!tem);
// Если в текущий момент существует выбранный элемент нижнего предельного // значения, его нужно сохранить if(lottoLowerList->Selected!tem != 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::OK, MessageBoxIcon: .-Error) ;
/*
MessageBox::Show(Ь"Верхний предел: " + upper + L" Нижний предел: " + lower + Ь"\пЗначение верхнего предела должно быть, по меньшей мере, на 5 больше значения нижнего предела." + Ь"\пПовторите ввод.", Ь"Предельные значения недопустимы", MessageBoxButtons::ОК, MessageBoxIcon::Error);
* /
DialogResult = ::DialogResult::None;
} else { upperLimit = upper; lowerLimit = lower;
Теперь функция сохраняет значения, выбранные в объектах 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
Перечислимая константа, указывающая пиктограмму, предназначенную для отображения в окне сообщения. Перечислимая константа MessageBoxicon определяет следующие значения: Asterisk, Exclamation, Error, Hand, Information, None, Question, Stop, Warning.
Существует большое количество перегруженных версий статической функции Show () — от очень простой с единственным параметром типа StringA до значительно более сложных, принимающих свыше десятка параметров.
В случае компиляции и выполнения программы с последующей установкой недопустимых предельных значений откроется диалоговое окно, подобное показанному на рис. 21.22.
=9 inr mg pplication
Play Limits Help
Lcrtc Euromillions 'Л'"к D'~"
- Values 1 to 49
Select lower limit valua37 2
Г1
Рис. 21.22. Диалоговое окно сообщения о недопустимом вводе (одна кнопка)
Круглая пиктограмма красного цвета с белым крестом определена четвертым аргументом функции 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> (lotto(JpperList->SelectedItem);
// Если в текущий момент существует выбранный элемент нижнего
// предельного значения, его нужно сохранить
if(lottoLowerList->SelectedItem != nullptr)
lower = safe_cast<Int32>(lottoLowerList->SelectedItem);
if (upper - lower < 5) {
::DialogResult result =
MessageBox::Show(L"Upper limit: " + upper + L" Lower limit: " -I- 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(Е"Верхний предел: " + upper + L" Нижний предел: " + lower +
Ь”\пЗначение верхнего предела должно быть, по меньшей мере, на 5 больше значения нижнего предела." +
Ь"\пПовторите ввод.",
L"Предельные значения недопустимы",
MessageBoxButtons::OKCancel, MessageBoxIcon::Error);
*/
if(result == ::DialogResult::OK)
DialogResult = :: DialogResult::None;
else
DialogResult = ::DialogResult::Cancel;
) else { upperLimit = upper; lowerLimit = lower;
}
}
Поскольку третьим аргументом функции Show () является MessageBoxButtons: : OKCancel, теперь окно сообщения содержит две кнопки, как показано на рис. 21.23.
.--■.
bJ Winning pplicatior
Play Limits Help
Lotto Euromillions 'M"u D'"~
values 1-to49 Set Limits for Lotto Values
Select lower limit value:36 S
Puc. 21.23. Диалоговое окно сообщения о недопустимом вводе (две кнопки)
В обработчике события Click кнопки ОК диалогового окна предельных значений возвращаемое значение функции Show () сохраняется в переменной result. Тип этой переменной должен быть указан с помощью операции разрешения контекста. В противном случае компилятор будет ее интерпретировать как свойство DialogResult объекта lottoLimitsDialog, и компиляция кода потерпит неудачу. Если переменная result содержит значение : : DialogResult: :ОК, мы устанавливаем значение свойства DialogResult объекта lottoLimitsDialog равным : : DialogResult: :None, что предотвращает закрытие диалогового окна и разрешает изменение предельного значения. В противном случае мы устанавливаем значение свойства DialogResult равным :: DialogResult:: Cancel, что для диалогового окна равносильно щелчку на кнопке Cancel, поэтому оно закрывается.
Обработчик события элемента меню Reset
Обработчик события элемента меню Reset можно реализовать так, как показано ниже.
System::Void resetMenu!tem_Click(System::ObjectА sender. System::EventArgsA e) {
if(lottoTab->Visible)
{
// Сброс устанавливаемых пользователем пределов лотереи Lotto lottoUserMaximum = lottoUpperLimit;
lottoUserMinimum = lottoLowerLimit;
lottoLimitsDialog->UpperLimit = lottoUpperLimit; lottoLimitsDialog->LowerLimit = lottoLowerLimit;
}
else if(euroTab->Visible)
{
И Сброс устанавливаемых пользователем пределов лотереи Euromillions euroUserMaximum = euroUpperLimit;
euroUserMinimum = euroLowerLimit;
euroStarsUserMaximum = euroStarsUpperLimit;
euroStarsUserMinimum = euroStarsLowerLimit;
// Код обновления диалогового окна пределов лотереи Euromillions... }
}
Этот код всего лишь сбрасывает предельные значения в полях объекта For ml, а затем соответствующим образом обновляет свойства объекта диалогового окна. В приведенную функцию придется добавить код, выполняющий сброс диалогового окна в состояние, которое позволит обрабатывать ввод предельных значений лотереи Euromillions.
Теперь можно повторно скомпилировать программу и попробовать изменить предельные значения лотереи Lotto. Типичное окно приложения показано на рис. 21.24.
Рис. 21.24. Изменение предельных значений для лотереи Lotto
Как видите, приложение автоматически генерирует линейку прокрутки списка в элементе управления списка. Обратите внимание, что прокрутка к какому-либо элементу не приводит к его выбору. Чтобы выбрать элемент, до щелчка на кнопке ОК необходимо щелкнуть также и на нем. Выбор элемента меню Limits*^Reset сбрасывает оба предела в их исходные значения.
Добавление второго диалогового окна
Создание второго диалогового окна для установки предельных значений лотереи Euromillions — достаточно простая задача. Процесс ничем не отличается от создания первого диалогового окна. Создайте в проекте новую форму, нажав комбинацию клавиш <Ctrl+Shift+A>, чтобы открыть диалоговое окно Add New Item, в котором выберите категорию UI и шаблон Windows Form. Присвойте форме имя EuroLimitsDialog.
Значения свойств этого диалогового окна можно установить аналогично свойствам предыдущего диалогового окна (табл. 21.3).
Таблица 21.3. Значения свойств диалогового окна EuroLimitsDialog
Свойство формы
Значение, которое нужно установить
FormBorderStyle
ControlBox
FixedDialog
False
MinimizeBox
False
MaximizeBox
False
Text
Set Euromillions Limits (Установка предельных значений лотереи Euromillions)
Добавьте в диалоговое окно кнопки ОК и Cancel. В качестве значений свойства Text кнопок установите соответственно "ОК” и "Cancel", а в качестве значений свойства (Name) — euroOK и euroCancel. Необходимо также установить значения свойств DialogResult равными ОК и Cancel. После того, как кнопки будут определены, можно вернуться к свойствам диалоговой формы и установить в качестве значений свойства CancelButton соответственно euroOK и euroCancel.
Для приобретения опыта работы с более широким множеством элементов управления и во избежание однообразия в приложении, для обработки вводимых значений мы не будем использовать элементы управления ListBox, как это имело место в первом диалоговом окне. В этом диалоговом окне потребуется предусмотреть средства для ввода верхнего и нижнего пределов набора пяти значений, а также для ввода набора двух “звездных” значений. Это будет мало способствовать изяществу реализации, но в целях приобретения опыта работы с как можно большим числом различных элементов управления мы используем элементы управления NumericUpDown в первом случае и элементы управления ComboBox — во втором. В диалоговую форму эти элементы управления можно добавить вместе с соответствующими элементами управления Label, поместив каждую группу элементов управления внутри GroupBox, как показано на рис. 21.25. Понятно, что вначале следует добавить элементы управления GroupBox, а затем поместить остальные элементы управления внутрь них.
Рис. 21.25. Диалоговое окно установки пределов для лотереи EuromiUions
Для указания функции элементов управления внутри каждой групповой рамки значение свойства 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.
Рис. 21.26. Меню ComboBox Tasks
Выберите пункт меню Edit Items в нижней части меню, чтобы открыть диалоговое окно String Collection Editor (Редактор коллекции строк), представленное на рис. 21.27.
На рис. 21.27 показаны значения, введенные для левого элемента управления ComboBox. Для правого элемента управления можно ввести значения от 2 до 9 включительно.
Рис. 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();
H
// TODO: здесь необходимо вставить код конструктора
И
}
В классе диалогового окна необходимо определить набор public-свойств, обеспечивающих доступ к предельным значениям из объекта окна приложения:
public:
property int LowerValuesLimit {
int get() { return lowerValuesLimit; }
void set(int limit) {
lowerValuesLimit = limit;
lowerValuesLimits->Value = limit; // Устанавливает как выбранное
II значение в 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->Selected!ndex =// Устанавливает индекс для
// выбранного элемента
lowerStarsLimits->FindString(limit.ToString());
} }
property int UpperStarsLimit {
int get() { return UpperStarsLimit; }
void set(int limit) {
UpperStarsLimit = limit;
upperStarsLimits->Selected!tem = limit; // Устанавливает как
// выбранное значение в ComboBox upperStarsLimits->Selected!ndex =// Устанавливает индекс для
// выбранного элемента upperStarsLimits->FindString(limit.ToString ());
} }
Для каждого свойства функция get () возвращает значение соответствующего приватного члена класса диалогового окна. Функция set () устанавливает значение члена данных и обновляет элемент управления в диалоговом окне, чтобы установленное значение стало выбранным. Значение свойства Selectedlndex — индекс выбранного
элемента. Для элемента управления ComboBox эта установка выполняется функцией Findstring (), которая возвращает значение индекса первого найденного в коллекции элементов совпадения с аргументом. Расположенное в этой позиции значение изначально отображается в элементе управления.
Добавьте обработчик события Click кнопки ОК в класс EuroLimitsDialog, дважды щелкнув на кнопке в окне Design. Реализация обработчика для кнопки Cancel не требуется. Обработчик кнопки ОК можно реализовать, как показано ниже.
System:’.Void euroOK_Click(System::ObjectA sender. System::EventArgsA e) {
::DialogResult result;
// Извлечение предельных значений
int valuesLower = Decimal::Tolnt32(lowerValuesLimits->Value);
int valuesUpper = Decimal::ToInt32(upperValuesLimits->Value);
if(valuesupper - valuesLower < 4)// Проверка достаточности
И введенного диапазона
{
result = MessageBox::Show(this, II Диапазон не достаточен, поэтому "Upper values limit: "+valuesUpper + // отображается окно сообщения " Lower values limit: " + valuesLower*
"\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, "Верхний предел: "+valuesUpper * " Нижний предел: "* valuesLower* "ХпЗначение верхнего предела должно быть, по меньшей мере, на 4 больше значения нижнего предела."*
"ХпПовторите ввод.",
"Предельные значения недопустимы",
MessageBoxButtons::OKCancel, MessageBoxIcon::Error);
*/
if (result == :: DialogResult::OK) // При щелчке на кнопке ОК окна сообщения DialogResult = ::DialogResult::None;II блокировка закрытия
// диалогового окна
else // Щелчок был выполнен на кнопке Cancel окна сообщения DialogResult = ::DialogResult::Cancel; // поэтому диалоговое окно // нужно закрыть return;
}
И Извлечение предельных "звездных" значений
int starsLower = lowerStarsLimits->SelectedItem == nullptr ?
lowerStarsLimit :
Int32::Parse(lowerStarsLimits->SelectedItem->ToString());
int starsUpper = upperStarsLimits->SelectedItem == nullptr ? upperStarsLimit :
Int32: : Parse (upperStarsLimits-'>SelectedItem->ToString () ) ;
if (starsUpper - starsLower < 1) H Проверка достаточности введенного диапазона {
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::OKCancel,
MessageBoxIcon::Error);
/*
result = MessageBox::Show(this,
"Верхний предел: "+starsUpper +
" Нижний предел: "+ starsLower*
"ХпЗначение верхнего предела должно быть, по меньшей мере, на 1 больше значения нижнего предела."*
"\пПовторите ввод.",
"Предельные значения недопустимы",
MessageBoxButtons::OKCancel, MessageBoxIcon::Error);
*/
if(result == ::DialogResult::0K) // При щелчке на кнопке ОК окна сообщения DialogResult = ::DialogResult::None; // блокировка закрытия
// диалогового окна else // Щелчок был выполнен на кнопке Cancel окна сообщения DialogResult = ::DialogResult::Cancel; // поэтому диалоговое окно / / нужно закрыть
}
// Сохранение новых предельных значений lowerValuesLimit = valuesLower;
upperValuesLimit = valuesUpper;
lowerStarsLimit = starsLower; upperStarsLimit = starsUpper;
}
Свойство Value элемента управления NumericUpDown возвращает значение типа Decimal. Чтобы его преобразовать к типу Int32, мы передаем его в качестве аргумента статической функции Tolnt32 () класса Decimal. Возвращаемое этой функцией значение автоматически распаковывается так, чтобы его можно было хранить в переменной типа int.
Типом значения, возвращенного свойством Selectedltem элемента управления ComboBox, является ObjectЛ, поэтому в качестве меры предосторожности мы проверяем, не является ли оно нулевым. Если оно нулевое, мы устанавливаем значение локальной переменной равным текущему значению, записанному в объекте диалогового окна; если оно не нулевое, мы сохраняем значение, представленное свойством Selectedltem. Значение нельзя сохранить непосредственно, но вызов функции ToStringO применительно к объекту создает строковое представление объекта, которое затем можно преобразовать к типу int с помощью статической функции Parse() класса Int32.
Нам потребуется приватный член класса Formi, хранящий дескриптор нового диалогового окна:
private:
EuroLimitsDialogA euroLimitsDialog; // Диалоговое окно установки предельных // значений лотереи Euromillions
Чтобы создать новый объект диалогового окна и обновить свойства предельных звездных значений, в конец кода конструктора класса Forml необходимо добавить следующие операторы:
euroLimitsDialog = gcnew EuroLimitsDialog;
euroLimitsDialog->LowerStarsLimit = euroStarsLowerLimit; euroLimitsDialog->UpperStarsLimit = euroStarsUpperLimit;
Установка свойств LowerStarsLimit и UpperStarsLimit объекта диалогового окна обеспечивает отображение этих значений в элементе управления ComboBox при первоначальном открытии диалогового окна. В случае отсутствия выбранного элемента в элементе управления ComboBox при открытии диалогового окна он не отображает никаких значений.
Не забудьте добавить в файл заголовка Forml .h директиву #include для включения определения класса EuroLimitsDialog:
#include "EuroLimitsDialog.h"
Отключение элементов управления ввода данных
При выборе пункта меню Limits*^ Upper необходимо предотвратить ввод нижнего предельного значения, а при выборе пункта меню Limits^ 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 нужно привести к виду, показанному ниже.
System: :Void upperMenuItem_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;
}
}
Логика этого кода полностью аналогична логике предыдущей функции обработчика.
Реализация элемента меню Help^About
Теперь, когда вы знакомы с классом MessageBox, реализация этого элемента меню не представляет сложности. При щелчке на элементе меню Help1^About (Справка1^ О программе) достаточно отобразить окно сообщения:
System::Void aboutToolStripMenuItem_Click(System::ObjectA sender.
System::EventArgsA e)
{
MessageBox::Show(L" (c) Copyright Ivor Horton", L"About A Winning Application", MessageBoxButtons::0K, MessageBoxIcon::Exclamation);
}
Щелчок на этом элементе меню приводит к тому, что функция обработчика отображает окно сообщения, показанное на рис. 21.28.
Обработка щелчка на кнопке
Щелчок на кнопке должен изменять отображаемое на кнопке значение на новое случайное значение. Естественно, оно должно также отличаться от значений, выводимых на других кнопках, и от значения, отображавшегося на кнопке, на которой выполнялся щелчок. Целесообразно представлять весь набор значений в упорядоченном виде; это может привести к отображению нового значения на другой кнопке, но, скорее всего, это было бы предпочтительнее вывода значений в неупорядоченном виде.
Процесс обработки щелчка на кнопке будет одинаковым для всех кнопок, поэтому объем кода можно сократить, создав для выполнения этой задачи обобщенную функ-
цию. Определите приватную функцию-член класса Formi, которая будет генерировать новое значение для данного объекта 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; // Сохранение нового значения кнопки
// Проверка его отличия от значений других кнопок
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].ToString();// как текста,
// отображаемого на кнопках }
Два первых параметра функции — кнопка, которой должно быть присвоено новое значение, и массив кнопок в группе, к которой принадлежит первая кнопка. Два следующих параметра указывают верхнее и нижнее предельные значения. В первом цикле текущие значения кнопок сохраняются в массиве значений values. В этом же цикле выясняется значение индекса в массиве buttons дескриптора Button А, переданного в первом аргументе. Оно требуется для выяснения того, какой элемент массива values должен быть заменен новым значением.
Новое значение создается в неопределенном цикле for. Этот же механизм применялся для создания первоначальных значений кнопок. После получения нового допустимого значения оно сохраняется в массиве values. Затем, прежде чем их сохранять в качестве значений свойств Text кнопок массива buttons, осуществляется сортировка элементов в массиве values. Эту функцию можно будет использовать для обработки событий Click всех кнопок.
Если вы еще не сделали этого, дважды щелкните на первой кнопке вкладки Lotto, чтобы сгенерировать для нее функцию обработчика. Имя функции обработчика можно изменить, открывая окно Properties кнопки, щелкая на кнопке Events (События) и изменяя значение для события Click. После нажатия клавиши <Enter> код будет обновлен новым значением. В данном случае это значение изменено на lottoValue Click.
Обработчик события Click потребуется изменить, чтобы он вызывал ранее добавленную в класс Formi функцию SetNewValue ():
System::Void lottoValue_Click(System::ObjectA sender. System::EventArgsA e) {
ButtonA button = safe_cast<ButtonA>(sender);
// Создание массива дескрипторов кнопок 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▼ Д
iottoVa Iue2 Syste m. Wi ndows. Fo rms. В utto n
X
Ч л
|
В Action
Click
lottoValue_Clickj^
M о u se Ca ptu ne Cha nged
Mo use Click
E Appearance
Paint
В Behavior
ChanaeUICues
playMenuItem_Click^
up ре rMe n ulte m_Cl ick lowerMenuItemjClfck aboutMenu!tem_Click chooseVa 1 ue_Cl ick reset! He n ulte m_Cl ick
Control Added
ottoValue_Cl.ck
Control Re moved HelpRequested
Q ue ry * ccessi b i 1 ityH e 1 p Sty Ie Cha nged
Sy stemColorschanged
В Data
В (Data В indings)
В Drag Drop
DragDrop
Drag Enter
DragLeave
DragOver
Give Feed back
Que-yContinueDrag
E Focus
Enter
Click
Occurs when the component is clicked
Рис. 21.29. Список существующих обработчиков событий для Click
Этот процесс можно повторить для остальных четырех кнопок вкладки Lotto, чтобы один и тот же обработчик события вызывался в ответ на событие Click для любой из упомянутых кнопок.
Обработчики события Click кнопок вкладки Euromillions будут очень простыми. Дважды щелкните на первой из пяти кнопок в группе Values (Значения), чтобы создать обработчик события. Откройте окно Properties для кнопки и измените значение события Click на euroValue_Click. Затем измените код обработчика, как показано ниже.
System::Void euroValue_Click(System::ObjectA 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::ObjectA 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 класса Formi, а затем дважды щелкните на элементе меню Choose, чтобы создать обработчик события Click.
Первая проблема, которую потребуется решить — определить группу кнопок, щелчок на которых вызвал событие. Каждая группа кнопок находится в собственном элементе управления GroupBox, а класс GroupBox имеет свойство Controls, которое возвращает ссылку на объект типа Control: :Controlcollection, представляющий собой коллекцию элементов управления в групповой рамке. В классе Control:: Controlcollection определена функция Contains (), которая возвращает значение true, если переданный в качестве аргумента элемент управления входит в состав кол-
лекции, и значение false — в противном случае. Таким образом, мы располагаем возможностью определения того, к какой группе кнопок принадлежит кнопка, сгенерировавшая событие Click. В общих чертах реализация обработчика события выглядит так, как показано ниже.
System::Void chooseValue_Click(System::ObjectA sender. System::EventArgsA e) {
// Извлечение кнопки, щелчок на которой вызвал контекстное меню; затем... if(lottoValues->Controls->Contains(theButton)) {
// кнопка принадлежит группе 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 может оставаться неизменным, независимо от задействованной группы кнопок, и сводится к выполнению описанных ниже действий.
Отображение диалогового окна для ввода значения.
Проверка допустимости введенного значения — то есть того, что оно находится в допустимом диапазоне и отличается от значений остальных кнопок.
Отображение окна сообщения, если значение недопустимо, и обеспечение возможности повторного ввода значения или закрытие диалогового окна.
Если значение допустимо, обновление соответствующей кнопки новым значением.
Первый шаг по реализации этого процесса — создание новой диалоговой формы.
Создание диалоговой формы
Нажмите комбинацию клавиш <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.
Разработка класса диалогового окна
Значение, введенное в элементе управления TextBox, должно быть доступным для объекта Formi, поэтому в класс UserValueDialog потребуется добавить свойство для его хранения:
public:
property int Value;
Это свойство — пример простейшего скалярного свойства, поэтому для него подходят функции get () и set (), предлагаемые по умолчанию.
Объекту диалогового окна необходима информация о предельных значениях, поскольку обработчик кнопки ОК в классе диалогового окна проверяет допустимость значения. По этой же причине объекту диалогового окна требуется информация о текущих значениях, отображаемых на кнопках для предотвращения их дублирования. Для хранения данных в класс UserValueDialog можно добавить следующие три члена свойств:
public:
property int LowerLimit;
property int UpperLimit;
property array<int>A Values;// Текущие значения кнопок
Объект Formi должен иметь возможность изменять значение свойства Text элемента управления label в зависимости от действующих предельных значений кнопки при открытии диалогового окна. Для этого в класс UserValueDialog следует добавить общедоступную функцию-член:
public:
void SetLabelText(int lower, int upper) {
label->Text = L"Enter your value between " + lower +L" and ” + upper;
}
Предельные значения можно было бы выбирать из свойств объекта диалогового окна, но для этого требовалось бы всегда вначале устанавливать значения свойств. Использование параметров для определения предельных значений устраняет эту зависимость.
Объект диалогового окна можно создать в конструкторе класса Formi, но в этот класс придется добавить приватный член для хранения дескриптора:
private: UserValueDialogA UserValueDialog;
Кроме того, в файл заголовка Formi. h нужно добавить директиву #include для включения файла UserDialog.h.
Добавление к конструктору приведенной ниже строки создает объект диалогового окна:
UserValueDialog = gcnew UserValueDialog;
Двойной щелчок на кнопке ОК в форме UserValueDialog создаст для кнопки обработчик события Click. Эта функция извлекает значение, введенное в элементе управления TextBox, и проверяет его нахождение между предельными значениями и отличие от текущего набора значений. Если по любой причине значение не допустимо, функция отображает окно сообщения. Ее можно реализовать следующим образом:
System::Void OK_Click(System::ObjectA sender, System::EventArgsA e) {
::DialogResult result; // Сохраняет значение, возвращенное функцией Show() if(String::IsNullOrEmpty(textBox->Text)) // Проверяет ввод нулевой или // пустой строки
{
result = MessageBox::Show(this,
L"No input - enter a value.", L"Input Error", MessageBoxButtons::RetryCancel, MessageBoxIcon::Error) ;
/*
result = MessageBox::Show(this,
Ь"Ввод отсутствует - введите значение.", Ь"0шибка ввода", 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;II Индикатор допустимости ввода
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) ;
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 возвращает дескриптор типа StringA. Его можно преобразовать в целочисленный, передавая дескриптор статической функции Parse () класса Int32. Значение, полученное из текстового поля, сравнивается с элементами массива values, который представляет текущий набор значений кнопок. Новое значение должно отличаться от всех существующих, поэтому при обнаружении любого совпадения значение переменной valid устанавливается равным false и выполняется выход из цикла.
В условии оператора if, следующего за каждым циклом for, значение сравнивается с предельными значениями и проверяется текущее значение переменной valid. Если значение любого из этих выражений равно false, значение всего условия— false, и выводится окно сообщения. Оно работает аналогично предыдущему, отображает сообщение об ошибке и содержит кнопки Retry и Cancel. Если значение действительно допустимо, мы сохраняем его в свойстве Value объекта диалогового окна, готовым для извлечения обработчиком события объекта Formi, который запустил весь процесс.
Обработка события Click элемента меню Choose
Теперь можно завершить скелет функции обработчика chooseValue_Click (), используя возможности, добавленные в класс UserValueDialog. Дескриптор кнопки, на которой был выполнен щелчок правой кнопкой мыши, уже хранится в члене contextButton, поскольку вначале выполняется добавленный ранее обработчик buttonContextMenu_Opening().
System::Void chooseValue_Click(System::ObjectА sender, System::EventArgsA e) {
array<int>A values;// Массив для хранения текущих значений кнопок
array<ButtonA>A theButtons; // Дескриптор массива кнопок
// Проверка, принадлежит ли кнопка групповой рамке lottoValues if(lottoValues->Controls->Contains(contextButton))
(
// Кнопка относится к группе lotto...
array<ButtonA>A buttons = {lottoValuel, lottoValue2, lottoValue3,
lottoValue4, lottoValue5, lottoValue6};
theButtons = buttons;// Сохранение дескриптора массива во
// внешней области определения
values = GetButtonValues (buttons) ; // Извлечение значений массива кнопок
// Подготовка диалогового окна к отображению
userValueDialog->Values = values = GetButtonValues(buttons);
userValueDialog->LowerLimit = lottoUserMinimum;
userValueDialog->UpperLimit = lottoUserMaximum;
userValueDialog->SetLabelText(lottoUserMinimum, lottoUserMaximum);
}
// Проверка, принадлежит ли кнопка групповой рамке euroValues else if(euroValues->Controls->Contains(contextButton)) {
II Кнопка относится к группе Values...
array<ButtonA>A buttons = {euroValuel, euroValue2, euroValue3, euroValue4, euroValueS};
theButtons = buttons;// Сохранение дескриптора массива во
// внешней области определения
values = GetButtonValues (buttons) ; // Извлечение значений массива кнопок
// Подготовка диалогового окна к отображению
userValueDialog->Values = values;
userValueDialog->LowerLimit = euroUserMinimum;
userValueDialog->UpperLimit = euroUserMaximum;
userValueDialog->SetLabelText(euroUserMinimum, euroUserMaximum); }
/1 Проверка, принадлежит ли кнопка групповой рамке euroStars
else if(euroStars->Controls->Contains(contextButton)) {
// Кнопка относится к группе Stars...
array<ButtonA>A buttons = { euroStarl, euroStar2 };
theButtons = buttons;// Сохранение дескриптора массива
/ / во внешней области определения
values = GetButtonValues (buttons) ; // Извлечение значений массива кнопок
// Подготовка диалогового окна к отображению
userValueDialog->Values = values;
userValueDialog->LowerLimit = euroStarsUserMinimum;
userValueDialog->UpperLimit = euroStarsUserMaximum;
userValueDialog->SetLabelText(euroStarsUserMinimum, euroStarsUserMaximum);
}
// Отображение диалогового окна
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();
Вначале определяются две переменных типа массива: одна для хранения кнопок, а вторая — для хранения значений кнопок. Эти переменные должны быть определены именно здесь, поскольку массивы создаются внутри блока либо одного, либо другого оператора if, и к ним требуется доступ извне блоков if.
Первые три оператора if определяют групповую рамку, содержащую кнопку, на которой был выполнен щелчок правой кнопкой мыши для открытия контекстного меню. Процесс, выполняемый внутри всех трех блоков if, по сути, одинаков, но создаваемые при этом массивы будут различными. Массив кнопок создается из переменных, содержащих дескрипторы того набора кнопок, к которому принадлежит contextButton. Затем дескриптор массива сохраняется в члене theButtons, что делает его доступным во внешней области. Затем вызывается функция, которую еще предстоит добавить, GetButtonValues (), возвращающая массив целочисленных значений кнопок. И, наконец, в блоке if устанавливаются значения трех свойств объекта диалогового окна, после чего вызывается его функция SetLabelText () для определения текста надписи в соответствии с предельными значениями. Дескриптор contextButton должен принадлежать одной из трех групповых рамок, поскольку контекстное меню доступно только для этих кнопок.
После выполнения того или иного блока if мы отображаем диалоговое окно, вызывая его функцию ShowDialog () в соответствии с условием четвертого оператора if. Если функция ShowDialog () возвращает значение : :DialogResult: :0К, выполняется код блока if. Вначале, сравнивая дескриптор contextButton с дескрипторами, хранящимися в массиве theButtons, код определяет, значение какой кнопки должно быть заменено. Как только соответствие найдено, соответствующий элемент в массиве values заменяется новым значением и осуществляется выход из цикла. После сохранения значений обновляются значения свойств Text для каждой кнопки массива theButtons, и на этом действие кода завершается.
Реализация функции GetButtonValues () в классе Formi выглядит следующим образом:
И Создание массива значений кнопок на основе массива кнопок 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, использующее элементы управления, которые, скорее всего, потребуются в большинстве программ. Теперь вам должно быть ясно, что программы Windows Forms предназначены исключительно для использования возможностей, предлагаемых средством Form Design. Весь код класса помещается в определение класса, поэтому класс очень сложной формы будет содержать множество строк кода. В реальных приложениях код состоит из ряда больших, достаточно слабо структурированных классов, которые трудно изменять и поддерживать на уровне кода. Поэтому всегда, когда это возможно, для внесения изменений следует использовать средство Form Design и окно Properties, а в тех случаях, когда необходим доступ в код для принятия нестандартных решений, следует применять представление Class View.
Ниже перечислены ключевые моменты, с которыми вы познакомились в настоящей главе.
Окно приложения — это форма, а форма определяется классом, производным от класса System: :Form.
Диалоговое окно — это форма, значение свойства FormBorderStyle которой установлено равным FixedDialog.
Диалоговое окно может быть создано как модальное путем вызова его функции ShowDialog () или как немодальное с помощью его функции Show ().
Возможностью закрытия диалогового окна можно управлять, устанавливая значение свойства DialogResult объекта диалогового окна.
Элемент управления ComboBox сочетает в себе возможности элементов управления ListBox и Text Box и позволяет выбирать элемент из списка либо вводить новое значение с клавиатуры.
Элемент управления NumericUpDown допускает ввод числовых данных из данного диапазона с определенным шагом.
Определение обработчика события Click для элемента управления можно добавить, дважды щелкая на элементе управления во вкладке Form Design.
Существующую функцию можно указать в качестве обработчика данного события элемента управления в окне Properties. Щелчок на кнопке Events в окне Properties отображает список доступных событий для данного элемента управления.
Имена автоматически сгенерированных членов класса следует изменять только в окне Properties, а не непосредственно в редакторе кода.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Измените код Ех21_01 так, чтобы он отображал диалоговое окно, созданное в виде формы, отображаемой при щелчке на элементе меню Help1^About.
Измените код Ех21 01 так, чтобы диалоговое окно, отображаемое для элемента контекстного меню Choose, вместо текстового поля использовало элемент управления ListBox и отображало полный набор доступных для выбора допустимых значений.
Исследуйте свойства и функции, доступные для элемента управления WebBrowser, и измените код Ех21 01 так, чтобы элемент управления WebBrowser отображал станицу, URL-адрес которой был введен в элементе управления TextBox.
22
Доступ к источникамданных в приложенииWindows Forms
В этой главе мы исследуем разработку приложений, построенных на основе форм, которые будут отображать данные, полученные из различных источников. В частности, будет рассмотрено создание программ, построенных на основе форм, обращающихся к существующей базе данных.
Ниже перечислены вопросы, которые будут рассматриваться в главе.
Виды классов, участвующие в инкапсуляции источника данных.
Использование элемента управления DataGridView для отображения своих данных.
Настройка внешнего вида элемента управления DataGridView.
Функции, выполняемые компонентом Bindingsource, и его использование совместно с элементом DataGridView.
Использование элемента управления BindingNavigator для перемещения по данным источника данных, управляемого элементом Bindingsource.
Ускорение обновления базы данных с помощью элемента управления BindingNavigator и компонента Bindingsource.
Visual C++ 2005 предоставляет большие возможности автоматизации создания приложений на основе форм, обращающихся к источникам данных, но мы начнем создание приложения без использования средств автоматизации, дабы вы смогли получить представление о программной работе с компонентами. Этот подход позволяет не только получить достаточно полное представление о работе компонентов, но и оценить преимущества, предоставляемые средствами автоматизации.
Работа с источниками данных
Источник данных может быть любым из числа существующих в приложении. Реляционные базы данных, обращающиеся к данным Web-службы и объекты — все они могут быть источниками данных. При разработке приложения, которое будет работать с существующим источником данных, обычно придется идентифицировать этот источник данных внутри разрабатываемого проекта. Это можно сделать в окне Data Sources (Источники данных), отображаемого при выборе пункта Data^ 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 данными и при необходимости может обновлять таблицу в базе данных.
Доступ и отображение данных
В пространстве имен System: :Windows: :Forms определены три компонента, которые все вместе предназначены для доступа к данным и их отображения в приложении Windows Forms (табл. 22.2).
Таблица 22,2. Компоненты пространства имен System: : Windows::Forms для доступа и отображения данных
Компонент
Описание
DataGridView
Этот элемент управления может отображать буквально любой вид данных в ячейках прямоугольной сетки. Этот элемент управления можно использовать в значительной мере независимо от остальных двух компонентов.
Bindingsource
Этот компонент служит для инкапсуляции данных, полученных из источника данных. Он позволяет управлять доступом к источнику данных и его обновлением, а также может использоваться в качестве механизма отображения данных в элементе управления DataGridView.
BindingNavigator
Этот элемент управления предоставляет панель управления, содержащую элементы управления для навигации по данным в источнике — как правило, в источнике данных, инкапсулированном в элементе управления Bindingsource — и манипулирования ими.
Компонент Bindingsource не является элементом управления, поскольку не имеет графического представления, с которым пользователь может взаимодействовать, однако он призван дополнять и использоваться в приложении базы данных совместно с элементами управления DataGridView и BindingNavigator. Компонент Bindingsource обеспечивает обмен информацией с источником данных, необходимый для выполнения запросов и команд обновления, элемент управления DataGridView предлагает интерфейс пользователя для просмотра и ввода данных, а элемент управления BindingNavigator предоставляет панель инструментов, которая упрощает навигацию по данным. Применение элемента управления BindingNavigator не обязательно. При желании записи можно изменять программно, вручную.
Хотя эти три компонента спроектированы для совместного использования, элемент управления DataGridView — особенно полезный инструмент и сам по себе, поскольку его можно применять в значительной степени независимо от остальных двух компонентов. Он обеспечивает поразительно широкие возможности изменения внешнего вида сетки, отображающей данные. Прежде чем приступать к изучению возможных способов использования этого элемента управления в сочетании с компонентами Bindingsource и BindingNavigator, рассмотрим некоторые способы его настройки.
Обратите внимание, что для получения доступа к источнику данных можно применять также элементы управления SqlConnection, SqlDataAdapter и DataSet. Если желаете использовать эти элементы управления, вам, вероятно, придется добавить их в панель инструментов ToolBox. Для этого необходимо выбрать пункт Tools^ Choose ToolBox Items (Инструменты^ Выбрать элементы панели инструментов) главного меню и в списке установить флажки напротив тех элементов управления, которые требуется добавить в панель ToolBox.
Использование элемента управления DataGridView
Элемент управления DataGridView позволяет отображать и изменять прямоугольный массив данных из множества различных источников данных. Его можно использовать также для отображения практически любых данных, созданных непосредственно в программе. По своей сущности это сложный элемент управления, который обеспечивает огромную гибкость при его применении, и преимуществами множества его функциональных возможностей можно воспользоваться через множество свойств, функций и событий. В то же время применение элемента управления DataGridView может быть поразительно простым. Можно не обращать внимания на внутреннюю сложность и использовать его посредством инструмента Form Design (Конструктор форм), которое берет на себя заботу обо всех основных характеристиках. Позднее в этой главе будет показано, как создать завершенный пример программы для доступа к базе данных North wind, вообще не прибегая к программированию вручную. Вся программа будет сгенерирована с помощью Form Design и путем определения свойств компонентов, используемых в проекте.
Данные элемента управления DataGridView отображаются в прямоугольном массиве ячеек, которые можно представить в виде коллекции строк или столбцов. Каждый столбец ячеек имеет в верхней части ячейку заголовка, которая, как правило, содержит идентифицирующий этот столбец текст, а в начале каждой строки расположена ячейка заголовка строки, как показано на рис. 22.1.
Рас. 22.1. Отображение данных в элементе управления DataGridView
Обращение к строкам и столбцам ячеек выполняется через свойства объекта DataGridView. Свойство Rows возвращает значение типа DataGridRowCollection, представляющее коллекцию всех строк, а обращение к конкретной строке выпол-
няется с помощью индекса, что можно видеть на рис. 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:
DataGridViewA 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. Свойства для добавления и удаления строк коллекции DataGridViewRowCollection
Функция
Описание
Add()
Добавляет в коллекцию одну или более строк.
Insert()
Вставляет в коллекцию одну или более строк.
Clear ()
Удаляет все строки коллекции.
AddCopy()
Добавляет копию строки, указанной в аргументе.
InsertCopy()
Вставляет копию строки, указанную первым аргументом, в позицию, которая задана вторым аргументом.
Remove()
Удаляет строку, указанную аргументом типа DataGridviewRowA.
RemoveAt()
Удаляет строку, указанную значением индекса, которое передано в качестве аргумента.
Функция Add (), применяемая к значению, возвращенному свойством Rows, существует в виде четырех перегруженных версий, которые позволяют добавлять в элемент управления строку данных рядом различных способов (табл. 22.5).
Все версии функции Add () возвращают значение типа int, представляющее собой индекс последней добавленной в коллекцию строки. Если значение свойства SataSource элемента управления DataGridView не нулевое, или если элемент управления не содержит столбцов, все версии функции Add () генерируют исключение типа System::InvalidOperationException.
Таблица 22.5. Перегруженные версии функции Add () коллекции DataGridViewRowCollection
Функция
Описание
Add О
Добавляет в коллекцию одну новую строку.
Add(int rowCount)
Добавляет в коллекцию rowcount новых строк.
Если значение rowcount нулевое или отрицательное, функция генерирует исключение типа System: ’ArgumentOutOfRangeException.
Add(DataGridViewRowA row)
Добавляет строку, указанную аргументом. Объект DataGridViewRow содержит коллекцию ячеек строки, а также параметры, которые определяют внешний вид ячеек в строке.
Add(... ObjectA 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 (Освободить столбец) выбран, поскольку никакой источник данных не указан для элемента управления — в данном примере так и должно быть.
Рис. 22.2. Меню DataGridView Tasks
Рис. 22.3. Диалоговое окно Add Column
Запись Name: (Имя:) — это значение свойства Name столбца, a Header text: (Текст заголовка:) — значение свойства Header Text, которое соответствует тексту, отображаемому в элементе управления в качестве заголовка столбца. Если развернуть список, предназначенный для выбора значения Туре: (Тип:), отобразится набор возможных вариантов выбора типа столбца. В данном случае необходимо оставить значение, выбранное по умолчанию — TextBoxColumn — поскольку в качестве данных для отображения мы будем добавлять строки. Остальные описанные в табл. 22.6 типы столбцов создают в ячейках, представляющих данные, различные элементы управления.
Таблица 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.
Рис. 22.4. Диалоговой окно Edit Columns
Диалоговое окно 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 book3 = {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 books = (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 bookd = {L"0-7493-9739-X", L"Made In America", L"Bill Bryson", L"Minerva"};
// Создание массива книг array<array<StringA>A>A books ={bookl, book2, ЬоокЗ, book4, book5, Ьоокб, 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, добавляет в коллекцию всю строку. Каждый элемент массива, переданный в качестве аргумента, соответствует столбцу в элементе управления, а тип элемента данных должен соответствовать типу, выбранному для столбца. В этом примере все столбцы имеют один и тот же тип, поэтому все ячейки строки
могут быть переданы в массив. Если бы столбцы имели различные типы, можно было бы указать элемент для каждого столбца с помощью отдельного аргумента функции Add () либо организовать массив элементов типа ObjectА.
Если скомпилировать и выполнить программу, нажав комбинацию клавиш <Ctrl+F5>, должно открыться окно приложения, показанное на рис. 22.5.
Рис. 22.5. Элемент управления DataGridView в работе
Линейки прокрутки отображаются автоматически, поскольку элемент управления DataGridView выходит за пределы границ клиентской области формы. При увеличении размеров окна линейки прокрутки рано или поздно исчезнут.
Было бы замечательно, если бы ширина столбцов была достаточно большой для вмещения максимально длинного текста, который они содержат. Этого можно добиться, изменяя значение свойства AutoSizeColumnsMode в группе Layout (Макет). Находясь на вкладке Forml [Design] панели Editor (Редактор) откройте окно Properties (Свойства) для элемента управления DataGridView и измените значение свойства AutoSizeColumnsMode на AllCells. Если вы выполните повторную компиляцию программы и снова ее запустите, то увидите, что теперь окно приложение имеет вид, показанный на рис. 22.6.
Book List
ISBN
Title
Ajthor
Pu
| (№1742714)
Wonaerful Ue
Stepnen Jay Gould
h
[№977172-5
The Emperors New Mind
Roger Penrose
Vin
0-14-017996-3
Metamagical Themas
Douglas R. Hofstadter
Pe
0-201-36030-2
The Meaning Of It All
Richard P. Feynman
Adi
М9М344»-Х
The Walpole Orange
Frank Muir
Ba
0430-99353-Х
The Amber Spyglass
Philip Pullman
Sc
0552-13461-9
Pyramids
Terri" Dratchett
Co
0-7493-973 9-Х
Made In Атенса
ВII Brvson
Mir
Рис. 22.6. Элемент управления DataGridView с измененной шириной столбцов
Ширина каждого столбца установлена так, чтобы в нем умещалась самая длинная строка, присутствующая в данном столбце. В целом результирующее окно приложения выглядит не так уж плохо, но программные методы позволяют значительно повысить степень персонификации его внешнего вида.
Персональная настройка элемента управления DataGridView
Как уже отмечалось ранее в этой главе, внешний вид элемента управления DataGridView в значительной степени можно изменять в соответствии с личными предпочтениями. Аспекты решения этой задачи мы будем исследовать, используя элемент управления в несвязанном режиме, но все, что будет при этом описано, равно применимо к использованию элемента управления и в связанном режиме. Внешний вид каждой ячейки в элементе управления DataGridView определяется объектом типа DataGridViewCellStyle, имеющим свойства, описанные в табл. 22.7.
Таблица 22.7. Свойства объекта DataGridViewCellStyle, влияющие на внешний вид ячейки
СвойствоОписание
BackColor
Это значение — объект System::Drawing: .-Color, который определяет цвет фона ячейки. В классе Color диапазон стандартных цветов определен в виде статических членов. Значение, используемое по умолчанию — Color:: Empty.
ForeColor
Это значение — объект color, который определяет цвет изображения ячейки. Значение, используемое по умолчанию — Color:: Empty.
SelectionBackColor
Это значение — объект Color, который определяет цвет фона выбранной ячейки. Значение, используемое по умолчанию — Color:: Empty.
SelectionForeColor
Это значение — объект Color, который определяет цвет изображения выбранной ячейки. Значение, используемое по умолчанию — color: .-Empty.
Font
Это значение — объект System: .-Drawing: :Font, определяющий шрифт, который должен использоваться для отображения текста в ячейке. Значение, используемое по умолчанию — null.
Alignment
Это значение определяет выравнивание содержимого ячейки. Допустимые значения определены перечислением DataGridviewAiignment, поэтому значение может быть любой из следующих констант: Bottomcenter, BottomLeft, BottomRight, MiddleCenter, MiddleLeft, MiddleRight, TopCenter, TopLeft, TopRight, NotSet. Значение, используемое по умолчанию — NotSet.
WrapMode
Это значение определяет переход текста в ячейке на следующую строку, если он слишком длинен, чтобы уместиться в ячейке. Допустимое значение — одна из констант, определенных перечислимым объектом DataGridViewTriState: True, False, NotSet. Значение, используемое по умолчанию — NotSet.
Padding
Это значение — объект типа System: -.Windows:: Forms: : Padding, который определяет пробел между содержимым и краем ячейки. Конструктор класса Padding требует передачи аргумента типа int, который представляет значение дополнения содержимого ячейки пробелами, измеренное в пикселях. Значение, используемое по умолчанию, соответствует отсутствию дополнения содержимого ячейки.
Format
Это значение — строка формата, которая определяет способ форматирования содержимого строки. Это форматирование аналогично используемому в функции Console:: WriteLine (). Значение, используемое по умолчанию — пустая строка.
Приведенный в табл. 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 в описанной далее последовательности.
Свойством 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: : Dat eTime, а остальные столбцы будут строками. Класс Dat eTime определяет момент времени, который, как правило, указывают в виде сочетания даты и времени. В данном примере интерес представляет только дата, поэтому мы используем конструктор, который принимает только три аргумента: год, месяц и день.
Определение данных
Прежде всего, необходимо подготовить данные, которые будут отображаться. Добавьте следующий код в конструктор класса Formi после вызова функции InitializeComponent():
// Создание данных книг, по одному массиву для каждой книги
array<ObjectA>A bookl = (gcnew DateTime(1999,11,5) , L"0-09-174271-4",
L"Wonderful Life”, L”Stephen Jay Gould”, L”Hutchinson Radius”};
array<ObjectA>A book? = (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-017 996-8", L"Metamagical Themas", "Douglas R. Hofstadter", L"Penguin"};
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"};
arrayCObjectA>A book6 = {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, book3, book4, book5, Ьоокб, book7, book8};
Общий принцип работы этого кода таков же, как в предыдущем примере. Различия обусловлены лишь тем, что теперь спецификация каждой книги содержит дополнительный элемент типа DateTime, поэтому тип элементов массива — ObjectA. Вспомните, что класс Object — базовый класс для любого класса C++/CLI; поэтому в элементе типа ObjectA можно хранить дескриптор объекта класса любого типа.
Затем в конструктор можно поместить следующий оператор:
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<ObjectA>A book in books) dataGridView->Rows->Add(book);
Цикл for each выбирает элементы из массива books в том порядке, в каком они передаются методу Add () применительно к ссылке, возвращенной свойством Rows элемента управления. Каждый элемент в массиве books представляет собой массив строк, количество которых равно числу столбцов в элементе управления.
Теперь элемент управления загружен данными, поэтому количество строк и столбцов определено, а содержимое заголовков столбцов указано. Теперь можно приступить к настройке внешнего вида элемента управления.
Настройка элемента управления
Нам необходимо, чтобы элемент управления размещался в фиксированной позиции в клиентской области формы. Это можно осуществить, устанавливая значение свойства Dock:
dataGridView->Dock = Dockstyle::Fill;
В качестве значения свойства Dock должна быть установлена одна из констант, определенных перечислением Dockstyle. Другими допустимыми значениями этого свойства являются Тор (Вверху), Bottom (Внизу), Left (Слева), Right (Справа) и None (Нет), которые указывают стороны элемента управления, привязанные к позиции размещения.
Привязку позиции элемента управления к клиентской области формы можно выполнять также посредством установки свойства Anchor элемента управления. Значение этого свойства указывает края элемента управления, которые должны быть привязаны к клиентской области формы. Значение является побитовой комбинацией констант, определенных перечислением Anchorstyles, и может принимать любые или все из значений Top, Bottom, Left и Right. Например, чтобы привязать верхнюю и левую стороны элемента управления, в качестве значения нужно было бы указать Anchorstyles: :Тор & Anchorstyles: .-Left. Определение свойства Anchor ведет к фиксации позиции элемента управления и его линеек прокрутки внутри контейнера заданного размера. Поэтому при изменении размера окна приложения элемент управления и его линейки прокрутки сохраняют свои размеры. Если установить значение свойства Dock так, как в предыдущем операторе, при изменении размера окна приложения отображаемая в нем часть элемента управления будет изменяться с соответствующим изменением линеек прокрутки. Поэтому теперь работать с приложением значительно удобнее.
Нам требуется, чтобы ширина столбцов автоматически изменялась, обеспечивая отображение полных строк данных в ячейках. Для этого можно вызывать функци- ю AutoResizeColumns():
dataGridView->AutoResizeColumns();
Этот оператор подбирает ширину всех столбцов в соответствии с текущим содержимым, в том числе в соответствии с содержимым ячеек заголовков. Обратите внимание, что эта настройка выполняется во время вызова функции, поэтому к моменту ее вызова содержимое уже должно существовать. При последующем изменении содержимого ширина столбца не изменяется. Если требуется, чтобы ширина столбца автоматически изменялась при каждом изменении содержимого ячеек, для элемента управления нужно установить также свойство AutoSizeColumnsMode:
dataGridView->AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode::AllCells;
Значением этого свойства должна быть одна из констант, определенных перечислением DataGridViewAutoSizeColumnsMode. Другими возможными значениями являются ColumnHeader (Заголовок столбца), AllCellsExceptHeader (Все ячейки, кроме заголовка), DisplayedCells (Отображаемые ячейки), DisplayedCellsExcep tHeader (Отображаемые ячейки, кроме заголовка), Fill (Заполнение) и None (Нет). Естественно, эти значения отображаются также в списке значений этого свойства на странице Properties элемента управления DataGridView.
В некоторых ситуациях требуется, чтобы автоматический подбор ширины при изменении содержимого выполнялся только для определенных столбцов. В этом случае значение свойства AutoSizeMode необходимо устанавливать для объекта столбца.
Существует еще две перегруженных версии функции 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;
Было бы прекрасно, если бы заголовок отображался более крупным шрифтом, и его можно определить, устанавливая значение свойства 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.
Форматирование столбца
Первый столбец содержит дескрипторы объекта Dat eTime. В настоящий момент для получения каких-либо данных для отображения приложение просто вызывает функцию ToString () для объектов, но мы можем его усовершенствовать. Для свойства DefaultCellStyle столбца можно установить свойство Format, и затем эта спецификация формата будет использоваться для отображения содержимого ячеек:
dataGridView->Columns[0]->DefaultCellStyle->Format = L"y";
Этот оператор устанавливает в качестве значения свойства Format строку, содержащую спецификацию формата у для объекта DateTime, которая представляет его в краткой форме даты в виде месяца и года. Для объектов DateTime существует еще несколько спецификаторов формата, которые можно было бы использовать. Например, спецификатор D отображает день, месяц и год, а спецификаторы f и F наряду с датой отображают и время.
Если вы добавили в конструктор класса Forml весь описанный код, пора скомпилировать и запустить пример. Должно открыться окно приложения, подобное показанному на рис. 22.7.
| ISB \| ISB \1 Da1 Da0-0917427140-091742714October 2111October 2111Januaiy 1993February 1994Januaiy 1993February 19940419-977173-50419-977173-50-14-017996^80-14-017996^80-201-360№20-201-360№2Му Other Book L’stМу Other Book L’stAihorAihorTileTileWonderful LifeWonderful LifeStephen Jay GouldThe Emperor’s New Mind Roger PenroseStephen Jay GouldThe Emperor’s New Mind Roger PenroseMetamagical ThemasMetamagical ThemasThe Meaning Of It AllThe Meaning Of It AllDouglas R. HofstadterDouglas R. HofstadterRichard P FeynmanRichard P FeynmanThe Walpole OrangeThe Walpole OrangeFrank MuirFrank MuirIO433-9935S-XIO433-9935S-XJuly 2004July 2004The Ar.iber SpyglassThe Ar.iber SpyglassPhilip PullmanPhilip PullmanPyramidsPyramidsTe^ PratchettTe^ PratchettMade in AmericaMade in AmericaBill BrysonBill BrysonSeptember 2002 0^552-13461-9Februaiy 19930-7 Э8-Э733ХSeptember 2002 0^552-13461-9Februaiy 19930-7 Э8-Э733ХNovember 1995 0^593-03443XNovember 1995 0^593-03443XPuc. 22.7. Элемент управления DataGridView после форматированияPuc. 22.7. Элемент управления DataGridView после форматирования
| ISB \
| ISB \
1 Da
1 Da
0-091742714
0-091742714
October 2111
October 2111
Januaiy 1993
February 1994
Januaiy 1993
February 1994
0419-977173-5
0419-977173-5
0-14-017996^8
0-14-017996^8
0-201-360№2
0-201-360№2
Му Other Book L’st
Му Other Book L’st
Aihor
Aihor
Tile
Tile
Wonderful Life
Wonderful Life
Stephen Jay Gould
The Emperor’s New Mind Roger Penrose
Stephen Jay Gould
The Emperor’s New Mind Roger Penrose
Metamagical Themas
Metamagical Themas
The Meaning Of It All
The Meaning Of It All
Douglas R. Hofstadter
Douglas R. Hofstadter
Richard P Feynman
Richard P Feynman
The Walpole Orange
The Walpole Orange
Frank Muir
Frank Muir
IO433-9935S-X
IO433-9935S-X
July 2004
July 2004
The Ar.iber Spyglass
The Ar.iber Spyglass
Philip Pullman
Philip Pullman
Pyramids
Pyramids
Te^ Pratchett
Te^ Pratchett
Made in America
Made in America
Bill Bryson
Bill Bryson
September 2002 0^552-13461-9
Februaiy 19930-7 Э8-Э733Х
September 2002 0^552-13461-9
Februaiy 19930-7 Э8-Э733Х
November 1995 0^593-03443X
November 1995 0^593-03443X
Puc. 22.7. Элемент управления DataGridView после форматирования
Puc. 22.7. Элемент управления DataGridView после форматирования
Ширина окна, представленного на рис. 22.7, была увеличена, чтобы показать больше столбцов. В этой книге окно приложения отображено в оттенках серого, но на экране оно должно отображаться в ярких цветах Technicolor.
Если щелкнуть на заголовке одной из строк в левой части элемента управления, строка должна быть выделена, как показано на рис. 22.8.
Рис. 22.8. Выделение строкиРис. 22.8. Выделение строки
Рис. 22.8. Выделение строки
Рис. 22.8. Выделение строки
Цвет фона ячеек строки определен в свойстве SelectionBackColor свойства DefaultCellStyle. Можно также выбрать отдельную ячейку, щелкая на ней. При этом ее цвет фона изменится на зеленый.
Возможность сортировки строк по любому из столбцов встроена в элемент управления DataGridView. Можете щелкнуть на заголовке столбца и убедиться, что строки сортируются по выбранному столбцу. Если вторично щелкнуть на заголовке столбца, порядок сортировки строк изменится на противоположный. К каждому столбцу
можно добавить контекстную подсказку, описывающую возможности сортировки. Добавленный в конструктор класса Formi цикл выглядит следующим образом:for each(DataGridViewColumnA column in dataGridView->Columns) column->ToolTipText = L"Click to\nsort rows";Значение свойства Columns — коллекция столбцов, в которой каждый столбец — объект типа DataGridViewColumn. Цикл последовательно выбирает каждый из столбцов и устанавливает для него значение свойства ToolTipText (Текст контекстной подсказки). Контекстная подсказка для одного из заголовков столбцов показана на рис. 22.9.можно добавить контекстную подсказку, описывающую возможности сортировки. Добавленный в конструктор класса Formi цикл выглядит следующим образом:for each(DataGridViewColumnA column in dataGridView->Columns) column->ToolTipText = L"Click to\nsort rows";Значение свойства Columns — коллекция столбцов, в которой каждый столбец — объект типа DataGridViewColumn. Цикл последовательно выбирает каждый из столбцов и устанавливает для него значение свойства ToolTipText (Текст контекстной подсказки). Контекстная подсказка для одного из заголовков столбцов показана на рис. 22.9.Му Other Book ListМу Other Book ListDD| ISB>| ISB>1 Air hot1 Air hotFebruary 1993 10-7493-973^X MalFebruary 1993 10-7493-973^X MalIn Ament Click toIn Ament Click toJanuary 19 9 3I M*4H 7996.8January 19 9 3I M*4H 7996.8Metamagical T ■Metamagical T ■sort rowssort rows!ill Biysonlouglas R. Hofstadter!ill Biysonlouglas R. HofstadterSeptember 2002 , 0^552-13461-9 Pyramids0 439-99358-X The Amber SpyglassSeptember 2002 , 0^552-13461-9 Pyramids0 439-99358-X The Amber SpyglassТепу PratchettТепу PratchettJuly 2004July 2004Philip PullmanPhilip PullmanOctober 2001,0419-977170-5 The Emperors New Mind Roger PenroseOctober 2001,0419-977170-5 The Emperors New Mind Roger PenroseNovember 1995 0 593-03449-X "The Walpole OrangeNovember 1995 0 593-03449-X "The Walpole OrangeFrank MuirFrank Muir0419-174271-4 Wonderful Life0419-174271-4 Wonderful LifeStephen Jay GouldStephen Jay GouldFebruary 19943 2D1-3608 0 2 The Meaning Of It All Richard P. FeynmanFebruary 19943 2D1-3608 0 2 The Meaning Of It All Richard P. Feynman
можно добавить контекстную подсказку, описывающую возможности сортировки. Добавленный в конструктор класса Formi цикл выглядит следующим образом:
for each(DataGridViewColumnA column in dataGridView->Columns) column->ToolTipText = L"Click to\nsort rows";
Значение свойства Columns — коллекция столбцов, в которой каждый столбец — объект типа DataGridViewColumn. Цикл последовательно выбирает каждый из столбцов и устанавливает для него значение свойства ToolTipText (Текст контекстной подсказки). Контекстная подсказка для одного из заголовков столбцов показана на рис. 22.9.
можно добавить контекстную подсказку, описывающую возможности сортировки. Добавленный в конструктор класса Formi цикл выглядит следующим образом:
for each(DataGridViewColumnA column in dataGridView->Columns) column->ToolTipText = L"Click to\nsort rows";
Значение свойства Columns — коллекция столбцов, в которой каждый столбец — объект типа DataGridViewColumn. Цикл последовательно выбирает каждый из столбцов и устанавливает для него значение свойства ToolTipText (Текст контекстной подсказки). Контекстная подсказка для одного из заголовков столбцов показана на рис. 22.9.
Му Other Book List
Му Other Book List
D
D
| ISB>
| ISB>
1 Air hot
1 Air hot
February 1993 10-7493-973^X Mal
February 1993 10-7493-973^X Mal
In Ament Click to
In Ament Click to
January 19 9 3I M*4H 7996.8
January 19 9 3I M*4H 7996.8
Metamagical T ■
Metamagical T ■
sort rows
sort rows
!ill Biyson
louglas R. Hofstadter
!ill Biyson
louglas R. Hofstadter
September 2002 , 0^552-13461-9 Pyramids
0 439-99358-X The Amber Spyglass
September 2002 , 0^552-13461-9 Pyramids
0 439-99358-X The Amber Spyglass
Тепу Pratchett
Тепу Pratchett
July 2004
July 2004
Philip Pullman
Philip Pullman
October 2001,0419-977170-5 The Emperors New Mind Roger Penrose
October 2001,0419-977170-5 The Emperors New Mind Roger Penrose
November 1995 0 593-03449-X "The Walpole Orange
November 1995 0 593-03449-X "The Walpole Orange
Frank Muir
Frank Muir
0419-174271-4 Wonderful Life
0419-174271-4 Wonderful Life
Stephen Jay Gould
Stephen Jay Gould
February 19943 2D1-3608 0 2 The Meaning Of It All Richard P. Feynman
February 19943 2D1-3608 0 2 The Meaning Of It All Richard P. Feynman
Puc. 22.9. Контекстная подсказка для одного из заголовков столбцов
Эти контекстные подсказки отображаются только при помещении указателя мыши над ячейкой заголовка столбца. Контекстную подсказку можно определить для любой ячейки с данными, устанавливая свойство ToolTipText для объекта ячейки.
Настройка внешнего вида чередующихся строк
При отображении множества одинаковых по виду строк может оказаться затруднительным фиксировать взгляд на нужной строке. Для облегчения этой задачи можно чередовать цвета строк, устанавливая другой цвет свойства BackColor (Цвет фона) для свойства AlternatingRowsDefaultCellStyle объекта управления:
dataGridView->AlternatingRowsDefaultCellStyle->BackColor = Color::Bluer-
Вероятно, для обеспечения приемлемого контраста между текстом и фоном потребуется изменить также значение свойства ForeColor для свойства Alternating- RowsDefaultCellStyle:
dataGridView->AlternatingRowsDefaultCellStyle->ForeColor = Color::White;
Теперь чередующиеся строки отображаются поочередно на розовом и синем фоне, как показано на рис. 22.10 (естественно, на рисунке строки различаются оттенком серого).
Теперь строки легко различать, а белый текст на синем фоне отчетливо виден. По- прежнему строки можно выбирать, щелкая на заголовке строки — при этом выбранная строка выделяется зеленым цветом. Щелчок на ячейке, расположенной слева от заголовков столбцов, приводит к выбору всех строк.
иШ Му Other Book List
00
ISBN
i Author
A
November 1999
309-17 271-4
Wonderful Life
Stephen Jay Goula |
В 0 .ober 2001
0-09-977170-C
Ilie ^•л1 s New find
Roger Per-ose
January 1993
3-1 -317996-8
Metamagical Themas
Douglas R.
Hof stad
1 February 1994
0-201-36060-2
The Meaning Of it AJ
Richard P I
November 1995
M9M3449-X
The Walpole Orange
Frank Muir
В .inly 20G4
3-439-99356-X
The Anber Spyglass
Ft^p Pullman
September 2002
0552-13461-9
Pyramids
Terry Pratchett
■ Feb шагу 1996
O7493 9739-X
Made h America
Bi лулол
■ v
<T
ini
J
>1
Рис. 22.10. Чередование фона строк
Динамическое определение стилей ячеек
Существует несколько возможностей изменения внешнего вида ячеек через обработку событий элемента управления DataGridView. Событие CellFormatting элемента управления DataGridView запускается, когда содержимое ячейки должно быть сформатировано в вид, готовый для отображения, поэтому, добавляя обработчик этого события, внешний вид любой ячейки можно настраивать в соответствии с ее содержимым. Рассмотрим, как можно было бы расширить пример Ех22_02 для решения этой задачи.
Например, предположим, что в примере Ех22_02 цвет фона ячеек в столбце Date необходимо изменить на красный, если значение даты меньше 2000 года. Вспомните, что, как было сказано в разделе 9 главы, посвященном событиям, для регистрации обработчика нужно добавить экземпляр делегата события. Тип делегата события CellFormatting — DataGridViewCellFormattingEventHandler, и этот тип ожидает передачи ему двух параметров: первого, типа ObjectA, который идентифицирует источник события, и второго — дескриптора объекта типа DataGridView CellFormattingEventArgs. Второй аргумент, переданный обработчику события CellFormatting, предоставляет дополнительную информацию о событии с помощью свойств, описанных в табл. 22.8.
Таблица 22.8. Свойства объекта DataGridViewCellFormattingEventArgs
Свойство
Описание
Value
Значение этого свойства — дескриптор содержимого ячейки, которая должна быть форматирована.
DesiredType
Значение этого свойства — дескриптор объекта типа Туре, который идентифицирует тип содержимого форматируемой ячейки.
Cellstyle
Извлекает или устанавливает стиль ячейки, связанной с событием форматирования, потому это значение — дескриптор объекта типа DataGridViewCellStyle.
Columnindex
Значение этого свойства — индекс столбца форматируемой ячейки.
Rowindex
Это значение — индекс строки форматируемой ячейки.
FormattingApplled
Значение этого свойства, которое может принимать значение true или false, указывает, применялось ли форматирование к содержимому ячейки.
Эти свойства позволяют выяснять все необходимые сведения о форматированной ячейке — индекс ее строки и столбца, действующий стиль и содержимое ячейки.
Первый шаг по обработке события CellFormatting состоит в определении для него функции обработчика. Код обработчика должен быть максимально кратким и эффективным, поскольку функция вызывается для каждой ячейки элемента управления каждый раз, когда возникает необходимость форматирования ячейки. В класс Formi можно добавить следующую функцию обработчика события CellFormatting:
private:
void OnCellFormatting(ObjectA sender, DataGridViewCellFormattingEventArgsA e)
I
// Проверка того, является ли данный столбец столбцом даты 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 мы проверяем действительное существование содержимого, и, если в них объект Date Time присутствует, то меньше ли значение года, чем 2000. В этом случае значение свойства объекта, возращенного свойством Cellstyle, мы устанавливаем равным Color: :Red. Значение свойства FormattingApplied мы устанавливаем равным false для указания того, что форматированное содержимое отсутствует. Последнее действие не обязательно, поскольку исходное значение этого свойства — false. Его нужно было бы установить равным true, если бы в обработчике выполнялось форматирование содержимого. Это препятствовало бы последующему форматированию посредством значения свойства Format.
Чтобы зарегистрировать эту функцию в качестве обработчика события CellFormatting, в конец конструктора класса Formi нужно добавить следующий оператор:
dataGridView->CellFormatting +=
gcnew DataGridViewCellFormattingEventHandler(this, &Forml::OnCellFormatting);
Первый аргумент делегата — дескриптор объекта, содержащего функцию обработчика и являющегося текущим объектом Formi. Второй аргумент — адрес функции нового обработчика события. Если теперь повторить компиляцию и выполнение примера, вы должны увидеть, что цвет фона всех ячеек в столбце Date, в которых значение даты предшествует 2000 году, изменился на красный.
Элемент управления DataGridView определяет события CellMouseEnter и CellMouseLeave, запускаемые при помещении указателя мыши на ячейку или при перемещении его от нее. Обработчики этих событий можно было бы реализовать так, чтобы ячейка с помещенным на нее указателем мыши выделялась за счет изменения цвета фона. Обработчик события CellMouseEnter мог бы устанавливать новые цвета изображения и фона, а обработчик события CellMouseLeave — восстанавливать первоначальные цвета. Решение этой задачи имеет несколько сложных аспектов, поэтому заслуживает особого рассмотрения.
Практическое занятиеПрактическое занятие
Практическое занятие
Практическое занятие
Выделение ячеек под указателем мыши
Этот пример создается как расширение примера Ех22_02 с учетом последнего добавленного в него обработчика события CellFormatting, то есть он не создается с нуля. Старые цвета фона и изображения должны быть где-то сохранены, поэтому добавим в класс For ml следующие приватные члены данных:
// Сохранение в обработчике события помещения указателя мыши
//на ячейку старых цветов ячейки для их последующего восстановления // в обработчике события перемещения указателя мыши из ячейки private: Color oldCellBackColor;
private: Color oldCellForeColor;
В классе конструктора Forml оба эти члена данных нужно инициализировать значением Color: :Empty.
Первый параметр делегата любого из событий CellMouseEnter или CellMouseLeave — это дескриптор объекта, запускающего событие, которым в данном случае является элемент управления 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) H Проверка того, что индексы //не отрицательны
{
// Идентификация ячейки, в которую помещен указатель DataGridViewCellA cell =
dataGridView->Rows[e->Row!ndex]->Cells[e->ColumnIndex];
// Сохранение любых старых установленных цветов oldCellBackColor = cell->Style->BackColor; oldCellForeColor = cell->Style->ForeColor;
// Установка цветов выделения cell->Style->BackColor = Color::White; cell~>Style->ForeColor = Color::Black; }
}
После проверки того, что значения обоих индексов неотрицательны, мы извлекаем дескриптор ячейки, в которую помещен указатель мыши. Для этого мы вначале выбираем строку, индексируя значение свойства Rows элемента управления с помо-
щью значения свойства 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, именно они будут восстановлены и определят цвета форматирования ячейки.
Чтобы зарегистрировать функции обработчиков, в конец конструктора класса Formi добавьте следующие операторы:
dataGridView->CellMouseEnter +=
gcnew DataGridViewCellEventHandler(this, SForml::OnCellMouseEnter); dataGridView->CellMouseLeave +=
gcnew DataGridViewCellEventHandler (this, &Forml: -.OnCellMouseLeave) ;
Ко всем событиям ячейки применяется один и тот же делегат, поэтому делегат DataGridViewCellEventHandler служит для регистрации обоих обработчиков.
Если теперь еще раз скомпилировать и выполнить программу, вы должны увидеть выделение ячеек в действии, как показано на рис. 22.11.
Рис. 22.11. Выделение ячеек
Что ж, эта программа работает — по крайней мере, в большинстве случаев. Однако ячейки красного цвета в столбце Date не выделяются. В чем же дело?
Причина возникновения этой проблемы кроется в последовательности событий. Событие CellFormatting запускается после события CellMouseEnter, поэтому последнее слово остается за функцией обработчика, которая устанавливает красный цвет фона, и она замещает результат обработчика события CellMouseEnter своими значениями. Поэтому необходимо, чтобы обработчик события CellFormatting распознавал выделенную ячейку и не предпринимал в этом случае никаких действий. Для этого в класс Formi можно было бы добавить еще один член для хранения дескриптора выделенной в текущий момент ячейки:
private: DataGridViewCellA highlightedCell; //Выделенная в текущий момент ячейка
В конструкторе класса Formi этот новый член нужно также инициализировать значением nullpt г.
Теперь необходимо дополнить обработчик события CellMouseEnter сохранением дескриптора выделенной ячейки в новом члене:
void OnCellMouseEnter(ObjectА sender, DataGridViewCellEventArgsA e)
{
if(e->ColumnIndex >= 0 && e->RowIndex >= 0) // Проверка того, что индексы //не отрицательны
{
// Идентификация ячейки, в которую помещен указатель highlightedCell = dataGridView->Rows[e->Row!ndex]->Cells[e->ColumnIndex];
// Сохранение любых старых установленных цветов oldCellBackColor = highlightedCell->Style->BackColor; oldCellForeColor = highlightedCell->Style->ForeColor;
II Установка цветов выделения hightLightedCell->Style->BackColor = Color::White; highlightedCell->Style->ForeColor = Color::Black; }
}
Реализацию обработчика события CellMouseLeve потребуется изменить:
void OnCellMouseLeave(ObjectA sender, DataGridViewCellEventArgsA e) {
if(e->ColumnIndex >=0 && e->RowIndex >=0) {
// Восстановление сохраненных значений цвета highlightedCell->Style->BackColor = oldCellBackColor; highlightedCell->Style->ForeColor = oldCellForeColor;
// Сброс значения переменой хранения цвета к "пустому” значению oldCellForeColor = oldCellBackColor = Color::Empty;
highlightedCell = nullptr;// Сброс дескриптора выделенной ячейки
Специальное определение дескриптора ячейки больше не требуется, так как он доступен в члене 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(e~>Value '= nullptr && safe_cast<DateTimeA>(e->Valuet->Year < 2000) {
e->CellStyle->BackColor = Color::Red;
e->FormattingApplied = false;// Мы не форматировали данные
Если теперь заново компилировать и выполнить программу примера, все выбранные ячейки будут выделены.
Теперь вы должны иметь достаточно полное представление о том, как реализовать обработчик события для элемента управления DataGridView. Существует множество других событий, поэтому вы обладаете огромными возможностями по динамической настройке элемента управления в соответствии с потребностями приложения.
Использование связанного режима
В связанном режиме источник данных, отображаемых элементом управления DataGridView, указан значением его свойства DataSource, которое в общем случае может быть установлено любым объектом ref class, тип которого реализует любой из интерфейсов, перечисленных в табл. 22.9.
Понятно, что можно создать собственные классы, которые реализуют тот или иной из описанных интерфейсов, а затем их можно будет применять в качестве источника данных для элемента управления DataGridView в связанном режиме. Однако в большинстве ситуаций требуется доступ к существующему источнику данных без необходимости заботиться о создании собственных классов для получения доступа к данным. В этом случае, скорее всего, целесообразно остановить свой выбор на компоненте Bindingsource.
Таблица 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, но можно работать с любой другой базой данных из числа доступных в системе.
Создайте проект 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.13.
Установленные подключения к существующим данным отображаются в раскрывающемся списке, из которого потребуется выбрать нужное. Можно также щелкнуть на кнопке New Connection (Новое подключение), чтобы установить новое подключение к источнику данных.
Рас. 22.12. Диалоговое окно Data Source Configuration Wizard
В результате откроется диалоговое окно Add Connection (Добавление подключения), изображенное на рис. 22.14.
Рис. 22.13. Окно Choose Your Data Connection мастера Dato Source Configuration WizardРис. 22.13. Окно Choose Your Data Connection мастера Dato Source Configuration Wizard
Рис. 22.13. Окно Choose Your Data Connection мастера Dato Source Configuration Wizard
Рис. 22.13. Окно Choose Your Data Connection мастера Dato Source Configuration Wizard
Microsoft ODBC Data Source (ODBC)Data source specificationjllse user or system data source namedMicrosoft ODBC Data Source (ODBC)Data source specificationjllse user or system data source namedUse connection string:Use connection string:RefreshRefreshBuild...Build...
Microsoft ODBC Data Source (ODBC)
Data source specification
jllse user or system data source named
Microsoft ODBC Data Source (ODBC)
Data source specification
jllse user or system data source named
Use connection string:
Use connection string:
Refresh
Refresh
Build...
Build...
Add Connection?
Enter information to connect to the selected data source or click ’'Change11 to choose a different data source and/or provider.
Data source:
Change...
Puc. 22.14. Диалоговое окно Add Connection
В этом диалоговом окне можно ввести спецификацию источника данных в виде имени либо, выбирая второй переключатель, в виде строки подключения к источнику данных. После ввода любой необходимой регистрационной информации работу подключения можно проверить, щелкая на кнопке Test Connection (Проверить подключение). В моей системе источник данных ODBC выбран по умолчанию, но если отображаемый по умолчанию источник данных вам не подходит, можно щелкнуть на кнопке Change (Изменить), чтобы открыть диалоговое окно Change Data Source (Изменение источника данных), показанное на рис. 22.15.
Рас. 22.15. Диалоговое окно Change Data Source
В этом диалоговом окне отображены доступные типы источников данных. Выберите тип источника данных, с которым желаете работать, и щелкните на кнопке ОК. После этого произойдет возврат к диалоговому окну, показанному на рис. 22.14, в котором можно дополнить и проверить идентификационные данные, прежде чем щелкнуть на кнопке ОК и возвратиться в диалоговое окно, приведенное на рис. 22.13. После щелчка на кнопке Next скорее всего откроется окно сообщения с вопросом о необходимости добавления источника данных в проект. Щелчок на кнопке Yes (Да) приведет к открытию окна Choose Your Database Objects (Выберите объекты базы данных) мастера Data Source Configuration Wizard, показанного на рис. 22.16, где можно выбрать объекты в базе данных, с которыми требуется работать.
В этом диалоговом окне можно выбирать объекты базы данных, которые должны быть добавлены в проект. Для этого необходимо развернуть дерево, чтобы можно было выбирать отдельные таблицы или даже отдельные поля внутри таблицы. Для данного примера можно упростить задачу и просто выбрать таблицу Customers (Клиенты). Можно также изменить имя набора данных — лично я изменил его на Customers (Клиенты). После щелчка на кнопке Finish (Готово) мастер создаст код, необходимый для получения доступа к базе данных.
База данных инкапсулирована в классе, производном от класса Dataset. Выбор каждой таблицы базы данных приводит к определению класса, производного от DataTable, в качестве внутреннего класса DataSet. В классе DataSet для каждого класса DataTable определен также класс адаптера таблицы, который выполняет задачу подключения к базе данных и загрузки данных из таблицы базы данных в соответствующий член DataTable объекта DataSet. Это ведет к генерации достаточно большого объема кода. При выборе в базе данных Northwind одной только табли-
цы Customers мы получаем около 2000 строк кода. Кроме того, мастер добавляет в проект компонент Bindingsource, который предоставляет интерфейс между таблицей Customers базы данных Northwind и элементом управления DataGridView. Вкладка Design в окне редактирования должна выглядеть подобно изображенной на рис. 22.17.
Рис. 22.16. Окно Choose Your Database Objects мастера Data Source Configuration WizardРис. 22.16. Окно Choose Your Database Objects мастера Data Source Configuration WizardXXForml.h [Design]Forml.h [Design]IS CustomersIS CustomerscustomersBindingSourcecustomersBindingSourceCustomersTableAdapterCustomersTableAdapter
Рис. 22.16. Окно Choose Your Database Objects мастера Data Source Configuration Wizard
Рис. 22.16. Окно Choose Your Database Objects мастера Data Source Configuration Wizard
X
X
Forml.h [Design]
Forml.h [Design]
IS Customers
IS Customers
customersBindingSource
customersBindingSource
CustomersTableAdapter
CustomersTableAdapter
Рис. 22.17. Вкладка Design после выбора таблицы Customers
Как видите, добавление источника данных привело к добавлению в проект трех объектов: NorthWindDataSet, инкапсулирующего базу данных, CustomersTable- Adapter, который осуществляет доступ к данным в таблице Customers базы данных, и CustomersBindingSource, управляющего обменом данными между базой данных и элементом управления DataGridView.
Теперь приложение готово, а мы пока не написали ни одной строки кода. Это достаточно удивительно, учитывая количество функций, выполняемых программой. Однако при запуске программы заголовки столбцов выглядят несколько зауженными. Я предпочитаю, чтобы ширина столбцов автоматически изменялась в соответствии с текстом строк, поэтому не смог удержаться от того, чтобы не добавить в конструктор класса Formi следующие две строки кода:
dataGridView->AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode::AllCells; dataGridView~>AutoResizeColumnHeadersHeight();
Теперь окно приложения выглядит, как показано на рис. 22.18.
Using a Binding Source Component
CustomerlD
CcmparyNameContact Name
Tv
► |alfki
Alfreds Futterobte
Mana Anders
ANATR
Ana Trujillc Emparedados у heiados
Ana Trujillo
ANTON
Antonio Moreno Taqueria
Antonio Moreno
AROUT
Around the Hom
Thomas Hardy
BERGS
Berglunds snanbkop
Christina Berglund
BLAUS
Blauer See Delikatessen
Hanna Moos
BLONP
Blondel pere et fils
Frederique Dteaux
BO LID
Bolido Comidas preparadas
Martin Sommer
BONAR
Bon зор1
Laurence Lenihan
'BOTTM
Bottom-Dollar Malets
Elizabeth Lincolnv
11 <
"I
>
Рис. 22.18. Отображение содержимого таблицы Customers
Для перемещения по данным можно использовать линейки прокрутки, а колесо мыши должно выполнять прокрутку строк. Элемент управления BindingNavigator мог бы несколько улучшить ситуацию, поэтому рассмотрим, как можно его использовать.
Использование элемента управления BindingNavigator
Элемент управления BindingNavigator специально предназначен для работы с компонентом Bindingsource. Использование элемента управления BindingNavigator для навигации по данным, полученным из источника данных, вряд ли могло бы быть проще. Достаточно добавить элемент управления в форму и в качестве значения свойства Bindingsource элемента управления установить переменную, которая инкапсулирует компонент Bindingsource. В следующем примере программа Ех22_03 соответствующим образом расширяется.
Практическое занятиеПрактическое занятиеI
Практическое занятие
Практическое занятие
Использование элемента управления
BindingNavigator
Щелкните на элементе управления BindingNavigator в окне Toolbox, а затем в клиентской области формы, чтобы добавить элемент управления в проект. Открыв окно Properties элемента управления, установите для свойства Bindingsource значение CustomersBindingSource, которое представляет собой имя дескриптора элемента управления Bindingsource, инкапсулирующего таблицу Customers. Вот и все. Если скомпилировать и запустить приложение, можно видеть, что теперь окно содержит линейку прокрутки данных, как показано на рис. 22.19.
ь Usmg a Binding Source Component□ В
И 4 22cf
91* fk ^ A
CENK,
Centro comercial Moctezuma
Francisco Chang
CHOPS
Chop-suey Chinese
Vang Wang
=
СОММ1
Comercio Mineiro
Pedro Afonso
CONSh
Consolidated Holdings
Bizabeth Brown
DRACD
Dracnenpiut Delicatessen
Sven Dtbieb
DUMON
Du mor.de entier
Janine Labrune
EASK
Eastern Connection
Ain Devon
ERNSH
Ernst Hande*
Roland Mendel
СДМ1А
■-amilia Arquibaldo
Ma Cruz
► |fissa
FISSA Fabrica Inter. Salchichas S.A.
Diego Roel
V
1
r>
Рис. 22.19. Добавление элемента управления BindingNavigator
Для перемещения по записям данных в прямом и обратном направлении можно использовать кнопки со стрелками. Окно содержит также кнопки для перемещения к первой или последней записи. Если ввести порядковый номер записи в текстовом поле панели инструментов и нажать клавишу <Enter>, программа выполнит переход непосредственно к указанной записи. Возможность перемещения по записям не зависит от их порядка. Щелкая на заголовке столбца Country (Страна), можно было бы упорядочить записи по странам, после чего перемещение по записям можно будет выполнять именно в этом порядке. Элемент управления BindingNavigator предлагает также кнопки для добавления и удаления записей.
Каждая из кнопок, предоставляемых элементом управления BindingNavigator, устанавливает связь с членом объекта Bindingsource, в котором выполняется навигация (табл. 22.10).
Таким образом, щелчок на кнопке навигационной панели инструментов инициирует действие в объекте Bindingsource, управляющем источником данных, но ни одна из применяемых по умолчанию операций не изменяет базу данных, которой управляет Bindingsource. Чтобы выполнить какие-либо изменения, потребуется написать определенный код.
Таблица 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 () применительно к объекту адаптера обновляемой таблицы. Эта функция генерирует исключение при наличии какой-либо ошибки, поэтому
ее вызов должен быть помещен в блок try, что позволит перехватывать любые возможные исключения. Функцию обработчика события Click кнопки Add New можно реализовать следующим образом:
System::Void bindingNavigatorAddNewItem_Click(System::ObjectA sender, System::EventArgsA e) {
try
{
CustomersTableAdapter->Update(Customers->_Customers);
}
catch (Exception"4 ex) {
MessageBox::Show(L”Обновление неудачно!\n"+ex,
Ь"Ошибка обновления записи базы данных”, MessageBoxButtons::0К, MessageBoxIcon::Error);
}
}
Аргументом функции Update () должно быть имя таблицы данных, которая содержит значения, предназначенные для записи в базу данных, поэтому в данном случае им является член Customer объекта Customers класса Forml. В случае возникновения каких-то проблем функция обработчика отображает окно сообщения с пояснением сути проблемы. Окно сообщения отображает текст, полученный из сгенерированного объекта Exception и поясняющий причину возникновения проблемы.
Реализация обработчика события Click кнопки Delete практически идентична предыдущей:
private: System::Void bindingNavigatorDeleteItem_Click(System: .'ObjectА sender, System::EventArgsA e)
{ try { CustomersTableAdapter->Update(Customers->_Customers);
}
catch (Exception"4 ex)
{
MessageBox::Show(L"Удаление неудачно!\n"+ex,
Ь”Ошибка удаления записи базы данных”, MessageBoxButtons::0К, MessageBoxIcon::Error);
}
}
Единственное различие между этими функциями — в тексте окна сообщения. Теперь, при наличии этих двух функций обработчиков, мы должны иметь возможность добавлять новые записи клиентов и удалять существующие.
Привязка к отдельным элементам управления
Можно создать также приложение Windows Forms, которое связывает каждый столбец таблицы базы данных с отдельным элементом управления; более того, создание такого приложения проще и быстрее, нежели выполнение предыдущего примера. Начните с создания нового проекта CLR по имени Ех22_04, используя шаблон Windows Forms Application (Приложение Windows Forms). Следующий шаг — добавле-
ние в проект источника данных. Как и в предыдущем примере, мы будем работать только с одной таблицей. Нажмите комбинацию клавиш <Shift+Alt+D>, чтобы открыть окно Data Sources (Источники данных), и щелкните на кнопке Add New Data Source (Добавить новый источник данных). Можно использовать базу данных Northwind или любую другую базу данных по своему выбору, но следует помнить, что форма будет содержать отдельные элементы управления для каждого столбца выбранной таблицы. Чтобы количество элементов управления оставалось в разумных пределах, в случае применения базы данных Northwind я предлагаю выбрать таблицу Order Details (Сведения о заказе), как показано на рис. 22.20, поскольку она содержит всего лишь пять столбцов.
Рис. 22.20. Выбор таблицы Order Details
После щелчка на кнопке Finish окно Data Sources отобразит в качестве источника данных базу данных Northwind с единственной доступной таблицей Order Details. Щелкните на имени таблицы Order Details, чтобы выбрать ее, а затем щелкните на расположенной справа от нее стрелке вниз, чтобы открыть меню, приведенное на рис. 22.21.
Три верхних элемента меню позволяют выбирать элементы управления для использования с таблицей при добавлении источника данных в форму. Если щелкнуть на элементе меню DatagridView, этот элемент управления будет выбран как отображающий таблицу, в то время как щелчок на элементе Details указывает, что для каждого столбца таблицы требуется отдельный элемент управления. Если же щелкнуть на элементе [None] ([Нет]), это свидетельствует, что при добавлении источника данных в форму создание элементов управления не требуется. Последний элемент меню открывает диалоговое окно, которое позволяет изменять элемент управления, используемый для таблицы по умолчанию (в настоящий момент им является DataGridView), и изменять выбор элементов управления в соответствии со своими потребностями. В данном случае следует просто щелкнуть на элементе Details, поскольку нам требуется по одному элементу управления для каждого столбца таблицы.
Рис. 22.21. Меню, связанное с таблицей
Теперь необходимо решить, какой элемент управления применять для каждого столбца таблицы. Дерево имен столбцов таблицы Order Details можно развернуть, щелкая на символе +, расположенном слева от имени таблицы. Если затем щелкнуть на имени первого столбца для его выбора, можно отобразить и его меню, щелкая на стрелке вниз. Это меню показано на рис. 22.22.
Ч ^ DataSetlр Order DetailsЧ ^ DataSetlр Order Details|ab]| |аЫ| № @В|ab]| |аЫ| № @ВOrderD= vзы TextBoxИ NumericUpDownJ^ Com bo BoxA LabelA LinkLabel£2 ListBoxО [None]Customize...OrderD= vзы TextBoxИ NumericUpDownJ^ Com bo BoxA LabelA LinkLabel£2 ListBoxО [None]Customize...
Ч ^ DataSetl
р Order Details
Ч ^ DataSetl
р Order Details
|ab]| |аЫ| № @
В
|ab]| |аЫ| № @
В
OrderD= v
зы TextBox
И NumericUpDown
J^ Com bo Box
A Label
A LinkLabel
£2 ListBox
О [None]
Customize...
OrderD= v
зы TextBox
И NumericUpDown
J^ Com bo Box
A Label
A LinkLabel
£2 ListBox
О [None]
Customize...
EgClass... ^Reso... I^Soluti... I Data...
Puc. 22.22. Меню, связанное co столбцом
Можно выбрать любой из элементов управления, приведенных в меню, но я полагаю, что для столбца Order ID (Идентификатор заказа) больше всего подходит элемент управления TextBox. Процесс необходимо повторить для каждого столбца таблицы. Для столбца Quantity (Количество) можно выбрать элемент управления NumericUpDown, а для каждого из остальных столбцов — элементы управления TextBox.
Обратите внимание, что выбор опции меню Customize (Настроить) в панели Data Sources (Источники данных), показанной на рис. 22.22, приводит к открытию диалогового окна Options (Параметры), в котором можно изменять набор элементов управления, доступных для отображения различных типов данных. Можно изменить также выбор элемента управления по умолчанию при перетаскивании элемента с панели Data Sources на форму. Диалоговое окно Options показано на рис. 22.23.
Рис. 22.23. Диалоговое окно Options
На рис. 22.23 видны элементы управления, связанные с отображением списка, причем для каждого доступного для выбора типа данных существует отдельный список связанных с ним элементов управления. Кроме выбранного по умолчанию элемента управления DataGridView были установлены флажки для элементов управления СошЬоВох и ListBox. Таким образом, все эти три элемента управления могут быть выбраны для отображения списка элементов данных. Другие типы данных можно выбрать в раскрывающемся списке, расположенном в верхней части диалогового окна, для которого требуется изменить набор доступных элементов управления.
Заключительный шаг по созданию программы связан с перетаскиванием таблицы Order Details из окна Data Sources в клиентскую область окна формы. После этого вкладка Design панели Editor будет выглядеть так, как показано на рис. 22.24.
Свойство Text формы было изменено, чтобы выделить текст в строке заголовка, а также слегка изменил расположение элементов управления, чтобы более равномерно разместить их внутри клиентской области. Кроме того, изменено расположение элементов в серой области в нижней части формы, чтобы они все были видны в панели Design. Как видите, пять элементов управления, соответствующие пяти столбцам таблицы, были автоматически добавлены в форму, вместе с надписями, указывающими, с каким столбцом таблицы связан каждый элемент управления. Верхняя часть клиентской области содержит элемент управления BindingNavigator, предназначенный для навигации по данным таблицы. Под формой отображены элементы, которые показывают, что приложение содержит компонент Bindingsource, связывающий базу данных с элементами управления, и классы DataSet и адаптера таблицы.
Если нажать комбинацию клавиш <Ctrl+F5> для сборки и запуска программы, вы получите готовую работающую программу для просмотра и редактирования таблицы
Order Details базы данных Northwind. В панель инструментов BindingNavigator была добавлена кнопка Save (Сохранить), служащая для сохранения любых выполненных изменений в таблице базы данных. Окно приложения приведено на рис. 22.25.
Рис. 22.24. Вкладка Design панели Editor после перетаскивания таблицы Order Details
Рис. 22.25. Работа приложения просмотра и редактирования Order Details
По сути, точно так же можно было бы создать программу, которая использует элемент управления DataGridView для отображения всей таблицы. Если хотите проверить это, просто создайте еще один проект и добавьте в него источник данных, как было сделано в предыдущем примере. Если затем перетащить таблицу из окна Data Sources на форму, приложение будет готово. Перед компиляцией и запуском программы нужно будет только установить значение свойства Text формы для отображения текста строки заголовка и определить значение свойства Dock элемента управления DataGridView.
Работа с множеством таблиц
Создание приложения, которое работает с несколькими таблицами, не намного сложнее создания предыдущего примера. В этом примере применяется элемент управления с вкладками, чтобы можно было получать доступ к трем различным таблицам базы данных 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>, чтобы скопировать его в буфер обмена. Перейдите к одной из остальных страниц вкладок, выберите ее панель и на-
жмите комбинацию клавиш <Ctrl+V>, чтобы вставить элемент управления из буфера обмена в панель данной страницы вкладки. Перейдите на третью страницу вкладки, выберите ее панель и снова нажмите комбинацию клавиш <Ctrl+V>, чтобы добавить элемент управления BingingNavigator в панель этой страницы вкладки. Затем в качестве значения свойства Dock каждого из трех элементов управления Binding Navigator установите Тор, а в качестве значения Dock каждого из элементов управления DataGridView — Fill.
Значения свойства Bindingsource двух элементов управления BindingNavigator, которые были созданы с помощью копирования, установлены неправильно. Поэтому выберите поле значения этого значения каждого из этих элементов управления и выберите требуемый компонент Bindingsource из раскрывающегося списка. Для элемента управления на странице вкладки Employees нужно выбрать EmployeesBindingSource, а для страницы вкладки Products — ProductsBindingSource.
Если теперь нажать комбинацию клавиш <Ctrl+F5> для сборки и запуска программы примера, должно открыться окно приложения, показанное на рис. 22.26.
Рис. 22.26. Приложение, работающее с множеством таблиц
Как видите, работающее приложение было получено без написания даже единой строчки кода. Думаю, вы согласитесь с тем, что описанная методика — замечательная возможность генерации приложений для получения доступа к источникам данных.
Резюме
Эта глава служит кратким введением в существующие возможности получения доступа к источникам данных в приложениях Windows Forms. Реальные возможности значительно шире описанных, тем не менее, вы получили хорошее представление о применении средств проектирования для компоновки элементов управления на форме и о совместной работе элементов управления при обращении к данным. В случае необходимости перехода к другим областям применения у вас не должно возникать особых проблем.
Ниже перечислены ключевые моменты, с которыми вы познакомились в настоящей главе.
Источником данных может быть реляционная база данных, Web-служба или объект.
Класс, производный от System: :Data: :DataSet, используется для инкапсуляции источника данных, а класс, производный от System: :Data: :DataTable инкапсулирует таблицу источника данных.
Строка данных в объекте DataTable представлена объектом типа System: : Data: :DataRow, а схема столбца описывается объектом типа System: :Data: : DataColumn.
Подключение к источнику данных и команды доступа к данным инкапсулированы в компоненте, который называется адаптером та&ии,и,ъи
Элемент управления DataGridView используется в форме для отображения данных в виде прямоугольной таблицы.
Элемент управления DataGridView можно привязывать к источнику данных для отображения содержимого таблицы. Этот элемент управления можно использовать также в несвязанном режиме для отображения данных, создаваемых непосредственно в программе.
Элемент управления DataGridView можно настраивать для изменения способа отображения строк, столбцов, заголовков и отдельных ячеек.
Компонент Bindingsource предоставляет интерфейс между источником данных и элементами управления формы. Этот компонент может связывать столбцы таблицы с отдельными элементами управления формы или содержимое всей таблицы с одним элементом управления DataGridView.
Элемент управления BindingNavigator предоставляет панель инструментов для навигации по данным, доступ к которым осуществляется с помощью компонента Bindingsource.
Упражнения
Исходные коды упражнений и их решения можно загрузить с Web-сайта издательства.
Измените код примера Ех22_04 так, чтобы заголовки столбов отображались шрифтом размером 12 пунктов с курсивным начертанием.
Измените код примера Ех22_05 так, чтобы столбцы стали достаточно широкими для вмещения текста каждой из ячеек.
Измените код примера Ех22_05 так, чтобы чередующиеся строки данных на каждой из страниц вкладок отображались затененными.
Создайте приложение Windows Forms, которое будет отображать содержимое таблицы Suppliers (Поставщики) базы данных Northwind.
A
Ключевые слова C++
В языке C++ ключевым словам придается особое значение, поэтому они не должны использоваться в программах в качестве идентификаторов. Компилятор Visual C++ 2005 распознает программы, написанные как на языке ISO/ANSI C++, так и предназначенные для работы в среде CLR, которые соответствуют спецификации C++/CLI. Поэтому компилятор распознает как ключевые слова 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
operator
typename
do
private
union
double
protected
unsigned
dynamic_cast
public
using
else
register
virtual
enum
reinterpret_cast
void
explicit
return
volatile
export
short
wchar_t
extern
signed
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
Десятичный код
Шестнадцатеричный код
Символ
Управляющая функция
016
10
DLE
017
11
DC1
018
12
DC2
019
13
DC3
020
14
DC4
021
15
NAK
022
16
SYN
023
17
ЕТВ
024
18
CAN
025
19
ЕМ
026
1А
SUB
027
1В
<■
ESC (Escape)
028
1С
L
FS
029
1D
GS
030
1Е
RS
031
1F
US
032
20
пробел
033
21
1
034
22
К
035
23
#
036
24
$
037
25
%
038
26
&
039
27
*
040
28
(
041
29
)
042
2А
*
043
2В
+
044
2С
045
2D
-
046
2Е
047
2F
/
048
30
0
049
31
1
050
32
2
051
33
3
052
34
4
053
35
5
054
36
6
055
37
7
Десятичный кодШестнадцатеричный кодСимволУправляющая функция
056
38
8
057
39
9
058
ЗА
059
ЗВ
060
ЗС
<
061
3D
=
062
ЗЕ
>
063
3F
?
064
40
@
065
41
А
066
42
В
067
43
С
068
44
D
069
45
Е
070
46
F
071
47
G
072
48
Н
073
49
I
074
4А
J
075
4В
К
076
4С
L
077
4D
М
078
4Е
N
079
4F
0
080
50
Р
081
51
Q
082
52
R
083
53
S
084
54
Т
085
55
и
086
56
V
087
57
W
088
58
X
089
59
Y
090
5А
Z
091
5В
[
092
5С
\
093
5D
1
094
5Е
*
095
5F
Десятичный код
Шестнадцатеричный кодСимволУправляющая функция
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
127
60
61а
62b
63с
64d
65е
66f
67g
68h
69i
6Aj
6Bk
6CI
6Dm
6En
6Fо
70p
71q
72r
73s
74t
75u
76v
77w
78x
79у
7Az
7B{
7C|
7D}
7E
7Fdeleteудаление
Коды 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
L
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
P
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
В
Ввод, 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
CFieldExchange, 928
Компилятор, 36
CGdiObject, 723
Компонент BindingSource, 1113
CLine, 738; 877
Компоновка, 898
CList, 763; 773
статическая, 898
CObject, 761; 870
Компоновщик, 36
CPen, 719
Консольное приложение, 32
CPenDialog, 826
CLR, 33
CPrintlnfo, 883; 884
Win32, 33
CProductset, 945
Константа
CRecordset, 928; 976
перечислимая, 87
CRecordView, 928; 940; 945
с плавающей точкой, 83
CRectangle, 743
Конструктор
CSketcherDoc, 867
класса, 356
CSketcherView, 726
копирования, 370; 417; 497
CText, 877
статический, 406
CTypedPtrList, 771
Контейнер, 1028
CWinApp, 643
Контекст устройства, 711
Debug, 605
Конфигурация, 46
Form, 1029
Координаты, 787
Forml, 1029
клиентские, 787
Trace, 605
логические, 787; 840
абстрактный, 512
страничные, 840
базовый, 483
устройства, 840
вложенный, 522
экранные, 840
интерфейсный, 530
Куча, 211
коллекции, 556
регистрация класса окна, 628
Л
типа значения, 124
Литерал, 84
Ключ
внешний, 922
М
первичный, 921
Макрос, 667; 680
Ключевое слово, 78
ASSERT_VALID(), 752
const, 95
BEGIN_MESSAGE_MAP(), 680
enum, 133
DECLARE_DYNAMIC(), 870
typedef, 85
DECLARE_DYNCREATE(), 668; 867; 870
Ключевые слова
DECLARE_MESSAGE_MAP(), 667; 680
C++/CLI, ИЗО
DECLARE_SERIAL(), 870
ISO/ANSI C++, 1129
END_MESSAGE_MAP(), 680
Код
IMPLEMENT_DYNCRETE(), 867
C++, 30
ON_COMMAND, 682
неуправляемый, 30
Манипулятор, 89
управляемый, 30
Массив, 81; 180
объектный, 36
CLR, 219
Коллекция, 760
зубчатый, 230
карта, 760; 769
инициализация, 184
массив, 760; 762
массивов, 230
список, 760; 764
многомерный, 189; 227
Комментарий, 70
объявление, 181
символьный, 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
#include, 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
::, 117
<, 140
«, 73; 88; 107; 110
<=, 140
”, 140
>, 140; 423
>=, 140
», 88; 107; ПО
Л, 107
I, Ю7
107
delete, 212
new, 212
safe_cast, 132
sizeof, 201
арифметическая, 93
битовая, 106
сдвига, ПО
ввода-вывода, 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
Пространство имен, 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
MM_TEXT, 711
MM_TWIPS, 712
Решение, 40; 62
сборка, 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
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
V, 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(), 976; 994
CanUpdate(), 978; 994
cell(), 897
Char::IsLetter(), 176
Char::ToLower(), 177
CheckDlgButton(), 828
CheckMenuItem(), 799
Clear(), 1092
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_Text(), 1012
Delete(), 976
DestructElements(), 763; 768
DisplayMessage(), 418
DllMain(),902; 906
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
GetDlgltem», 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
HashKey», 770
InflateRect(), 742
InitializeComponent», 1030
InitializeView», 1019
Initlnstance», 671
Insert», 1092
InsertAfter(), 765; 772
InsertBeforeO, 765; 772
InsertCopy», 1092
InvalidateRect», 732
IsEOF», 964IsOnFirstRecord», 992LineTo», 744LPtoDP», 843main», 64; 72; 267; 1031max», 777min(), 777Move», 984; 1014MoveElement», 879MoveFirst», 1014MoveLast», 1014MovePrev», 1014MoveTo», 715OnActivateView», 964; 969OnBeginPrinting(), 896OnCancel», 825OnContextMenu», 799OnDraw», 714OnEndPrinting», 896 OnGetRecordset», 955; 1002OnlnitDialog», 838OnlnitialUpdate», 844; 968; 1001OnLButtonDown», 728OnLButtonUp», 728OnMouseMove», 728OnOK(), 825OnOrders», 968OnPrepareDCO, 841; 896OnPreparePrinting(), 886; 896OnPrint», 896OnProducts», 968PFX_(), 939PolyLine», 744Read», 131ReadKey», 131ReadLine», 131Rectangle», 744Remove», 1092RemoveAll», 772RemoveAt», 772; 1092RemoveHead», 772RemoveTail», 772ReportError(), 994RFX_(),984Rollback», 980Run», 673SelectObject», 720SelectStockObject», 723IsEOF», 964IsOnFirstRecord», 992LineTo», 744LPtoDP», 843main», 64; 72; 267; 1031max», 777min(), 777Move», 984; 1014MoveElement», 879MoveFirst», 1014MoveLast», 1014MovePrev», 1014MoveTo», 715OnActivateView», 964; 969OnBeginPrinting(), 896OnCancel», 825OnContextMenu», 799OnDraw», 714OnEndPrinting», 896 OnGetRecordset», 955; 1002OnlnitDialog», 838OnlnitialUpdate», 844; 968; 1001OnLButtonDown», 728OnLButtonUp», 728OnMouseMove», 728OnOK(), 825OnOrders», 968OnPrepareDCO, 841; 896OnPreparePrinting(), 886; 896OnPrint», 896OnProducts», 968PFX_(), 939PolyLine», 744Read», 131ReadKey», 131ReadLine», 131Rectangle», 744Remove», 1092RemoveAll», 772RemoveAt», 772; 1092RemoveHead», 772RemoveTail», 772ReportError(), 994RFX_(),984Rollback», 980Run», 673SelectObject», 720SelectStockObject», 723IsEmpty», 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
OnBeginPrinting(), 896
OnCancel», 825
OnContextMenu», 799
OnDraw», 714
OnEndPrinting», 896 OnGetRecordset», 955; 1002
OnlnitDialog», 838
OnlnitialUpdate», 844; 968; 1001
OnLButtonDown», 728
OnLButtonUp», 728
OnMouseMove», 728
OnOK(), 825
OnOrders», 968
OnPrepareDCO, 841; 896
OnPreparePrinting(), 886; 896
OnPrint», 896
OnProducts», 968
PFX_(), 939
PolyLine», 744
Read», 131
ReadKey», 131
ReadLine», 131
Rectangle», 744
Remove», 1092
RemoveAll», 772
RemoveAt», 772; 1092
RemoveHead», 772
RemoveTail», 772
ReportError(), 994
RFX_(),984
Rollback», 980
Run», 673
SelectObject», 720
SelectStockObject», 723
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
OnBeginPrinting(), 896
OnCancel», 825
OnContextMenu», 799
OnDraw», 714
OnEndPrinting», 896 OnGetRecordset», 955; 1002
OnlnitDialog», 838
OnlnitialUpdate», 844; 968; 1001
OnLButtonDown», 728
OnLButtonUp», 728
OnMouseMove», 728
OnOK(), 825
OnOrders», 968
OnPrepareDCO, 841; 896
OnPreparePrinting(), 886; 896
OnPrint», 896
OnProducts», 968
PFX_(), 939
PolyLine», 744
Read», 131
ReadKey», 131
ReadLine», 131
Rectangle», 744
Remove», 1092
RemoveAll», 772
RemoveAt», 772; 1092
RemoveHead», 772
RemoveTail», 772
ReportError(), 994
RFX_(),984
Rollback», 980
Run», 673
SelectObject», 720
SelectStockObject», 723
SELECTView(), 959
X
SelectView», 1008; 1015 SendToBack», 812
Serialize», 867; 868; 872; 876
SerializeElements», 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 ToString», 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
Context MenuStrip, 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 ЧАСА ЧЕТВЕРТОЕ ИЗДАНИЕ
Джесс ЛибертиДэвид Б. Хорват, ССРДжесс ЛибертиДэвид Б. Хорват, ССРДжесс Либерти, Дэвид Б. Хорват, ССРSAMS®gЧЕТВЕРТОЕ ИЗДАНИЕДжесс Либерти, Дэвид Б. Хорват, ССРSAMS®gЧЕТВЕРТОЕ ИЗДАНИЕwww.williamspublishing.comwww.williamspublishing.comЭта книга поможет самостоятельно изучить язык C++, его принципы и концепции. Здесь изложены фундаментальные основы программирования, описаны принципы управления вводом- выводом, циклы, массивы, объектно-ориентированные подходы, а также создание полнофункционального приложения. Все главы содержат листинги программ, результаты их выполнения и анализ кода. Приведены ответы на часто задаваемые вопросы, а также упражнения и контрольные вопросы. Изложение книги не предполагает наличия у читателя предварительных знаний в области C++, а четкая организация материала позволит быстро и просто изучить язык.
Джесс Либерти
Дэвид Б. Хорват, ССР
Джесс Либерти
Дэвид Б. Хорват, ССР
Джесс Либерти, Дэвид Б. Хорват, ССР
SAMS
®gЧЕТВЕРТОЕ ИЗДАНИЕ
Джесс Либерти, Дэвид Б. Хорват, ССР
SAMS
®gЧЕТВЕРТОЕ ИЗДАНИЕ
www.williamspublishing.com
www.williamspublishing.com
освой САМОСТОЯТЕЛЬНО C++ ЗА 21 ДЕНЬ, пятое издание
Джесс Либерти, Брэдли ДжонсДжесс Либерти, Брэдли Джонсwww.williamspublishing.comwww.williamspublishing.comКнига поможет самостоятельно изучить язык C++, его принципы и концепции. Здесь изложены фундаментальные основы программирования, управление вводом-выводом, циклы, массивы, объектно-ориентированные подходы, а также создание полнофункционального приложения. Все главы содержат листинги программ, результаты их выполнения и анализ кода. Приведены ответы на часто задаваемые вопросы, а также упражнения и контрольные вопросы. Изложение не предполагает наличия у читателя каких либо знаний в области C++, а четкая организация книги позволит быстро и просто изучить язык.
Джесс Либерти, Брэдли Джонс
Джесс Либерти, Брэдли Джонс
www.williamspublishing.com
www.williamspublishing.com
Герберт ШилдтГерберт Шилдтwww.williamspublishing.comwww.williamspublishing.com
Герберт Шилдт
Герберт Шилдт
www.williamspublishing.com
www.williamspublishing.com
C++: БАЗОВЫЙ КУРС третье издание
В этой книге описаны все основные средства языка C++: от элементарных понятий до супервозможностей.
После рассмотрения основ программирования на C++ (переменных, операторов, инструкций управления, функций, классов и объектов) читатель освоит такие более сложные средства языка, как механизм обработки исключительных ситуаций (исключений), шаблоны, пространства имен, динамическая идентификация типов, стандартная библиотека шаблонов (STL), а также познакомится с расширенным набором ключевых слов, используемым в программировании для .NET. Автор справочника — общепризнанный авторитет в области программирования на языках С и C++, Java и C# — включил в текст своей книги и советы программистам которые позволят повысить эффективность их работы.
язык
ПРОГРАММИРОВАНИЯ C++.
ВВОДНЫЙ КУРС
Стэнли Б. Липпман, Жози Лажойе, Барбара МуСтэнли Б. Липпман, Жози Лажойе, Барбара Муwww.williamspublishing.comwww.williamspublishing.comНынешнее издание столь популярного вводного курса стандартного языка C++ было полностью переписано так, чтобы помочь быстрее и эффективнее научиться программировать на этом языке. Теперь стандартная библиотека C++ описана с самого начала, что позволяет читателю сразу приступить к созданию работоспособных программ, еще до изучения подробностей языка. Здесь содержатся полезные советы, которые помогут облегчить создание программ, а также повысить их эффективность. Примеры, в которых используются возможности библиотек, позволяют продемонстрировать достоинства языка C++, а также наиболее эффективные приемы его применения. Как и в предыдущих изданиях, здесь обсуждаются фундаментальные концепции и методы языка C++, что делает книгу ценнейшим ресурсом даже для опытных программистов.
Стэнли Б. Липпман, Жози Лажойе, Барбара Му
Стэнли Б. Липпман, Жози Лажойе, Барбара Му
www.williamspublishing.com
www.williamspublishing.com
C++ ДЛЯ "ЧАЙНИКОВ"
5-Е ИЗДАНИЕ
С. ДэвисС. Дэвисwww.dialektika.comwww.dialektika.comКнига представляет собой введение в язык программирования C++. Основное отличие данной книги от предыдущих изданий C++ для “чайников” в том, что это издание не требует от читателя каких-либо дополнительных знаний, в то время как предыдущие издания опирались на знание читателем языка программирования С. Книга отличается также тем, что несмотря на простоту изложения материала, он подан достаточно строго. Поэтому, изучив основы программирования на C++, читателю не придется пересматривать свои знания при дальнейшем изучении языка.Книга отличается широким охватом тем — от простейших объявлений переменных до концепций объектно-ориентированного программирования, вопросов перегрузки операторов и множественного наследования, причем весь материал снабжен множеством практических примеров.
С. Дэвис
С. Дэвис
www.dialektika.com
www.dialektika.com
полный СПРАВОЧНИК по C++, 4-Е ИЗДАНИЕ
Герберт Шилдт
www.williamspublishing.comwww.williamspublishing.com
www.williamspublishing.com
www.williamspublishing.com
В четвертом издании "Полного справочника по C++" полностью описаны и проиллюстрированы все ключевые слова, функции, классы и свойства языка C++, соответствующие стандарту ANSI/ISO. Информацию, изложенную в книге, можно использовать во всех современных средах программирования.
Освещены все аспекты языка C++, включая его основу — язык С. Справочник состоит из пяти частей:
подмножество С;
язык C++;
библиотека стандартных функций;
библиотека стандартных классов;
приложения на языке C++. Для широкого круга программистов.
Стивен ПрагаСтивен Прагаwww.williamspublishing.comwww.williamspublishing.com
Стивен Прага
Стивен Прага
www.williamspublishing.com
www.williamspublishing.com
язык
ПРОГРАММИРОВАНИЯ C++
ЛЕКЦИИ И УПРАЖНЕНИЯ
5-е издание
Книгу отличает простой и доступный стиль изложения, изобилие примеров и множество рекомендаций по написанию высококачественных программ. Подробно рассматриваются такие вопросы, как представление данных, операции и операторы, управляющие структуры и функции. Немалое внимание уделяется работе с классами, шаблонами и пространствами имен, а также генерации и обработке исключений. Исчерпывающие сведения о концепциях объектно-ориентированного программирования дадут возможность максимально успешно и эффективно создавать живучий программный код. Приводимые в конце каждой главы вопросы и упражнения для самостоятельной проработки позволят надежно закрепить полученные знания. Книга рассчитана на программистов разной квалификации, а также будет полезна для студентов и преподавателей.
Николас А. Солтер Скотт Дж. КлеперНиколас А. Солтер Скотт Дж. Клеперwww.dialektika.comwww.dialektika.com
Николас А. Солтер Скотт Дж. Клепер
Николас А. Солтер Скотт Дж. Клепер
www.dialektika.com
www.dialektika.com
C++ ДЛЯ ПРОФЕССИОНАЛОВ
В этом практическом руководстве с большим количеством примеров представлены все грани разработки С++- приложений, включая этапы проектирования, тестирования и отладки. Здесь описаны простые, но мощные методы, используемые С++-профессионалами, малознакомые, но весьма полезные средства и многократно применяемые шаблоны проектирования.
В книге демонстрируются различные методики и хороший стиль программирования, а также предлагаются пути повышения качества кода и эффективности программирования в целом. Книга предназначена для программистов и разработчиков, которые хотят поднять свои навыки С++-программирования на профессиональный уровень.
Программистам от программистов
Айвор ХортонАйвор ХортонБазовыйкурсБазовыйкурс
Айвор Хортон
Айвор Хортон
Базовыйкурс
Базовыйкурс
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++ приложений для Microsoft Windows Предварительные навыки программирования не требуются
языки программирования
Visual C++ 2005
Издательство “Диалектика www.dialektika.com
p2p.wrox.com
Информационный центр для программистов
Категория
Предмет рассмотрения
wrox
www.wrox.com
II Функция для отображения объема 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 CCan: public CContainer {
public:
II Деструктор ~CCan()
{ cout « "Вызван деструктор CCan* " « endl; }
/ / Функция вычисления объема банки virtual double Volume() const
{ return 0.25*PI*m_Diameter*m_Diameter*m_Height; }
// Конструктор
CCan(double hv = 4.0, double dv = 2.0): m_Height(hv), m_Diameter(dv){}