Содержание

Продавай мобильный контент со своего сайта

Учебник по программированию в среде С++ Builder

Содержание

  • Введение. О чем эта книга
  • Часть I.
    Глава 1. Основные принципы и понятия
  • Глава 2. Знакомство с С++Builder
  • Глава 3. Обзор языка С
  • Глава 4. Препроцессор и особенности компилятора
  • Глава 5. Отладка программ
  • Часть II.
    Глава 6. Модифицированный С
  • Глава 7. Объектно-орентированное программирование
  • Глава 8. Классы С++
  • Глава 9. Потоки ввода-вывода
  • Глава 10. Шаблоны
  • Глава 11. Библиотека стандартных шаблонов
  • Глава 12. Управления исключениями
  • Глава 13. RTTI и приведение типов
  • Часть III.
     Глава 14. Введение в визуальное программирование
  • Глава 15. Создание визуальных компонентов
  • Приложение. Классы AnsiString и Set


Введение. О чем этот диск

Продавай мобильный контент со своего сайта

  • Введение. О чем этот диск
    • Организация диска

Введение. О чем этот диск

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

Последнее не обязательно означает, что вы являетесь профессионалом, пишущим приложения на продажу. Вы можете быть любителем, программирующим пока в свободное время ради собственного удовольствия или для решения каких-либо личных, домашних задач. Или, может быть, вы занимаетесь научно-техническими расчетами, обработкой данных и т. п. Конечно, часто для этого достаточно примитивной DOS и традиционных языков, таких, как Pascal или даже BASIC, однако обидно писать программы с минимальным пользовательским интерфейсом, ориентированным на командную строку, когда у вас есть 32-битная операционная система Windows и превосходные средства разработки приложений для нее, позволяющие создавать программы профессионального качества. А если вы уже программировали для Windows, то и сами прекрасно понимаете, зачем вам нужен объектно-ориентированный C++...

Однако даже этот мощный язык в своем голом виде, так, как он есть, мало подходит для создания приложений Windows. Интерфейс прикладного программирования этой системы насчитывает сотни функций, и даже самая элементарная программа с графическим интерфейсом пользователя состоит из нескольких сот строк кода на С или C++. Поэтому на практике программисту-одиночке или небольшой рабочей группе придется использовать какие-то более мощные средства, вроде визуальных систем программирования (Visual Basic, C++Builder, Delphi) или же универсальную среду разработки типа Borland C++ в сочетании с библиотекой классов, такой, как OWL или MFC.

Существуют неплохие книги, посвященные визуальным системам программирования и библиотекам классов, но собственно языку программирования, на которых они базируются, в этих книгах уделяется мало внимания, а иногда и вовсе не уделяется. А в книгах академического характера, более или менее полно и строго излагающих синтаксис и семантику языка, отсутствуют описания конкретных средств разработки программ и особенностей реализации тех или иных его аспектов. Такие книги могут быть очень полезны в условиях учебного заведения, где они будут дополняться практическими занятиями, но, как показывает опыт, самостоятельно изучить по ним язык почти невозможно. Ничто не может заменить живого опыта работы с языком, когда вы можете сразу запустить написанную программу, отладить ее и немного поэкспериментировать, внося изменения в исходный код. Поэтому мы и решили написать книгу, в которой по возможности полное описание языка C++ сопровождалось бы практическими рекомендациями и пояснениями, относящимися к конкретной среде разработки, а именно — C++Builder 5 (можно пользоваться и 4-й версией) фирмы Borland (ныне Inprise Corporation).

На одном диске нельзя, конечно, охватить всю тематику программирования для Windows в C++Builder, однако там, где это уместно, мы постараемся рассказать об основных принципах того или иного аспекта работы в этой среде. Как уже говорилось, главная цель книги — описать собственно язык и научить программировать на C++ в полном его объеме; все остальное направлено на то, чтобы дать представление о доступных вам возможностях создания развитых приложений Windows и о том, что для этого потребуется освоить.

И если говорить о создании программ с интерфейсом Windows, то мы, конечно, не обойдем вниманием визуальные средства программирования C++Builder. Они-то, собственно, и делают этот инструмент (как и его Pascal-аналог — Delphi) тем, что он есть — средством ускоренной разработки приложений. Rapid Application Development. Так как основным предметом книги является изложение языка программирования C++, мы не сможем подробно рассказать об использовании библиотеки визуальных компонентов (VCL), о написании оригинальных компонентов VCL или ActiveX и т. п. Ясно, что объектно-ориентированное и визуальное программирование — это разные вещи; и как нам кажется, очень важно, чтобы читатель четко понимал, что представляет собой визуальный подход к программированию с точки зрения его реализации на базе C++. Какие для этого требуются расширения языка, как организуется программа в целом, каковы особенности потока управления... короче — “как это все устроено”. Кстати, это касается, на другом уровне, и реализации самого языка программирования; настоящий программист не только формальным образом “знает язык”, но и представляет себе, как он устроен. Как, скажем, в C++ реализуется механизм виртуальных функций или как работает стек при вызове процедуры. В общем, мы постараемся при обсуждении визуального программирования не упускать из виду именно подобные моменты.

Организация диска

Первая часть диска обсуждает основные особенности среды C++Builder и фундаментальные принципы программирования на языке С. Далее следует описание более сложных аспектов этого языка, включая расширения, характерные для C++Builder. Вторая часть рассказывает о языке C++ и объектно-ориентированном программировании.

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

Описания синтаксических конструкций обязательно сопровождаются фрагментами программного кода, иллюстрирующими и разъясняющими обсуждаемые концепции; часто от короткого примера бывает больше пользы, чем от пространных теоретических объяснений. В разделах, посвященных визуальному программированию, приводятся примеры законченных программ, чтобы дать читателю представление о всем процессе создания приложений в C++Builder.

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

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


Часть I. Язык С

Продавай мобильный контент со своего сайта

  • Часть I. Язык С
    • Глава 1. Основные принципы и понятия
      • Немного теории и терминологии
      • Процесс построения программы
        • Проблема раздельной компиляции
        • О библиотеках

Часть I. Язык С

  • Основные принципы и понятия
  • Знакомство с C++Builder
  • Обзор языка С
  • Препроцессор и особенности компилятора
  • Отладка программ

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

После этого мы перейдем к описанию языка С, послужившего основой для разработки C++. Знание непосредственного предшественника языка, изучению которого посвящена книга, представляется необходимым. Освоение С, а затем C++ — естественный путь обучения, хотя, конечно, возможны и другие подходы к этому вопросу. Например, можно было бы начать сразу с классов и объектов, но такая методика потребовала бы как от автора, так и от читателя (который, как предполагается, имеет некоторое представление о традиционном программировании) изменения “интуитивного подхода к окружающему миру”, если выражаться несколько высокопарно. Короче, переход на объектно-ориентированную технологию программирования — процесс непростой, и мы решили пойти традиционным путем, представив сначала краткий курс по языку С; и только потом, в части II, мы займемся собственно программированием на C++.

Естественно, тем читателям, кто хорошо знает С, достаточно будет только просмотреть эти главы книги, хотя совсем игнорировать их мы не рекомендуем.

Глава 1. Основные принципы и понятия

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

Немного теории и терминологии

Что собой представляет персональный компьютер? Если говорить о его устройстве, то в нем имеется центральное процессорное устройство (CPU), процессор плавающей арифметики, оперативная память (RAM), кэш-память, постоянная память (ROM) с зашитыми в ней подпрограммами BIOS, контроллеры внешних устройств (дисков, клавиатуры, дисплея и т.д.)... В общем, это весьма сложная система, которая, по-видимому, должна производить обработку данных.

С точки зрения программиста все выглядит несколько проще. Программисту нет дела, например, до кэш-памяти — это “прозрачный” буфер между процессором и основной памятью. Процессорное устройство — это просто набор регистров, содержимое которых интерпретируется и изменяется (в частности, вследствие обмена данными с памятью) на каждом шаге того процесса, который называют исполнением программы. Отдельный шаг — это исполнение машинной инструкции. Инструкции по очереди извлекаются из памяти машины.

Примечание

Может быть, не все знают, что современные компьютеры, если рассматривать их абстрактно, являются так называемыми машинами фон Неймана. Это означает, что программа, т.е. машинные инструкции, и данные, которые машина должна обрабатывать (числа, текст и т.п.), физически никак не отличаются друг от друга и хранятся в одной и той же оперативной памяти. В каком качестве — инструкций или данных — будет интерпретироваться та или иная информация, зависит от контекста, наличного в данный момент времени. Пока программа обрабатывается компилятором, сохраняется на диске или загружается операционной системой в память, она — “данные”. Только когда программа загружена и операционная система передает управление на ее входную точку, она начинает интерпретироваться как собственно программа, состоящая из инструкций.

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

Нам, как программистам, пишущим на C++, нужно очень хорошо представлять себе логическую организацию памяти компьютера, поскольку в этом языке чрезвычайно большую роль играют указатели и ссылки, а также динамическое распределение памяти. Логически память представляет собой непрерывную последовательность 8-битных байтов, идентифицируемых своими адресами. Если некоторый объект (в широком смысле слова — число, строка, структура и т.д.) занимает несколько байтов, то его адресом будет являться адрес его младшего (начального) байта. Адрес на машинах класса Pentium — это, по существу, 32-битное целое число без знака. Указатель (или ссылка) фактически является переменной, которая содержит целое число, представляющее адрес другого объекта.

Примечание

Важнейшей технической характеристикой компьютера является его разрядность. Разрядность определяется размером регистров процессора; это — размер порции данных, которую процессор способен обработать за один раз, при исполнении одиночной инструкции. Мы с вами будем говорить о программировании 32-битных машин семейства Pentium в той мере, в какой это, будет относиться к конкретным программам, создаваемым с помощью C++Builder; что касается стандартов языка C++, не имеет значения, на какой машине вы работаете.

На практике прикладному программисту не приходится иметь дела с машинными инструкциями и прочими “низменными материями” — он пишет программу на языке высокого уровня, таком, как C++ или Pascal. Заботу об управлении внешними устройствами берет на себя операционная система, оснащенная соответствующими драйверами, в нашем случае — 32-битная система Windows (NT, 95, 98, 2000). Любое устройство графического отображения информации (например, дисплей) представляется контекстом устройства (DC) системы Windows. Действия пользователя (нажатие клавиш, манипуляции мышью) преобразуются системой в события, или сообщения, получаемые прикладной программой. Кстати, Windows и называется системой, управляемой событиями.

Ну вот, мы, кажется, закончили с изложением необходимых сведений об организации персонального компьютера. Этот коротенький параграф может вызвать у читателя недоумение. Мы ввели ряд терминов, по существу никак их не определяя. Честно говоря, все написанное выше предназначено только для того, чтобы просто ввести в обиход некоторые важные слова; что они означают, будет выясняться впоследствии. А теперь мы поговорим о том, как происходит преобразование кода на языке высокого уровня, написанного прикладным программистом, в инструкции процессора, в машинный код, сохраняемый в ехе-файлах системы Windows.

Процесс построения программы

В этом разделе мы опишем “классический” процесс подготовки и трансляции программы на языке высокого уровня (в нашем случае это C++') в исполняемый файл, содержащий машинные инструкции и все остальное, что необходимо для работающей программы системы Windows. В C++Builder, как мы увидим в дальнейшем, детали этого процесса в основном скрыты от программиста и, кроме того, имеются дополнительные моменты, обусловленные спецификой визуального подхода к программированию. Создание программы на языке C++ выглядит примерно так. Прежде всего, программист с помощью того или иного текстового редактора готовит файлы исходного кода на C/C++. После этого происходит построение программы, в котором можно выделить такие этапы:

  • Компиляцию исходных файлов в файлы объектного кода (с расширением .obj).
  • Компоновку объектных файлов с присоединением необходимых библиотек (в том числе, возможно, динамических), в результате чего получается уже машинный код.
  • Компоновку ресурсов (ресурсы включают в себя битовые матрицы, курсоры, строковые таблицы, пиктограммы и т.п.). Это завершающий этап, на котором формируется конечный ехе-файл, запускаемый на выполнение. Этот процесс иллюстрируется рис. 1.1.

Рис. 1.1 Упрощенная схема построения программы

1. Source1.cpp

2. Source2.cpp

3. Source3.cpp

4. Компилятор

5. Source1.obj

6. Source2.obj

7. Source3.obj

8. Addon.lib

9. Код запуска

10. Исполнительная библиотека

11. Ресурсы App.res

12. Компоновщик

13. Компоновщик ресурсов

14. Приложение App.exe

Некоторые системы (и в том числе C++Builder) сразу выполняют компоновку объектных файлов с ресурсами, совмещая два последних этапа. Что касается первого шага — компиляции — то здесь возникает одна проблема, которую стоит обсудить.

Проблема раздельной компиляции

Когда-то давно программа для машины вроде БЭСМ-4, написанная на языке Алгол-60 или FORTRAN, состояла из одного-единственного файла, а точнее, являлась просто одной “колодой” перфокарт. Также и первые версии языка Pascal для PC (например, Turbo Pascal 2) допускали, по существу, компиляцию только программ, состоящих из единственного исходного файла. Компилятор “видел” сразу весь текст программы и мог непосредственно проконтролировать, например, количество и тип фактических параметров в вызове процедуры, — соответствуют ли они тем, что указаны в заголовке ее определения (формальным параметрам). Компилятор транслировал программу сразу в машинный код (исполняемый файл).

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

Примечание

Компилятор C/C++ генерирует стандартные объектные файлы с расширением .obj. (Их формат определен фирмой Intel и не зависит от конкретной операционной системы.) Файлы эти содержат машинный код, который снабжен дополнительной информацией, позволяющий компоновщику разрешать ссылки между объектными модулями. Так, в начале файла формируются две таблицы: таблица глобальных символов (это имена объектов, определяемых в данном файле, на которые могут ссылаться другие модули программы) и таблица внешних ссылок (имена объектов в других файлах, к которым обращается данный модуль). Пользуясь информацией этих таблиц, компоновщик модифицирует код, подставляя в него соответствующие адреса.

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

Примечание

В языке Turbo Pascal (и позднее — в Delphi) эта проблема была решена благодаря определению специального формата промежуточных файлов. Эти “объектные” файлы (в Delphi они имеют расширение ,dcu) содержат, помимо всего прочего, информацию о параметрах процедур и функций, об определяемых в модуле типах данных (классах) и т.д.

C/C++ был задуман как максимально универсальный язык, и потому он не может использовать нестандартный формат объектных файлов. Это сделало бы невозможным, например, создание файлов подпрограмм, которые могли бы включаться в программы, написанные на других языках. Возможность раздельной компиляции обеспечивается в C/C++ применением заголовочных файлов, присоединяемых к компилируемым исходным файлам на этапе препроцессорной обработки. Заголовки содержат прототипы функций, объявления типов и другую информацию, необходимую для раздельной компиляции исходных модулей программы.

Заголовочные файлы (они имеют расширение .h или .hpp) подключаются к компилируемому файлу исходного кода (.с или .срр) с помощью директивы препроцессора #include, за которой следует имя заголовочного файла в кавычках или угловых скобках, например:

#include <stdlib.h>

#include "myfile.h"

Препроцессор заменяет директиву #include содержимым указанного файла; после завершения препроцессорной обработки полученный текст передается компилятору, который транслирует его в объектный код.

Примечание

Во многих системах программирования на C/C++ препроцессор составляет единое целое с компилятором (это верно и для C++Builder). Тем самым ускоряется компиляция исходных файлов, и нет необходимости создавать промежуточный файл, содержащий обработанный препроцессором код. Однако в C++Builder имеется отдельный препроцессор срр32.ехе, запускаемый из командной строки. Вы можете им воспользоваться, если нужно просмотреть текст, получаемый в результате препроцессорной обработки; это может быть полезно, например, при отладке макросов препроцессора. Директивы препроцессора будут подробно рассматриваться в 4-й главе.

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

О библиотеках

Выше мы уже упоминали о библиотеках, не объясняя, впрочем, что они собой представляют. Конкретнее, мы имели в виду библиотеки объектных модулей; такая библиотека является просто собранием модулей объектного кода, скомпонованных в одном файле с расширением lib, своего рода архивом. На рис. 1.1 показана гипотетическая библиотека пользователя Addon.lib. Она могла бы быть создана из нескольких объектных модулей путем обработки их библиотекарем tlib32.exe.

Примечание

На рисунке показаны также код запуска и исполнительная библиотека языка C/C++. Это необходимые элементы компоновки любой программы на С. Код запуска исполняется перед тем, как управление будет передано на входную точку вашей программы (функцию main, WinMain и т. п.). Среди задач, им выполняемых, можно указать следующие:

  • инициализация исполнительной библиотеки

  • конструирование глобальных объектов C++

  • завершение программы при отсутствии необходимых ресурсов.

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

  • управление файлами

  • управление памятью

  • преобразование данных и многое другое.

Что касается динамически присоединяемых библиотек — DLL, — то во многих случаях они, с точки зрения программиста, мало чем отличаются от обычных, статически компонуемых библиотек объектных модулей. Функция, которая находится в DLL, вызывается так же, как и всякая другая функция. Правда, “снаружи” динамическая библиотека выглядит скорее как исполняемый файл; эти библиотеки создаются не библиотекарем, а компоновщиком. Вообще-то вопросы разработки и использования DLL выходят за рамки этой книги, но в следующей главе мы приведем пример простейшей DLL, чтобы читатель получил представление о том, как это делается в C++Builder.

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

Заключение

В этой очень короткой главе мы попытались обрисовать “классический” процесс создания прикладной программы на языке C/C++. По сути наше изложение было ориентировано на то, чтобы дать читателю представление о терминологии, которую мы будем постоянно применять, и создать тем самым некоторый первоначальный контекст, для дальнейшего обсуждения. То, что осталось непонятным, будет проясняться в дальнейшем.


Глава 2. Знакомство с C++Builder

Продавай мобильный контент со своего сайта

  • Глава 2. Знакомство с C++Builder
    • Интегрированная среда разработки C++Builder
      • Редактор кода
    • Hello World
      • Hello World — консольное приложение
      • Hello World — приложение GUI
    • Пример DLL

Глава 2. Знакомство с C++Builder

В этой главе мы познакомимся с интегрированной средой разработки C++Builder и посмотрим, как с ее помощью выполняются некоторые простейшие задачи.

Интегрированная среда разработки C++Builder

На рис. 2.1 показан C++Builder сразу после запуска. То, что вы видите — это интегрированная среда разработки (IDE), включающая в себя четыре основных элемента. Наверху находится главное окно. Оно содержит обычную линейку меню, инструментальную панель (слева) и палитру компонентов (многостраничная панель справа).

Рис. 2.1 C++Builder 5 с пустой формой

Правее инспектора объектов располагается конструктор форм. При запуске C++Builder конструктор отображает пустую форму. Форма — это центральный элемент визуального программирования. Она может представлять главное окно программы, дочернее окно, диалоговую панель. На ней вы размещаете различные элементы управления (типичный и самый распространенный — командная кнопка), называемые визуальными компонентами. Существуют также и невизуальные компоненты, например, таймеры и компоненты связи с базами данных. В инспекторе объектов вы сопоставляете событиям компонентов написанные вами процедуры обработки. Это, по существу, и есть визуальное программирование, базирующееся на компонентной модели. Подробнее мы расскажем о нем в части III этой книги.

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

Редактор кода

Окно редактора кода, показанное на рис. 2.2, является основным рабочим инструментом программиста. Его функции не ограничиваются редактированием исходного текста программы.

Нужно сказать, что почти все инструментальные окна C++Builder являются стыкуемыми окнами. Вы, наверное, не раз встречались в различных программах со стыкуемыми инструментальными панелями. Их масса в Microsoft Word, в котором я пишу этот текст. Такая панель может быть плавающей, а может быть состыкована с главным окном программы. В этом случае она становится обычной инструментальной линейкой.

При первоначальном запуске C++Builder к левой стороне редактора кода пристыковано окно обозревателя классов. Это не просто инструмент просмотра структуры классов. При некотором навыке можно использовать его с большой выгодой, поскольку он позволяет автоматически вводить в описание класса новые элементы (функции, данные и свойства).

Рис. 2.2 Окно редактора кода с обозревателем классов

Инструментальное окно C++Builder может быть состыковано с другим окном в одном из пяти его портов стыковки: либо вдоль какой-либо стороны окна, либо по центру. Если стыковка производится вдоль стороны окна, получается что-нибудь подобное показанному на рис. 2.2. В случае стыковки по центру окно становится многостраничным, с закладками, позволяющими переключаться между страницами.

Советую вам поэкспериментировать со стыковкой окон. Сделайте, например, такое упражнение:

  • Выберите в меню View Project Manager. Появится окно менеджера проектов, который служит в основном для управления исходными файлами разрабатываемых приложений. Вызовите контекстное меню окна (щелкнув правой кнопкой мыши) и убедитесь, что его пункт Dockable помечен.
  • Захватите мышью заголовок менеджера проектов и передвиньте курсор на окно обозревателя классов (которое само состыковано с редактором кода). При перемещении окно будет показано серым контуром. Когда курсор окажется в центре окна обозревателя классов, серый контур будет “захвачен” в центральном порте стыковки, не касаясь сторон окна. (Когда курсор пересекает границу окна, серый контур будет захватываться в порте стыковки вдоль этой границы).
  • Отпустите кнопку мыши. Окно обозревателя классов станет двухстраничным окном с закладками Project Manager и Class Explorer.
  • Поместите курсор на захват (двойную полоску в верхней части окна) и “вытащите” инструментальное окно из окна редактора. В результате у вас получится плавающее двухстраничное окно, в котором совмещаются менеджер проектов и обозреватель классов.
  • Снова состыкуйте комбинированное окно с окном редактора кода вдоль левой стороны последнего.

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

Рис. 2.3 показывает окно редактора кода с двумя открытыми файлами и пятью пристыкованными инструментальными окнами.

Может показаться, что все эти экзерсисы со стыковкой не более чем игра. Отнюдь нет. В процессе работы над программой, особенно во время отладки, вам понадобятся три-четыре вспомогательных окна, а может быть, и больше. Это небольшие окна, и они легко могут потеряться за более крупными. Если же все необходимые окна будут сосредоточены в одном месте, получить доступ к нужному инструменту очень просто. Любой из них находится, как говорят, “на расстоянии двух щелчков мыши” от вас.

Примечание

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

Поэкспериментируйте с различными контекстными меню; посмотрите, какие имеются пункты в контекстном меню редактора. Многое понятно и без всяких объяснений, а если непонятно, вы можете при открытом меню нажать F1 для вызова справки C++Builder. Может быть, станет понятнее.

Рис. 2.3 Редактор кода с пристыкованными окнами различных инструментов

Ну а теперь лучше всего перейти, наконец, прямо к делу и написать пару работающих программ на С.

Hello World

По традиции изучение любого языка программирования начинается с того, что новичку предлагают написать программу, которая выводит на экран слова “Hello World”. Мы не будем отступать от этой традиции и покажем, как реализовать такую программу в C++Builder. Мы составим даже две программы; одну — с выводом на консоль, а другую — с графическим интерфейсом пользователя (GUI), т. е. настоящее приложение Windows.

Hello World — консольное приложение

Консольное приложение Windows похоже на программу DOS, но только внешне. Оно работает в “окне MS-DOS”, которое на самом деле в 32-битных системах Windows таковым не является. Консольное приложение — 32-битное, и запустить его в обычной 16-битной DOS невозможно. Однако, подобно примитивной программе DOS, оно ориентировано на символьный ввод-вывод, что делает консольные приложения полезными при изучении стандартных функций ввода-вывода языка С и классов стандартных потоков C++.

Чтобы создать в C++Builder консольное приложение, выполните следующие действия:

  • Выберите в главном меню File | New...; появится многостраничная диалоговая панель New Items (рис. 2.4). Этот диалог является интерфейсом так называемого хранилища объектов C++Builder (Object Repository). Помимо уже имеющихся объектов вы можете сохранять в нем свои собственные формы, диалоговые панели и прочие элементы, пригодные для повторного использования в новых программах.
  • На странице New выберите Console Wizard и нажмите кнопку ОК. Если у вас 4-я версия компилятора, в появившейся панели “консольного мастера” оставьте установки по умолчанию и нажмите кнопку Finish. Если же вы работаете с C++Builder 5, установите состояние флажков в соответствии с рис. 2.8 (внизу). C++Builder создаст проект консольного приложения и откроет окно редактора кода с файлом Project1.cpp или Unit1.cpp (Unit1.c) в зависимости от версии.

Рис. 2.4 Диалог New Items

  • Выберите в главном меню File | Save Project as... и сохраните файлы проекта в отдельном каталоге (папке). Назовите проект и модуль, например, HWConsole — необходимые расширения файлов C++Builder добавит автоматически. Сейчас ваш исходный файл имеет такой вид:

#pragma hdrstop

#include <condefs.h>

//--------------------------

#pragma argsused

int main(int argc, char* argv[ ])

{

return 0;

}

  • Строчку #include <condefs.h> вставляет только C++Builder 4-й версии. Для C++Builder 5 она не нужна. В дальнейшем не обращайте на нее внимания, если она будет попадаться в листингах. Модифицируйте код файла HWConsole. срр так, чтобы он выглядел следующим образом (строчки, которые требуется ввести, выделены жирным шрифтом):

#pragma hdrstop

#include <stdio.h>

#include <conio.h>

#include <condefs.h>

//------------------------------------------

#pragma argsused int main(int argc, char* argv[])

{

printf("Hello World from Console!\n"), printf("Press any key...");

getch() ;

return 0;

}

  • Сохраните файл (File Save в главном меню) и нажмите кнопку Run на инструментальной панели (кнопка с зеленой стрелкой). C++Builder компилирует исходный файл, произведет компоновку исполняемого файла и запустит программу. На экране должно появиться окно MS-DOS, показанное на рис. 2.5. Если вы нажмете любую клавишу на клавиатуре, программа завершится и ее окно закроется.
  • Закройте проект (File | Close All в главном меню).

Вот и все — консольное приложение готово и работает. Для этого вам потребовалось ввести вручную пять строк кода на С. Первые две из них — директивы ^include, включающие в исходный код два заголовочных файла стандартной библиотеки. Файл stdio.h содержит прототипы общеупотребительных функций буферизованного ввода-вывода (мы здесь использовали функцию prinf() — форматируемый вывод на консоль). Файл соnio.h необходим потому, что для ожидания нажатия клавиши мы применили низкоуровневую функцию ввода символа getch() ; символ, возвращаемый функцией, мы игнорировали.

Рис. 2.5 Окно программы HWConsole.exe

Функция main () присутствует (иногда неявно) в каждой программе C/C++ и является ее входной точкой. Именно этой функции передается управление после загрузки и инициализации программы.

Директивы препроцессора #pragma мы будем обсуждать в 4-й главе. Пока скажем, что они здесь не имеют принципиального значения и их можно было бы удалить. Но это привело бы к выдаче предупреждений при компиляции, а в некоторых случаях замедлило ее.

Строки, начинающиеся с двойной дробной черты, содержат комментарии и не принимаются во внимание при компиляции файла.

Примечание

Написанная программа является по сути программой на языке С. В 5-й версии компилятора, если вы хотите написать “честную” программу на С и компилировать ее как такую, проблем никаких — исходный язык задается прямо в панели консольного мастера. В 4-й версии дела обстоят несколько сложнее. Дело в том, что в C++Builder 4 главный исходный файл консольного приложения должен обязательно быть срр-файлом и определять функцию main. Если же вы хотите использовать в качестве главного файла программы именно с-файл, придется пойти на некоторые ухищрения. Сделайте следующее:

  • Создайте с помощью Console Wizard новый проект.
  • Удалите из исходного файла функцию main() и добавьте директиву ^define main, чтобы файл принял такой вид:

#pragma hdrstop

#include <condefs.h>

#define main
  • Выберите в главном меню File [ New... (или нажмите кнопку New — чистый листок — на инструментальной панели); появится диалог New Items.
  • Выберите на странице New значок Unit; к проекту будет добавлен новый модуль исходного кода с именем Unit1 .cpp. (Вы можете открыть менеджер проекта, чтобы видеть все изменения в составе исходных файлов, — для этого нужно выбрать в меню View | Project Manager.) Удалите из файла все, кроме директивы hdrstop, и введите код программы:

//---------------------------------

#pragma hdrstop

#include <stdio.h>

#include <c6nio.h>

//----------------------------

main ()

{

printf("Hello World - С main file.\n");

getch();

return 0;

}

  • Сохраните файл под каким-нибудь другим именем, явно указав расширение .с в диалоге Save As...
  • Компилируйте и запустите программу, нажав на кнопку Run инструментальной панели.

Еще раз подчеркнем, что сказанное относится только к 4-й версии. C++Builder 5 проделывает практически то же самое, но автоматически.

Теперь мы покажем, как вывести на экран сакраментальное “Hello World”, используя графический интерфейс системы Windows. Другими словами, мы применим технологию визуального программирования, реализованную в C++Builder.

Hello World — приложение GUI

Процедура создания приложения с графическим интерфейсом пользователя даже проще, чем в случае консольного приложения, поскольку C++Builder предназначен именно для этого. Выполните такие действия:

  • Создайте новый проект, выбрав в меню File | New Application (это эквивалентно выбору значка Application в диалоге New Items). При этом будет открыт конструктор форм с новой, пустой формой (см. рис. 2.1). Инспектор объектов будет показывать ее свойства.
  • Выберите в левой колонке страницы Properties свойство Caption. Замените его значение (в правой колонке) на “Hello World”. Обратите внимание, что этот текст сразу же появляется в строке заголовка формы.

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

  • Откройте в палитре компонентов главного окна C++Builder страницу Standard (она открыта по умолчанию) и выберите в ней компонент Button (значок, изображающий командную кнопку). После этого щелкните кнопкой мыши в том месте формы, где должен находиться верхний левый угол компонента. Кнопка будет помещена на форму; вокруг нее будут находиться черные маркеры, которые позволяют изменять размер кнопки с помощью мыши и, кроме того, служат признаком активного (выбранного) компонента формы. Перемещать компонент по форме можно, поместив курсор внутрь компонента.

Вы наверняка заметили, что при перемещении компонента или изменении размеров положение его фиксируется узлами сетки, показанной на форме маленькими точками. Шаг сетки можно изменять или вообще отменить .привязку к сетке.

  • В инспекторе объектов измените свойство Caption кнопки на “Message”, аналогично тому, как это было сделано для самой формы. Введенный текст задает надпись на кнопке. (По умолчанию это Buttonl.)
  • Разместите на форме вторую кнопку с надписью “Exit”. (Руководствуйтесь рис. 2.6.)

Рис. 2.6 Форма программы в процессе проектирования

  • Выберите в палитре компонентов значок Label (буква А) и поместите компонент метки на форму ниже командных кнопок. Выберите в инспекторе объектов свойство Font. Если нажать маленькую кнопку с многоточием в правой колонке инспектора, появится обычный диалог выбора шрифта, в котором можно задать желаемую гарнитуру и размер текста (рис. 2.6). Кнопка с многоточием в колонке значений — обычный признак того, что для свойства предусмотрен специализированный редактор.
  • Удалите из свойства Caption весь текст (“ Label 1”).

К данному моменту мы практически завершили то, что называют этапом визуального проектирования программы. Теперь нужно написать программный код, который будет решать требуемую задачу, в данном случае — вывод (по команде) на экран строки текста “Hello World”.

  • Выберите на форме первую из кнопок и перейдите в инспекторе объектов на страницу событий (Events).
  • Дважды нажмите кнопку мыши на событии OnClick; C++Builder создаст заготовку процедуры обработки ButtonlClick() и установит курсор редактора кода внутри нее. Введите строку кода, чтобы функция выглядела так:

void _fastcall TFormI::ButtonlClick(TObject *Sender) {

Labell->Caption = "Hello World from GUI!";

}

  • Точно так же создайте процедуру обработки события для второй командной кнопки:
    •

void _fastcall TFormI::Button2Click(TObject *Sender) {

Forml->Close () ;

}

  • При желании измените размер формы (как это делается с обычным окном) и сохраните файлы проекта, дав исходному модулю имя, например, HWGuiU, а исходному файлу проекта — HWGui.
  • Осталось только компилировать и запустить программу. Нажмите на инструментальной панели C++Builder кнопку с зеленой стрелкой (Run). Если при компиляции возникают ошибки, значит вы, скорее всего, сделали где-то опечатку; другие проблемы маловероятны.
    •

Вот и все. Нажмите кнопку Message. Будет выведена строка сообщения. Кнопка Exit завершает работу программы, закрывая ее главное окно (его можно закрыть и системной кнопкой в правом верхнем углу — это то же самое). Ниже показана запущенная программа.

Рис. 2.7 Работающая программа

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

Те строки кода, что мы ввели в функции OnClick () для обеих командных кнопок, достаточно понятны. Первая из них присваивает свойству Caption расположенной на форме метки требуемую символьную строку. Вторая просто вызывает функцию закрытия формы Close (). Детали синтаксиса этого кода будут проясняться по ходу изучения языка.

Примечание

Мы реализовали приложение с графическим интерфейсом пользователя, применив визуальные возможности C++Builder; однако можно построить и стандартное оконное приложение, так сказать, на пустом месте, ориентированное на интерфейс прикладного программирования (API) Windows. Для этого нужно воспользоваться тем же мастером Console Wizard, с помощью которого мы создавали заготовку консольного приложения:

  • Выберите File | New... и запустите Console Wizard. Если у вас C++Builder 4, вы берите радиокнопку Window (GUI) и нажмите кнопку Finish (рис. 2.8, вверху) Если вы работаете с 5-й версией, сбросьте все флажки в правой части панели консольного мастера, показанной на рис. 2.8 внизу.

Рис. 2.8 Console Wizard 4-й версии с установками для оконного приложения и 5-й версии для консольного приложения на С

  • В открывшемся окне редактора кода введите единственный оператор (он рас положен в листинге на двух строчках), чтобы файл принял следующий вид:
    •

#include <windows.h>

#pragma hdrstop

#include <condefs.h>

//--------------------------------

#pragma argsused

WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {

MessageBox(NULL, "Hello World from Windows!",

"Simple Greeting", MB_OK | MB_ICONEXCLAMATION);

return 0;

}

  • Сохраните проект, дав ему какое-нибудь новое имя, затем компилируйте и запустите программу, нажав кнопку Run инструментальной панели. На экране появится стандартная панель сообщения (рис. 2.9).

Рис. 2.9 Панель сообщения, выведенная функцией MessageBox

Примечание

Эта панель отображается функцией API MessageBox (), аргументы которой задают текст сообщения, заголовок панели и ее вид (тип кнопок и значок). Главной функцией программы Windows является, как видите, не main (), а WinMain () .

Приведенные примеры показывают, каким образом в C++Builder можно экспериментировать с различными языковыми конструкциями, которые мы будем изучать. Можно написать “честное” (консольное) приложение C/C++, пользуясь в качестве интерактивных средств функциями ввода с клавиатуры и символьного вывода в окно MS-DOS. Либо с тем же (и даже большим) успехом можно воспользоваться методикой визуального программирования и общаться с тестовым приложением в рамках GUI Windows. Правда, получаемые таким способом программы не являются в строгом смысле программами на C++, поскольку язык визуально-компонентной модели содержит некоторые расширения; однако в плане изучения стандартных элементов языка это несущественно. Существенно то, что стандарт ANSI C++ поддерживается в полном объеме.

Напоследок мы продемонстрируем пример группы проектов C++Builder, т. е. совместного создания нескольких целевых модулей. Мы построим простейшую DLL и тестирующее ее приложение.

Пример DLL

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

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

  • Откройте новый проект приложения (File New Application в главном меню). Сохраните его, дав исходному модулю имя HWD11U, а исходному файлу проекта — HWD11.

Примечание

Как я уже, кажется, говорил, желательно сохранять файлы каждого проекта в отдельном пустом каталоге.
  • Поместите на форму две командных кнопки, назвав первую Call DLL, a вторую — Exit.
  • Создайте (с помощью инспектора объектов) процедуру обработки события OnClick для первой кнопки и введите в нее строку: SayHelloO ;
  • Вторая кнопка должна закрывать программу; проделайте для нее все то же самое, что и в примере предыдущего раздела.
  • В верхней части файла, после всех директив #include, добавьте такую строчку: void export SayHello(void);

Файл теперь должен иметь такой вид:

#include <vcl.h> ftpragma hdrstop

#include "HWDllU.h"

void export SayHello(void);

//--------------------------------

#pragma package(smart_init) #pragma resource "*.dfm" TFormI *Forml;

//---------------------------------------

_fastcall TFormI::TFormI(TComponent* Owner)

: TForm(Owner) {

}

//------------------------------------

void_fastcall TFormI::ButtonlClick(TObject *Sender) {

SayHello() ;

} //------------------------------------

void_fastcall TFormI::Button2Click(TObject *Sender)

forml->Close() ;

}

  • С нашей главной программой пока все. Теперь откройте менеджер проектов (View | Project Manager). Наверху показан узел группы проектов с именем по умолчанию ProjectGroupl. Эта группа включает в себя пока всего один проект (HWDU.exe). Щелкните правой кнопкой мыши на узле группы и в контекстном меню выберите Add New Project...
  • Появится знакомый вам диалог New Items; выберите в нем значок DLL (4-я версия) или DLL Wizard (версия 5). В группе проектов появится новый узел, и для него будет открыто новое окно в редакторе кода. Введите код функции SayHelloO и добавьте в верхней части директиву f include, чтобы файл принял следующий вид (комментарии я удалил):
    •

#include <vcl.h>

#include <windows.h>

#pragma hdrstop //----------------------------------

void _export SayHello(void)

{

MessageBox(NULL, "Hello World from DLL!",

"Simple Greeting", MB__OK | MB_ICONEXCLAMATION);

}

Lnt WINAPI DilEntryPoint(HINSTANCE hinst,

unsigned long reason, void*) {

return 1;

}

  • Сохраните проект DLL под именем SHello и постройте библиотеку, для чего вызовите для узла SHello. dll контекстное меню и выберите в нем Make. C++Builder компилирует исходный файл и создаст файл DLL; при этом он также генерирует (по умолчанию) библиотеку импорта SHello lib, которая требуется для компоновки исполняемого файла нашей программы.
    •

Примечание

Рядом с пунктом контекстного меню Make (собрать) имеется пункт Build (построить). Если вы откроете Project в главном меню, то увидите там эквивалентные пункты Make SHello и Build SHello. Обе команды выполняют построение текущего проекта (его целевого файла). Разница в том, что Build компилирует и компонует заново все входящие в проект файлы, в то время как Make производит те или иные действия лишь при необходимости, например, при модификации кода в одном исходном файле будет компилироваться только этот файл, после чего будет выполнена компоновка программы.
  • Теперь мы вернемся к проекту главной программы. (В верхней части менеджера проектов имеется выпадающий список; выберите в нем HWDlI.exe.) К нему следует присоединить библиотеку импорта уже построенной DLL, чтобы компоновщик мог разрешить ссылку на функцию SayHelLo () , вызываемую из главной программы (HWDlI.exe), но физически находящуюся в отдельно загружаемом файле (SHello.dll). Для этого вызовите контекстное меню ехе-узла и выберите в нем пункт Add. Появится стандартный диалог открытия файла.
  • В выпадающем списке типов файлов выберите Library file (.lib). Вы должны увидеть библиотеку SHello.lib; выберите ее и закройте диалог. Библиотека будет присоединена к проекту HWDll.exe (рис. 2.10). Со храните группу проектов, выбрав в ее контекстном меню Save Project Group As...

Рис. 2.10 Менеджер проектов, показывающий Структуру группы

  • Теперь осталось только компилировать и запустить наше главное приложение. Для этого просто нажмите кнопку Run инструментальной панели. Когда появится окно программы, нажмите кнопку Call DLL. Будет вызвана функция из DLL SayHello(), которая отобразит панель сообщения (рис. 2.11).

Рис. 2.11 Вызов функции DLL

Примечание

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

Возможна и динамическая загрузка DLL, которая, однако, выполняется вручную, т. е. специально написанным для этого программным кодом; достоинство ее в том, что она может производиться лишь при необходимости. При этом разрешение ссылок на DLL происходит уже во время выполнения, — другими словами, программе приходится “на ходу” как-то искать нужную функцию в DLL (например, по имени).

Вот, собственно, и все о DLL. Хотя динамические библиотеки (да и всякие другие) выходят за рамки тематики этой книги и в дальнейшем изложении нам встречаться больше не будут, нам показалось полезным дать читателю некоторое представление о том, как создание и использование этих библиотек выглядит в C++Builder.

Заключение

В этой главе мы показали типичные действия, выполняемые при разработке приложений в интегрированной среде C++Builder. Конечно, представленные здесь программы совершенно ничтожны, однако они работают, а заставить что-то заработать в Windows — это не шутка. В следующих главах мы переходим уже собственно к вопросам программирования и изучению различных конструкций языка C/C++.


Глава 3. Обзор языка С

Продавай мобильный контент со своего сайта

  • Глава 3. Обзор языка С
    • Элементы простой программыr
      • Комментарии
      • Директивы #pragma
      • Директивы #inclide
      • Функция Main()
    • Представление данных в С
      • Литералы
      • Встроенные типы данных
      • Переменные
      • Типизированные константы
    • Операции и выражения
      • Семантика операций
    • Функциия
      • Ввод и вывод в С
      • Пример функции
      • Область действия переменных и связанные с ней понятия
    • Управляющие конструкции С
      • Условный оператор if...else
      • Оператор выбора Switch
      • Циклы
      • Операторы преобразования блока
      • Блоки и локальные переменные
    • Массивы и указатели
      • Массивы
      • Указатели
      • Указатели и массивы
    • Типы, определяемые пользователем
      • Переименование типов
      • Перечислимые типы
      • Структуры
      • Объединения

Глава 3. Обзор языка С

Как уже говорилось, хорошее понимание языка С представляется необходимым для успешного программирования на C++. C++Builder в полной мере поддерживает стандарт ANSI С и, кроме того, некоторые другие версии языка (Керниган & Ричи, Unix V). Мы в основном будем ориентироваться на стандарт ANSI, который, кстати, является в какой-то мере результатом “обратного воздействия” языка C++.

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

Элементы простой программы

Давайте немного поближе познакомимся со строением консольной программы Hello World, написанной в предыдущей главе:

#pragma hdrstop

#include <stdio.h>

#include <conio.h>

#include <condefs.h>

//---------------------------------------------

#pragma argsused

int main(int argc, char* argv[ ])

{

printf("Hello World from Console!\n");

printf("Press any key...");

getch() ;

return 0;

}

Комментарии

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

Комментарии совершенно игнорируются при компиляции программы, поэтому они могут содержать что угодно.

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

Вот пример комментария в стиле С, который можно было бы поместить в самое начало исходного файла:

/ *

** Простейшая консольная программа C++Builder.

** Выводит на экран "Hello World" и ждет, пока

** пользователь не нажмет какую-нибудь клавишу.

*/

Он занимает, как видите, пять строк. А вот комментарий в стиле C++:

getch(); // Ожидание нажатия клавиши.

В данном случае комментарий размещен в конце строки и поясняет смысл расположенного в ней оператора.

Директивы # pragma

Строки исходного кода, начинающиеся со знака #, являются, как правило, директивами препроцессора, т. е. управляют обработкой текста программы еще до его передачи собственно компилятору (сюда относятся текстовые подстановки, вставка содержимого других файлов и некоторые специальные операции). Директивы #pragma в этом смысле являются исключением, поскольку они адресованы непосредственно компилятору и служат для передачи ему различных указаний. Например, #pragma argsused говорит компилятору, что следует подавить выдачу предупреждающего сообщения о том, что параметры функции main () никак в ней не используются.

Примечание

Часто директивы tpragma эквивалентны некоторым установкам компилятора, задаваемым в диалоге Project Options. Например, упомянутые выше сообщения о неиспользуемых параметрах можно было бы запретить, открыв этот диалог (Project | Options... в главном меню) на странице Compiler и нажав кнопку Warnings..., после чего будет открыто окно со списком всех возможных предупреждений; в нем следует сбросить флажок напротив сообщения “Parameter 'parameter' is never used (-wpar)”.

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

Подробнее о #pragma и других директивах мы поговорим в следующей главе.

Директивы #include

Директива # include заменяется препроцессором на содержимое указанного в ней файла. Обычно это заголовочные файлы с расширением .h. Они содержат информацию, обеспечивающую раздельную компиляцию файлов исходного кода и корректное подключение различных библиотек. Имя файла может быть заключено либо в угловые скобки (о, меньше—больше), либо в обычные двойные кавычки (""). Эти случаи различаются порядком поиска включаемых файлов; если использованы угловые скобки, поиск будет сначала производиться в стандартных каталогах C++Builder, если кавычки — в текущем каталоге.

Функция main()

После всех директив в программе расположено определение функции па in () . Как уже говорилось, в строгом смысле любая программа на С содержит эту функцию, которая является ее входной точкой. Однако в среде Windows вместо main () часто используется WinMain () .

Функция main () — это, конечно, частный случай функции вообще. Функции являются основными “строительными блоками” программы, или подпрограммами. Они, в свою очередь, строятся из операторов, составляющих тело функции. Каждый оператор оканчивается точкой с запятой (;). В общем виде функция определяется таким образом:

Возвращаемый_ тип_ имя функции(список_ параметров)

{

// В фигурных скобках заключено тело функции,

// составленное из отдельных операторов.

тело_функции

}

Примечание

Функции — единственный тип подпрограмм С, в отличие, например, от языка Pascal, который различает функции и процедуры. Под процедурой обычно понимают подпрограмму, не возвращающую никакого значения. В С формально любая функция возвращает какой-либо тип, хотя в ANSI С этот тип может быть пустым (void). В первоначальном варианте языка (Керниган & Ричи) функция, для которой возвращаемый тип не определялся, считалась возвращающей int (целое значение). Мы иногда будем называть функции С процедурами, хотя это, строго говоря, и неправильно.

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

Параметры функции main()

Примечание

Параметры функции main () служат для передачи программе аргументов командной строки, т. е. имен файлов, ключей, опций и вообще всего, что вы вводите с клавиатуры после подсказки DOS, запуская программу. Конечно, программа не обязана воспринимать какие-либо команды, указываемые в строке запуска, однако в любом случае функции main () передаются два параметра — число аргументов/включая имя, под которым запущена программа (argc), и массив указателей (argv) на отдельные аргументы (выделенные элементы командной строки). Забегая вперед, приведем пример, который распечатывает по отдельности все “аргументы” строки, введенной пользователем при запуске:

#include <stdio.h>

int main(int argc, char *argv[])

{

int i;

for (i=0; i<argc; i++)

printf ( "%s\n", argv[i]);

return 0;

}

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

Представление данных в С

Любая программа так или иначе обрабатывает данные. Наша маленькая программа обрабатывает свои данные — строку сообщения “Hello World”, выводя ее на экран. Рассмотрим, какие возможны варианты представления информации в С.

Литералы

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

Символьный литерал служит для представления одиночного знака. Это символ, заключенный в одиночные кавычки (апострофы).

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

В таблице 3.1 перечислены все упомянутые выше виды литеральных констант и даны соответствующие примеры.

Таблица 3.1. Литеральные константы

Литерал | Описание | Примеры
Символьный | Одиночный символ, заключенный в апострофы | 'W', '&', 'Ф'
Строковый | Последовательность символов, заключенная в обычные (двойные) кавычки | "Это строка \n"
Целый | Десятичный — последовательность цифр, не начинающаяся с нуля | 123, 1999
 | Восьмеричный — последовательность цифр от нуля до семерки, начинающаяся с нуля | 011, 0177
 | Шестнадцатеричный — последовательность шестнадцатеричных цифр (0 - 9 и А - F), перед которой стоит 0X или Оx | ОХ9А, Oxffff
Вещественный | Десятичный — [цифры].[цифры] | 123., 3.14, .99
 | Экспоненциальный — [цифры]Е|е[+|-] цифры | Зе-10, 1.17е6

Примечание

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

#define PI 3.14159265

#define TRUE 1

#define FALSE 0

Примечание

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

Встроенные типы данных

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

Таблица 3.2. Встроенные типы данных

Тип данных | Размер (бит) | Диапазон
char | 8 | -128 - 127
signed char | 8 | -128 - 127
unsigned char | 8 | 0 - 255
short | 16 | -32768 - 32767
unsigned short | 16 | 0 - 65535
int | 32 | -2147483648 - 2147483647
unsigned int | 32 | 0 - 4294967295 .
long | 32 | -2147483648 - 2147483647
unsigned long | 32 | 0 - 4294967295
float | 32 | 3.410-38 - 3.41038
double | 64 | 1.71010-308 - 1.710308
long double | 80 | 3.410-4932 - 3.4104932

Примечание

Может быть, стоит напомнить, что отрицательные целые числа представляются в машине в форме дополнения до двух. Чтобы изменить знак числа на противоположный, нужно инвертировать все его разряды (0 заменить на 1 и наоборот и прибавить к полученному числу единицу. Например, взяв +1 типа char (00000001), инвертировав все биты (11111110) и прибавив 1, мы получим -1 (11 111 111).

Ключевые слова short, long и unsigned являются, строго говоря, модификаторами для типа int. Однако допускается сокращенная запись. Так, unsigned short — на самом деле сокращение для unsigned short int.

Следует, вероятно, повторить, что мы говорим здесь о C++Builder 5, т. е. 32-разрядном компиляторе. Размер и допустимый диапазон значений приведены именно для данного случая. Поэтому, например, тип int имеет размер 32 бита (4 байта) и эквивалентен типу long; на 16-разрядной машине int имел бы размер 2 байта, как short. О таких вещах не следует забывать, особенно если вы занимаетесь переносом программ на машину с другой разрядностью.

Переменные

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

Переменные создаются с помощью оператора объявления переменных, в котором указывается тип, имена переменных и (при необходимости) начальные значения, которыми переменные инициализируются. Вот несколько примеров:

short i;

// Объявление короткой целой

// переменной.

char quit = 'Q';

// Инициализация символьной

// переменной.

float fl, factor = 3.0, f2;

// Три переменных типа float,

// одна из которых

// инициализируется.

Синтаксис оператора объявления можно описать примерно так:

тип имя_переменной [= инициализирующее_значение][, ...];

Как и любой другой оператор С, он оканчивается точкой с запятой.

Примечание

Имена в С могут состоять из букв латинского алфавита, цифр и символов подчеркивания, причем первый символ имени не может быть цифрой. Следует помнить, что компилятор С различает регистр (прописные и строчные буквы). Таким образом, имена aVariable и AVariable считаются различными.

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

Примечание

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

Типизированные константы

Разновидностью переменных являются типизированные константы. Это переменные, значение которых (заданное при инициализации) нельзя изменить. Создание типизированной константы ничем не отличается от инициализации переменной, за исключением того, что перед оператором объявления ставится ключевое слово const:

const тип имя_константы = значение [, ...];

Например:

const double Pi = 3.14159265;

Ранее мы демонстрировали определение символической константы:

#define PI 3.14159265

Примечание

Чем этот макрос отличается от показанной выше типизированной константы? Здесь следует иметь в виду два момента. Во-первых, типизированная константа по своему смыслу относится к конкретному типу данных, поэтому компилятор генерирует совершенно определенное представление для ее значения. Представление символической константы не определено.

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

Операции и выражения

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

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

aResult = (first - second * RATE) <<3

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

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

В следующей таблице дана сводка всех операций языка С в порядке убывания приоритета.

Таблица 3.3. Операции языка С

Операция | Описание | Приоритет |  | Ассоциация
Первичные и постфиксные операции
[] | индексация массива | 16 | слева направо
() | вызов функции | 16 | слева направо
. | элемент структуры | 16 | слева направо
-> | элемент указателя | 16 | слева направо
++ | постфиксный инкремент | 15 | слева направо
-- | постфиксный декремент | 15 | слева направо
Одноместные операции
++ | префиксный инкремент | 14 | справа налево
-- | префиксный декремент | 14 | справа налево
sizeof | размер в байтах | 14 | справа налево
(тип) | приведение типа | 14 | справа налево
~ | поразрядное NOT | 14 | справа налево
! | логическое NOT | 14 | справа налево
- | унарный минус | 14 | справа налево
& | взятие адреса | 14 | справа налево
* | разыменование указателя | 14 | справа налево
Двухместные и трехместные операции
Мультипликативные
* | умножение | 13 | слева направо
/ | деление | 13 | слева направо
% | взятие по модулю | 13 | слева направо
Аддитивные
+ | сложение | 12 | слева направо
- | вычитание | 12 | слева направо
Поразрядного сдвига
<< | сдвиг влево | 11 | слева направо
>> | сдвиг вправо | 11 | слева направо
Отношения
< | меньше | 10 | слева направо
<= | меньше или равно | 10 | слева направо
> | больше | 10 | слева направо
>= | больше или равно | 10 | слева направо
== | равно | 9 | слева направо
Операция | Описание | Приоритет | Ассоциация
! = | не равно | 9 | слева направо
Поразрядные
& | поразрядное AND | 8 | слева направо
^ | поразрядное XOR | 7 | слева направо
| | поразрядное OR | 6 | слева направо
Логические
&& | логическое AND | 5 | слева направо
|| | логическое OR | 4 | слева направо
Условные
? : | условная операция | 3 | справа налево
Присваивания
= | присваивание | 2 | справа налево
*= | присвоение произведения | 2 | справа налево
/= | присвоение частного | 2 | справа налево
%= | присвоение модуля | 2 | справа налево
+= | присвоение суммы | 2 | справа налево
-= | присвоение разности | 2 | справа налево
<<= | присвоение левого сдвига | 2 | справа налево
>>= | присвоение правого сдвига | 2 | справа налево
&= | присвоение AND | 2 | справа налево
^= | присвоение XOR | 2 | справа налево
|= | присвоение OR | 2 | справа налево
, | запятая | 1 | слева направо

Семантика операций

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

Арифметические операции

К арифметическим мы отнесем те операции, которые перечислены в таблице под рубриками “Мультипликативные” и “Аддитивные”. Нужно сказать, что только эти операции (да и то за исключением взятия по модулю) имеет смысл применять к вещественным операндам (типам float, double и long double). Для таких операндов все обстоит вполне понятным и конвенциональным образом; это обычные умножение, деление, сложение и вычитание.

Операция взятия по модулю применяется только к целочисленным операндам (char, short, int. long) и дает остаток от деления первого операнда на второй. Специальной операции деления нацело в С нет — для него применяется обычная операция деления (/). Если оба операнда ее являются целыми, то результат этой операции также будет целым, равным частному от деления с остатком первого операнда на второй.

Примечание

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

volume = 4/3 * Pi * r*r*r;

Все операции в выражении правой части имеют одинаковый приоритет, и оценка выражения производится в последовательности слева направо. На первом шаге производится деление 4/3, но это будет делением нацело с результатом, равным 1. Эта единица преобразуется далее в вещественное 1.0 (возведение типа, описанное ниже), а дальше все идет как положено. Коэффициент в формуле, таким образом, получается равным 1.0 вместо ожидаемого 1.333...

Операции присваивания

Операция присваивания (=) не представляет особых трудностей. При ее выполнении значением переменной в левой части становится результат оценки выражения справа. Как уже говорилось, эта операция сама возвращает значение, что позволяет, например, написать:

а = b = с = someExpression;

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

s += i;

эквивалентно

s = s + i;

Другими словами, оператор вроде

х *= 10;

означает “присвоить переменной х ее текущее значение, умноженное на 10”.

Примечание

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

Приведение типа

Если в операторе присваивания тип результата, полученного при оценке выражения в правой части, отличен от типа переменной слева, компилятор выполнит автоматическое приведение типа (по-английски typecast или просто cast) результата к типу переменной. Например, если оценка выражения дает вещественный результат, который присваивается целой переменной, то дробная часть результата будет отброшена, после чего будет выполнено присваивание. Ниже показан и обратный случай приведения:

int p;

double pReal = 2.718281828;

p = pReal; // p получает значение 2

pReal = p; // pReal теперь равно 2.0

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

р = рО + (int)(pReal + 0.5); // Округление pReal

Примечание

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

Смешанные выражения

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

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

char short

int, long

float

double

long double

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

Примечание

Некоторые считают (я даже встречал это в книгах), что в выражении все операнды заранее приводятся к наиболее экстенсивному типу, а уж потом производится оценка. Это, конечно, не так. Возведение типов выполняется последовательно для каждой текущей пары операндов. (См. пример в замечании к параграфу “Арифметические операции”.)

Логические операции и операции отношения

В языке С нет специального логического или булева типа данных. Для представления логических значений используются целочисленные типы. Нулевое значение считается ложным (false), любое ненулевое — истинным (true).

Операции отношения служат для сравнения (больше — меньше) или проверки на равенство двух числовых операндов. Операции возвращают “логическое” значение, т. е. ненулевое целое в случае, если условие отношения удовлетворяется, и нулевое в противном случае.

Логические операции и отношения мы рассмотрим подробнее, когда будем обсуждать управляющие структуры С.

Поразрядные операции и сдвиги

Эти операции применяются к целочисленным данным. Последние рассматриваются просто как набор отдельных битов.

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

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

Результат сдвига вправо зависит от того, является ли операнд знаковым или без знаковым. Биты операнда перемещаются вправо на заданное число позиций. Младшие биты теряются. Если операнд — целое со знаком, производится расширение знакового бита (старшего), т. е. освободившиеся позиции принимают значение 0 в случае положительного числа и 1 — в случае отрицательного. При без знаковом операнде старшие биты заполняются нулями.

Сдвиг влево эквивалентен умножению на соответствующую степень двойки, сдвиг вправо — делению. Например,

aNumber = aNumber <<4;

умножает aNumber на 16.

Инкремент и декремент

Операции инкремента (++) и декремента (--) соответственно увеличивают или уменьшают свой операнд (обязательно переменную) на единицу. Они изменяют значение самой переменной, т. е. являются скрытыми присваиваниями. Иногда эти операции применяют в качестве самостоятельного оператора:

i++; или ++i;

И то и другое эквивалентно

i = i + 1;

Но эти операции могут использоваться и в выражениях:

sum - sum + х * --i;

Инкремент и декремент реализуются в двух формах: префиксной (++i) и постфиксной (i--). Префиксные операции выполняются перед тем, как будет производиться оценка всего выражения. Все постфиксные операции выполняются уже после оценки выражения, в которое они входят.

Условная операция

Условная операция (? :) позволяет составить условное выражение, т. е. выражение, принимающее различные значения в зависимости от некоторого условия. Эта операция является трехместной. Если ее условие (первый операнд) истинно, оценкой выражения будет второй операнд; если ложно — третий. Классический пример:

max_ab = b > b? а : b;

Запятая

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

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

Выражение1, выражение2[, ...]

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

i++, j++;

// Значение выражения

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

res = (j = 4, j += n, j++);

// res присваивается n + 4.

// j равно n + 5.

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

Функции

Функция, как уже говорилось, является основным структурным элементом языка С. Выше мы уже показывали синтаксис определения функции:

возвращаемый_тип имя_функции(список_параметров)

{

тело_функции

} . .

Примечание

Мы уже не раз пользовались подобной формой описания синтаксиса. Это нечто вроде метаязыка (первый из них назывался формой Бэкуса-Наура), которые широко используются для формального определения языков программирования. Надеемся, что смысл написанного достаточно ясен. Курсивом_без_ пробелов обозначаются синтаксические элементы, имеющие самостоятельное значение. Например, список_параметров является отдельной синтаксической единицей, хотя он обладает собственной внутренней структурой. Можно было бы раскрыть его определение примерно так:

Список__параметров:

Void

объявление_параметра[, объявление_параметра...]

Далее требовалось бы раскрыть смысл элемента объявление_параметра и т. д. (Несколько строк под определяемым понятием показывают различные варианты его раскрытия.)

Необязательные элементы помещаются в квадратные скобки. Взаимоисключающие варианты отделяются друг от друга вертикальной чертой (например, [+ | -] означает: “здесь может стоять либо плюс, либо минус, либо вообще ничего"). Многоточие показывает, что последний синтаксический элемент может повторяться произвольное число раз.

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

Тело функции состоит из операторов, каждый из которых завершается точкой с запятой. (В отличие, скажем, от языка Pascal точка с запятой является в С элементом оператора, а не разделителем.) Заметьте, что сам заголовок функции (его иногда называют сигнатурой) не содержит точки с запятой.

Примечание

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

Помимо определения для функции обычно пишется также ее объявление, или прототип, который размещается в заголовочном файле с расширением .h и служит для проверки корректности обращений к функции при раздельной компиляции исходных файлов. Об этом мы уже рассказывали в 1-й главе. Прототип идентичен заголовку функции, но заканчивается точкой с запятой. Тело функции отсутствует:

возвращаемый тип имя функции(список параметров);

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

имя_функции(параметры)

параметры:

пусто

параметр[, параметр...]

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

Примечание

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

DoSomething(argi, arg2);

Мы так и поступали, когда выводили на экран сообщения функцией

printf().

Примечание

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

aResult = 1. - cos(arg);

Примечание

Функция в С может иметь переменное или, точнее, неопределенное число параметров. В этом случае за последним обязательным параметром в заголовке функции следует многоточие (...). Подобным образом объявляется функция printf:

int printf(const char *format, ...);

Неопределенное число параметров означает, что количество и тип действительных аргументов в вызове должно так или иначе ей сообщаться, как это и происходит в случае printf () — там число аргументов определяется по числу спецификаторов в строке формата (см. следующий параграф). Тело функции с переменным числом параметров должно быть реализовано на языке ассемблера или, возможно, при помощи каких-то не вполне “законных” ухищрений.

Пока мы имели дело всего с тремя функциями: main (), printf () и getch () . Давайте поближе познакомимся с printf () и другими функциями ввода-вывода стандартной библиотеки С.

Ввод и вывод в С

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

Спецификация преобразования

Синтаксис спецификатора преобразования имеет такой вид:

%[флаги] [поле][.точность][размер]символ типа

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

Таблица 3.4. Элементы спецификатора преобразования

Элемент | Символ | Аргумент | Описание
флаг | - |  | Выровнять вывод по левому краю поля.
 | 0 |  | Заполнить свободные позиции нулями вместо пробелов.
 | + |  | Всегда выводить знак числа.
 | пробел |  | Вывести пробел на месте знака, если число положительное.
 | # |  | Вывести 0 перед восьмеричным или Ох перед шестнадцатеричным значением.
поле | число |  | Минимальная ширина поля вывода.
точность | число |  | Для строк — максимальное число выводимых символов; для целых — минимальное число выводимых цифр; для вещественных — число цифр дробной части.
размер | h |  | Аргумент -- короткое целое.
 | 1 |  | Аргумент — длинное целое.
 | L |  | Аргумент имеет тип long double.
Элемент | Символ | Аргумент | Описание
символ типа | d | целое | Форматировать как десятичное целое со знаком.
 | i | целое | То же, что и d.
 | о | целое | Форматировать как восьмеричное без знака.
 | U | целое | Форматировать как десятичное без знака.
 | х | целое | Форматировать как шестнадцатеричное в нижнем регистре.
 | Х | целое | Форматировать как шестнадцатеричное в верхнем регистре.
 | f | вещественное | Вещественное в форме [-]dddd.dddd.
 | е | вещественное | Вещественное в форме [-]d.dddde[+|-]dd.
 | Е | вещественное | То же, что и е, с заменой е на Е.
 | ё | вещественное | Использовать форму f или е в зависимости от величины числа и ширины поля.
 | G | вещественное | То же, что и g — но форма f или Е.
 | с, | символ | Вывести одиночный символ.
 | s | строка | Вывести строку.
 | п | указатель | Аргумент — указатель на переменную типа int. В нее записывается количество выведенных к данному моменту символов.
 | р | указатель | Вывести указатель в виде шестнадцатеричного числа ХХХХХХХХ.

Как видите, флаги задают “стиль” представления чисел на выводе, поле и точность определяют характеристики поля, отведенного под вывод аргумента, размер уточняет тип аргумента и символ_типа задает собственно тип преобразования. Следующий пример показывает возможности форматирования функции printf () . Советую не полениться и поэкспериментировать с этим кодом, меняя флаги и параметры поля вывода.

Листинг 3.1. Возможности функции printf ()

/*

** Printf.с: Демонстрация форматирования вывода на консоль

** функцией printf().

* /

#pragma hdrstop

#include <stdio.h>

#include <conio.h>

#pragma argsused

int main(int argc, char *argv[])

{

double p = 27182.81828;

int j = 255;

char s[] = "Press any key...";

/* Вывести 4 цифры; вывести обязательный знак: */

printf("Test integer formatting: %13.4d %4-8d\n", j, j);

/* Вывести по левому краю со знаком; заполнить нулями: */ printf("More integer formatting: %-+13d % 08d\n", j, j);

printf("Test octal and hex: %#13o %#8.6x\n", j, j);

printf("\nTest e and f conversion: %13.7e %8.2f\n", p, p) ;

printf("\n%s", s); /* Вывести строку подсказки. */

getch () ;

return 0;

}

Рис. 3.1 Тестирование функции printf()

Примечание

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

Escape-последовательности

В строках языка С для представления специальных (например, непечатаемых) символов используются escape-последователъности, состоящие из обратной дробной черты, за которой следует один или несколько символов. (Название появилось по аналогии с командами управления терминалом или принтером, которые действительно представляли собой последовательности переменной длины, начинающиеся с кода ESC.) В приведенных примерах функции printf () вы уже встречались с одной такой последовательностью — \n. Сама обратная косая черта называется escape-символом..

В таблице 3.5 перечислены возможные esc-последовательности.

Таблица 3.5. Escape-последовательности языка С

Последовательность | Название | Описание
\а | Звонок | Подает звуковой сигнал.
\b | Возврат на шаг | Возврат курсора на одну позицию назад.
\f | Перевод страницы | Начинает новую страницу.
\n | Перевод строки | Начинает новую строку.
\r | Возврат каретки | Возврат курсора к началу текущей строки.
\t | Табуляция | Переход к следующей позиции табуляции.
\v | Вертикальная табуляция | Переход на несколько строк вниз.
\\ |  | Выводит обратную дробную черту.
\' |  | Выводит апостроф (одинарную кавычку).
\" |  | Выводит кавычку (двойную).

Кроме того, esc-последовательности могут представлять символы в ASCII-коде — в восьмеричном или шестнадцатеричном формате:

\000 | От одной до трех восьмеричных цифр после esc-символа.
\хНН или \ХНН | Одна или две шестнадцатеричных цифры после esc-символа.

Функции ввода строки — scanf() и gets()

В языке С для ввода имеется “зеркальный двойник” printf() — функция scant (). Функция читает данные со стандартного ввода, по умолчанию — клавиатуры. Она так же, как и printf () , принимает строку формата с несколькими спецификаторами преобразования и несколько дополнительных параметров, которые должны быть адресами переменных, куда будут записаны введенные значения.

Примечание

В языке С функция не может изменять значение передаваемых ей аргументов, поскольку ей передается только временная копия содержимого соответствующей переменной. Это называется передачей параметра.по значению. В языке Pascal возможна также передача по ссылке, позволяющая функции изменить саму переменную-аргумент. Параметр, передаваемый по ссылке, объявляется с ключевым словом var. В С нет автоматического механизма передачи по ссылке. Чтобы передать из функции некоторое значение через параметр, ее вызывают с указателем на переменную (грубо говоря, ее адресом), подлежащую модификации. Функция не может изменить переданный ей аргумент, т. е. сам адрес, но она может записать информацию в память по этому адресу. Адрес получают с помощью операции &, например, SaVar. Подробнее мы обсудим это, когда будем говорить об указателях.

Примером вызова scanf () может служить следующий фрагмент кода:

int age;

printf("Enter your age: "); //' Запросить ввод возраста

// пользователя. scanf ("%d", &age); // Прочитать введенное число.

Функция возвращает число успешно сканированных полей, которое в приведенном фрагменте игнорируется. При необходимости вы можете найти полную информацию по scanf () в оперативной справке C++Builder. Однако следует сказать, что программисты не любят эту функцию и пользуются ей очень редко. Причина в том, что опечатка при вводе (скажем, наличие буквы в поле, предполагающем ввод числа и т. п.) может привести к непредсказуемым результатам. Контролировать корректность ввода и обеспечить адекватную реакцию программы на ошибку при работе со scanf () довольно сложно. Поэтому часто предпочитают прочитать целиком всю строку, введенную пользователем, в некоторый буфер, а затем самостоятельно декодировать ее, выделяя отдельные лексемы и преобразуя их в соответствующие значения. В этом случае можно контролировать каждый отдельный шаг процесса преобразования.

Ввод строки с клавиатуры производится функцией gets ():

char s[80] ;

gets (s) ;

Примечание

Буфером, в который помещается введенная строка, является здесь символьный массив s [ ]. О массивах чуть позже, пока же скажем, что в данном случае определяется буфер, достаточный для хранения строки длиной в 79 символов — на единицу меньше, чем объявленная длина массива. Одна дополнительная позиция необходима для хранения признака конца строки; все строки в С должны оканчиваться нуль-символом \0, о котором программисту, как правило, заботиться не нужно. Функции обработки строк сами распознают эти символы или, как gets (), автоматически добавляют нуль-символ в конец строки-результата. Функция gets () возвращает данные через параметр, поэтому, как говорилось выше, ей нужно передать в качестве параметра адрес соответствующего символьного массива. Операция взятия адреса, однако, здесь не нужна, поскольку имя массива (без индекса) само по себе является указателем на начало массива. Забегая вперед, скажем, что показанная нотация эквивалентна

gets(&s[0]);

// Аргумент - указатель на начальный элемент

// массива s.

Для преобразования строк, содержащих цифровое представление чисел, в численные типы данных могут применяться функции atoi(), ato1 () и atof (). Они преобразуют строки соответственно в целые, длинные целые и вещественные числа (типы int, long и double). Входная строка может содержать начальные пробелы; первый встреченный символ, который не может входить в число, завершает преобразование. Прототипы этих функций находятся в файле stdlib.h.

Пример функции

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

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

Листинг 3.2. Пример создания функции

/*

** Convert.с: Пример функции, преобразующей число

** в строку специального вида.

*/

#pragma hdrstop

#include <stdio.h>

#include <stdlib.h>

#include <conio.h>

/* Прототип функции */

void Convert(char *buffer, long num);

//-----------------------------------------

#pragma argsused

int main(int argc, char* argv[])

{

long number;

char s[80], name[80] ;

printf("Enter name: ");

gets(name) ;

printf("Enter phone number: ");

gets (s) ;

number = atol(s);

/* Преобразовать номер в обычную форму. */

Convert(s, number);

/* Вывести результат. */

printf("\n%-30s %10s\n", name, s);

getch () ;

return 0;

}

/* Определение функции */

void Convert(char *buffer, long num)

{

int grp1, grp2, grp3;

grp3 = num % 100; // Две последние цифры.

num /= 100;

grp2 = num % 100; // Две средние цифры

grp1 = num / 100; // Три старшие цифры /* Преобразовать в строку. */ sprintf (buffer, "%03d-%02d-%02d", grp1, grp2, grp3) ;

}

Функция Convert () описана как void и не возвращает значения, вследствие чего в ее теле можно опустить оператор return. (Я, кажется, еще не говорил, что именно этот оператор служит для возврата значения функции в вызывающую программу.) Она преобразует переданный ей телефонный номер (второй параметр) и записывает его в указанный строковый буфер (первый параметр). Центральный момент преобразования — разбиение номера на группы — является довольно характерным примером применения операций деления с остатком.

Рис. 3.2. Программа Convert

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

В основной программе, т. е. в функции main (), использована функция ato1 (), преобразующая строку в длинное целое.

В верхней части файла мы поместили прототип функции Convert () Определение функции мы поместили после main(), поэтому прототип в данном случае необходим — без него компилятор не сможет корректно генерировать вызов Convert () .

Примечание

Подытожим некоторые правила относительно прототипов и определений функций:

  • Функция может возвращать значение практически любого типа (включая определяемые пользователем) или не возвращать его вообще. В последнем случае функция описывается как void.
  • Функция может не иметь параметров. В этом случае на месте списка параметров в прототипе или определении также ставится ключевое слово void или список оставляют пустым; в вызове функции на месте списка аргументов также ничего не пишется (однако скобки необходимы).
  • В прототипе, в отличие от определения, нет необходимости указывать имена параметров; список параметров может состоять из перечисления только их типов, разделенных запятыми, например: void Convert(char*, long);
  • Прототип не обязателен, если определение функции расположено в тексте программы выше того места, где она вызывается (точнее говоря, в этом случае прототипом служит само определение функции).

Область действия переменных и связанные с ней понятия

Теперь, когда мы более-менее разобрались с принципами функциональной организации программы, следует обсудить некоторые весьма важные вопросы относительно переменных.

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

Область действия

Область действия — это та часть программы, где переменная в принципе доступна для программного кйда (что означает это “в принципе”, выяснится чуть позже). По своей области действия переменные делятся на локальные и глобальные.

Локальные переменные объявляются внутри функции и вне ее тела недоступны. Вернитесь к последнему примеру. Там программа состоит из двух функций. В main () объявляются переменные number, s и name (две последних — не простые переменные, а массивы, но это несущественно). В функции Convert объявлены grp1, grp2 и grp3.

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

Примечание

Параметры в определении функции (формальные параметры) можно рассматривать как локальные переменные, инициализируемые значениями аргументов при ее вызове.

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

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

/***************************************************

** Область действия и видимость переменных.

*/

#include <stdio.h>

int iVar = 111; // Глобальная переменная.

void Funci(void)

{

int iVar = 222; // Локальная переменная Funci().

/* Локальная переменная скрывает глобальную. */

printf("Значение iVar в Func1() равно %d.\n", iVar);

}

void Func2(void)

{

/* Глобальная переменная доступна. */

printf("Значение iVar в Func2 () равно %d.\n", iVar) ;

iVar = 333; // Изменяет глобальную переменную.

}

int main(void)

(

printf ("Начальное значение iVar: %d.\n", -iVar) ;

// Печатает 111. Funci (); // Печатает 222, но не изменяет

// глобальную iVar.

printf("После вызова Func1(): %d.\n", iVar) ;

Func2 (); // Печатает 111 и изменяет iVar на 333.

printf ("После вызова F'unc2(): %d.\n", iVar) ;

return 0;

}

Время жизни

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

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

Если же вы хотите, чтобы локальная переменная сохраняла значение между вызовами функции, ее следует объявить с модификатором static, как в следующем:

int AFunc(void)

{

/* Так можно организовать счетчик вызовов функции. */

static int callCount = 0;

// Здесь что-то делается...

return ++callCount;

}

Примечание

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

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

Модификаторы переменных

Помимо static, в С имеются и другие модификаторы, применяемые к объявлениям переменных. Опишем их вкратце.

  • static. Его воздействие на локальную переменную описано выше. Примененный к глобальной переменной, он ограничивает область ее действия текущим файлом (модулем компиляции).
  • auto. Специфицирует локальную переменную как создаваемую автоматически и подразумевается по умолчанию. Зачем он вообще нужен? Наверное, на всякий случай — для реализации языка, в которых все переменные по умолчанию статические. Никогда не видел, чтобы его кто-то применял.
  • register. Этот модификатор рекомендует компилятору разместить локальную переменную в регистре процессора, если это возможно.
  • extern. Модификатор говорит компилятору, что переменная является внешней, т. е. объявлена в другом файле.

Модификатор volatile

Об этом модификаторе следует сказать отдельно. Он применяется для объявления переменных, которые можно назвать нестабильными. Модификатор volatile сообщает компилятору, что значение переменной может изменяться как бы само по себе, например, некоторым фоновым процессом или аппаратно. Поэтому компилятор не должен пытаться как-либо оптимизировать выражения, в которые входит переменная, — предполагая, например, что ее значение не менялось с предыдущего раза и потому выражение не нужно заново пересчитывать.

Есть и другой момент. В программе могут быть так называемые критические участки кода, во время исполнения которых изменение значения нестабильной переменной приведет к абсурдным результатам. (Возьмите случай оценки “А или не-А”, если А нестабильно!) Для таких критических участков компилятор должен создавать копию, например, в регистре, и пользоваться этой копией, а не самой переменной.

Примечание

Можно написать такое объявление:

volatile const int vciVar = 10;

Другими словами, “нестабильная константа” типа int. В этом нет никакого противоречия — компилятор не позволит вашей программе изменять переменную, но и не будет предполагать ее значение известным априори, так как оно может меняться в силу внешних причин.

Управляющие конструкции С

Программа, операторы которой исполняются строго последовательно — так, как они записаны, — обладает весьма скромными возможностями. Именно такими и были приводимые нами до сих пор примеры. Настоящая программа должна уметь “принимать решения”, т. е. изменять последовательность исполнения своих операторов в зависимости от текущей ситуации — входных данных, результатов вычислений, сообщений Windows и т. д. Простейшим примером может служить выполнение каких-то операторов, если некоторое условие истинно, и пропуск их в противном случае.

Примечание

С и Pascal называют языками структурного программирования', не столько потому, что в этих языках имеется понятие “структуры данных”, но в основном благодаря структурированному потоку управления. Что имеется в виду? Существуют всего три основных структуры потока управления (поток управления можно определить как алгоритм перехода от текущего оператора к следующему):

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

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

Условный оператор if... else

Условный оператор реализует структуру выбора. Он имеет такой вид:

if (условие) оператор1 else оператор 2

Если условие оценивается как истинное (ненулевое), выполняется onepamop1, если как ложное (нулевое), выполняется onepamop2. Простейший пример:

if (а > b)

max_ab = a;

else

max_ab = b;

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

if (условие)

{опера торы_блока_if)

else

(опера торы_блока_еlsе}

В случае, когда при ложности условия не нужно выполнять никаких действий, а требуется только пропустить операторы блока if, ключевое слово else и соответствующий ему оператор (блок) могут отсутствовать, как в следующем примере:

if (а > b) { // Если а > b, поменять их местами;

temp = а; // в противном случае оставить все, как есть.

а = b;

b = temp;

}

//...Продолжение программы...

В соответствии с правилом суперпозиции можно строить вложенные структуры if...else, например:

if (a > b)

if (a > с)

max_abc = а;

else

max abc = с;

else

if (b > с)

max_abc = b;

else

max_abc = с ;

Эта конструкция всего-навсего определяет наибольшее из трех чисел, но разобраться в ее логике не так-то просто. Кроме того, следует помнить, что если во вложенных условных структурах используются как полные, так и неполные операторы if (без else), то могут возникать неоднозначности. Попробуем, например, переписать предыдущий фрагмент чуть более экономно, заранее присвоив максимуму значение с:

Max_abc = с;

if (a > b)

if (a > c)

max_abc == a;

else

if (b > C) max_abc = b;

К которому из двух первых if относится это else? По задуманной нами логике — к первому, однако компилятор считает по-другому; он разрешает подобные неоднозначности, ставя спорное else в соответствие ближайшему if, т. е. в данном случае второму. В результате все работает неправильно. Чтобы устранить неоднозначность, нужно применить операторные скобки:

max_abc = с;

if (а > b) {

if (а > с)

max_abc = а;

else if (b > с)

max abc = b;

Об условиях в операторе if

Условие оператора if может быть сколь угодно сложным выражением. Можно было бы сказать, что это выражение должно быть “логическим”, но в С нет логического типа данных. Как уже говорилось, выражение считается ложным, если его значением является нуль, и истинным, если значение ненулевое.

Вот несколько примеров условий оператора if:

if (x) DoSomething();

// Если х не равно

// нулю.

if (!x) DoAnotherThing();

// Если х равно нулю.

if (b == с) DoAnotherThing();

// Если b равно с.

if (b != с) DoSomething();

// Если b не равно с.

if ((key = getch()) == 'q') DoQuitO;

// Сохранить код

// клавиши в key

// и проверить, равен

// ли он ' q ' .

#define ERR_FLAG 0х80

// Если бит ERR_FLAG

if (flags & ERR_FLAG) ReportError();

// переменной flags

// установлен.

if (a >= b && a <= c) DoSomething();

// Если а лежит между

// b и с.

Операции отношения (==, !=, <, >= и т. д.) возвращают ненулевой целый результат, если значения операндов удовлетворяют отношению. В большинстве реализации С это 1, но полагаться на это не стоит. Если отношение не удовлетворяется, результатом операции будет нуль.

Обратите внимание на три последних примера. В пятом примере вы можете видеть разницу между присваиванием (=) и отношением равенства (= =). Не забывайте, что в С присваивание является операцией, возвращающей значение.

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

Примечание

Кстати о флагах и поразрядных операциях. Битовые флаги — довольно распространенный и очень эффективный прием хранения и передачи информации о состоянии какого-то объекта или процесса, хотя и не очень безопасный. Вот примеры манипуляций с флагами:

flags | = ERR_FLAG; // Установка флага операцией OR.
flags &= ~ERR_FLAG; // Сброс флага операцией AND.
flags "= ERR_FLAG; // Переключение флага операцией XOR.

Оператор выбора switch

Часто возникают ситуации, когда некоторая переменная может принимать несколько возможных значений-вариантов, и для каждого варианта требуется выполнить какие-то свои действия. Например, пользователю может быть предложено меню, когда нажатие различных клавиш инициирует соответствующие команды. Управляющая конструкция, реализующая такую логику, может использовать “последовательно вложенные” операторы if...else if...:

int key;

printf("\nSelect command (F, M or Q): ");

// Вывести подсказку. key = getch();

// Прочитать символ. key = toupper(key);

// Преобразовать в верхний регистр. if (key == 'F')

// Определение команды...

printf("\n\"F\" selected - means File.\n");

else if (key == 'M')

printf("\n\"M\" selected - means Message.\n");

else if (key == 'Q')

printf("\n\"Q\" selected - means Quit.\n");

else

printf("\nlnvalid key!");

Примечание

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

Условия операторов if содержат проверку кода нажатой клавиши на равенство одному из допустимых символов. Если код клавиши не соответствует никакой команде, выводится сообщение об ошибке.

Для подобных случаев в С существует специальная конструкция выбора switch. Выглядит она так:

switch (выражение)

{

case константное_выражение: группа_операторов case константное_выражение: группа_операторов

[default: группа операторов] }

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

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

И еще одна особенность, о которой следует помнить при написании структур switch. Если найдена метка case, совпадающая со значением проверяемого выражения, то выполняется группа_операторов данного case. Однако дело на этом не заканчивается, поскольку, если не принять никаких дополнительных мер, управление “провалится” ниже, на следующую по порядку метку case и т. д., и в результате будут выполнены все операторы до самого конца блока switch. Если это нежелательно (как чаще всего и бывает), в конце группы_операторов case нужно поставить оператор break. Он прерывает выполнение блока switch и передает управление оператору, непосредственно следующему за блоком.

Для иллюстрации мы перепишем предыдущий пример “меню”, оформив его в виде законченной программы, и продемонстрируем попутно еще один управляющий оператор С.

Листинг 3.3. Демонстрация структуры switch

/*

** Switch.с: Оператор выбора.

*/

#pragma hdrstop

#include <stdio.h>

#include <conio.h>

#pragma argsused

int rriain(int argc, char* argv[])

{

int key;

loop:

printf("\nEnter command (F, M or Q): ");

key = getche(); // Прочитать клавишу.

switch (key) ( // Определение команды... case 'f':

case 'F":

printf("\n\"File\" command selected.\n");

break;

case 'm':

case 'M':

printf ("\n\"Mess.age\" command selected.\n");

break;

case 'q':

case 'Q':

printf("\n\"Quit\" command selected.\n");

printf("\nPress a key to Exit...");

getch() ;

return 0; // Возврат в Windows. default:

printf("\nlnvalid command!\n") ;

}

goto loop; // Следующая команда.

}

Рис. 3.3 Программа Switch

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

Примечание

Чтение команды производится функцией getche(). Она, как и getch (), возвращает код нажатой клавиши, однако в отличие от getch () отображает введенный символ на экране.

Для реализации цикла мы использовали архаический оператор goto, исполнение которого приводит к передаче управления на метку, указанную в операторе. В примере метка с именем loop стоит в самом начале программы. Таким образом, дойдя до конца функции main (), управление возвращается к ее началу и все повторяется снова.

Примечание

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

Циклы

В языке С структуры многократного повторения реализуются тремя разновидностями операторов цикла. Это циклы while, do... while и for.

Цикл while

Синтаксис оператора while выглядит так:

while (условие продолжения) оператор

Сначала оценивается условие_продолжения. Если оно истинно, выполняется оператор, после чего управление возвращается заголовку цикла, и все повторяется снова. Когда условие оказывается ложным, управление передается следующему после цикла оператору.

Как обычно, одиночный оператор тела цикла можно заменить блоком, заключенным в фигурные скобки:

whi1е (условие_продолжения)

{

операторы_тела цикла

}

Используя этот оператор, можно было бы переписать предыдущий пример (со структурой switch) следующим образом:

int main(int argc, char* argv[ ])

{

int key, done = 0;

while (!done) {

printf("\nEnter command (F, M or Q): ");

key = getche(); // Прочитать клавишу.

switch (key) ( // Определение команды... case 'f':

case 'F':

printf("\n\"File\" command selected.\n");

break;

case 'm':

case 'M':

printf("\n\"Message\" command selected.\n");

break;

case 'q':

case 'Q':

printf("\n\"Quit\" command selected.\n");

done = 1; // Завершить цикл.

break; default:

printf("\nlnvalid command!\n") ;

}

} printf("\nPress a key to Exit...");

getch() ;

return 0; // Возврат в Windows.

}

Это более “грамотная” версия цикла обработки команд. Пока done равняется нулю, цикл продолжает выполняться. Когда нажимают клавишу 'q', done присваивается единица и при очередной оценке условия оно оказывается ложным; цикл завершается.

Обратите внимание, что в цикле while проверка условия делается перед выполнением тела цикла. Если условие изначально ложно, то тело цикла не исполняется вообще, ни одного раза.

Цикл do—while

Этот цикл имеет такой вид:

do оператор while (условие продолжения);

Здесь сначала выполняется оператор, а затем производится проверка условия_продолжения. Если условие истинно, управление возвращается в начало цикла; если ложно, цикл завершается и управление переходит к оператору, следующему за циклом.

Отличие от предыдущей конструкции очевидно — тело цикла do... while исполняется хотя бы один раз вне зависимости от каких-либо условий, в то время как в цикле while при ложном условии тело не исполняется вообще. Хотя конкретную программную задачу можно решить, применив любую из этих конструкций, чаще всего одно из двух решений оказывается более экономным.

Цикл for

Цикл for, наиболее универсальный из всех циклов языка С, выглядит так:

for ([инициализация]; [условие]; [модификация]) оператор

Прежде всего выполняется инициализация цикла; секция инициализации может содержать любое выражение. Инициализация производится только один раз перед началом работы цикла.

Оценивается выражение условия. Если оно истинно, выполняется оператор тела цикла; если условие ложно, происходит выход из цикла и управление передается следующему оператору.

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

Как видно из синтаксического описания, любую секцию заголовка цикла for можно опустить, но разделители — точки с запятой — все равно должны присутствовать. Если опущено условие, цикл будет выполняться бесконечно.

Простейшей и самой популярной конструкцией на основе цикла for является цикл с управляющей переменнои-счетчиком:

int i;

for (i =0; i < REPEAT; i++)

DoSomething (i);

Счетчик инициализируется значением 0. В начале каждого прохода цикла проверяется, не достиг ли он значения REPEAT. Как только i станет равным REPEAT, тело цикла пропускается и управление передается следующему оператору. В конце каждого прохода i увеличивается на единицу. Как нетрудно подсчитать, тело цикла выполняется для значений i от О до REPEAT-1, т. е. REPEAT раз.

Примечание

Любую конкретную структуру повторения, требуемую для решения некоторой задачи, можно реализовать на основе любого из циклов С, однако всегда какой-то из них подходит к данному случаю наилучшим образом, позволяя написать более ясный и компактный код. Так, если необходимое число итераций цикла известно заранее (как при обработке массива), проще всего применить цикл for. Если же число итераций заранее определить нельзя, как в нашем примере обработки команд (момент завершения цикла определяется пользователем) или при операциях поиска в списке, применяют цикл while или do. . .while.

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

unsigned Fac(unsigned n)

{

if (n)

return n * Fac(n - 1);

else

return 1;

}

Когда аргумент в очередном вызове оказывается равен 0, рекурсия завершается — функция возвращает 1. До этого момента не происходило, по существу, реальных вычислений (умножений). На стеке накапливались вызовы Fac () с последовательно уменьшающимися аргументами. Теперь стек начинает “разматываться”, и возвращаемые на каждом шаге значения умножаются на последовательно увеличивающиеся n. Глубина рекурсии ограничивается только размером стека программы.

Факториал, конечно, вообще никто никогда не вычисляет, во всяком случае, подобным образом. Однако существуют задачи, для которых метод рекурсии оказывается естественным решением и реализуется намного проще, чем это можно

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

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

Операторы прерывания блока

Часто бывает необходимо “досрочно” выйти из некоторого цикла, до того, как будет удовлетворено условие его завершения (говоря точнее, до того, как условие продолжения станет ложным). Например, вы просматриваете массив на предмет поиска заданного значения. Как только нужный элемент массива найден, выполнять цикл далее нет необходимости. Для досрочного завершения циклов в С применяются операторы break, return и continue. С оператором break вы уже встречались — помимо циклов, он используется в блоках switch.

  • Оператор break вызывает прерывание ближайшего (самого внутреннего) заключающего его блока switch, while, do... while или for. Управление немедленно передается следующему за блоком оператору.
  • Оператор continue воздействует только на блоки циклов. Он передает управление в конец тела цикла, пропуская, таким образом, все следующие за ним операторы блока. Здесь досрочно завершается не сам цикл, а его текущая итерация.

Примечание

Разумеется, операторы прерывания циклов должны выполняться условно, т. е. должны входить в блок if или else некоторого условного оператора и исполняться только при наступлении условий досрочного завершения цикла.

Эти два оператора эквивалентны следующим конструкциям с goto:

// Эквивалент break:

while (...) {

…

goto brkLabel;

…

} // Закрывающая скобка блока. brkLabel:

// Метка следующего за блоком оператора.

// Эквивалент continue:

while (...) (

…

goto cntLabel;

…

CntLabel:; // Пустой помеченный оператор.

} // Закрывающая скобка блока.

Примечание

Использование break и continue для прерывания циклов, вообще говоря, нежелательно — по тем же причинам, что и использование goto (от операторов break в структуре выбора switch никуда не уйти). Они нарушают структурную организацию потока управления и затрудняют чтение текста программы.
  • Оператор return прерывает выполнение текущей функции и возвращает ее значение в вызывающую программу. Он имеет вид:

return [выражение];

Примечание

Если функция “возвращает” тип void, выражение опускается. Если код функции при ее вызове не исполняет ни одного оператора return, подразумевается, что return присутствует в качестве последнего оператора тела функции. Возвращаемое значение при этом не определено.

Не следует ставить операторы return где попало. Вернитесь и посмотрите на листинг 3.3. Это пример того, как не надо программировать. По правилам “хорошего тона” оператор возврата должен быть только один (от силы два) и он должен располагаться в конце тела функции.

Блоки и локальные переменные

Поскольку при описании управляющих конструкций мы попутно ввели понятие блока, нужно сделать одно уточнение касательно объявлений и области действия локальных переменных. На самом деле локальные переменные могут объявляться не только в начале тела функции, но и в любом другом блоке (if, while и т. д.). Областью действия переменной является блок, в котором она объявлена; она скрывает любую переменную с тем же именем, объявленную вне данного блока. За пределами блока переменная недоступна.

Массивы и указатели

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

Массивы

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

int myArray[8];

Массив, как и переменную, можно инициализировать при объявлении. Значения для последовательных элементов массива отделяются друг от друга запятыми и заключаются в фигурные скобки:

int iArray[8] = {7, 4, 3, 5, 0, 1, 2, 6);

Обращение к отдельным элементам массива производится путем указания индекса элемента в квадратных скобках, например:

myArray[3] = 11;

myArray[i] = iArray[7-i];

Индекс должен быть целым выражением, значение которого не выходит за пределы допустимого диапазона. Поскольку индексация массивов начинается в С всегда с нуля (т. е. первый элемент имеет индекс 0), то, если массив состоит из N элементов, индекс может принимать значения от О до N-1.

Примечание

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

Массивы естественным образом сочетаются с циклами for. Мы приведем пример программы, работающей с массивом целых чисел. Она выполняет так называемую “пузырьковую сортировку” введенных пользователем чисел в порядке возрастания. Работу программы иллюстрирует рис. 3.4.

Листинг 3.4. Программа пузырьковой сортировки

/*

** Loop.с: Программа пузырьковой сортировки.

*/

#pragma hdrstop

#include <stdio.h>

#include <stdlib.h>

#include <conio.h>

/********************************************************

** Процедура сортировки

*/

void DoSort(int array[ ], int n)

{

int i, j, temp;

for (i = n-1; i > 0; i--)

for (j = 0; j < i; j++)

if (array[j] > array[j+l]) {

temp = array[j];

array[j] = array[j+l];

array [j+1] = temp;

}

} /* Конец DoSortO */

#pragma argsused

int main(int argc, char* argv[])

{

const int N = 8;

int i, iArray[8];

char s[80], *endPtr;

printf("Enter %d integers separated by spaces:\n", N);

gets(s); // Прочитать строку пользователя.

endPtr = s; // Инициализировать указатель строки.

for (i =0; i < N; i++) // Преобразование чисел.

iArray[i] = strto1(endPtr, &endPtr, 10);

DoSort(iArray, N); // Вызов программы сортировки.

printf("Sorted array:\n");

for (i =0; i < N; i++)

// Вывод отсортированного

// массива. printf("%8d ", iArray[i]);

printf("\n\nPress a key...");

getch() ;

return 0;

}

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

Примечание

Строка, содержащая все введенные пользователем числа, считывается целиком в символьный массив s. После этого для преобразования символьного представления чисел в данные целого типа в цикле вызывается функция strtol (). Ее прототип (в stdlib.h) имеет вид

long strtol(const char *str, char **endptr, int radix);

Эта функция действует подобно atol (), преобразуя строку str в значение типа long, однако обладает более широкими возможностями. Параметр radix задает основание системы счисления (8, 10 или 16). В параметре endptr функция возвращает указатель на непреобразованную часть строки str (т. е. на строку, оставшуюся после исключения из нее первого из чисел). Таким образом, в цикле мы последовательно вычленяем из строки все восемь чисел и записываем их в элементы целого массива.

Функция позволяет довольно просто контролировать корректность ввода. Если при ее вызове происходит ошибка, например, строка содержит меньше чисел, чем их должно быть, возвращаемый в endptr указатель будет совпадать с аргументом str.

Ключевое слово const в объявлении первого параметра говорит компилятору, что функция не должна изменять элементы строки, на которую указывает str. Попытка модифицировать строку в теле функции вызовет ошибку при ее компиляции.

Рис. 3.4 Робота программы сортировки

Указатели

Указатель — это переменная, которая содержит адрес другого объекта. Этим объектом может быть некоторая переменная, динамический объект или функция. Говорят, что указатель ссылается на соответствующий объект. Хотя адрес, по существу — 32-битное целое число, определяющее положение объекта в виртуальной памяти программы, указатель является не просто целым числом, а специальным типом данных. Он “помнит”, на какого рода данные ссылается. Объявление указателя выглядит так:

тип_указываемого_объекта *имя_указателя [= значение];

Вот примеры объявлений:

int *pIntVar; // Указатель на целое.

double *pDouble = SdoubleVar; // Инициализация указателя

// на double.

char *arrStr[16]; // Массив указателей на char.

char (*arrStr) [16][16]; // Указатель на матрицу char.

Примечание

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

Чтобы получить доступ к объекту, на который указатель ссылается. последний разыменовывают, применяя операцию-звездочку. Например. *pDouble будет представлять значение переменной, на которую ссылается

pDouble:

double doubleVar = 3.14159265;

double *pDouble = SdoubleVar;

printf("Значение самого указателя (адрес): %р", pDoubie) ;

printf("Число, на которое он ссылается: %f", *pDouble);

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

чения которых могут ими изменяться (передача по ссылке). Но главная “прелесть” указателей в том, что они позволяют создавать и обрабатывать динамические структуры данных. В языке С можно выделить память под некоторый объект не только с помощью оператора объявления, но и динамически, во время исполнения программы. Объект создается в свободной области виртуальной памяти функцией та 11 о с (). Вот пример (предполагается, что переменные объявлены так, как выше):

pDouble = malice(sizeof(double)); // Динамическое выделение

// памяти. *pDouble = doubleVar; // Присвоение значения

// динамическому объекту.

printf("Значение динамического объекта: %f", *pDouble) ;

free(pDouble); // Освобождение памяти.

Аргументом malloc () является размер области памяти, которую нужно выделить; для этого можно применить операцию sizeof, которая возвращает размер (в байтах) переменной или типа, указанного в качестве операнда.

Функция malloc () возвращает значение типа void* — “пустой указатель”. Это указатель, который может указывать на данные любого типа. Такой указатель нельзя разыменовывать, поскольку неизвестно, на что он указывает — сколько байтов занимает его объект и как их нужно интерпретировать. В данном случае операция присваивания автоматически приводит значение malloc () к типу double*. Можно было бы написать в явном виде

pDouble = (double*)malloc(sizeof(double));

Если выделение памяти по какой-то причине невозможно, malloc () возвращает NULL, нулевой указатель. На самом деле эта константа определяется в stdlib.h как целое — “длинный нуль”:

#define NULL OL

Примечание

Хорошо ли вы поняли смысл различия двух последних примеров? В первом из них указателю pDouble присваивается адрес переменной doubleVar. Во втором указателю присваивается адрес динамически созданного объекта типа double;

после этого объекту, на который ссылается pDouble, присваивается значение переменной doubleVar. Создается динамическая копия значения переменной.

Память, выделенную malloc (), следует освободить функцией free () , если динамический объект вам больше не нужен. Возьмите это себе за правило и не полагайтесь на то, что система Windows автоматически уничтожает все динамические объекты программы по ее завершении.

Перед тем, как разыменовывать указатель, его нужно обязательно инициализировать, либо при объявлении, либо путем присвоения ему адреса какого-либо объекта, возможно, динамического — как в последнем примере. Аналогично, если к указателю применяется функция free(), он становится недействительным и не ссылается больше ни на какие осмысленные данные. Чтобы использовать его повторно, необходимо снова присвоить ему адрес некоторого объекта.

Примечание

Разыменование нулевого указателя также приводит к ошибке. Поэтому при работе с объектами, создаваемыми с помощью malloc (), обычно всегда проверяют, не возвратила ли эта функция нулевое значение.

Указатель на функцию

Можно объявить, инициализировать и использовать указатель на функцию. В вызовах API Windows часто применяют, например, “возвратно-вызываемые функции”. В вызове API в качестве аргумента в этом случае употребляется указатель на соответствующую функцию.

Вот пример, из которого все станет ясно.

/**********************************************

** Некоторая функция:

*/

void ShowString(char *s)

{

printf (s);

}

/***********************************************

** Главная функция:

*/

int main(void) {

void (*pFunc)(char*); // Объявление указателя на функцию.

pFunc = ShowString; // Инициализация указателя адресом

// функции. (*pFunc)("Calling a pointer to function!\n");

return 0;

}

Примечание

Вы, возможно, обратили внимание, что в примере указателю присваивается значение, представленное просто именем функции без скобок со списком параметров. То есть “значение”, представленное именем функции, имеет тот же тип, что и объявленный здесь указатель. Поэтому и вызвать функцию через указатель можно было бы проще:

pFunc("Calling a pointer to function!\n");

Соотношение между указателями и функциями примерно такое же, как между указателями и массивами, о чем говорится в следующем разделе.

Указатели и массивы

Между указателями и массивами в С существует тесная связь. Имя массива без индекса эквивалентно указателю на его первый элемент. Поэтому можно написать:

int iArray[4] ;

int *piArr;

piArr = iArray; // piArr указывает на начальный элемент // iArray.

Последнее эквивалентно

piArr = &iArray[0];

И наоборот, указатель можно использовать подобно имени массива, г. е. индексировать его. Например, piArr [3] представляет четвертый элемент массива iArray [ ] .

К указателю можно прибавлять или отнимать от него целочисленные выражения, применять операции инкремента и декремента. При этом значение указателя изменяется в соответствии с размером объектов, на которые он указывает. Так, (piArr + 2) указывает на третий элемент массива. Это то же самое, что и & iArray [ 2 ]. Когда мы прибавляем к указателю единицу (piArr++) , адрес, который в нем содержится, в действительности увеличивается на 4 — размер типа int.

Примечание

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

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

Типы, определяемые пользователем

Встроенные типы данных, указатели и массивы образуют основу для представления и обработки информации на языке С. Подлинная же сила языка состоит в том, что он позволяет пользователю (под “пользователем” в подобного рода выражениях понимается программист) самому определять наиболее подходящие для конкретной задачи типы, способные адекватно представлять сложно структурированные данные реального мира. Эти средства С мы начнем изучать со сравнительно простого вспомогательного оператора typedef.

Переименование типов

Любому типу в С можно присвоить простое имя или переименовать его. Это делается с помощью ключевого слова typedef:

typedef тип новое_имя_типа;

или

typedef тип новое_имя_типа [размер_массива][...];

для типов-массивов. (Квадратные скобки здесь означают не необязательность синтаксического элемента, а “настоящие” скобки.) Кроме того, мож-

но вводить имена для типов указателей на функцию и т. п. Формально описать все возможные typedef довольно сложно, поэтому мы этого делать не будем. Вообще следует руководствоваться таким правилом: если вы объявляете объект как принадлежащий к определенному в typedef типу, имя объекта нужно подставить вместо нового_имени_типа. Убрав typedef, вы получите эквивалентное объявление объекта. Вот примеры:

typedef short Arrlndex;

// Псевдоним для short.

typedef char MessageStr[80];

// Имя типа для массивов

// char[80].

typedef int *IPtrFunc(void);

// Функция, возвращающая

// указатель на int.

typedef int (*IFuncPtr)(void);

// Указатель на функцию,

// возвращающую int.

В общем, typedef является просто средством упрощения записи операторов объявления переменных.

Перечислимые типы

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

enum имя-этикетка {имя_константы [= значение], ...};

Значение равно по умолчанию нулю для первого из перечислителей (так обычно называют определяемые в enum константы). Любая другая константа, для которой значение не указано, принимается равной значению предыдущей константы плюс единица.

Например:

enum Status

{

Success = 1,

Wait, Proceed,

Error = -1

};

Примечание

В операторе enum после закрывающей фигурной скобки можно сразу объявить несколько переменных данного типа:

enum этикетка {список_констант} переменная[, ...];

Нужно иметь в виду, что имя-этикетка не является настоящим именем типа. Именем типа будет в вышеприведенном примере enum Status. Соответственно переменные должны объявляться как

enum Status ProclStatus, Proc2Status;

Однако всегда можно воспользоваться ключевым словом typedef и ввести для перечисления подлинное новое имя. Обычно это делается сразу:

typedef enum этикетка {список_констант) имя_типа;

Предыдущее объявление можно переписать так:

typedef enum _Status {

Success = 1,

Wait, Proceed,

Error = -1 } Status;

Тогда Status будет полноценным именем перечислимого типа. (Обратите внимание, что для этикетки мы указали имя _Status. Это обычная практика.)

Структуры

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

struct этикетка [список элементов] [переменные];

Список_элементов состоит из объявлений, аналогичных объявлениям переменных. Объявления элементов оканчиваются точкой с запятой. Вот простой пример структуры, предназначенной для хранения основных сведений о человеке:

struct Person (

char lastName[32]; // Фамилия.

char firstName[32]; // Имя.

Sex sex;

// Пол: перечислимый тип

// (male, female}.

short age; // Возраст.

long phoneNum; // Телефон как длинное целое.

}

aPerson; // Объявляет переменную типа

// struct Person.

Как видите, все довольно просто. Структура группирует различные данные, относящиеся к конкретному человеку. Как и в случае перечислений, в определении структуры можно сразу объявить переменные структурного типа, указав их имена после закрывающей фигурной скобки. Аналогично именем типа является struct этикетка, и его можно сразу переопределить с помощью ключевого слова typedef.

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

struct Person *pPerson - SaPerson;

// Указатель на

// структуру.

aPerson.age = atol(ageStr);

// Записать в структуру

// возраст aPerson.sex - male;

// и т.д.

pPerson->phoneNum = atol(phoneStr); //

/* Напечатать имя и фамилию (предполагается, что они уже инициализированы).*/

printf("%s %s\n", pPerson->firstName, pPerson->lastName);

Битовые поля

В качестве элементов структуры можно определять битовые поля. Для них задается ширина поля в битах, и компилятор отводит под элемент ровно столько бит, сколько указано. Несколько битовых полей может быть таким образом упаковано в одном слове. Синтаксис битового поля:

тип [имя поля]: ширина поля;

Тип поля может быть int или unsigned int. Доступ к битовым полям осуществляется так же, как и к регулярным элементам структуры. Если имя_поля отсутствует, место под поле отводится, но оно остается недоступным. Это будут просто “заполняющие” биты.

Примечание

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

Объединения

Объединения, определяемые с помощью ключевого слова union, похожи по своему виду на структуры:

union этикетка {список_элементов] [переменные];

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

Примечание

Объединения полезны, когда требуется обеспечить своего рода “полиморфное поведение” некоторого объекта. Например, вы хотите определить тип, реализующий представление различных геометрических фигур — прямоугольников, окружностей, линий, многоугольников. В зависимости от того, чем конкретно является данная фигура, для ее описания необходимы различные наборы значений. Круг описывается иначе, чем многоугольник и т. п. И вот из структур, описывающих различные фигуры, можно в свою очередь составить обобщенный тип-объединение, который будет обрабатываться различно в зависимости от значения специального поля, определяющего род фигуры. В то же время на все фигуры можно будет ссылаться через указатели одного и того же типа, что, в частности, позволит составлять динамические связанные списки из любых фигур.

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

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

Листинг 3.5. Демонстрация работы со структурами

/*

** Struct.с: Структуры и объединения.

*/

#pragma hdrstop

#include <stdio.h>

#include <conio.h>

#include <string.h>

/* Тип для определения вида объекта */

typedef enum {Rect=l, Labi) Type;

/***********************************************************

** Структура для хранения прямоугольников и текстовых меток.

*/ typedef struct _GForm { Type type;

struct _GForm *next;

/* Указатель для связанного списка. */

union {

/* Анонимное объединение. */

struct {

/* Прямоугольник. */

int left, top;

int right, bottom;

} rect;

struct {

/* Текстовая метка. */

int x, у;

char text [20];

} labi;

}

} Gform;

/****************************************

** Функция вывода данных объекта.

*/ void ShowForm(GForm *f)

{

switch (f->type) {

case Rect:

/* Прямоугольник. */

printf("Rectangle: (%d, %d) (%d, %d)\n",

f->data.rect.left, f->data.rect.top,

f->data.rect.right, f->data.rect.bottom);

break;

case Labi: /* Метка. */

printfC'Text label: (%d, %d) \"%s\"\n",

f->data.labl.x, f->data.labi.y, f->data.labi.text);

}

int main(void)

{

GForm formi, form2;

/* Инициализация первого объекта. */

forml.type = Rect;

forml.data.rect.left = 50;

forml.data.rect.top = 25;

forml.data.rect.right = 100;

forml.data.rect.bottom = 75;

/* Инициализация второго объекта. */

form2.type = Labi;

form2.data.labi.x = 60;

form2.data.labl.у = 40;

strcpy(form2.data.labi.text, "This is a Label!");

/* Распечатка... */ ShowForm(&formi);

ShowForm(&form2);

printf("\nPress any key...");

getch() ;

return 0;

}

Работу программы иллюстрирует рис. 3.5.

Рис. 3.5 Программа Struct.c (Project2)

Примечание

Обратите внимание, что перечисления, структуры и объединения могут быть анонимными, т. е. не иметь имен-этикеток.

Внимательно рассмотрите определение типа Gform:

typedef struct _GForm { Type type;

struct GForm *next;

/* Указатель для связанного списка. */

union {

/* Анонимное объединение. */

struct {

/* Прямоугольник. */

int left, top;

int right, bottom;

} rect;

struct {

/* Текстовая метка. */

int x, у;

char text[20] ;

} labl;

} data;

} GForm;

Структура _Gform имеет, как таковая, три элемента: type, next (не используется) и data. Последний является анонимным объединением разделов rect и labl, каждый из которых, в свою очередь, является анонимной структурой. Элементы первой хранят значения координат верхнего левого и правого нижнего углов прямоугольника; элементами второй являются координаты, задающие положение текста, и сама текстовая строка. Получаются довольно длинные выражения для доступа к элементам данных (forml.data.rect.bottom).

Заключение

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

Кроме того, вам предстоит еще познакомиться с расширениями С, поддерживаемыми C++Builder, но выходящими за рамки стандарта ANSI. Такие расширения, связанные в основном с особенностями процессора и операционной системы, почти всегда имеются в любой реализации языка.

О них также будет говориться в следующей главе.


Глава 4. Препроцессор и особенности компилятора

Продавай мобильный контент со своего сайта

  • Глава 4. Препроцессор и особенности компилятора
    • Директивы препроцессора
      • Макроопределения
      • Директива #include
      • Условная компиляция
      • Директива #еггог
      • Директива #line
      • Директива #pragma
      • Типичное применение препроцессорных директив
    • Особенности C++Builder
      • Расширения языка С
    • Управление компилятором
      • Страница Compiler
      • Страница Advanced Compiler
      • Страница Directories/Conditionals

Глава 4. Препроцессор и особенности компилятора

Чтобы закончить наш, так сказать, пропедевтический курс по языку С, нам нужно еще рассказать о препроцессоре; и о некоторых ключевых словах C++Builder, которые не укладываются в стандарт ANSI. Кроме того, мы расскажем о диалоге Project | Options, в котором задаются различные параметры, управляющие компиляцией программы.

Директивы препроцессора

Препроцессорная обработка представляет собой первую фазу того процесса, что именуется компиляцией программы на C/C++. Компилятор C++Builder не генерирует промежуточного файла после препроцессорной обработки. Однако, если хотите, можно посмотреть на результат работы препроцессора, запустив отдельную программу срр32.ехе из командной строки:

срр32 myfile.c

Макроопределения

Макроопределения, называемые в просторечии макросами, определяются директивой препроцессора #define. Можно выделить три формы макросов #define: простое определение символа, определение символической константы и определение макроса с параметрами.

Простое определение выглядит так:

#define NDEBUG

После такой директивы символ NDEBUG считается определенным. Не предполагается, что он что-то означает; он просто — определен (как пустой). Можно было бы написать:

#define NDEBUG 1

Тогда NDEBUG можно было бы использовать и в качестве символической константы, о которых говорилось в предыдущей главе. Всякое вхождение в текст лексемы NDEBUG препроцессор заменил бы на “I”. Зачем нужны макроопределения, которые ничего не определяют, выяснится при обсуждении условных конструкций препроцессора.

Как вы могли бы догадаться, #define может определять не только константы. Поскольку препроцессор выполняет просто текстовую подстановку, можно сопоставить символу и любую последовательность операторов, как показано ниже:

#define SHUTDOWN \

printf("Error!"); \ return -1

…

if (ErrorCondition()) SHUTDOWN; // "Вызов" макроса.

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

Определенный ранее макрос можно аннулировать директивой #undef:

#undef NDEBUG

После этого макрос становится неопределенным, и последующие ссылки на него будут приводить к ошибке при компиляции.

Предопределенные макросы

Компилятор C++Builder автоматически определяет некоторые макросы. Их можно разбить на две категории: макросы ANSI и макросы, специфические для C++Builder. Сводки предопределенных макросов даны соответственно в таблицах 4.1 и 4.2.

Таблица 4.1. Предопределенные макросы ANSI

Макрос | Описание
DATE | Литеральная строка в формате “mmm dd yyyy”, представляющая дату обработки данного файла препроцессором.
FILE | Строка имени текущего файла (в кавычках).
LIME | Целое, представляющее номер строки текущего файла.
STDC | Равно 1, если установлена совместимость компилятора со стандартом ANSI (ключ -А командной строки). В противном случае макрос не определен.
TIME | Строка в формате “hh:mm:ss”, представляющее время препроцессорной обработки файла.

Примечание

Значения макросов _file_ и _line_ могут быть изменены директивой #line (см. далее).

Таблица 4.2. Предопределенные макросы C++Builder

Макрос | Значение | Описание
ВСОРТ | 1 | Определен в любом оптимизирующем компиляторе.
BCPLUSPLUS | 0х0540 | Определен, если компиляция производится в режиме C++. В последующих версиях будет увеличиваться.
BORLANDC | 0х0540 | Номер версии.
CDECL | 1 | Определен, если установлено соглашение о вызове cdecl; в противном случае не определен.
CHARUNSIGNED | 1 | Определен по умолчанию (показывает, что char по умолчанию есть unsigned char). Можно аннулировать ключом -К.
CONSOLE |  | Определен при компиляции консольных приложений.
CPPUNWIND | 1 | Разрешение разматывания стека; определен по умолчанию. Для аннулирования можно применить ключ -xd-.
cplusplus | 1 | Определен при компиляции в режиме C++.
DLL | 1 | Определен, если компилируется динамическая библиотека.
FLAT | 1 | Определен при компиляции в 32-битной модели памяти.
MIХ86 |  | Определен всегда. Значение по умолчанию — 300. (Можно изменить значение на 400 или 500, применив соответственно ключи /4 или /5 в командной строке.)
MSDOS | 1 | Целая константа.
MT | 1 | Определен, если установлена опция -WM. Она означает, что будет присоединяться мультили-нейная (multithread) библиотека.
PASCAL | 1 | Определен, если установлено соглашение о вызове Pascal.
TCPLUSPLUS | 0х0540 | Определен, если компиляция производится в режиме C++ (аналогично bcplusplus ).
TEMPLATES | 1 | Определен для файлов C++ (показывает, что поддерживаются шаблоны).
TLS | 1 | Thread Local Storage. В C++Builder определен всегда.
TURBOC | 0х0540 | Номер версии (аналогичен BORLANDC ).
WCHAR T | 1 | Определен только в программах C++ (показывает, что wear t — внутренне определенный тип.
WCAR T DEFINED | 1 | То же, что и WCHAR Т.
Windows |  | Определен для кода, используемого только в Windows.
WIN32 | 1 | Определен для консольных и GUI-приложений.

Как видите, многие предопределенные макросы C++Builder отражают те или иные установки параметров компиляции, задаваемые в командной строке (при ручном запуске компилятора Ьсс32.ехе). Те же самые установки могут быть выполнены и в интегрированной среде через диалог Project Options, который мы еще будем рассматривать в этой главе.

Макросы с параметрами

Макросы могут выполнять не только простую текстовую подстановку. Возможно определение макросов с параметрами, напоминающих функции языка С, например:

#define PI 3.14159265

#define SQR(x) ( (x) * (x) )

#define AREA(x) (PI * SQR(x))

#define MAX(a, b) (<a)>(b) ? (a): (b))

…

circleArea = AREAfrl + r2);

cMax = MAX(i++, j++);

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

Примечание

Обратите внимание на скобки в показанных 'выше определениях. Можно сформулировать такое правило: каждый параметр и все определение в целом должны заключаться в скобки. Иначе при вхождении макроса в выражение могут появляться ошибки, связанные с различным приоритетом операций. Рассмотрите такой случай:

#define SQR(x) х*х binom = -SQR(a + b) ;

При расширении макроса получится:

binom = -a + b*a + b;

Порядок оценки выражения окажется совсем не тем, что подразумевался.

Преобразование в строку

В макросах может применяться специальная операция преобразования в строку (#). Если в расширении макроса параметру предшествует эта опе-

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

#define SHOWINT(var)

printf(#var " = %d\n", (int)(var))

int iVariable = 100;

SHOWINT(iVariable) ;

Последняя строчка расширяется в

printf("iVariable"" = %d\n", (int)(iVariable));

и печатает

iVariable = 100

Примечание

В С примыкающие друг к другу литеральные строки при компиляции соединяются в одну строку.

Конкатенация

Операция конкатенации (##) позволяет составить из нескольких лексем единое слово. Получившийся элемент повторно сканируется для обнаружения возможного идентификатора макроса. Рассмотрите такой код:

#define DEF_INT(n) int iVar ## n

…

DEF_INT(One); // Расширяется в int iVarOne;

DEF_INT(Two); // Расширяется в int iVarTwo; и т.д.

Директива # include

Об этой директиве мы уже достаточно говорили в прошлой главе. Напомним вкратце, что препроцессор заменяет директиву содержимым указанного в ней файла. Есть две ее формы:

#include <filename> #include "filename"

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

Условная компиляция

Можно производить выборочную компиляцию различных участков кода в зависимости от оценки некоторого константного выражения или определения идентификатора. Для этого служат директивы #if, #elif, #else и #endif. Общая форма применения директив условной компиляции следующая:

# выражение_1

группа_операторов 1

[# elif выражение_2

группа_опера торов_2

# elif выражение_3

группа_ операторов_ 3...]

[# else группа операторов else]

#endif

Первая группа операторов компилируется, если выражение_1 истинно; в противном случае операторы ее опускаются. Вторая группа компилируется, если выражение_1 ложно и выражение_2 истинно и т. д. Группа #else компилируется только в том случае, если все условные выражения ложны. Конструкция условной компиляции должна заканчиваться директивой #endif.

Разделы #elifH#else могут отсутствовать. Необходимыми элементами условной конструкции являются только директивы #if и #endif.

Операции в условиях #if

Выражения в директивах могут содержать обычные операции отношения <, >, <=, >= и ==. С их помощью можно проверять, например, значения предопределенных макросов или идентификаторов, определяемых директивой #define. В директивах препроцессора имеется также одна специальная операция defined. Она позволяет проверить, определен ли некоторый символ, например:

#define TEST

#if defined(TEST)

testFile = open("TEST.$$$", 0_CREAT | 0_TEXT) ;

#else testFile = -1;

#endif

Операция defined может комбинироваться с логическим отрицанием (!). ! defined (Sym) будет истинным, если Sym не определен.

Директивы #ifdef и ftifndef

Эти две директивы эквивалентны соответственно #if defined и #if !defined.

Директива #error

С помощью этой директивы можно вывести сообщение об ошибке при компиляции.

#error сообщение

Сообщение представляет собой любую строку (не заключенную в кавычки!), которая может содержать макросы, расширяемые препроцессором. Обычно директива применяется, чтобы обеспечить определение некоторого идентификатора:

#ifndef WIN_VERSION

terror He определена версия Windows!

#endif

Директива # linе

Директива позволяет установить внутренний счетчик строк компилятора, возвращаемый макросом _LINE_. Она имеет следующий вид:

#line номер строки ["имя файла"]

Номер_строки должен быть целой константой. Если указано необязательное имя_файла, то модифицируется также значение макроса_FILE_.

Директива # pragma

Эта директива служит для установки параметров, специфичных для компилятора. Часто выполняемые с ее помощью установки можно выполнить и другим способом, например, в диалоге Project Options или в командной строке компилятора. Директива имеет вид

#pragma директива

Что такое директива, описывает следующая таблица.

Таблица 4.3. Директивы “pragma компилятора C++Builder

Директива |  | Описание
alignment |  | Выдает сообщение о текущем выравнивании данных и размере enum-типов.
anon struct |  | Синтаксис: #pragma anon struct on ^pragma anon struct off Разрешает или запрещает использование вложенных анонимных структур в классах.
argsused |  | Подавляет выдачу сообщения о неиспользуемых параметрах для функции, непосредственно следующей за директивой.
checkoption |  | Синтаксис: #pragma checkoption строкаопций Проверяет, установлены ли опции, указанные в директиве. Если нет, выдается сообщение об ошибке.
codeseg |  | Синтаксис: #pragma codeseg [имясегмента] ["класс"] [группа] Позволяет указать имя или класс сегмента либо группу, где будут размещаться функции. Если все опции директивы опущены, используется сегмент кода по умолчанию.
comment |  |  | Записывает строку-комментарий в объектный или исполняемый файл. Синтаксис: ftpragma comment (тип, "строка")
exit |  |  | Позволяет указать функцию (функции), которая будет вызываться непосредственно перед завершением программы. Синтаксис директивы: #pragma exit имя функции [приоритет] Необязательный приоритет в диапазоне 64-255 определяет порядок вызова подобных функций (чем он выше, т. е. меньше, тем позже вызывается функция).
hdrfile |  |  | Специфицирует имя файла прекомпилируемых заголовков.
hdrstop |  |  | Запрещает включать дальнейшую информацию в файл прекомпилируемых заголовков.
inline |  |  | Говорит компилятору, что файл должен компилироваться через ассемблер (компилятор генерирует код ассемблера, затем запускает TASM, который выдает конечный obj-файл).
intrinsic |  |  | Синтаксис: #pragma intrinsic [-]имяфункции Управляет inline-расширением внутренних (intrinsic) функций (к ним относятся в основном функции исполнительной библиотеки для работы со строками, такие, как strncpy, memset и другие).
link |  |  | Синтаксис: #pragma link "имяфайла" Заставляет компоновщик подключить к исполняемому модулю указанный объектный файл.
message |  |  | Синтаксис: #pragma message ("текст"...) ttpragma message текст Выдает сообщение при компиляции.
nopushoptwarn |  |  | Подавляет предупреждение о том, что опции компилятора, имевшиеся в начале обработки текущего "файла, не были восстановлены к концу его компиляции (см. ниже о #pragma option).
obsolete |  |  | Синтаксис: #pragma obsolete имяфункции Выдает предупреждение о том, что данная функция является устаревшей (если имеются обращения к ней). Директивой можно информировать других программистов, что вы усовершенствовали свой код и предусмотрели новую функцию для данной задачи.
option | Синтаксис: #pragma option опции #pragma option push опции #pragma option pop Директива позволяет указать необходимые опции командной строки прямо в коде программы. Форма option push сначала сохраняет текущие установки в стеке компилятора; option pop, соответственно, выталкивает из стека последний набор опций.
pack | Синтаксис: #pragma pack(n) #pragma pack (push, n) #pragma pack(pop) Задает выравнивание данных в памяти и эквивалентна ftpragma option -an.
package | Синтаксис: #pragma package(smart init) #pragma package(smart init, weak) Управляет порядком инициализации модулей в пакетах C++Builder; по умолчанию включается в начало каждого автоматически создаваемого модуля.
resource | Синтаксис: tpragma resource "*.dfm" Текущий файл помечается как модуль формы; в текущем каталоге должны присутствовать соответствующий dfrn-файл и заголовок. Всеми этими файлами IDE управляет автоматически.
startup | Аналогична pragma exit; позволяет специфицировать функции, исполняющиеся при запуске программы (перед вызовом main). Функции с более высоким приоритетом вызываются раньше.
warn | Позволяет управлять выдачей предупреждений. Синтаксис: #pragma warn +|-\.www Www может быть трехбуквенным или четырехзначным цифровым идентификатором конкретного сообщения. Предшествующий ему плюс разрешает выдачу предупреждения, минус запрещает, точка — восстанавливает исходное состояние.

О различных ключах командной строки (и эквивалентных установках диалога Project Options) мы расскажем в разделе об особенностях компилятора.

Типичное применение препроцессорных директив

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

Предотвращение включения файлов

Иногда при использовании заголовков может происходить дублирование кода из-за повторного включения некоторого файла. (Допустим, у вас имеется исходный файл myprog.c, который подключает директивой # include два заголовка headerl.h и header2.h. Если, в свою очередь, оба этих файла подключают некоторый headerO.h, то последний будет дважды включен в исходный файл myprog.c. Это ни к чему, хотя обычно и не приводит к ошибке.)

Чтобы предотвратить повторное включение кода заголовочного файла, можно организовать контроль следующим образом (как говорят, “поставить часового”):

/*

** header0.h: Заголовочный файл, который может оказаться

** многократно включенным...

*/

#ifndef _HEADERO_H

#define _HEADERO_H

/* /

** Здесь идут макросы, определения типов

** и т.д. вашего заголовочного файла...

*/

*endif

Переключение разделов кода

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

/*

** Измените определение на 0, чтобы вернуться к старому варианту.

*/

*define NEW_VER I

#if NEW_VER /*

** Экспериментальный код.

*/

#else /*

** Старый код.

*/

*endif

Или, если не вводить дополнительный идентификатор:

/*

** Измените на 1, когда новый код будет отлажен.

*/

*if 0 /*

** Экспериментальный код.

*/

#else /*

* * Старый код.

*/

*endif

Отладочные диагностические сообщения

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

#define INFO(msg)

printf(#msg "\n")

#define DIAG(msg)

printf("File " _FILE_ " Line %d: " \ #msg "\n", _LINE_)

void SomeFunc(void)

{

INFO(Entering SomeFunc.);

/* Выводит информационное сообщение. */

if (someError)

DIAG(Error encountered!);

/* Выводит сообщение об ошибке. */

INFO(Exiting SomeFunc...) ;

}

Макрос assert()

В заголовочном файле assert.h определен макрос assert (), выполняющий примерно то же самое, что и показанный выше пример. Его “прототип” можно записать как

void assert(int test);

Макрос расширяется в оператор if, проверяющий условие test. Если его значение равно 0, печатается сообщение Assertion failed: с указанием имени файла и номера строки. Вот пример:

#include <assert.h>

…

assert(!someError) ;

Если перед включением 41айла assert.h определить символ ndebug, операторы assert () будут “закомментированы”.

Особенности C++Builder

В этом разделе мы обсудим два вопроса: особенности реализации языка в C++Builder и управление компилятором в IDE (диалог Project Options).

Расширения языка С

C++Builder поддерживает использование ряда ключевых слов, отсутствующих в стандартных ANSI C/C++. В таблице 4.4 перечислены все такие ключевые слова, которые могут применяться в программах на С. Многие из них могут записываться с одним или двумя начальными символами подчеркивания либо без них. Это сделано для того, чтобы можно было переопределить в препроцессоре какое-либо ключевое слово (например, форму без подчеркивания), сохранив возможность использования исходного слова (в форме с подчеркиванием). Рекомендую вам всегда пользоваться формой с двумя подчеркиваниями.

Таблица 4.4. Расширения набора ключевых слов языка С

Ключевые слова | Описание
asm _asm __asm | Позволяет вводить код ассемблера непосредственно в текст программы на C/C++. Синтаксис: __asm операция операнды ;_ или перевод_ строки Можно сгруппировать сразу несколько инструкций ассемблера в одном блоке asm: __asm { группа_ инструкций }
cdecl _cdecl __cdecl | Специфицирует функцию как вызываемую в соответствии с соглашениями языка С. Перекрывает установки по умолчанию, сделанные в IDE или препроцессорных директивах.
_Except | Служит для управления исключениями в программах на С.
_Export __export | Служит для экспорта из DLL классов, функций или данных. (См. главу 2, где приведен пример построения DLL.)
_fastcall __fastcall | Специфицирует функцию как вызываемую в соответствии с соглашением fascall (передача параметров в регистрах).
_Finally | Служит для управления исключениями в программах на С.
_Import __import | Импортирует классы, функции или данные, находящиеся в DLL.
_Inline | Применяется для объявления в программах на С расширяемых функций (inline). Соответствует ключевому слову inline, которое имеется только в C++.
_Pascal __pascal ___pascal | Специфицирует функцию как вызываемую в соответствии с соглашениями языка Pascal.
_stdcall __stdcall | Специфицирует функцию как вызываемую в соответствии со стандартными соглашениями о вызове.
_Thread | Позволяет определять глобальные переменные, имеющие тем не менее отдельные копии для каждой из параллельно выполняющихся линий кода (threads).
_Try | Служит для управления исключениями в программах на С.

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

Соглашения о вызове

Соглашение о вызове определяет способ передачи параметров от вызывающей функции в вызываемую. То или иное соглашение может быть установлено по умолчанию в диалоге Project Options либо определяться модификатором в объявлении конкретной функции, например:

void _stdcall SomeDLLFunc(void);

Рассмотрим по порядку различные протоколы вызова, поддерживаемые в C+4-Builder.
  • Соглашение _cdecl является стандартным для программ на C/C++. Оно характеризуется тем, что аргументы при вызове помещаются на стек в порядке справа налево, и за очистку стека отвечает вызывающий. Кроме того, для функций _cdecl компилятор генерирует внутренние имена, начинающиеся с подчеркивания и сохраняющие регистр букв. Таким образом, внутренним именем функции SomeCFunc •будет _SomeCFunc.
  • Соглашение _pascal соответствует протоколу вызова функций в языке Pascal. Параметры помещаются на стек в порядке слева направо, а за очистку стека отвечает вызываемый. Внутреннее имя образуется переводом всех символов в верхний регистр; например, функция SomePascalFunc получит имя SOMEPASCALFUNC. Этот протокол вызова может быть более эффективен, чем _cdecl, особенно если функция вызывается из многих различных мест программы. Однако вызываемые таким образом функции не могут иметь переменного списка аргументов, как функции С.
  • Соглашение stdcall принято в 32-битных версиях Windows в качестве стандартного. Оно является своего рода гибридом двух предыдущих. Параметры помещаются в стек справа налево, однако за очистку стека отвечает вызываемый. Внутреннее имя совпадает с объявленным.
  • Соглашение fastcall широко применяется в визуальном программировании C++Builder, т. е. в библиотеке VCL. Первые три параметра, если это возможно, передаются в регистрах ЕАХ, ЕСХ и EDX. Параметры с плавающей точкой или структуры передаются через стек. Внутреннее имя образуется присоединением символа @; например, внутренним именем функции SomeFastFunc будет @SomeFastFunc.

Примечание

Несколько слов о стеке. На стеке сохраняется состояние процессора при прерываниях, распределяется память для автоматических (локальных) переменных, в нем сохраняется адрес возврата и передаются параметры процедур. Адресация стека (в 32-битных системах) производится посредством специальных адресных регистров процессора — указателя стека ESP и базы стека ЕВР. Адрес, на который указывает регистр ESP, называют вершиной стека. Основные операции при работе со стеком — это PUSH (втолкнуть) и POP (вытолкнуть). Операция PUSH уменьшает значение указателя стека и записывает последний по полученному адресу. Операция POP считывает значение со стека в свой

операнд и увеличивает указатель стека. (В 32-битном режиме адресации стек выравнивается по границе двойного слова, т. е. при операциях PUSH и POP значение ESP всегда изменяется на 4.) Таким образом, стек при заполнении расширяется сверху вниз, и вершина стека является на самом деле нижней его точкой, т. е. имеет наименьший адрес.

Посмотреть, что происходит на уровне машинного кода при различных типах вызовов, проще всего с помощью отладчика, о котором мы будем говорить в следующей главе. Можно также компилировать исходный модуль программы в код ассемблера, для чего придется запустить компилятор из командной строки с ключом -S:

bсс32.ехе -S myfile.c

Псевдопеременные

Псевдопеременные C++Builder служат представлением аппаратных регистров процессора и могут использоваться для непосредственной записи и считывания их содержимого. Регистровые псевдопеременные имеют такие имена:

_AL _AH _AX _ЕАХ

_BL _BH _ВХ _ЕВХ

_CL _CH _СХ __ЕСХ

_DL _DH _DX __EDX

_CS _DS _ES _SS

_SI _DI _ESI _EDI

_BP _SP _EBP _ESP

_FS _GS _FLAGS

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

Управление исключениями

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

Примечание

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

В стандартном С нет средств для управления исключительными ситуациями, однако в C++Builder имеются три дополнительных ключевых слова (_try, _except и _finally), которые позволяют организовать в программе т. н. структурированное управление исключениями, которое отличается от стандартного механизма исключений, встроенного в C++.

Мы не будем сейчас подробно обсуждать механизмы обработки исключений, отложив это до тех времен, когда мы будем изучать специфические средства языка C++. Сейчас мы покажем только синтаксис _try/_except/_finally с краткими пояснениями.

Возможны две формы структурированной обработки исключений:

try

защищенный_блок_операторов except(выражение) блок_обработки исключения

либо

_try

защищенный_блок_опера торов

finally

блок_обработки_завершения

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

EXCEPTION_EXECUTE_HANDLER EXCEPTION_CONTINUE_SEARCH EXCEPTION_CONTINUE_EXECUTION

Эти значения вызывают соответственно исполнение обработчика (блока __except), продолжение поиска обработчика (перевозбуждение исключения во внешнем блоке _try, если таковой имеется) или возобновление выполнения кода с той точки, где было возбуждено исключение.

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

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

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

void RaiseException(DWORD ее, DWORD ef, DWORD na,

const DWORD *a) ;

где еc — код исключения,

ef — флаг исключения (EXCEPTION_CONTINUABLE либо EXCE.PTI-

ONONCONTINUABLE),

па — число аргументов,

а — указатель на первый элемент массива аргументов.

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

В этом разделе мы рассмотрим установки диалога Project Options, имеющие отношение к программам на С. В основном это будет касаться страниц Compiler и Advanced Compiler этого диалога. Он открывается выбором Project | Options в главном меню.

Страница Compiler

Эта страница диалога показана на рис. 4.1.

В нижней части страницы вы видите две кнопки: Full debug и Release. Первая из них выполняет все установки параметров, позволяющие в полной мере использовать возможности отладчика C++Builder; вторая запрещает генерацию какой-либо отладочной информации и оптимизирует код для получения максимальной скорости выполнения. При изучении языка вам лучше всего воспользоваться кнопкой Full debug и не задумываться больше об установках, влияющих на отладку и эффективность кода.

Рис. 4.1 Страница Compiler диалога Project Options

Коротко о разделах страницы Compiler.
  • Группа радиокнопок Code optimization позволяет полностью отключить оптимизацию, задать оптимизацию по скорости или выбрать отдельные опции оптимизации, включив радиокнопку Selected и нажав Optimizations. При этом откроется окно диалога со списком опций, в котором, кстати, показаны эквивалентные ключи командной строки, управляющие оптимизацией.
  • Группа Warnings управляет выдачей предупреждений. Включив соответствующую радиокнопку, можно запретить все, разрешить все предупреждения или управлять выдачей отдельных предупреждений. Диалог Compiler Warnings также показывает ключи командной строки. Я бы советовал вам разрешить выдачу всех предупреждений. Грамотно написанный код должен транслироваться не только без ошибок, но и без всяких замечаний со стороны компилятора.
  • Раздел Pre-compiled headers управляет прекомпиляцией заголовочных файлов.

Примечание

Объем кода заголовочных файлов, включаемых в модуль исходный модуль, может достигать тысяч, если не десятков и сотен тысяч строк. К тому же часто эти заголовочные файлы включаются в каждый модуль проекта. Поскольку при разработке программы заголовочные файлы приходится изменять сравнительно редко (а стандартные заголовки вообще не меняются), имеет смысл компилировать все необходимые заголовки один раз и создать файл специального вида, который будет содержать всю необходимую “заголовочную” информацию в форме, обеспечивающей максимально быстрый доступ к ней. Компилятор C++Builder может генерировать такие файлы (с расширением .csm), •во много раз ускоряющие повторное построение проектов. Недостатком их можно считать разве что весьма большой размер — типичный файл прекомпилируемых заголовков может занимать от пяти до десяти мегабайт.

  • Кнопка None запрещает использование прекомпилируемых заголовков. Кнопка Use pre-compiled headers разрешает генерацию и использование файла компилированных символов (это другое название файлов .csm). Кнопка Cache pre-compiled headers заставляет компилятор кэшировать прекомпилируемые заголовки, т. е. хранить их информацию в памяти, а не загружать csm-файл заново, когда в этом возникает необходимость. Это полезно, когда вы транслируете сразу несколько файлов, но может и замедлять компиляцию, если память системы невелика. В поле редактирования File name задается имя файла компилированных символов. В поле Stop after можно ввести имя файла, после компиляции которого генерация прекомпилируемых заголовков прекращается. Это должен быть файл, включаемый в исходный модуль непосредственно, а не в другом заголовке (как, например, windows.h включает массу других заголовочных файлов). • Раздел Debugging управляет включением отладочной информации в объектные файлы, создаваемые компилятором (флажки Debug information и Line numbers). Кроме того, флажок Disable inline expansions позволяет запретить расширения inline-функций, т. е. непосредственную вставку кода функции на месте ее вызова. Это упрощает отладку.

Примечание

Если вы хотите отлаживать программу, то должны убедиться, что флажок Create debug information на странице Linker также установлен.

  • Раздел Compiling управляет общими аспектами компиляции. При помеченном флажке Merge duplicate strings компилятор сопоставляет встречающиеся литеральные строки и, если две или более строк совпадают, генерирует только одну строку. Это делает программу несколько более компактной, но может приводить к ошибкам, если вы модифицируете одну из строк. При установке флажка Stack frames компилятор генерирует стандартные кадры стека функций, т. е. стандартный код входа и возврата. Этот флажок должен быть установлен, если вы хотите отлаживать 'функции модуля. Если флажок сброшен, то для функций, не имеющих параметров и локальных переменных, будет генерироваться нестандартный, сокращенный код. При установке Treat enum types as ints компилятор отводит под перечисления 4-байтовое слово. Если флажок сброшен, отводится минимальное целое (1 байт, если значения перечислимого типа лежат в диапазоне 0-255 или -128-127). Show general messages разрешает выдачу общих сообщений компилятора (не являющихся предупреждениями или сообщениями об ошибках). Флажок Extended error information разрешает выдачу расширенных сообщений об ошибках компилятора (вплоть до контекста синтаксического анализатора и т. п. — простому человеку ни к чему).

Страница Advanced Compiler

Эта страница (рис. 4.2) позволяет управлять деталями генерации объектного кода.

Рис. 4.2 Страница Advanced Compiler диалога Project Options

  • Группа радиокнопок Instruction set задает тип процессора целевой системы. Установки этой группы эквивалентны ключам командной строки -3, -4, -5 и -6.
  • Группа Data alignment управляет выравниванием данных в памяти. Выравнивание может отсутствовать (Byte), либо данные могут располагаться по адресам, кратным 2, 4 или 8 байтам. В структурах, если необходимо, вводятся байты заполнения. Установки группы эквивалентны ключам командной строки -an, где п — 1, 2, 4 или 8.
  • Группа Calling convention задает соглашение о вызове, применяемое по умолчанию. (Register обозначает соглашение _fastcall.) Эквивалентные ключи командной строки — -рс (или -р-), -р, -рг и -ps.
  • Группа Register variables управляет созданием регистровых переменных. None запрещает их использование. Automatic разрешает компилятору размещать переменные в регистрах там, где это целесообразно, и Register keyword разрешает размещать в регистрах только переменные, объявленные как register. Радиокнопки соответствуют ключам -г-, -г и -rd.
  • Раздел Output имеет два флажка: Autodepedency information и Generate
  • underscores. Первый из них определяет, будет ли включаться в объектный файл информация о зависимостях между исходными файлами (необходимо для работы команды Make). Второй флажок указывает, что имена функций _cdecl должны снабжаться начальным символом подчеркивания. Установленные флажки соответствуют ключам -Х- и -и.
    •
  • Раздел Floating point управляет генерацией кода арифметики с плавающей точкой. None показывает, что ваш код не использует чисел с плавающей точкой. Fast исключает промежуточные преобразования типов при арифметических вычислениях, как явные, так и неявные;

при сброшенном флажке все преобразования выполняются в строгом соответствии с правилами ANSI С. Correct Pentium FDIV генерирует код, исключающий возможность ошибки из-за дефекта в ранних версиях процессора Pentium. Соответствующие ключи командной строки — -f-, -ff и -fp.
  • Группа радиокнопок Language compliance задает версию языка, с которой совместим компилируемый исходный код. Вы можете выбрать стандартный ANSI С, С с расширениями Borland, “классическую” версию Кернигана-Ричи и язык Unix V. При написании приложений Windows имеет смысл выбрать либо ANSI, либо Borland, если вы хотите использовать какие-либо ключевые слова из расширенного набора, описанного в этой главе. Флажкам соответствуют ключи -A (ANSI), -AT или -A- (Borland), -АК (К & R) и -AU (Unix V).
  • Раздел Source управляет некоторыми аспектами интерпретации исходного кода. Флажок Nested comments разрешает вложенные С-комментарии, т. е. конструкции вида /*.../*...*/...*/. MFC compatibility позволяет транслировать код библиотеки MFC, используемой компилятором Microsoft Visual С. Поле Identifier length задает максимальное число значимых символов в идентификаторах языка С (в C++ длина идентификаторов не ограничивается).

Страница Directories/Conditionals

На этой странице диалога Project Options (рис. 4.3) расположены несколько полей редактирования, позволяющих задавать стандартные каталоги по умолчанию — библиотек, заголовочных файлов и т. д. Нас на этой странице интересует сейчас только раздел Conditionals.

В поле Conditional defines можно определять символы C/C++, языка Object Pascal и компилятора ресурсов, которые будут, например, управлять директивами условной компиляции в исходных файлах. Для присвоения символам значений используется знак равенства. Можно ввести в это поле сразу несколько определений, отделяя их друг от друга точкой с запятой, например:

NDEBUG;ххх=1;yyy-YES

Для ввода определений можно также воспользоваться редактором строк, отрывающимся при нажатии кнопки с многоточием.

Рис. 4.3 Страница Directories/Conditronals

В командной строке символы определяются с помощью ключа -D:

bcc32 -DNDEBUG -Dxxx=l -Dyyy=YES ...

Примечание

Мы немного рассказали о ключах командной строки компилятора не столько для того, чтобы вы умели запускать bcc32.ехе вручную, а чтобы дать вам некоторые г начальные сведения, которые помогут вам разбираться в Ьрг-файлах проектов C++Builder. Полное руководство по запуску компилятора из командной строки вы можете найти в оперативной справке в разделах command-line compiler и command-line options.

Заключение

Эта глава завершает предварительный курс по С, включая имеющиеся в C++Builder различные расширения языка. Прежде чем заняться вплотную объектно-ориентированным программированием на C++, мы считаем целесообразным познакомить вас с принципами отладки программ в IDE, чем мы и займемся в следующей главе.


Глава 5. Отладка программ

Продавай мобильный контент со своего сайта

  • Глава 5. Отладка программ
    • Предварительные шаги
      • Отладочные пункты меню
    • Элементы отладки
      • Контрольные точки
      • Наблюдение за переменными
      • Другие инструменты отладки
      • Пошаговое исполнение кода

Глава 5. Отладка программ

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

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

Предварительные шаги

Прежде всего, для исследования отладчика нам понадобится программа. В листинге 5.1 показана тестовая программа, вызывающая функции DoSomeMath () и DoSort () . Первая из них довольно бессмысленна и включена в программу только для того, чтобы продемонстрировать плавающую арифметику и соглашение _pascal. Вторая представляет собой вариант пузырьковой сортировки, частично реализованный на языке ассемблера.

Примечание

При создании нового модуля debugC.C автоматически создается заготовка включаемого файла debugC.h, причем C++Builder сразу вводит в него директивы защиты от повторных включений, о которых мы говорили в прошлой главе. Кстати, главный исходный файл консольного проекта называется в 5-й версии Debug, bpf, а не Debug.срр

Листинг 5.1. Тексты программы Debug

/**********************************************************

* * Debug.срр: Главный файл проекта.

*/

#pragma hdrstop

#include <condefs.h>

USEUNIT("debugC.с") ;

#define main

/**********************************************************

* * debugC.h: Заголовок для модуля debugC.с.

*/

#ifndef debugCH

#define debugCH

double _pascal DoSomeMath(double r, double h);

void DoSort(int array[], int n) ;

*endif

/*******************************************

* * debugC.с: Программа для демонстрации отладчика.

*/

#pragma inline

#pragma hdrstop

#include <stdio.h>

#include "debugC.h"

const double Pi = 3.14159265;

#pragma argsused

int main(int argc, char *argv[])

{

double rad, vol;

int i, n = 8;

int iArr[8] = {-1, 23, 7, -16, 0, 11, 24, 3};

rad = 2.0;

vol = DoSomeMath(rad, 3.0);

printf("Volume = %10.6f\n", vol);

DoSort(iArr, n) ;

printf("Sorted array:");

for (i=0; i<n; i++)

printf("%6d", iArr[i]);

printf("\n") ;

return 0;

} /* main */

/************************************************

** Просто чтобы продемонстрировать вызов pascal.

*/

double _pascal DoSomeMath(double r, double h)

{

double s;

s = Pi * r*r;

return s * h;

} /* DoSomeMath */

** Сортировка с inline-ассемблером.

*/

void DoSort(int array[], int n)

{

int i, j;

for (i = n-1; i > 0; i-)

for (j = 0; j < i; j++)

_asm {

push esi

mov ecx, j

mov eax, array

mov edx, [eax+ecx*4]

mov esi, [eax+ecx*4+0x04]

cmp edx, esi

jle skip

mov [eax+ecx*4], esi

mov [еах+есх*4+0хб4], edx

skip:

pop esi }

} /* DoSort */

Прежде чем компилировать программу, нужно убедиться, что сделаны все необходимые установки проекта (диалог Project Options) и отладчика (диалог, вызываемый выбором Tools | Debugger Options... в главном меню).

Открыв уже известный вам диалог Project Options на странице Compiler, нажмите кнопку Full debug. Будут установлены все параметры компилятора и компоновщика, необходимые для отладки.

Диалог Debugger Options, показанный на рис. 5.1, имеет четыре страницы, из которых нам пока понадобится только одна — General. Рекомендую вам пометить на этой странице флажки Inspectors stay on top и Rearrange editor local menu on run — просто для удобства. При установленном втором флажке, например, контекстное меню редактора при запуске программы преобразуется таким образом, чтобы упростить доступ к пунктам управления отладкой.

Рис. 5.1 Страница General диалога Debugger Options

Примечание

Если вы хотите во время отладки иметь доступ к исходным текстам библиотеки VCL, то нужно установить флажок Use debug libraries на странице Linker диалога Project Options. Правда, компоновка отладочных библиотек может значительно замедлить компиляцию, поэтому не стоит прибегать к этому средству без необходимости.

Теперь я предлагаю вам посмотреть на различные меню, имеющие отношение к отладке.

Отладочные пункты меню

При отладке вам понадобится обращаться в основном к трем меню; это каскадное меню View | Debug Windows, меню Run и контекстное меню редактора кода. Пункты этих меню для управления отладкой приведены ниже в таблицах 5.1 - 5.3.

Таблица 5.1. Пункты меню Viev | Debug Windows

Пункт | Клавиша | Описание
Breakpoints | Ctrl+Alt+B | Открывает окно списка контрольных точек, показывающее активные контрольные точки и их свойства.
Call Stack | Ctrl+Alt+S | Открывает окно стека вызовов. Стек показывает, какие и в каком порядке вызывались функции, прежде чем управление достигло текущей точки программы.
Watches | Ctrl+Alt+W | Открывает окно наблюдения за переменными. Окно отображает список наблюдаемых переменных с их текущими значениями.
Local Variables | Ctrl+Alt+L | Открывает окно локальных переменных. В нем отображаются значения всех локальных переменных текущей функции.
Threads | Ctrl+Alt+T | Окно активных процессов и линий потока управления (threads).
Modules | Ctrl+Alt+M | Окно загруженных модулей — исполняемых файлов, динамических библиотек и пакетов запущенного проекта.
Event Log | Ctrl+Alt+E | Отображает протокол событий, происходящих при запуске проекта; какие события будут регистрироваться, можно задать на странице Event Log диалога Debugger Options.
CPU | Ctrl+Alt+C | Открывает окно состояния процессора. Отображает, в частности, компилированный код программы и содержимое регистров.
FPU | Ctrl+Alt+F | Открывает окно состояния FPU, отражающее содержимое его регистров и флагов.

Таблица 5.2. Пункты меню Run

Пункт | Клавиша | Описание
Run | F9 | Запускает программу, при необходимости производя перед этим её сборку (Make).
Attach to Process... |  | Прикрепляет отладчик к уже выполняющемуся в данный момент процессу.
Parameters... |  | Позволяет ввести аргументы командной строки или указать приложение, которое является “хозяином” отлаживаемой DLL.
Step Over | F8 | Исполняет текущую строку исходного кода и переходит к следующей строке.
Trace Into | F7 | Исполняет текущую строку исходного кода; если строка содержит вызов функции, переходит к трассировке последней.
Trace to Next Source Line | Shift+F7 | Исполняет программу до следующей строки исходного кода. Например, если программа вызывает функцию API, требующую возвратно-вызываемой процедуры, отладчик остановит выполнение на входе в эту процедуру.
Run to Cursor | F4 | Исполняет программу до строки исходного кода, в которой установлен курсор редактора.
Run Until Return | Shift+F8 | Исполняет программу до возврата из текущей функции
Show Execution Point |  | Устанавливает курсор редактора кода на строку, в которой приостановлена программа.
Program Pause |  | Приостанавливает выполнение программы, как только управление попадает в наличный исходный код.
Program Reset | Ctrl+F2 | Закрывает программу.
Inspect... |  | Открывает диалог Inspect, в котором можно ввести имя инспектируемого объекта.
Evaluate/Modify... | Ctrl+F7 | Открывает диалог Evaluate/Modify
Add Watch... | Ctrl+F5 | Открывает диалог Watch Properties
Add Breakpoint |  | Каскадное меню, позволяющее устанавливать контрольные точки различного вида (в исходном коде, на адресе, на данных, точки загрузки модуля).

Примечание

Для любой из вышеперечисленных команд'меню можно поместить на инструментальную панель соответствующую кнопку. (Откройте правой кнопкой мыши контекстное меню инструментальной панели и выберите Customize...; на странице Commands открывшегося диалога выберите нужную кнопку и. перетащите ее на инструментальную панель. Чтобы убрать с панели какую-нибудь кнопку, просто вытащите ее мышью за пределы главного окна C++Builder.) По умолчанию на панели инструментов размещены кнопки Run, Pause, Trace Into и Step Over.

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

Таблица 5.3. Отладочные пункты контекстного меню редактора

Пункт | Клавиша | Описание
Toggle Breakpoint | F5 | Переключает (устанавливает или сбрасывает) контрольную точку в строке, где находится курсор редактора.
Run to Cursor | F4 | То же, что и в меню Run.
Goto Address... |  | Позволяет указать адрес области памяти, которая будет отображаться в панели дизассемблера окна CPU.
Inspect... | Alt+F5 | Открывает окно инспекции объекта, на имени которого находится курсор.
Evaluate/Modify... |  | То же, что и в меню Run.
Add Watch at Cursor | Ctrl+F5 | Вносит в список наблюдения переменную, на имени которой находится курсор.
View CPU |  | То же, что Viev меню.| Debug Windows| CPU в главном

Ну а теперь мы поговорим о том, чем управляют все эти меню и какие вообще инструментальные средства отладки имеются в распоряжении программиста.

Элементы отладки

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

Контрольные точки

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

Контрольные точки в C++Builder 4 могут быть четырех видов: в исходном коде, на адресе, на данных и точки загрузки модуля.

Контрольные точки в исходном коде

Это самый распространенный вид контрольных точек. Точка представляет собой маркер, установленный на некоторой строке исходного кода. Когда управление достигает этой строки, программа приостанавливается.

Проще всего установить контрольную точку такого типа прямо в редакторе кода, щелкнув кнопкой мыши на пробельном поле редактора (слева от текста) рядом со строкой, на которой требуется приостановить программу. В пробельном поле появится красный маркер, и сама строка будет выделена красным цветом фона (рис. 5.2). Повторное нажатие кнопки мыши удаляет контрольную точку.

Рис. 5.2 Установка контрольных точек

Если теперь запустить программу кнопкой Run, она будет остановлена на контрольной точке (рис. 5.3).

Рис. 5.3 Остановка программы на контрольной точке

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

Текущая точка исполнения показана в пробельном поле зеленой стрелкой. Она указывает строку, которая должна исполняться следующей. Программу можно продолжить кнопкой Run или выполнять ее операторы в пошаговом режиме, о чем будет сказано ниже.

То, что мы сейчас показали — это простые контрольные точки в исходном коде; контрольные точки могут быть также условными, со счетчиком проходов или комбинированного типа.

Если вы в данный момент экспериментируете с отладчиком, откройте окно списка контрольных точек (View Debug Windows Breakpoints). Оно отображает все имеющиеся контрольные точки. Контекстное меню окна позволяет запретить остановку программы на контрольной точки, не удаляя ее (пункт Enable). Кроме того, выбрав пункт Properties..., вы получите доступ к свойствам выбранной точки (рис. 5.4 и 5.5).

Рис. 5.4 Окно Breakpoint List

Рис. 5.5 Диалог Source Breakpoint

В поле Condition диалога Source Breakpoint Properties можно задать условие остановки на контрольной точке. Условие может быть любым допустимым выражением языка C/C++, которое можно оценить как истинное или ложное. Остановка по достижении контрольной точки будет происходить только в том случае, если условие истинно.

Контрольные точки со счетчиком проходов можно считать разновидностью условных. Требуемое число проходов вводится в поле Pass count. Если число проходов установлено равным п, остановка программы произойдет только на п-ом проходе через контрольную точку. Точки со счетчиком удобны при отладке циклов, когда вам нужно выполнить тело цикла определенное число раз и только потом перейти к пошаговому выполнению программы.

Примечание

Счетчик может быть очень полезен, когда вам нужно определить, на каком проходе цикла возникает ошибка, вызывающая завершение программы. В окне списка контрольных точек отображается не только заданное, но и текущее число проходов точки (например, “7 of 16”). Задав число проходов, равное или большее максимальному числу итераций цикла, вы при завершении программы сразу увидите, сколько раз на самом деле он выполнялся.

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

Примечание

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

Адресные контрольные точки

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

Адресные контрольные точки и их свойства устанавливаются в диалоге, вызываемом командой Run I Add Breakpoint | Address Breakpoint... главного меню или из контекстного меню окна Breakpoint List. Установить адресную точку можно только во время исполнения программы или при ее остановке (например, в другой контрольной точке). При дальнейшем выполнении программы отладчик приостановит ее на инструкции с указанным адресом. Если эта инструкция соответствует некоторой строке исходного кода, контрольная точка будет показана в окне редактора. В противном случае она будет отображена в панели дизассемблера окна CPU.

Контрольные точки данных

Контрольные точки на данных также устанавливаются при запущенной программе в диалоге, вызываемом командной Run | Add Breakpoint | Data Breakpoint... или Add Data Breakpoint в контекстном меню списка контрольных точек (рис. 5.6).

Рис. 5.6 Диалог Add Data Breakpoint

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

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

Контрольные точки загрузки модуля

Команда Run Add Breakpoint | Module Load Breakpoint... открывает диалог Add Module, в котором задается имя файла (.exe, .dll, .осх или .bpl) для внесения его в список окна Modules. Загружаемые в память во время исполнения программы модули заносятся в это окно автоматически, однако если вы хотите, чтобы загрузка модуля вызывала остановку, то должны вручную ввести имя файла в список окна Modules до того, как модуль будет загружен (например, перед запуском программы).

На рис. 5.7 показано окно Modules. Добавить новый модуль в окно можно и через его контекстное меню.

(левая половина окна)

(правая половина окна)

Рис. 5.7 Окно Modules

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

Команда Run to Cursor

Если установить курсор редактора кода на некоторую строку исходного кода и запустить программу командой Run to Cursor главного или контекстного меню редактора (можно просто нажать F4), то курсор будет играть роль “временной контрольной точки”. Достигнув строки, где находится курсор, программа остановится, как если бы там находилась простая контрольная точка.

Команда Pause

Выполняющуюся в IDE программу можно приостановить, выбрав в главном меню Run | Program Pause или нажав кнопку Pause на инструментальной панели. Это более или менее эквивалентно остановке в контрольной точке. Если адресу, на котором остановилось выполнение, соответствует доступный исходный код, он будет показан в редакторе. В противном случае будет открыто окно CPU, отображающее машинные инструкции компилированной программы.

Наблюдение за переменными

Итак, вы остановили программу в контрольной точке. Обычно затем смотрят, каковы значения тех или иных переменных. Это называется наблюдением переменных (watching variables).

В IDE имеется специальное окно списка наблюдаемых переменных (рис. 5.8). Его можно открыть командой View | Debug Windows | Watches и ввести в него любое число переменных.

Рис. 5.8 Окно Watch List

Проще всего добавить переменную в список наблюдения можно, поместив курсор редактора кода на ее имя и выбрать в контекстном меню редактора Add Watch at Cursor. В окне наблюдений будет показано имя переменной и ее текущее значение либо сообщение, показывающее, что переменная в данный момент недоступна или наблюдение отключено (<disabled>). Можно ввести в список и целое выражение, если выделить его в редакторе и вызвать контекстное меню. Альтернативным методом добавления переменных или выражений является выбор в контекстном меню окна наблюдений пункта Add Watch... (пункт Edit Watch... служит для редактирования свойств уже имеющегося в списке наблюдения). Будет открыт диалог Watch Properties (рис. 5.9).

Рис. 5.9 Диалог Watch Properties

Помимо выражения, которое будет наблюдаться, диалог позволяет задать формат представления его значения. Поле Repeat count определяет число отображаемых элементов, если наблюдаемый объект — массив.

Быстрый просмотр данных

Редактор кода имеет встроенный инструмент, позволяющий чрезвычайно быстро узнать текущее значение переменной или выражения. Он называется подсказкой оценки выражения. Достаточно на секунду задержать курсор мыши над именем переменной или выделенном выражением, и под курсором появится окошко инструментальной подсказки с именем переменной и ее текущим значением (рис. 5.10). Причем — в отличие от окна наблюдений — таким способом можно просматривать и переменные, находящиеся за пределами текущей области действия (поскольку здесь не может возникнуть неоднозначности с именами).

Рис. 5.10 Подсказка с оценкой элементов массива

Примечание

Выдачей подсказок управляет диалог Tools | Editor Options..., страница Code Insight (рис. 5.11). Чтобы разрешить отображение подсказок с оценками, следует убедиться, что флажок Tooltip expression evaluation помечен. Ползунок Delay задает задержку появления подсказок.

Эта страница управляет и другими “подсказочными инструментами” редактора кода, о которых мы расскажем при случае.

Рис. 5.11 Страница Code Insight диалога Editor Properties

Инспектор отладки

Инспектор отладки — это самый универсальный инструмент IDE для просмотра и модификации значений объектов данных, прежде всего объектов, принадлежащих классам. В их число входят и визуальные компоненты C++Builder. Они, в сущности, тоже не более чем представители классов, а инспектор отладки в этом случае является “инспектором объектов времени выполнения”.

Открыть инспектор отладки можно либо командой Run | Inspect... главного меню, либо из контекстного меню редактора, установив курсор на имени нужного объекта. На рис. 5.12 показан инспектор отладки, отображающий состояние помещенной на форму метки.

Примечание

Инспектор отладки может использоваться только после остановки программы в контрольной точке.

Инспектор отладки имеет три страницы: Data, Methods и Properties.

Страница Data показывает все элементы данных класса с их значениями; тип выбранного элемента отображается в строке состояния инспектора.

Страница Methods показывает методы (элементы-функции) класса. В некоторых случаях эта страница отсутствует, например, при инспекции переменных простых типов.

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

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

В приведенной ниже таблице перечислены пункты контекстного меню инспектора отладки.

Рис. 5.12 Инспектор отладки

Таблица 5.4. Пункты контекстного меню инспектора отладки

Пункт меню | Описание
Range... | Позволяет указать диапазон элементов массива, которые будут показаны.
Change... | Позволяет присвоить новое значение элементу данных. Если элемент можно изменять, в поле его значения имеется кнопка с многоточием. Ее нажатие эквивалентно команде Change.
Show Inherited | Помечаемый пункт, влияющий на объем отображаемой инспектором информации. Если помечен, то инспектор показывает не только элементы, объявленные в классе, но и унаследованные от базовых классов.
Inspect | Открывает новое окно инспекции для выбранного элемента данных. Это полезно при исследовании деталей структур, классов и массивов.
Descend | То же, что и Inspect, но выбранный элемент отображается в том же самом окне; нового окна инспекции не создается. Чтобы вернуться к инспекции прежнего объекта, выберите его в выпадающем списке в верхней части окна.
Type Cast... | Позволяет указать новый тип данных для инспектируемого объекта. Полезен в случаях, когда вы исследуете, например, указатель типа void*.
New Expression... | Позволяет ввести новое выражение, задающее инспектируемый объект.

Инспекция локальных переменных

Командой View | Debug Windows | Local Variables можно открыть окно локальных переменных (рис. 5.13).

Рис. 5.13 Окно Local Variables

Оно внешне похоже на окно Watch List, но работает автоматически, отображая все локальные переменные и параметры текущей функции. Кроме того, его контекстное меню имеет пункт Inspect, открывающий окно инспектора для выбранного элемента списка. Выпадающий список наверху позволяет выбрать контекст, для которого нужно отобразить локальные переменные. Список совпадает с содержимым окна Call Stack, которое описано ниже.

Другие инструменты отладки

В IDE имеются и другие инструменты отладки помимо описанных выше. Мы расскажем о них очень коротко, поскольку применять их приходится не слишком часто.

Диалог Evaluate/Modify

Этот диалог (рис. 5.14) служит для оценки выражений и изменения значений переменных. Его можно открыть командой Run | Evaluate/Modify или из контекстного меню редактора, установив курсор на нужной переменной или выделенном выражении.

Рис. 5.14 Диалог Evaluate/Modify

В поле Expression вводится выражение, которое требуется оценить. При нажатии кнопки Evaluate результат оценки отображается в поле Result. Если вы хотите изменить значение переменной, введите новое значение в поле New value и нажмите кнопку Modify.

Примечание

Диалог Evaluate/Modify можно использовать в качестве простого калькулятора, позволяющего вычислять арифметические выражения и оценивать логические условия. В выражениях можно смешивать десятичные, восьмеричные и шестнадцатеричные значения. Результат вычисления выводится всегда в десятичном виде, поэтому очень просто, например, перевести шестнадцатеричное число в десятичное. Введите численное выражение в поле Expression и нажмите Evaluate. Поле Result покажет результат вычисления.

Окно CPU

Это окно, показанное на рис. 5.15, открывается командой View | Debug Windows | CPU главного или View CPU контекстного меню редактора.

Окно имеет пять отдельных панелей. Слева вверху находится панель дизассемблера. Она показывает строки исходного кода (если в контекстном меню панели помечен флажок Mixed) и генерированные для них машинные инструкции. В окне CPU можно устанавливать контрольные точки, как в редакторе, и выполнять отдельные инструкции командами Step Over

Рис. 5.15 Окно CPU

и Trace Into. На рисунке вы видите фрагмент программы, приведенной в начале главы — заголовок цикла for и начало блока ассемблерных инструкций.

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

Под панелью дизассемблера расположена панель дампа памяти. Панель справа внизу показывает “сырой” стек программы. Положение указателя стека соответствует зеленой стрелке. Каждая из панелей окна CPU имеет свое собственное контекстное меню, позволяющее выполнять все необходимые операции. Мы не будем здесь подробно разбирать отладку с помощью окна CPU, поскольку, чтобы им пользоваться, нужно хорошо знать язык ассемблера. Однако, если вы имеете о нем хотя бы смутное представление, вам будет интересно посмотреть на инструкции, которые генерирует компилятор. Разобраться в них не так уж и трудно, и иногда это помогает написать более эффективный исходный код.

У отладчика имеется также окно FPU, отображающее состояние процессора плавающей арифметики.

Стек вызовов

Окно стека вызовов (рис. 5.16) открывается командой View Debug Windows 1 Call Stack.

Рис. 5.16 Окно Call Stack

В окне показан список функций, вызванных к данному моменту и еще не завершившихся. Функция, вызванная последней, находится вверху списка. Для функций, имена которых неизвестны, указывается только адрес и имя модуля, как, например, для третьей сверху функции на рисунке. Если дважды щелкнуть на имени некоторой функции, в редакторе будет показан ее исходный код или, если он недоступен, будет открыто окно CPU на соответствующем адресе.

Исследование стека вызовов бывает полезно при возникновении ошибок типа нарушения доступа. На вершине стека будет находиться функция, получившая управление непосредственно перед ошибкой.

Команда Go to Address

Эта команда позволяет отыскать в исходном коде строку, соответствующую некоторому адресу, например, адресу инструкции, вызвавшей ошибку. Если выбрать Goto Address в меню Search или контекстном меню редактора кода (программа должна быть запущена), то появится диалоговая панель, в которой вводится нужный адрес. Отладчик попытается найти соответствующий ему исходный код, который в случае успешного поиска будет показан в редакторе. Если адрес находится за пределами вашего кода, будет выведено сообщение о том, что адрес не может быть найден.

Команда Program Reset

Иногда отлаживаемая программа “зависает” так, что никаким образом нельзя довести ее до сколько-нибудь нормального завершения. В этом случае можно прибегнуть к команде Run | Program Reset, которая аварийно завершает программу приводит ее в исходное состояние. Нужно сказать, что это крайнее средство и не следует им пользоваться просто для того, чтобы побыстрее закончить сеанс отладки. Windows этого не любит, и после команды Program Reset с IDE и системой могут происходить странные вещи.

Пошаговое исполнение кода

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

Эти команды могут выполняться из главного меню Run или с помощью кнопок инструментальной панели.

Step Over

Команда Step Over выполняет оператор, на котором приостановлено выполнение, и останавливается на следующем по порядку операторе. Текущий оператор выделяется в редакторе кода синим фоном и помечается зеленой стрелкой в пробельном поле. Если текущий оператор содержит вызов функции, она выполняется без остановок, и текущим становится первый оператор, следующий за возвратом из функции. Step Over “перешагивает” через вызов функции.

Trace Into

Команда Trace Into эквивалентна Step Over в случае, когда текущий оператор не содержит вызовов функций. Если же оператор вызывает некоторую функцию, то отладчик по команде Trace Into переходит на строку ее заголовка (заголовок при желании тоже можно рассматривать как исполняемый оператор, ответственный за инициализацию локальных переменных-параметров). При следующей команде (все равно — Step Over или Trace Into) текущим станет первый исполняемый оператор тела функции. Trace Into “входит внутрь” функции.

Примечание

При выполнении команд Step Over и Trace Into в окне CPU отладчик исполняет не операторы, а отдельные машинные инструкции.

Заключение

Как видите, мы уделили отладчику IDE довольно много внимания. Но одно изучение, так сказать, “теории” отладки не научит вас отыскивать причины ошибок в неправильно работающей программе. Даже при наличии такого мощного инструмента, как отладчик C++Builder, отладка все равно остается чем-то вроде искусства. А любое искусство требует, во-первых, овладения техникой, т. е. знания имеющихся в вашем распоряжении средств, а во-вторых, опыта, приобретаемой в результате практической работы с отладчиком. Поэтому я советую вам не жалеть времени на изучение отладчика и побольше экспериментировать.


Часть II. Объектно-ориентированное программирование и язык C++

Продавай мобильный контент со своего сайта

  • Часть II. Объектно-ориентированное программирование и язык C++
    • Глава 6. Модифицированный С
      • Отличия C++от ANSI С
        • Указатели типа void*
        • Символьные типы
        • Символьные константы
        • Глобальные константы
      • "Улучшенный С"
        • Дополнительные обозначения операций
        • Тип boot
        • Тип wchar_t
        • Модификатор const
        • Объявления переменных
        • Имена-этикетки
        • Анонимные объединения
        • Аргументы по умолчанию
        • Ссылки
        • Расширяемые функции
        • Операция разрешения области действия
        • Пространства имен
        • Перегружаемые функции
        • Операции распределения памяти

Часть II. Объектно-ориентированное программирование и язык C++

  • Модифицированный С
  • Объектно-ориентированное программирование
  • Классы C++
  • Потоки ввода-вывода
  • Шаблоны
  • Библиотека стандартных шаблонов
  • Управление исключениями
  • RTTI и приведение типов

В этой части мы будем изучать объектно-ориентированное программирование на C++. Конечно, C++ не единственный язык, работающий с классами и объектами. Есть Object Pascal, есть Java, есть и более старые языки вроде Smalltalk. Поэтому, наверное, имеет смысл поговорить и об общих принципах организации подобных языков.

Помимо общих принципов объектной архитектуры и классов языка C++ мы поговорим о таких вещах, как стандартные потоки, исключения, шаблоны и RTTI. К концу этой части книги вы узнаете практически все о стандартном ANSI C++, и в части III мы перейдем к изучению средств визуального программирования C++Builder.

Глава 6. Модифицированный С

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

Отличия C++ от ANSI С

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

Указатели типа void*

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

SomeStruct *sPtr;

void AllocBuffer(int n)

{

sPtr = malloc(n * sizeof(SomeStruct));

}

вызовет ошибку в C++. Необходимо явное приведение типа указателя:

sPtr = (SomeStruct *)malloc(n * sizeof(SomeStruct));

Символьные типы

В языке С тип char эквивалентен либо signed char, либо unsigned char; какому именно, определяется реализацией или установками компилятора. Обычно считается, что char — это signed char. В C++ char.

Примечание

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

void OutC(char с)

{

printf("Unspec: %c\n", c);

}

void OutC(signed char c)

{

printf("Signed: %c\n",'c);

}

void OutC(unsigned char c)

{

printf("Unsigned: %c\n", c);

}

Для сравнения отметим, что перегрузить подобным образом функции для типа int невозможно:

void OutI(int i)

{

printf("Unspec: %d\n", i);

}

void OutI(signed int i)

{

printf("Signed: %d\n", i);

} void OutI(unsigned int i)

{

printf("Unsigned: %d\n", i);

}

Такие определения вызовут сообщение об ошибке компиляции, поскольку типы int и signed int эквивалентны.

Символьные константы

Типом символьной константы (символ, заключенный в апострофы) в С является int, В языке C++ символьная константа имеет тип char. Конечно, это различие практически несущественно, но можно придумать случай, когда из-за него код С окажется несовместимым с C++.

Глобальные константы

В С глобальная константа, т. е. инициализированная глобальная переменная с модификатором const, имеет своей областью действия всю программу (доступна для внешней компоновки), как и любая переменная без const. Другими словами, ее имя заносится в список глобальных символов объектного модуля и поэтому к ней можно обращаться из любого другого исходного файла программы.

В C++ областью действия глобальных констант является текущий файл, аналогично глобальным переменным static. Для того, чтобы к глобальной константе можно было обращаться из других компилируемых модулей, она должна быть определена как extern const, например:

extern const double eConst = 2.718281828;

В модулях, которые будут обращаться к такой константе, она, как и в С, должна быть объявлена внешней:

extern const double eConst;

Такое поведение глобальных констант C++ упрощает их объявление. Можно поместить определение константы (без extern) в заголовочный файл и включать его во все исходные модули, где она используется. Тем самым будет генерированы отдельные константы для каждого модуля, с одним именем и одинаковыми значениями. В С такие объявления вызывали бы ошибку дублирования определения.

“Улучшенный С”

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

Дополнительные обозначения операций

Для ряда операций, в основном логических, в ANSI C++ введены альтернативные обозначения (ключевые слова) в стиле языка Pascal. Это было сделано потому, что на некоторых национальных клавиатурах трудно вводить знаки вроде “^” или “~”. Ниже перечислены все такие обозначения.

Ключевое слово | Эквивалентный знак | Операция
and | && | Логическое AND
and eq | &= | Присвоение поразрядного AND
bitand | & | Поразрядное AND
bitor | | | Поразрядное OR
coiripl | ~ | Поразрядное NOT (дополнение до 1)
not | ! | Логическое NOT
not eq | ! = | Отношение “не равно”
or | || | Логическое OR
or eq | |= | Присвоение поразрядного OR
xor | ^ | Поразрядное исключающее OR
xor eq | ^= | Присвоение исключающего OR

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

Тип bool

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

#define FALSE0

#define TRUE 1

int done = FALSE;

while (!done) { // И т.д...

}

Теперь в ANSI C++ есть тип bool, позволяющий объявлять переменные специально булева типа. Кроме того, для представления булевых значений имеются предопределенные константы true и false. Внутренним представлением true является 1, представлением fal5e .— 0. С их помощью можно присваивать значения булевым переменным:

bool done;

const bool forever = true;

done = false;

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

bool Continue(bool showPrompt);

В условиях циклов и выражениях условных операторов булевы переменные ведут себя точно так же, как прежние “логические” типа int.

Тип wchar_t

Это еще один новый встроенный тип C++. Он предназначен для хранения “широких” символов (в противоположность “узким” символам типа char). Тип wchar_t имеет достаточный диапазон, чтобы отображать очень большие наборы символов — вплоть до китайских иероглифов. В С++Вuilder он эквивалентен типу short int.

Модификатор const

В языке С модификатор const означает, что значение переменной после инициализации не может быть изменено. В C++ переменные с const рассматриваются как истинные константные выражения. Поэтому в отличие от С в C++ допускается их использование в объявлении массивов:

const int aSize = 20 * sizeof(int);

char byteArray[aSize];

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

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

#include <stdio.h>

int main(void) {

int n = 10;

printf("Hello! i =") ;

int i;

for (i=0; i<n; i++)

{

printf("%4d", i);

}

printf("\nAfter loop i = %d\n", i);

return 0;

Счетчик i объявляется непосредственно перед заголовком цикла for, а не в начале блока.

Примечание

Можно объявлять переменную счетчика прямо в заголовке цикла, как это часто и делается:

for (int i=0; i<n; i++) {...}

Ранее считалось, что при таком объявлении i остается доступной и после завершения цикла. Но стандарт ANSI постулирует, что область действия объявленной в заголовке цикла переменной ограничивается телом цикла. То же самое относится к циклам while.

Имена-этикетки

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

struct Person {

struct Person *link;

char firstName[32];

char lastName[32];

};

struct Person aPerson;

или введения нового имени с помощью typedef достаточно будет написать

struct Person {

Person *link;

char firstName[32];

char lastName[32] ;

};

Person aPerson;

Person, таким образом, будет настоящим именем типа.

Анонимные объединения

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

Глобальные анонимные объединения должны объявляться как static.

Аргументы по умолчанию

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

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

Вот небольшая иллюстрация:

//////////////////////////////////////////////////

// Defaults.срр: Аргументы по умолчанию.

//

#include <stdio.h>

int Sum(int a, int b, int с = 0, int d = 0);

//Корректный прототип.

//

// Следующий прототип был бы некорректным:

// int Sum(int а, int b = 0, int с, int d = 0) ;

// int Sum(int a, int b, int с, int d)

// Определение функции.

{

return а + b + с + d;

}

int main(void) (

printf("l + 2 + 3 + 4 = %d\n", Sum(l, 2, 3, 4));

printf("1 + 2 + 3 = %d\n", Sum(l, 2, 3) ) ;

printf("1 + 2 = %d\n", Sum(1, 2));

//

// Недопустимый вызов:

// printf(" 1 + 2 + 4 = %d\n", Sum(l, 2,, 4)) ;

//

return 0;

}

Примечание

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

Главная функция определяется в примерах как int main (void), но это не существенно, и если вы захотите посмотреть, как они работают, можно оставить заголовок, который генерирует C++Builder.

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

Примечание

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

Ссылки

В C++ имеется модифицированная форма указателей, называемая ссылкой (reference). Ссылка — это указатель, который автоматически разыменовывается при любом обращении к нему. Поэтому ссылке, как таковой, нельзя присвоить значение, как это можно сделать с обычным указателем. Ссылку можно только инициализировать.

Ссылки как псевдонимы переменных

Переменная-ссылка объявляется со знаком “&”, предшествующим ее имени; инициализирующим выражением должно быть имя переменной. Рассмотрите такой пример:

///////////////////////////////////////////////////////////////////

// Alias.cpp: Ссылка как псевдоним.

//

#include <stdio.h>

int main(void)

{

int iVar = 7777;

// Инициализация целой переменной.

int *iPtr = siVar;

// Инициализация указателя адресом

// iVar.

int &iRef = iVar;

// Инициализация ссылки как

// псевдонима iVar.

int *irPtr = &iRef;

// Инициализация указателя "адресом"

// ссылки.

printf("iVar = %d\n", iVar) ;

printf("*iPtr = %d\n", *iPtr);

printff'iRef = %d\n", iRef);

printf("*irPtr = %d\n", *irPtr) ;

printff'iPtr = %p, irPtr = %p\n", iPtr, irPtr);

return 0;

}

Первые четыре оператора printf выводят одно и то же число 7777. последний оператор печатает значения двух указателей, первый из которых инициализирован адресом переменной, а второй адресом ссылки. Эти адреса также оказываются одинаковыми. Как видите, после инициализации ссылки ее имя используется в точности как имя ассоциированной с ней переменной, т. е. как псевдоним.

Еще раз подчеркнем, что ссылку после инициализации нельзя изменить; все обращения к ссылке будут относиться на самом деле к переменной, именем которой она была инициализирована.

Ссылки в качестве параметров функции

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

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

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

Тем самым параметры-ссылки делают возможной настоящую “передачу по ссылке” без использования явных указателей и адресов. Вот сравнение различных видов передачи параметров:

//////////////////////////////////////////////////

// RefPar.cpp: Передача параметров по ссылке.

//

#include <stdio.h>

void SwapVal(int x, int y)

// Передача значений.

{

int temp;

temp = x;

x = y;

у = temp;

}

void SwapPtr(int *x, int *y)

// Передача указателей.

{

int temp;

temp = *x;

*x = *y;

*y = temp;

}

void SwapRef(int &x, int &y)

// Передача ссылок.

{

int temp;

temp = x;

x = y;

y = temp;

}

int main(void)

(

int i = 11, i = 22;

printf("Initial values: i=%d,j=%d.\n", i,j ) ;

SwapVal(i, j);

printf ("After SwapVal():i = %d,j =%d.\n",i,j);

SwapPtr (&i, &j) ;

printf("After SwapPtr():i =%d,j=%d.\n",i,j);

SwapRef(i, j) ;

printf("After SwapRef():i =%d,j=%d.\n",i,j);

return 0;

Программа напечатает:

Initial values: i = 11, j = 22.

After SwapValO i = 11, j = 22.

After SwapPtr():i = 22, j = 11.

After SwapRef () i = 11, j =22.

Как и должно быть, функция SwapVal () не влияет на значения передаваемых ей переменных, поскольку она обменивает лишь локальные их копии. Функция SwapPtr () , принимающая указатели, обменивает значия исходных переменных, но требует явного указания адресов при вызове и явного их разыменования в теле переменной. Наконец, SwapRef () , с параметрами-ссылками, также обменивает значения переданных ей переменных. Причем она отличается от функции, принимающей параметры-значения, только заголовком. Тело SwapRef () и ее вызов выглядят совершенно аналогично функции SwapVal () .

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

Примечание

Передавая параметры по ссылке, нужно всегда помнить о возможных побочных эффектах. Если только функция не должна явно изменять значение своего аргумента, лучше объявить параметр как ссылку на константу (например, const int SiVal). Тогда при попытке присвоить параметру значение в теле функции компилятор выдаст сообщение об ошибке.

Ссылка в качестве возвращаемого значения

Возвращаемое функцией значение также может быть объявлено ссылкой. Это позволяет использовать функцию в левой части присваивания. Рассмотрите такой пример:

////////////////////////////////////////////////////////

// массива.

//

#include <stdio.h>

#include <assert.h>

const int arrSize = 8;

int &refItem(int indx)

// Возвращает ссылку на элемент

//

iArray[indx] {

static int iArray[arrSize];

//

// Проверка диапазона:

//

assert(indx >= 0 && indx< arrSize);

return iArray[indx];

}

int main(void) {

int i;

for (i=0; .KarrSise; i++)

refltem(i) = 1 << i;

// Присваивает значение элементу

// iArray[i].

for (i=O; i<arrSize; i++)

printf("iArray[%02d] = %4d\n'\ i, refltem(i));

refltem(i) = 0; // Попытка выхода за пределы

// массива.

return 0;

}

В первом из операторов for функция refltem() вызывается в левой части присваивания. Во втором for она возвращает значение, которое передается функции printf (). Обратите внимание, что, во-первых, массив iArray[] объявлен как статический локальный в refltem(), благодаря чему непосредственное обращение к нему вне этой функции невозможно. Во-вторых, refltem() попутно проверяет допустимость переданного ей индекса. Ссылка позволяет на элементарном уровне организовать механизм, который мы в следующей главе назовем сокрытием или абстрагированием данных.

Расширяемые функции

Определение функции транслируется компилятором в отдельный и относительно автономный блок объектного кода. При вызове функции в вызывающей процедуре (программе) генерируется инструкция CALL с предшествующим ей кодом, ответственным за передачу параметров, и последующим кодом очистки стека. Инструкция CALL передает управление коду функции, являющемуся внешним по отношению к вызывающей процедуре. Все это вы можете увидеть воочию в окне CPU отладчика.

По своем завершении функция возвращает управление инструкцией RET. Выполнение вызывающей процедуры возобновляется с инструкции, следующей за call.

Однако, если определить функцию с модификатором inline, компилятор, по крайней мере в принципе, генерирует встроенное расширение функции на месте вызова. Это напоминает расширение макроса. Тем самым из кода исключаются инструкции вызова, возврата, передачи параметров и т. д., что, естественно, уменьшает общее время выполнения. Но следует иметь в виду, что код функции будет воспроизводиться в исполняемом файле столько раз, сколько имеется операторов вызова в исходной программе. Если функция велика и вызывается из многих мест, объем исполняемого файла может заметно возрасти.

Вот простейший пример расширяемой функции:

#include <stdio.h>

inline int. Max(int a, int b)

{

return a > b? a : b;

}

int main(void)

int x = 11, у = 22;

printf("Max(%d, %d) = %d.\n", x, у, Мах(х, у)) ;

return 0;

}

Примечание

Если зы хотите, чтобы компилятор генерировал расширения iniine-функций, нужно сбросить флажок Disable inline expansions на странице Compiler диалога Project Options.

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

Прежде всего, в самом стандарте ANSI сказано, что модификатор inline является лишь рекомендацией компилятору генерировать встроенное расширение функции. Наподобие модификатора register, который рекомендует разместить переменную в регистре процессора. Это не всегда возможно, поэтому компилятор может безо всяких предупреждений игнорировать модификатор inline.

Привлекая самые общие соображения, можно сформулировать следующее правило: для того, чтобы вызов iniine-функций замещался ее расширением, компилятор должен видеть определение функции в точке ее вызова. Если он ее не видит, то рассматривает функцию как внешнюю, т. е. определенную ниже или вообще в другом файле. Современные компиляторы по большей части однопроходные. Термин не имеет отношения к животному миру Австралии, а означает, что компилятор сканирует текст транслируемого модуля единственный раз и не может “заглянуть вперед”.

Примечание

Компилятор Borland C++ не может расширять встроенные функции также в следующих случаях:

  • Если функция содержит операторы цикла или оператор выбора switch.
  • Если функция имеет тип не void и не содержит при этом оператора return.
  • Если она содержит встроенный код ассемблера.

В качестве иллюстрации рассмотрите такой пример:

/////////////////////////////////////////////////

// Inline.срр: Расширение inline-функций.

//

#pragma hdrstop

#include <condefs.h>

#include <stdio.h>

inline int Max(int, int);

int МахЗ(int, int, int);

int main(void) {

int x = 11, у = 22, z = 33, v = 44;

x = МахЗ(х, z, v) ;

z = Мах(х, у);

// Не расширяется - генерируется вызов!

printf("Max(%d, %d) = %d.\n", х, у, z);

return 0;

}

inline int Max(int a, int b) {

return a > b? a : b;

}

int МахЗ (int a, int b, int c)

{

b = Max(a, b);

// Эти вызовы расширяются как встроенные.

return Max(с, b) ;

}

Здесь функция Мах () определяется после main (), поэтому ее вызов из main () не расширяется как встроенный. Однако после определения Мах () дальнейшие ее вызовы (в Мах3 ()) уже расширяются. Воспользуйтесь отладчиком, чтобы это проверить — внутрь встроенной функции нельзя войти командой Trace Into; ее можно отлаживать только в окне CPU.

Примечание

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

Операция разрешения области действия

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

В C++ имеется операция разрешения области действия, позволяющая в такой ситуации обращаться к глобальной переменной, предваряя ее имя символами “ : : ”. Вот пример, поясняющий разрешение области действия:

#include <stdio.h> int aVar = 111; // Глобальная переменная.

int main(void)

{

int aVar == 222; // Локальная переменная.

printf("Local aVar is %d.\n", aVar);

printf("Global aVar is, %d.\n", ::aVar);

return 0;

}

Пространства имен

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

Язык C++ решает эту проблему, позволяя разбить проект на сегменты с различными пространствами имен (namespaces). Заключив свои символы в отдельное пространство имен, вы, по существу, снабжаете имя всякого типа, переменной и т. д. некоторым скрытым префиксом. Например, если определить пространство имен

namespace MYSPACE { int х;

}

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

MYSPACE::х = 11;

Пространства имен могут быть вложенными:

namespace OUTER { int x;

namespace INNER { int x;

}

Соответственно, чтобы ссылаться на эти переменные, нужно будет написать:

OUTER::х = 11;

OUTER::INNER::x = 22;

Примечание

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

Создание пространства имен

К созданию именных пространств можно отнести три момента:

  • Первоначальное объявление пространства.
  • Расширение пространства имен.
  • Определение анонимного пространства.

Первоначальное объявление пространства имен — это определение пространства с именем, еще не встречавшимся в программе. Синтаксис его следующий:

namespace новое_имя {тело пространства имен}

Тем самым в программе объявляется пространство имен с именем новое_имя.

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

namespace существующее_имя [тело_пространства__имен]

Тела всех пространств с одинаковыми именами, определенных на глобальном уровне, составляют одно именное пространство. Каждый идентификатор в его пределах должен быть уникален.

Синтаксис C++ позволяет определять анонимные пространства имен:

namespase{

int x;

double у;

}

Все анонимные пространства имен в глобальной области действия данного файла составляют единое целое. Говоря проще, объявление глобальных символов как относящихся к анонимному пространству имен эквивалентно их объявлению с модификатором static:

static int x;

static double у;

Доступ к пространству имен

Доступ к элементам конкретного пространства имен может осуществляться тремя способами:

  • Путем явной квалификации имени.
  • Посредством квалифицирующего объявления.
  • С помощью директивы using.

Примеры явной квалификации имен мы уже приводили чуть выше. Имя снабжается префиксом, состоящим из идентификатора (квалификатора) пространства имен со знаком операции разрешения области действия.

Квалифицирующее объявление выглядит следующим образом:

using иденгификатор_пространства_имен::имя;

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

namespace ALPHA {

int RetJ(int j) {return j;} } namespace BETA {

int RetJ(int j) {return (j * 2);}

}

int main(void) {

using BETA::RetJ; // Квалифицирующее объявление RetJ().

int x = 11;

printf("Calling RetJ(): %d\n"/ RetJ(x));

// Вызывает BETA::RetJ(). return 0;

}

Последняя форма доступа к пространству имен — директива using. Она имеет вид:

using namespace идентификатор пространства_имен;

В результате указанное пространство имен будет приниматься по умолчанию, если ссылка на некоторое имя не может быть разрешена локально (как и в общем случае, локальное объявление скрывает любое объявление в “более внешней” области действия). Вот пример, демонстрирующий различные моменты применения именных пространств:

#include <stdio.h>

namespace ALPHA {

int x = 11;

int у = 111;

int v = 1111;

} namespace BETA {

int x = 22;

int у - 222;

int z = 2222;

}

int main(void) {

using namespace ALPHA; // Конфликта не возникает до

using namespace BETA; // момента действительного

// обращения к элементам х и у.

using ВЕТА::х;

// Локальная квалификация х.

int z = 3333; // Локальная переменная.

printf ("Global х = %d, у = %d, z = %d, v = %d.\n", х, ALPHA::y, ::z, v);

// х квалифицирована

// локально, у - явно.

printf("Local z = %d.\n", z) ;

return 0;

}

Программа печатает:

Global х = 22, у = 111, z = 2222, v = 1111. Local z = 3333.

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

Псевдонимы именных пространств

Можно объявлять псевдонимы для именных пространств, если, например, полная квалификация символа оказывается слишком уж длинной:

namespace BORLAND_INTERNATIONAL

{

// Тело именного пространства...

namespace NESTED_BORLAND_INTERNATIONAL

{

// Тело вложенного именного пространства... }

// Псевдонимы:

namespace BI = BORLAND_INTERNATIONAL;

namespace NBI = BORLAND_INTERNATIONAL::

NESTED_BORLAND_INTERNATIONAL;

Перегруженные функции

В программировании то и дело случается писать функции для схожих действий, выполняемых над различными типами и наборами данных. Возьмите, например, функцию, которая должна возвращать квадрат своего аргумента. В C/C++ возведение в квадрат целого и числа с плавающей точкой — существенно разные операции. Вообще говоря, придется написать две функции — одну, принимающую целый аргумент и возвращающую целое, и вторую, принимающую тип double и возвращающую также double. В С функции должны иметь уникальные имена. Таким образом, перед программистом встает задача придумывать массу имен для различных функций, выполняющих аналогичные действия. Например, Square-Int() и SquareDbl() .

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

Рассмотрите следующий пример с вышеупомянутыми квадратами. Мы предусмотрели еще “возведение в квадрат” строки, когда результатом функции должна быть строка, в которой любые символы, кроме пробелов, удваиваются.

#include <stdio.h>

int Square(int arg)

{

return arg*arg;

}

double Square(double arg)

{

return arg*arg;

char *Square(const char *arg, int n)

{

static char res[256];

int j = 0;

while (*arg && j < n) { if (*arg != ' ') res[j++] = *arg;

res[j++] = *arg++;

}

res[j] = 0;

return res;

}

int main(void)

{

int x = 11;

double у = 3.1416;

char msg[] = "Output from overloaded Function!";

printf("Output: %d, %f, %s\n", Square (x) , Square (y) , Square (msg, 32) ) ;

return 0 ;

}

}

Результат работы программы показан на рис. 6.1.

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

Рис. 6.1 Пример с тремя перегруженными функциями

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

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

Как это делается

Ничего сложного в механизме перегруженных функций, конечно, нет. Перегруженные функции являются, по сути, совершенно различными функциями, идентифицируемыми не только своим именем (оно у них одно и то же), но и списком параметров. Компилятор выполняет т. н. декорирование имен. дополняя имя функции кодовой последовательностью символов, кодирующей тип ее параметров. Тем самым формируется уникальное внутреннее имя.

Вот несколько примеров того, как для различных прототипов производится декорирование имени функции:

void Func(void); // @Func$qv

void Func(int); // @Func$qi

void Func(int, int); // @Func$qii

void Func(*char); // 8Func$qpc

void Func(unsigned); // @Func$qui

void Func(const char*); // @Func$qpxc

Тип возвращаемого значения никак не отражается на декорировании имени.

Спецификация связи

Если функция, написанная на C++, должна вызываться из программы на С, Pascal или языке ассемблера (и наоборот, что часто бывает при использовании существующих динамических библиотек), то механизм декорирования имен C++ создает некоторые трудности. Они могут быть причиной сообщений об ошибках при компоновке вроде “Неопределенный символ ххх в модуле уyу”.

Чтобы обойти возникающие из-за декорирования проблемы, используется спецификация extern "С". Она применяется в двух случаях: во-первых, когда следует запретить декорирование имени определяемой вами функции, и во-вторых, когда вы хотите информировать компилятор, что вызываемая вами функция не соответствует спецификации связи C++.

Вот примеры:

// Функция, которую можно вызывать из С (прототип):

extern "С" void FuncForC(void);

// Прототип функции из некоторой библиотеки не на C++:

extern "С" void SomeExtFunc (int);

// Определение - extern "С" не требуется, если включен

// прототип:

void FuncForC(void)

{

printf("Hello!\n");

}

// Вызов внешней функции:

SomeExtFunc(count);

Примечание

Следует отличать спецификацию связи от соглашений o вызове. Функция, например, может вызываться в соответствии с соглашением Pascal (что означает определенный порядок передачи параметров и имя в верхнем регистре), но иметь спецификацию связи C++, т. е. декорированное имя. Алгоритм здесь такой: сначала формируется (если не указано extern "С") декорированное имя, а затем оно подвергается модификации в соответствии с соглаЕсли вы вызываете функции библиотек, написанных на С или языке ассемблера и у вас есть необходимые заголовочные файлы на С, то соответствующие директивы включения можно поместить в блок extern "С", как показано ниже. В противном случае нужно было бы модифицировать эти файлы, добавив спецификации связи к прототипам функций.

extern "С" { #include "asmlib.h" #include "someclib.h" }

Операции распределения памяти

В языке C++ для управления динамической памятью введены операции new и delete (для массивов new [ ] и delete [ ]). В С для этого применялись в C++, однако новые операции имеют некоторые преимущества.

Переопределение операций new и delete

Стандартные (глобальные) версии операций new и delete можно переопределить или перегрузить, чтобы придать им некоторые дополнительные свойства или обеспечить возможность передачи им дополнительных аргументов. Это бывает полезно при отладке, когда требуется проследить все выделения и освобождения динамической памяти:

#include <stdlib.h>

#include <stdio.h>

////////////////////////////////////////////////////

/ / Переопределение операций new и delete.

//

void* operator new(size_t size)

{

printf("%u bytes requested.\n", size);

return malloc(size);

void operator delete(void *ptr)

{

printf("Delete performed.\n") ;

free(ptr) ;

}

/////////////////////////////////////////////////////////////

// Перегрузка new для выдачи дополнительной информации.

//

void* operator new (size t size, char *file, int line)

printf("%u bytes requested in file %s line %d.\n", size, file, line) ;

return malloc(size);

}

int main(void) {

double *dptr = new double; // Вызывает новую new.

*dptr = 3.14159265;

delete dptr; // Вызывает новую delete.

// Вызов перегруженной new.

dptr = new(_FILE_, _LINE_) double;

*dptr = 1.0;

delete dptr;

return 0;

}

Примечание

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

Обратите внимание на синтаксис определения и вызова функций-операций new и delete. Операция new должна возвращать пустой указатель, а ее первым параметром всегда является число затребованных байтов. Компилятор автоматически вычисляет это число в соответствии с типом создаваемого объекта. Возвращаемый указатель приводится к этому типу.

Примечание

Перегруженная версия new может быть, кстати, определена таким образом, что она не будет выделять никакой новой памяти, а будет использовать уже существующий объект. Адрес, где нужно разместить “новый” объект, должен быть одним из дополнительных параметров функции-операции. Эта форма new известна как синтаксис размещения (placement syntax).

Размещающая форма new полезна, если требуется вызвать конструктор класса для уже существующего объекта или организовать какую-то специальную схему управления памятью.

Операция delete имеет тип void, а ее параметр — void*.

Обработка ошибок

В C++ имеется библиотечная функция 5et_new_handler().Он будет вызываться при любой ошибке выделения памяти.

#include <stdlib.h>

#include <stdio.h>

#include <new.h>

void MyHandler(void)

{

prir-tf("No memory!\n");

exit(l) ;

}

int main(void) {

set_new_handler (MyHandler) ; //Установка обработчика.

return 0;

}

Обработчик ошибок new должен:
  • либо возвратить управление, освободив память;
  • либо вызвать abort () или exit ();
  • либо выбросить исключение bad_alloc или производного класса.

Заключение

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


Глава 7. Объектно-ориентированное программирование

Продавай мобильный контент со своего сайта

  • Глава 7. Объектно-ориентированное программирование
    • Об объектном подходе к программированию
      • Объект
      • Класс
    • Введение в классы C++
      • Определение класса

Глава 7. Объектно-ориентированное программирование

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

Об объектном подходе к программированию

Существует разные подходы к программированию. Любому из них присущ свой собственный способ абстрагирования сущностей, с которыми он работает. Так, процедурно-ориентированный подход оперирует абстракцией алгоритма. С, кстати, — типичный процедурный язык, хотя на нем возможно писать программы, напоминающие по стилю объектно-ориентированные. Логико-ориентированный имеет в виду цели, выражаемые на языке логических предикатов (таков язык Prolog). Наконец, объектно-ориентированное программирование абстрагирует классы и объекты. В чем же состоит его суть?

Известный теоретик Грейди Буч так определяет этот подход:

“Объектно-ориентированное программирование — это методология программирования, основанная на организации программы, в виде совокупности объектов, каждый из которых является представителем определенного класса, а классы образуют иерархию наследования.”

Примечание

В “строго” объектно-ориентированных языках объектами (или их составными частями) являются решительно все элементы программы, в том числе она сама. В языке Java, например. Язык C++ таковым, кстати, не является хотя бы потому, что он сохраняет все возможности процедурного языка С. В C++ можно создать, например, совершенно отдельно стоящую глобальную переменную, да и сама функция main)) — совершенно “внеклассовая”. Такая универсальность C++ может быть как преимуществом, так и недостатком, если ею злоупотреблять. У программиста, впервые применяющего объектный подход, всегда имеется тенденция мыслить старыми, процедурными категориями.

Итак, центральным элементом абстракции объектно-ориентированной методологии является, очевидно, объект.

Объект

Объекты нашего программного мира моделируют объекты реального или воображаемого мира (например, мира некоторой игры, хотя это мир тоже мыслимый, а стало быть, реальный в смысле логики). Как модель, программный объект представляет собой некоторую абстракцию объекта реального, что предполагает выделение существенных свойств последнего и игнорирование тех, что безразличны с насущной, “сиюминутной”, точки зрения.

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

Наконец, заметим, что о программном объекте можно говорить точно так же, как о реальном. Мы можем не только говорить о нем, но и описывать на специальном формальном языке — языке программирования.

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

Состояние объекта

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

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

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

Поведение объекта

Поведение — это то, как объект взаимодействует с окружением (другими объектами). Объект может подвергаться воздействию окружения или сам воздействовать на него. Объект может рассматриваться как аналог предмета, а поведение — как реакция на манипуляции с ним или действия, инициированные самим объектом. В некоторых объектных системах (например, OLE 2) говорят о глаголах, т. е. действиях, которые могут связываться с объектом. Для каждого объекта существует определенный набор возможных действий над ним.

Действия в отношении к объектам иногда называют передачей сообщений между ними. В языках, подобных Object Pascal, операции над объектами называют обычно методами. В C++ благодаря его “процедурному наследству” чаще говорят о функциях-элементах объекта. Эти функции являются структурными элементами определения класса, к которому принадлежит объект.

Отношения между объектами

Естественно, программа, т. е. цельная система, реализуется только во взаимодействии всех ее объектов. Здесь можно выделить в основном два типа взаимодействий, или отношений: связь и агрегацию.

Связь является довольно очевидной разновидностью взаимодействий — один объект может воздействовать на другой, являющийся в известном смысле автономной сущностью. Тут существует отношение подчинения — “А использует В”. Один объект является активным, другой — пассивным. Понятно, что в системе один и тот же объект может выступать как в активной, так и в пассивной роли по отношению к различным объектам.

Другой тип отношений — агрегация, когда один объект является составной частью, т. е. элементом другого — “А содержит В”. Агрегация может означать физическое вхождение одного объекта в другой; в C++ это соответствует описанию первого объекта в качестве элемента данных другого объекта. Но это не обязательно. Например, в Windows есть понятие дочернего окна. Здесь имеет место отношение агрегации, хотя на физическом уровне родительское и дочернее окна автономны.

Класс

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

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

Наследование

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

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

Как и в логике, здесь существует взаимосвязь между “содержанием” и “объемом” понятия. Производный класс имеет большее содержание, но меньший объем, чем базовый.

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

Полиморфизм

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

В C++ полиморфное поведение объектов обеспечивается механизмом виртуальных функций-элементов. В одной из предыдущих глав мы на самом деле уже приводили пример “полиморфизма”, реализованного на языке С. Допустим, программа должна в числе всего прочего выводить на экран различные геометрические фигуры. Она определяет класс “фигура”, в котором предусмотрен виртуальный метод “нарисовать” (в C++ это был бы абстрактный класс). От данного класса можно произвести несколько классов — “точка”, “линия”, “круг” и т. д., — каждый из которых будет по-своему определять этот метод.

Указатель на класс “фигура” может ссылаться на объект любого из производных классов (поскольку все они являются фигурами), и для указываемого им объекта можно вызвать метод “нарисовать”, не имея представления о том, что это на самом деле за фигура.

Абстракция и инкапсуляция

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

Чтобы абстрагировать объект, он должен быть сравнительно “слабо связан” с окружающим миром. Он должен обладать сравнительно небольшим набором (существенных) свойств, характеризующих его отношения с другими объектами. С другой стороны, выделение класса как некоторого понятия, охватывающего целый ряд различных объектов, также является моментом абстракции.

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

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

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

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

Введение в классы С++

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

После этого мы, опираясь на показанный пример, объясним элементарные моменты строения класса в C++.

Определение класса

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

////////////////////////////////////////////////////////////

// Classesl.h: Пример двух геометрических классов. //

const int MaxX = 200; // Максимальные значения координат.

const int MaxY = 200;

//

struct,Point { // Класс точек.

int GetX(void) ;

int GetY(void) ;

void SetPoint(int, int);

private:

int x;

int y;

};

class Line

{

// Класс линий.

Point p0;

Point p1;

public:

Line(int x0, int y0, int xl, int yl);

// Конструктор.

~Line(void); // Деструктор.

void Show(void);

};

Ну вот, такие вот классы. Теперь разберем различные моменты этих определений.

Примечание

Иногда может потребоваться предварительное объявление класса, если нужно, например, объявить указатель на объект класса прежде, чем будет определен сам класс. Предварительное объявление в этом смысле подобно прототипу функции и выглядит так:

class SomeClass;

Заголовок определения

Определение класса начинается с ключевых слов class, struct или union. Правда, union применяется крайне редко. И структуры, и классы, и объединения относятся к “классовым” типам C++. Разницу между этими типами мы рассмотрим чуть позже.

Спецификации доступа

Ключевые слова private и public называются спецификаторами доступа. Спецификатор private означает, что элементы данных и элементы-функции, размещенные под ним, доступны только функциям-элементам данного класса. Это так называемый закрытый доступ.

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

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

Структуры, классы и объединения

Типы, определяемые с ключевыми словами struct, class и union, являются классами. Отличия их сводятся к следующему:

  • Структуры и классы отличаются только доступом по умолчанию. Элементы, не помеченные никаким из спецификаторов, в структурах имеют доступ public (открытый); в классах — private (закрытый).
  • В объединениях по умолчанию принимается открытый доступ.
  • Элементы (разделы) объединения, как и в С, перекрываются, т. е. начинаются с одного и того же места в памяти.

Я никогда не видел, чтобы в C++ применяли объединения. Хотя это, возможно, и имело бы смысл в некоторых ситуациях, когда требовалось бы объединить несколько разнородных классов в один тип.

Элементы данных и элементы-функции

Элементы данных класса совершенно аналогичны элементам структур в С, за исключением того, что для них специфицирован определенный тип доступа. Объявления элементов-функций аналогичны прототипам обычных функций.

Конструктор и деструктор

В классе могут быть объявлены две специальные функции-элемента. Это конструктор и деструктор. Класс Line в примере объявляет обе эти функции.

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

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

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

Заключение

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


Глава 8. Классы C++

Продавай мобильный контент со своего сайта

  • Глава 8. Классы C++
    • Элементы класса
      • Элементы данных
      • Элементы-функции
      • Класс как область действия
      • Доступ к элементам данных
      • Вызов функций-элементов класса
    • Указатель this
    • Статические элементы класса
      • Статические элементы данных
      • Статические элементы-функции
    • Специальные функции-элементы класса
      • Конструктор
      • Конструктор копии
      • Операция присваивания
      • Деструктор
      • Операции класса new и delete
      • Функции преобразования
    • Константные объекты и функции-элементы
      • Ключевое слово mutable
    • "Друзья"
    • Перегрузка функций-элементов
    • Перегрузка операций
      • Примеры
      • Операция вызова объекта
      • Некоторые замечания
    • Наследование
      • Доступ к базовым классам
      • Простое наследование
      • Конструкторы, деструкторы и наследование
      • Сложное наследование
    • Виртуальные функции
      • Чисто виртуальные функции и абстрактные классы
      • Реализация виртуального механизма

Глава 8. Классы С++

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

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

Элементы класса

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

Элементы данных

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

  • Они не могут быть объявлены как auto, extern или register.
  • Они могут быть перечислениями, битовыми полями, а также объектами ранее объявленных классов.
  • Элемент данных класса не может быть представителем самого класса.
  • Элемент данных класса может быть указателем или ссылкой на сам класс.

Элементы-функции

Функция-элемент класса объявляется внутри определения класса. Там же может быть расположено и оределение тела функции. В этом случае функцию-элемент называют встроенной и компилятор будет генерировать ее встроенное расширение на месте вызова. Если определение функции располагается вне тела класса, то к ее имени добавляется префикс, состоящий из имени класса и операции разрешения области действия. В этом случае функцию-элемент также можно определить как встроенную с помощью ключевого слова inline. Вот несколько модифицированный класс Point из предыдущей главы вместе с его реализацией:

#include <assert.h>

const int MaxX = 200; // Максимальные значения координат.

const int MaxY = 200;

//

struct Point { // Класс.точек.

private:

int fx;

int fy;

public:

int GetX(void) ( return fx; }

int GetY(void) { return fy; }

void SetPoint(int, int);

};

void Point::SetPoint(int x, int y)

{

assert(x >=0 && x < MaxX);

assert(y >= 0 && у < MaxY);

fx = x;

fy = y;

}

Здесь обе функции Get () определены как встроенные, а функция SetPoint () определяется вне тела класса и не является встроенной.

Класс как область действия

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

  • Для существующего представителя класса с помощью операции доступа к элементу (точки).
  • Через указатель на существующий представитель класса с помощью операции косвенного доступа к элементу (стрелки).
  • С помощью префикса, состоящего из имени файла и операции разрешения области действия (::).

Доступ к элементам данных

Поскольку функции-элементы класса находятся в его области действия, они могут обращаться к элементам данных непосредственно по имени, как можно видеть в последнем примере. Обычные функции или функции-элементы других классов могут обращаться к элементам существующего представителя класса с помощью операций “.” или “->”:

class Time { public:

int hour;

int min;

} ;

int main()

{

Time start; // Локальный объект Time.

Time *pTime = Sstart; //Указатель на локальный объект.

start.hour = 17; // Операция доступа к элементу.

pTime->min = 30; // Косвенный доступ к элементу.

return 0;

}

Вызов функций-элементов класса

Совершенно аналогично тому, что имеет место в случае элементов-данных, функции-элементы класса могут вызываться функциями-элементами того же класса просто по имени. Обычные функции и элементы других классов могут вызывать функции-элементы данного класса для существующих его представителей с помощью операций “ . ” или “->” (через указатель). Приведенный ниже пример это иллюстрирует.

#include <stdio.h>

class Time ( int hour;

int min;

public:

void SetTime(int h, int m)

{

hour = h; min = m; } void ShowTime(void)

{

printf("Time: %02d:%02d.\n", hour, min);

}

};

int main()

{

Time start;

Time *pStart = &start;

int hr, min;

start.SetTime(17, 15); // Вызов элемента для объекта

// start.

pStart~>ShowTime(); // вызов элемента через указатель

//на объект.

return 0;

}

Указатель this

Любая функция-элемент класса, не являющаяся статической (что это такое, выяснится позднее) имеет доступ к объекту, для которого она вызвана, через посредство ключевого слова this. Типом this является имя_класса*.

class Dummy {

void SomeFunc(void) {...};

public:

Dummy();

};

Dummy::Dummy)

{

SomeFunc();

this->SomeFunc();

(*this).SomeFunc();

}

В этом примере каждый оператор конструктора вызывает одну и ту же функцию SomeFunc (). Поскольку функции-элементы могут обращаться к элементам класса просто по имени, подобное использование указателя this довольно бессмысленно. Это ключевое слово чаще всего применяется для возврата из функции-элемента указателя или ссылки на текущий объект (вы увидите это позже, когда будут рассматриваться функции-операции).

Примечание

На самом деле this является скрытым параметром, передаваемым функции-элементу класса при вызове. Именно этим функция-элемент (не статическая) отличается от обычной функции. При вызове функции-элемента компилятор генерирует код, который после всех указанных в вызове параметров помещает на стек указатель на объект, для которого функция вызвана. Поэтому, например, нельзя вызвать функцию-элемент через обычный указатель на функцию. Указатель на функцию-элемент объявить можно, но в нем явно должен быть специфицирован класс, например:

long (Dummy::*fPtr)();

fPtr = &Dummy::SomeFunc;

Привести такой указатель к обычному типу указателя на функцию невозможно. При его разыменовании всегда нужно указывать конкретный объект, и для этого в C++ предусмотрены две новых операции “. *” и “->*”. Первая из них применяется с непосредственным объектом, вторая — с указателем на объект. Вызов фун.кции-элемента через указатель выглядит так:

1 = (dummyObj.*fptr)();

1 = (dummyPtr->*fptr)();

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

double Dummy::*dptr;

dptr = &Dummy::someData;

d = dumrnyObj . *dptr;

d = duinmyPtr->*dptr;

Приоритет и правило ассоциации у этих специфических операций те же, что и у обычного разыменования (*) и других одноместных операций (14, справа налево).

Статические элементы класса

Можно объявить элемент класса (данные или функцию) как статический.

Статические элементы данных

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

Помимо объявления в определении класса, статический элемент данных должен еще и определяться:

class SomeClass

{

static int iCount;

// Объявление статического

// элемента.

//.. .

};

int SomeClass::iCount = 0;

// Определение статического

// элемента.

Примечание

Обращаться к открытым статическим элементам класса можно либо через любой его представитель операциями “.” и “->”, либо с помощью операции разрешения области действия (SomeClass : : iCount). Последний способ предпочтительнее, так как ясно показывает, что элемент не связан с конкретным объектом.

Статические элементы-функции

Функция класса, объявленная с модификатором static, не связывается ни с какими его конкретными представителями. Другими словами, ей не передается указатель this в качестве скрытого параметра. Это означает, что:

  • Статическая функция-элемент может вызываться, даже если никаких
  • Статическая функция-элемент может обращаться только к статическим элементам данных класса и вызывать только другие статические функции-элементы класса.
  • Такая функция не может быть объявлена виртуальной.

Примечание

Статические функции-элементы класса могут передаваться процедурам API Windows в качестве возвратно-вызываемых, поскольку не предполагают наличия на стеке параметра this. Обычные функции-элементы для этого не годятся.

Специальные функции-элементы класса

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

  • Конструктор. Инициализирует представители класса.
  • Конструктор копии. Инициализирует новый представитель, используя значения существующего объекта.
  • Операция присваивания. Присваивает содержимое одного представителя класса другому.
  • Деструктор. Производит очистку уничтожаемого объекта.
  • Операция new. Выделяет память для динамически создаваемого объекта.
  • Операция delete. Освобождает память, выделенную под динамический объект.
  • Функции преобразования. Преобразуют представитель класса в другой тип (и наоборот).

Конструктор

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

  • Для него не объявляется тип возвращаемого значения.
  • Он не может возвращать значений оператором return.
  • Конструктор не наследуется.
  • Конструктор не может быть объявлен как const, volatile, virtual или static.

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

Примечание

Можно вызвать конструктор для инициализации уже существующего объекта, если перегрузить глобальную операцию new таким образом, чтобы она принимала дополнительный аргумент — указатель типа void*. Это называют размещающей формой операции; мы о ней уже упоминали в прошлой главе. Такая методика иногда применяется для глобальных представителей класса, если их нужно инициализировать после выполнения каких-то предварительных действий. Вот пример:

#include <new.h>

// Операция new, допускающая форму размещения:

inline void *operator new(size_t, void* p)

{

return p;

}

class Dummy

{

public:

Dummy() // Конструктор.

};

Dummy gblDummy;// Глобальный объект.

int main ()

{

InitSystem(); // Какие-то проверки

// и инициализации.

new(&gblDummy) Dummy; // Вызывает конструктор

// для gblDummy.

return 0;

}

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

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

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

class Time { int hr, min;

public:

Time(int h)

{

hr = h; min = 0;

}

Time(int h, int m): hr(h), min(m)

{

}

};

Тело второго конструктора, как видите, пусто.

Примечание

Список инициализации является единственным средством присвоения значений элементам данных класса, объявленным как const или как ссылки (а также закрытым элементам базовых классов).

Конструктор копии

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

class Time {

int hr, min;

public:

Time(int h, int m): hr(h), min(m) {}

Time(const Time &src) // Конструктор копии.

{ hr = src.hr; min = src.min; } //

};

int main()

(

Time start (17,45); // Вызывается первый конструктор.

Time current = start; // Вызывается конструктор копии.

return 0;

}

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

Примечание

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

Операция присваивания

Операция присваивания — это функция-элемент класса с именем operator=, которая принимает в качестве своего единственного параметра ссылку или константную ссылку на объект данного класса. Она вызывается компилятором, когда существующему объекту присваивается другой объект. Если операция присваивания не предусмотрена, компилятор генерирует ее по умолчанию. В этом случае при присваивании будет выполняться поэлементное (как говорят, поразрядное) копирование данных объекта.

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

Вот пример класса с операцией присваивания:

class Time { int hr, min;

public:

Time(int h, int m): hr(h), min (m) {}

Time &operator=(const Times); // Операция присваивания.

};

Time &Time::operator=(const Time &src)

{

if(&src == this) // Проверка на самоприсваивание.

error("Self assignment!");

hr = src.hr;

min = src.min;

}

return *this; // Возвращает ссылку на свой объект.

int main() {

Time start (17,45);

Time current (18, 0);

start = current; // Вызывает operator=.

return 0;

}

Здесь, кстати, показан прием проверки на самоприсваивание, позволяющей предотвратить присваивание объекта самому себе.

Примечание

Обычно операцию присваивания определяют так, чтобы она возвращала ссылку на свой объект. В этом случае сохраняется семантика арифметических присваивании, допускающая последовательные присваивания в выражении (т. е. с = b = а;).

Параметры конструктора копии и операции присваивания могут иметь тип либо имя_класса&, либо const имя_класса&. Последнее предпочтительнее, так как простая ссылка на класс не позволяет копировать константные объекты.

Примечание

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

Деструктор

Деструктор является противоположностью конструктора. Он вызывается при уничтожении объектов и должен производить необходимую очистку объекта перед освобождением занимаемой им памяти

Именем деструктора должно быть имя класса, которому предшествует тильда (~). Свойства деструкторов таковы:

  • Деструктор не имеет параметров.
  • Он не может возвращать значений.
  • Деструктор не наследуется.
  • Деструктор не может объявляться как static, const или volatile.
  • Деструктор может быть объявлен виртуальным.

Операции класса new и delete

Класс может определять свои собственные операции new и delete (new[] и delete [] для массива объектов):

  • Функция имя_класса: : operator new () вызывается при создании динамических объектов.
  • Функция имя_класса: : operator new [ ] () вызывается при создании динамических массивов объектов.
  • Функция имя_класса:: operator delete() вызывается при удалении динамических объектов.
  • Функция имя_класса:: operator delete [] () вызывается при удалении динамических массивов.

Ниже приведен довольно длинный пример, демонстрирующий определение операций класса new и delete, а также глобальных new [ ] и delete []. Вывод программы позволяет проследить порядок вызовов конструкторов/деструкторов и операций new/delete при создании автоматических и динамических объектов.

Листинг 8.1. Определение операций класса new и delete

//////////////////////////////////////////////

// NewDel.cpp: Операции класса new и delete.

//

#pragma hdrstop

#include <condefs.h>

#include <stdlib.h>

#include <stdio.h>

#include <string.h>

#define trace(msg)

printf(#msg "\n")

#define MaxStr 32

void *operator new[](size_t size)

// Замена глобальной

// new[].

{

trace(Global new [ ] .);

return malloc(size);

}

void operator delete[](void *p)

// Замена глобальной

// delete[].

{

trace(Global delete [].);

free(p) ;

}

class Hold {

char *str;

// Закрытый указатель на строку.

public:

Hold(const char*) ;

~Hold() ;

void *operator new(size t);

// Операция new класса.

void operator delete(void*);

// Операция delete класса.

void Show(void)

{ printf("%s\n", str);

}

};

Hold::Hold(const char *s)

// Конструктор.

{

trace (Constructor.) ;

str = new char[MaxStr];

// Вызов глобальной new[].

strncpy(str, s, MaxStr);

// Копирование строки в объект.

str[MaxStr-1] = 0;

// На всякий случай...

}

Hold::~Hold ()

// Деструктор.

{

trace(Destructor.);

delete[] str;

// Очистка объекта.

}

void *Hold::operator new(size_t size)

{

trace(Class new.);

return malloc (size);

)

void Hold::operator delete(void *p)

{

trace(Class delete.);

free(p) ;

)

void Func()

// Функция с локальным объектом.

{

Hold funcObj ("This is a local object in Func.");

funcObj.Show() ;

}

int main () {

Hold *ptr;

Hold mainObj ("This is a local object in main.");

mainObj.Show ();

trace (*);

ptr = new Hold("This is a dynamic object.");

ptr->Show();

delete ptr;

trace (*);

FuncO ;

trace (*);

return 0;

}

Результат работы программы показан на рис. 8.1.

Рис. 8.1 Программа NewDel

Примечание

Пример заодно поясняет, зачем нужен деструктор. Здесь он удаляет динамически созданную строку, адрес которой хранится в объекте. Если не определить деструктор, то генерированный компилятором деструктор по умолчанию удалит сам объект, но не строку, которая окажется “потерянной”. Подобные упущения являются распространенной причиной утечек памяти.

Функции преобразования

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

Конструкторы преобразования

Если конструктор класса А имеет единственный параметр типа В, то объект В может быть неявно преобразован в класс А с помощью такого конструктора. Другими словами, компилятор может сам вызывать такой конструктор, чтобы “из В сделать А”. Возьмите пример из предыдущего раздела. Локальный объект можно было бы инициализировать по-другому:

class Hold {

char *str;

public:

Hold(const char*);

//...

};

main () {

Hold mainObj = "This is a local object in main.";

//. . .

return 0;

Таким образом, в этом примере объявленный в классе конструктор Hold(const char*) является по сути конструктором преобразования.

Ключевое слово explicit

Можно запретить вызовы конструктора с одним параметром в качестве конструктора преобразования, объявив его с ключевым словом explicit. Тем самым будут запрещены неявные вызовы конструктора, подобные показанному в предыдущем параграфе:

class Hold {

char *str;

public:

explicit Hold(const char*);

//. . .

};

main () {

//

// Неявное преобразование недопустимо:

//

// Hold mainObj = "This is a local object in main.";

//...

return 0;

}

Такой конструктор можно вызывать только явным образом, т. е.

Hold mainObj("This is a local object in main.");

или

Hold mainObj = Hold("This is a local object in main.");

Примечание

Последняя форма предполагает вызов конструктора копии, который должен обязательно определяться, если класс содержит указатели на динамические объекты подобно классу из листинга 8.1. Там этого не сделано.

Операции приведения

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

operator имя_нового_типа();

Процедуры преобразования характеризуются следующими правилами:

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

Вот пример процедуры преобразования:

#include <stdio.h>

class Time { int hr, min;

public:

Time(int h, int m): hr(h), min(m) {}

// Конструктор.

operator int();

// Процедура преобразования.

};

Time::operator int() {

// Преобразует время в число секунд от начала суток:

return (3600*hr + 60*min);

}

main ()

{

int h = 7;

int m = 40;

Time t (h, m);

//

// Последний параметр вызывает Time::operator int():

//

printf("Time: %02d:%02d = %5d seconds.\n", h, m, (int)t);

return 0;

Константные объекты и функции-элементы

Можно создать представитель класса с модификатором const. Тем самым гарантируется, что после инициализации содержимое объекта нельзя будет изменить. Компилятор C++Builder выдает предупреждение в случае, если для объекта вызывается функция-элемент, не объявленная как const. Другие компиляторы могут выдать сообщение об ошибке и отказаться транслировать подобный код.

Со своей стороны, константная функция-элемент

  • объявляется с ключевым словом const после списка параметров;
  • не может изменять содержимое элементов данных класса;
  • не может вызывать не-константные функции-элементы. Вот пример константной функции и константного объекта:

class Time {

int hr, min;

public:

Time(int h, int m): hr(h), min(m) {}

void SetTime(int h, int m) { hr = h; min = m;

}

void GetTime(int&, int&) const;

}

void Time::GetTime(int &h, int &m) const {

h = hr;

m = min;

//

// Следующий оператор здесь был бы некорректен:

//

// min = 0;

int main ()

{

Time t(17, 45); // Обычный объект.

const Time ct(18, 0); // Константный объект.

int h, m;

ct.GetTime(h, m); // Вызов const-функции для const-объекта. t.SetTime(h, m) ;

//

// Следующий вызов некорректен:

// // ct.SetTime(0, 0) ;

return 0;

}

Ключевое слово mutable

Константная функция-элемент “обещает” не изменять значений элементов данных класса, что позволяет применять ее на константных объектах. Тем не менее, в некоторых ситуациях имеет смысл разрешить некоторым элементам меняться даже у константных объектов. Например, некоторый существенный набор данных изменять ни в коем случае нельзя, в то время как отдельный элемент, скажем, некоторое сообщение, может и должно меняться. В этом случае можно объявить элемент данных с ключевым словом mutable:

class AnyClass {

int value;

mutable const char *msg;

public.:

AnyClass (): value (0), msg(NULL) {}

int GetValueO const;

// ... };

j nt AnvClass::Get Value() const

{

msg - "New message!";

// Допускается, поскольку msg - mutable.

//

// value изменять нельзя:

//

// value = 111;

//

return value;

}

Модификатор mutable не может применяться совместно с const или static (в приведенном примере все верно, поскольку const относится не к msg, а к содержимому строки, на которую он ссылается).

“Друзья”

Спецификаторы доступа позволяют указать, к каким элементам класса могут обращаться функции, в него не входящие. Однако могут быть случаи, когда целесообразно разрешить некоторому классу или функции обращаться к закрытым или защищенным элементам данного класса. Это можно сделать с помощью ключевого слова friend.

“Друзьями” класса могут быть объявлены другие классы или отдельные функции, как обычные, так и являющиеся элементами некоторых классов. Друзья могут объявляться в любом из разделов определения класса (закрытом, защищенном или открытом), — в каком именно, не имеет значения. В любом случае дружественный класс или функция будет иметь полный доступ к элементам класса.

Вот пример объявления Друзей класса:

class SomeClass (

friend class AnotherClass;

friend void regularFunc (int);

friend void OtherClass::MemFunc(double);

//...

};

Следует иметь в виду такие правила:
  • Дружественность не обратима: если А объявляет В другом, это не значит, что А будет другом В. Дружба “даруется”, но не “присваивается”.
  • Дружественность не наследуется: если А объявляет В другом, то классы, производные от В, не будут автоматически друзьями А.
  • Дружественность не транзитивна: если А объявляет В другом, то классы, производные от А, не будут автоматически признавать В своим другом.

Перегрузка функций-элементов

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

class Time {

long sec; public:

Time(): sec(O) {}

Time(long s): sec(s) {}

Time(int h, int m) {

sec = 3600*h + 60*m;

}

//... };

int main ()

{

Time tl; // Вызывает Time::Time().

Time t2(86399); // Вызывает Time::Time(long).

Time t3(ll, 33); // Вызывает Time::Time(int, int).

//. . .

return 0;

}

Перегрузка операций

+ | — | * | / | % | /\ | & | | | ~ | ! |  | =
< | > | += | -= | *= | /= | %= | ^= | &= | |= |  | <<
>> | >>= | <<= | = = | ! = | <= | >= | && | || | ++ |  | —
' | ->* | -> | () | [] | new | delete |  | new[] | delete | [ ]

Язык C++ позволяет переопределять для классов существующие обозначения операций. Это называется перегрузкой операций. Благодаря ей класс можно сделать таким, что он будет вести себя подобно встроенному типу. В классе можно перегрузить любые из следующих операций:

Нельзя перегружать операции:

.

.*

::

?:

Функции-операции, реализующие перегрузку операций, имеют вид

operator операция ([операнды]) ;

Примечание

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

Для перегрузки операций существуют такие правила:
  • Приоритет и правила ассоциации для перегруженных операций остаются теми же самыми, что и для операций над встроенными типами.
  • Нельзя изменить поведение операции по отношению к встроенному типу.
  • Функция-операция должна быть либо элементом класса, либо иметь один или несколько параметров типа класса.
  • Функция-операция не может иметь аргументов по умолчанию.
  • Функции-операции, кроме operator=(), наследуются.

Примеры

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

Листинг 8.2. Перегрузка операции сложения

//////////////////////////////////////////////////////

// StrAdd.cpp: Демонстрация перегрузки сложения для строк.

//

#pragma hdrstop

#include <condefs.h>

#include <string.h>

#include <stdio.h>

class String {

char *str; // Указатель на динамическую строку.

int len; // Длина строки.

String(int); // Вспомогательный конструктор. public:

String(const Strings); // Конструктор копии.

String(const char*); // Конструктор преобразования.

~String () ;

String Soperator=(const Strings);

String operators- (const Strings);

friend String operator+(const Strings, const Strings);

void Show () ;

};

String::String(int size)

{

len = size;

str = new char[len + 1]; // +1 байт для завершающего 0.

}

String::String(const String ssrc)

{

len = src.len;

str = new char[len + 1];

strcpy(str, src.str);

}

String::String(const char *s)

{

len = strlen(s) ;

str = new char[len + 1];

strcpy(str, s);

String::~String()

{

delete [] str;

///////////////////////////////////////////////////

// Операция присваивания.

//

String SString::operator=(const String &op)

{

delete [] str; // Удаление старых данных.

len = op.len;

str = new char[len + 1]; // Выделение новой строки.

strcpy(str, op.str);

return *this;

}

///////////////////////////////////////////////////

// Функция-элемент operator+0.

//

String String::operators- (const String &op)

{

String temp(len + op.len); // Временный объект.

strcpy(temp.str, str); // Копирование первой строки.

strcat(temp.str, op.str); // Присоединение второй строки.

return temp;

}

///////////////////////////////////////////////////

// Дружественная функция operator+() Аналогична предыдущей,

// но допускает С-строку в качестве первого операнда.

//

String operator+(const String Sfop, const String &sop)

{

String temp(fop.len + sop.len);

strcpy(temp.str, fop.str);

strcat(temp.str, sop.str);

return temp;

}

void String::Show()

{

printf("%s: %d \n", str, len);

}

irit main()

{

char cStr[] °= "This is а С string! ";

String rirst = "First String string. ;

String second = "Second String string. ";

String resStr = "";

resStr.Show() ;

resStr = first + second; // Вызывает операцию класса.

resStr.:Shp,w ();

resStr = cStr + resStr; // Вызывает дружественную

// операцию reeStr.Show()

resStr = first + second + cStr; // Обе операции - из

// класса. resStr.Show () ;

return 0;

}

На рис. 8.2 показан вывод программы. Пример позволяет пояснить, почему перегруженные функции операций часто делают друзьями, а не элементами класса. Это делается для того, чтобы передать функции первый операнд в параметре, а не как объект *this. В одном из операторов функции main () из примера первый операнд сложения — С-строка. Но это не объект класса, и компилятор никак не может вызвать String: :operator+ (Strings) . Однако есть дружественная функция operator+ (Strings, Strings). Поскольку имеется конструктор преобразования char* в String, компилятор автоматически приводит первый операнд к типу String, создавая на стеке временный объект, и затем выполняет сложение с помощью дружественной функции. Это аналог “возведения типа”, происходящего в арифметических выражениях.

На самом деле функция-элемент operator+ (Strings) здесь является излишней. Можно было бы'обойтись одной дружественной функцией сложения.

Примечание

В данном классе необходима реализация конструктора копии. Компилятор вызывает его при передаче возвращаемого значения функциями-операциями operator+ (). На стеке конструируется копия локального автоматического объекта temp (см. листинг), который при завершении функции выходит из области действия и удаляется. Конструктор копии по умолчанию не годится, так как класс содержит указатель на динамическую строку.

Рис. 8.2 Программа StrAdd

Листинг 8.3. Перегрузка операции индексации

////////////////////////////////////////////////////////////

// Index.срр: Строка в качестве индекса.

//

#pragma hdrstop

#include <condefs.h>

#include <stdio.h>

#include <string.h>

const int Maxltems = 16;

class AArr {

int nitems;

char * keys[Maxltems];

char *items[Maxltems];

static const char error [];

public:

AArr() { nitems =0; }

~AArr();

void Addltem(const char*, const char*);

const char ^operator[](const char*);

};

consk char AArr::error[] = "*** Not found. ***";

////////////////////////////////////////////////////////////

// Деструктор: удаляет динамические строки,

// созданные Addltem().

//

AArr::~AArr ()

{

for (int j=0; j<nltems; j++) {

delete [] keys[j];

delete[] items[j];

}

////////////////////////////////////////////////////////////

// Создает новую запись с указателями в keys[] и items[].

//

void AArr::Addltem(const char *key, const char *data)

{

if (nitems < Maxltems) {

keys[nitems] == new char[strlen(key)+1];

items[nitems] = new char[strlen(data)+1] ;

strcpy(keys[nitems], key);

strcpy(items[nitems], data);

n Items++; }

////////////////////////////////////////////////////////////

// Перегруженная индексация: ищет запись с указанным ключом. //

const char *AArr::operator[1 (const char *idx)

(

int j ;

for (j=0; j<nltems; j++)

if (!strcmp(keys[j], idx)) break;

if (j < nitems) return items[j];

else

return error;

}

int main() {

AArr a;

// Несколько записей... a.AddItem("first", "String One!");

a.AddItem("second", "String Two!");

a.AddItem("another", "Another String!");

a.AddItem("one more", "One More String!");

a.AddItem("finish", "That's all folks!");

// Проверка:

char *i;

i = "second";

printf("\n%s: %s\ri", i, a[i]);

i = "one more";

printf ("%s: %s\n", i, a[i]);

i = "abracadabra";

printf ("%s: %s\n", i, a[i]);

i = "finish"; printf("%s: %s\n", i, a[i]);

return 0;

}

Этот пример не требует особых пояснений. Здесь перегружается функция-операция с именем Aarr: : operator []. Получившийся класс ведет себя как массив с “индексами”-строками. Вывод программы показан на рис. 8.3.

Рис. 8.3 Программа Index

Операция вызова объекта

Перегрузка операции вызова operator () () позволяет “вызывать” объект класса, как функцию. Возвращаемое значение будет чем-то вроде значения объекта по умолчанию. Но вообще эта операция может производить любые действия над объектом. Вот пример операции вызова:

class AClass {

int x;

public:

AClass(int n) { x = n; }

int operator ()(void) { return x; }

//. . .

};

int main() {

AClass object = 100;

//...

int у = objectO; // Объект вызывается, как функция.

return 0;

}

Некоторые замечания

При перегрузке операций полезно помнить следующее:
  • C++ не умеет образовывать из простых операций более сложные. Например, в классе со сложением строк мы определили присваивание и сложение; но это не значит, что тем самым будет автоматически определено присвоение суммы (+=). Такую операцию нужно реализовывать отдельно.
  • Невозможно изменить синтаксис перегруженных операций. Одноместные операции должны быть одноместными, а двухместные — двухместными.
  • Нельзя изобретать новые обозначения операций. Возможные операции ограничиваются тем списком, что приведен в начале этого раздела.
  • Желательно сохранять смысл перегружаемой операции. Например, конкатенация — естественная семантика сложения для строк.

Наследование

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

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

Наследование позволяет также модифицировать поведение базового класса. Производный класс может переопределять некоторые функции-элементы базового класса, оставляя основные свойства класса в неприкосновенности .

Синтаксис производного класса следующий:

class имя класса: ключ доступа имя_базового класса [, ...] {

тело_объявления_класса } ;

Ключ_доступа — это одно из ключевых слов private, protected или public.

Доступ к базовым классам

Ключ доступа определяет “внешний” доступ к элементам базового класса через объекты производного. Что касается доступа самого производного класса к элементам базового класса, то ключ доступа на него не влияет. Для производного класса доступны разделы protected и public базового класса; раздел private строго недоступен вне области действия базового класса.

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

Например, при наследовании с ключом public права доступа к элементам базового класса остаются неизменными; при закрытом наследовании (ключ private) все элементы базового класса будут недоступны за пределами производного класса.

Примечание

При закрытом наследовании можно сделать некоторые открытые функции базового класса открытыми в производном, если переобъявить их имена в производном классе:

class First { public:

void FFunc(void) ;

//... }

class Second: private First { public:

First::FFunc; // First::FFunc() открыта в классе Second.

//.. .

}

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

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

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

#include <stdio.h>

#include <string.h>

class Time { // Базовый класс - время.

int hr, min;

public:

Time(int h=12, int m=0): hr(h), min(m) {}

void SetTime(int h, int m) ( hr = h; min = m; }

void Show() ;

};

void Time::Show() {

printf("%02d:%02d", hr, min);

}

class Alarm: public Time { // Класс сообщений таймера.

char *msg;

public:

Alarm(char*);

~Alarm() { delete [] msg; }

void SetMsg(char*);

void Show(); // Переопределяет Time::Show (). };

Alarm::Alarm(char *str) ;

{

msg = new char[strlen (str) + 1];

strcpy(msg, str);

}

void Alarm::SetMsg(char *str) {

delete [] msg;

msg = new char[strlen (str) + 1];

strcpyfmsg, str);

}

void Alarm::Show()

{

Time::Show(); // Вызов базовой Show(). printf(": %s\n", msg);

int main () {

Alarm a = "Test Alarm!!!"; // Время по умолчанию 12:00.

а.Show ();

a.SetTime(7, 40); // Функция базового класса. а.Show () ;

а.SetMsg("It's time!); // Функция производного класса. a.Show();

return 0;

}

Конструкторы, деструкторы и наследование

Конструкторы не наследуются. Это утверждение требует некоторых пояснений. Оно означает, что если в базовом классе имеются конструктор с некоторыми параметрами, он не будет вызываться автоматически, если вы . попробуете создать объект производного класса с такими параметрами. Для этого нужно написать конструктор производного класса, в котором конструктор базового класса будет вызываться через посредство списка инициализации. О нем мы уже говорили выше в связи с инициализацией элементов данных класса. Базовые классы в смысле инициализации ничем от них не отличаются.

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

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

class Alarm: public Time { // Класс сообщений таймера.

char *msg;

public:

Alarm(char*);

Alarmfchar*, int, int); // Новый конструктор.

~Alarm() { delete[] msg; }

void SetMsg(char*) ;

void Show(); // Переопределяет Time:: Show ().

//. . .

Alarm::Alarm(char *str, int h, int m): Time(h, m) {

msg = new char[strlen(str) + 1];

strcpy(msg, str);

}

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

Сложное наследование

Язык C++ допускает не только простое, но и сложное наследование, т. е. наследование от двух и более непосредственных базовых классов. Это позволяет создавать классы, комбинирующие в себе свойства нескольких независимых классов-предков.

Это чаще всего имеет смысл, когда у вас есть некоторый набор понятий, которые могут более или менее независимо комбинироваться с различными элементами другого набора понятий. Простейший пример. Имеется понятие “растение”. Бывают “культурные растения”, и бывают “дикорастущие растения”. С другой стороны, растение может иметь или не иметь “товарной ценности”, т. е. быть полезным или бесполезным с коммерческой точки зрения. Если говорить о товарной ценности, то тут у растений бывают “цветы” и “плоды” и т. д. Все это образует довольно развернутую структуру, которая может порождать понятия вроде “дикое растение, цветы которого можно продавать на рынке”. (Возможно, кстати, и такое: “дикое растение, цветы которого имеют товарную ценность, но которые нельзя продавать на рынке”!) А можно, с некоторыми модификациями, говорить то же самое не о растениях, а о животных или веществах, минералах. И есть не только “товарные” сущности, но и сорняки, вредители. И так далее.

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

Примечание

Кстати, в языке Object Pascal, реализованном в сходном продукте Борланда — Delphi, — нет сложного наследования, что в ряде случаев может значительно ограничить его полезность в сравнении с C++.

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

#include <stdio.h>

#include <string.h>

//////////////////////////////////////////////////////////////////////

// Базовый класс - время.

//

class Time {

protected;

int hr, min;

public:

Time(int h=12, int m=0): hr(h), min (m):{}

void Show() ;

};

void Time::Show() {

printf("%02d:%02d\n", hr, min);

}

//////////////////////////////////////////////////////////

// Базовый класс - сообщение. //

class Message { protected:

char *msg;

public:

Message(char*) ;

~Message () { delete[]msg; }

void Show () ;

}

Message::Message(char*msg)

// Конструктор Message. {

msg = new char[strlen(str)+1];

strcpy(msg, str);

}

void Message::Show()

{

printf(%s\n", msg);

}

///////////////////////////////////////////////////////

// Производный класс сообщений таймера.

//

class Alarm: public Time, public Message { public:

Alarm(char* str, int h, int m): Time(h, m), Message(str) {}

void Show ();

};

Alarm::Show() // Переопределяет базовые Show().

{

printf("%02d:%02d: %s\n", hr, min, msg);

}

int main() {

Alarm a("Test Alarm!!!", 11, 30);

a.Show() ;

return 0;

}

Вы видите, что конструктор производного класса Alarm имеет пустое тело и список инициализации, вызывающий конструкторы базовых классов. Элементы данных базовых классов объявлены как protected, чтобы можно было непосредственно обращаться к ним в функции Show () производного класса.

Неоднозначности при сложном наследовании

В иерархии классов со сложным наследованием вполне может получиться так, что класс косвенно унаследует несколько экземпляров некоторого базового класса. Если В и С оба являются наследниками A, a D наследует В и С, то D получает двойной набор элементов класса А. Это может приводить к неоднозначностям при обращении к ним, что будет вызывать ошибки времени компиляции. Вот иллюстрация:

class A { public:

int AData;

void AFunc ();

II... };

class B: public A

{

// ... };

class C: public A {

// ...

};

class D: public B, public С // Двукратно наследует А.

{

// ... ,

};

int main (void)

{

D d;

d.AData = 0; // Ошибка! d. AFunc ();

// Ошибка!

return 0;

}

В этом примере строки в main () , содержащие обращения к унаследованным от А элементам, будут вызывать ошибку компиляции с выдачей сообщения о том, что элемент класса неоднозначен. Однако эту неоднозначность несложно устранить, применив операцию разрешения области действия, например, так:

d.B::AData= 0;

d.С::AFunc();

Виртуальные базовые классы

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

class A { public:

int AData;

void AFunc ();

// . .. };

class B: public virtual A // A - виртуальный базовый класс.

{

}:

class C: public virtual A // A - виртуальный базовый класс.

{

// ...

};

class D: public B, public С // Содержит только одну копию А.

{

// ...

};

int main(void) {

D d;

d.AData = 0; // Теперь неоднозначности не возникает.

d.AFunc();

//

return 0;

}

Примечание

Виртуальные базовые классы — более хитрая вещь, чем может показаться с первого взгляда. Если, допустим, конструкторы “промежуточных” классов В и С явно вызывают в своих списках инициализации какой-то конструктор с параметрами класса А, то снова возникает неоднозначность — какой набор параметров компилятор должен использовать при конструировании той единственной копии А, которая содержится в С?

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

D: :D(...) : В(. . .) , С(. . .) , А(.. .) {

// ... }

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

TMyWindow::TMyWindow(TWindow *parent, const char *title):

TFrameWindow(parent, title), TWindow(parent, title) {

// . . . }

TWinoow — виртуальный базовый класс, поэтому он должен инициализироваться отдельно, хотя это выглядит совершенно излишним, так как TFrameWindow, промежуточный класс, сам вызывает конструктор TWindow с теми же параметрами.

Виртуальные функции

Функции-элементы класса могут объявляться в C++ как виртуальные. Ключевое слово virtual заставляет компилятор генерировать для класса некоторую дополнительную информацию 6 функции. Происходит следующее: если виртуальная функция переопределяется в производном классе, и если имеется указатель или ссылка на базовый класс (которые могут с тем же успехом ссылаться на производный класс, поскольку производный объект есть в то же время и объект базового класса), то при обращении к функции через указатель (ссылку) будет вызвана правильная функция-элемент (т. е. соответствующая типу действительного объекта) — базового или одного из производных классов, в зависимости от типа конкретного объекта.

Примечание

Не хочу показаться занудой, но, как мне кажется, стоит повторить, чем косвенное обращение к объекту (указатель или ссылка) в данном отношении отличается от прямого. Можно недвусмысленно объявить объект базового и объект производного классов. Потом можно присвоить объект производного класса переменной базового типа. Не требуется даже никаких приведений, потому что, как я уже говорил, производный объект является объектом базового класса. “Автомобиль” есть “средство передвижения”. Однако при этом будет потеряна всякая специфика “автомобиля”, отличающая его от всех других средств передвижения, наземных, водных или воздушных. Но применение указателей или ссылок в объектно-ориентированных языках типа C++ приводит к тому, что объект сам может помнить, к какому типу он относится, и указатель на базовый тип может быть в данном случае снова приведен

Следующий пример покажет вам разницу между виртуальным и не виртуальным переопределением функции.

//////////////////////////////////////////////////////////

// Virtual.cpp: Демонстрация виртуальной функции.

//

#pragma hdrstop

#include <condefs.h>

#include <stdio.h>

class One

{

// Базовый класс. public:

virtual void ShowVirtO

// Виртуальная функция.

{

printf("It's One::ShowVirt()!\n");

}

void ShowNonVirt() // Не-виртуальная функция.

{

printf("It's One::ShowNonVirt()!\n") ;

}

};

class Two: public One

{

// Производный класс. public:

virtual void ShowVirt()

(

printf ("It's Two::ShowVirtO !\n") ;

)

void ShowNonVirt ()

(

printf("If s Two::ShowNonVirt ()!\n") ;

) };

int main(void)

{

Two derived;

One *pBase = sderived;

pBase->ShowVirt(); // Вызовет Two::ShowVirt().

pBase->ShowNonVirt(); // Вызовет One::ShowNonVirt().

//

// Следующий вызов подавляет виртуальный механизм:

// pBase->One::ShowVirt();

// Явно вызывает One::ShowVirt().

return 0;

}

Результат работь! программы (рис. 8.4) показывает, что при обращении к виртуальной функции через базовый указатель будет'вызвана “правильная” функция, соответствующая типу действительного (производного) объекта.

Ключевое слово virtual при объявлении функции в производных классах не обязательно. Функция, однажды объявленная виртуальной, остается таковой во всех производных классах иерархии.

Рис. 8.4 Демонстрация виртуальной и не-виртуальной функции

Примечание

Виртуальная функция не может быть статической.

Желательно, а иногда просто необходимо, объявлять виртуальным деструктор базового класса. Если этого не сделать, то при удалении динамического объекта, на который ссылается указатель базового класса, всегда будет вызываться только базовый деструктор вне зависимости от того, чем является данный объект. Это можно проиллюстрировать такой схемой:

class One { public:

~One () { /* ... */ }

};

class Two: public One

{

Something *s;

public:

Two()

{

s = new Something; // Выделение ресурса.

}

~Two()

{

delete s; // Очистка. } };

int main() {

One *pBase = new Two;

// ...

delete pBase; // Удаление динамического объекта.

return 0;

}

Примечание

В данном примере при удалении объектаоперацией delete будет вызван только базовый деструктор ~0nе (), хотя объект принадлежит к производному классу. Чтобы вызывался правильный деструктор, следовало бы объявить его виртуальным в базовом классе:

virtual ~One() { /* ... */}

Чисто виртуальные функции и абстрактные классы

Виртуальная функция-элемент некоторого класса может быть объявлена чистой. Это выглядит так:

virtual тип имя функции(список параметров} = 0;

Другими словами, тело функции объявляется как ='0 (т. н. чистый спецификатор). Действительная реализация ее не нужна (хотя и возможна). Предполагается, что чисто виртуальная функция будет переопределяться в классах, производных от него. Класс, объявляющий одну или несколько чисто виртуальных функций, является абстрактным базовым классом. Нельзя создать представитель такого класса. Он может служить только в качестве базового для порождения классов, которые полностью реализуют его чисто виртуальные функции.

Примечание

Если класс не определяет see чисто виртуальные функции абстрактного базового класса, то он также является абстрактным.

Абстрактные классы
  • не могут, как уже сказано, иметь представителей;
  • не могут использоваться в качестве типа параметров или возвращаемых значений;
  • не могут участвовать в явных приведениях типа.

Тем не менее, можно объявлять указатели или ссылки на абстрактный класс.

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

Примечание

Предположим опять-таки, что вы хотите работать с геометрическими фигурами — линиями, кругами и т. д. Конечно, имеет смысл определить общий базовый класс “фигура”. Тогда можно будет представлять наборы разнородных фигур в виде единообразных списков или массивов указателей. Но нет смысла создавать представителей собственно класса “фигура”. Не бывает в мире фигур просто. Фигура — это абстракция.

С другой стороны, каждый конкретный класс фигур должен иметь метод “нарисовать”. Чтобы этот метод был виртуальным, его должен объявлять базовый класс (как виртуальный, естественно). Но что может делать такой метод, если неизвестно, что рисовать? Конечно, можно написать функцию, которая ничего не делает или возвращает состояние ошибки. Но гораздо лучше объявить такую функцию-элемент базового класса как чистую. Тем самым будет образована виртуальная база для переопределения в производных классах, а также запрещено явное или неявное создание представителей базового класса.

Сказанное иллюстрирует следующий набросок иерархии классов:

class Shape { // Абстрактный базовый класс.

// . . .

public:

virtual -Shape () {} //На всякий случай...

virtual void Draw() =-0; //Чисто виртуальная функция. // . . . };

//

// Производные классы:

//

class Line: public Shape

{ // . . . public:

void Draw() {

// Определение тела

class Rectangle: public Shape

{

// . . .

public:

void Draw()

{

// . . .

}

//...

// И т.д.

Реализация виртуального механизма

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

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

Примечание

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

Заключение

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


Глава 9. Потоки ввода-вывода

Продавай мобильный контент со своего сайта

  • Глава 9. Потоки ввода-вывода
    • Классы потоков
    • Предопределенные потоки
    • Операции извлечения и передачи в поток
      • Перегруженные операции для встроенных типов
    • Форматирование
      • Форматирующие функции-элементы
      • Манипуляторы
      • Форматирующие флаги
    • Состояние потока
    • Файловые потоки
      • Конструирование объекта потока
      • Режимы открытия файла
      • Закрытие файла
      • Примеры файловых потоков
    • Бесформатный ввод-вывод
      • Двоичный режим ввода-вывода
      • Чтение и запись сырых данных
      • Некоторые функции потоков
      • Чтение символов и строк
    • Ввод-вывод с произвольным доступом

Глава 9. Потоки ввода-вывода

До сих пор в программных примерах мы пользовались только функциями стандартной библиотеки С. Однако в C++ имеются собственные средства, основанные на принципах классовой модели. Другими словами, в исполнительной библиотеке C++ имеется набор классов для управления вводом-выводом.

В отличие от функций буферизованного ввода-вывода С (таких, как print f и scanf, не выполняющих никаких проверок на соответствие аргументов форматной строке) классы потоков C++ безопасны в отношении типа. Ввод-вывод использует механизм перегрузки операций, гарантирующий вызов нужной функции-операции для указанного типа данных. Это главное преимущество потоков языка C++.

Классы потоков

К классам потоков относятся следующие:
  • Класс streambuf управляет буфером потока, обеспечивая базовые операции заполнения, опорожнения, сброса и прочих манипуляций с буфером.
  • Класс ios является базовым классом потоков ввода-вывода.
  • Классы istream и ostream — производные от ios и обеспечивают работу потоков соответственно ввода и вывода.
  • Класс iоstream является производным от двух предыдущих и предусматривает функции как для ввода, так и для вывода.
  • Классы ifstream, of stream и f stream предназначены для управления файловым вводом-выводом.
  • Классы istrstream и ostrstream управляют резидентными потоками (форматированием строк в памяти). Это устаревшая методика, оставшаяся в C++Builder в качестве пережитка.

Для работы с потоками вам потребуется включить в программу заголовочный файл iostream.h. Кроме того, может потребоваться подключить файлы fstream.h (файловый ввод-вывод), iomanip.h (параметризованные манипуляторы) и strstream.h (форматирование ь памяти).

Предопределенные потоки

Библиотека ввода-вывода C++ предусматривает четыре предопределенных объекта-потока, связанных со стандартными входным и выходным устройствами. Ниже дана сводка этих объектов.

Таблица 9.1. Предопределенные объекты-потоки C++

Имя | Класс | Описание
cin | istream | Ассоциируется со стандартным вводом (клавиатурой).
cout | ostream | Ассоциируется со стандартным выводом (экраном).
cerr | ostream | Ассоциируется со стандартным устройством ошибок (экраном) без буферизации.
clog | ostream | Ассоциируется со стандартным устройством ошибок (экраном)с буферизацией.

Операции извлечения и передачи в поток

Основными классами ввода-вывода C++ являются istream и ostream. Первый из них перегружает операцию правого сдвига (>>), которая служит в нем для ввода данных и называется операцией извлечения из потока. Класс ostream перегружает соответственно операцию левого сдвига (<<); она применяется для вывода и называется операцией передачи в поток.

Примечание

Нужно сказать, что стандартной русской терминологии как таковой в C++ не существует. Каждый изобретает свою собственную; иногда удачно, иногда — нет.

Вот простейшие операторы ввода и. вывода на стандартных потоках:

#include <iostream.h>

int main()

{

char name [ 8.0] ;

cout<< "Enter your name: ";

cin>> name;

cout <<"Hello " << name << "!";

return 0;

}

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

Перегруженные операции для встроенных типов

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

#include <iostream.h>

void check(void) {

if (!cin.good())

{

// Либо просто if (!cin) {

cout << "Error detected!";

exit (1);

}

int main(void)

{

double d;

long 1;

cout << "Enter a floating point value: ";

cin >> d;

check () ;

cout << "You entered: " << d << '\n';

cout << "Enter an integer value: ";

cin >> 1;

check () ;

cout << "You entered: " << 1 << '\n';

return 0;

}

Примечание

Операции извлечения и передачи в поток (соответственно для классов istream и ostream) можно перегрузить таким образом, чтобы можно было применять их для ввода или вывода объектов класса, определенного пользователем. Приведенный ниже пример демонстрирует эту методику. Вообще-то в подобных случаях совершенно необходимо предусмотреть детектирование и обработку ошибок ввода, но здесь мы этого не сделали.

#include <iostream.h>

class Point { int x, у;

public:

Point(int xx = 0, int yy = 0) {

x = xx; у = yy;

}

friend istream &operator>>(istream&, Points);

friend ostream &operator“(ostream&, Points);

};

istream &operator”(istream &is, Point &p)

//

// При вводе точка представляется просто парой чисел,

// разделенных пробелом.

// is >> р.х > р.у;

return is;

}

ostream &operator<<(ostream &os.Point &p) {

//

// Вывод в виде (х, у).

//

os<< ' ( '<< р. х<< ", "<< р. у<<') ' ;

return os;

}

int main() {

Point р;

cout<< "Enter point coordinates: ";

cin>> р;

cout<< "The point values are " << р;

return 0;

}

Форматирование

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

Форматирующие функции-элементы

Эти функции являются элементами класса ios и перегружены таким образом, чтобы можно было либо читать, либо устанавливать значение соответствующего атрибута потока. Если аргумент в вызове отсутствует, функция возвращает текущее значение атрибута. Если аргумент указан, функция устанавливает новое и возвращает предыдущее значение атрибута.

long width(long)

Эта функция предназначена для чтения или установки атрибута ширины поля.

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

char fill(char)

Функция позволяет прочитать или установить текущий символ заполнения.

По умолчанию символ заполнения — пробел.

long precision(long)

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

  • Точность по умолчанию равна шести цифрам.
  • Если установлен флаг scientific или fixed, точность задает число цифр после десятичной точки.
  • Если ни один из этих флагов не установлен, точность задает общее число значащих цифр.

Пример

Ниже приводится программа, демонстрирующая форматирование потока с помощью функций-элементов класса ios.

Листинг 9.1. Демонстрация форматирующих функций потока

///////////////////////////////////////////////

// Format.срр: Форматирующие функции-элементы ios.

//

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

#pragma argsused

int main(int argc, char* argv[])

{

//

// Ширина поля при вводе и выводе.

//

cnar sir [16];

cout<< "Enter something: ";

cin.width(16); // Ввод не более 15 символов. cin>> str;

cout.width(32); // Вывести в поле шириной 32. cout<< str<< "\n\n";

//

// Заполняющий символ и ширина поля. Ширина сбрасывается

// после каждой операции, поэтому она устанавливается

// для каждого числа.

//

int h = 7, m = 9, s = 0; // Выводятся в виде hh:mm:ss.

cout.fill('0'); cout << "Time is ";

cout.width (2); cout << h << ' : ' ; cout.width (2) ;

cout<< m<< ' : ' ;

cout.width (2) ;

cout<< s<< ".\n\n";

cout.fill (' '); // Восстановить пробел.

//

// Точность.

//

double d = 3.14159265358979;

float f = 27182.81828;

cout.precision (5);

cout << f << '\n'; . // Выводит "27183" .

cout << d << '\n'; ' // Выводит "3.1416".

cout .precision (4) ;

cout << f << '\n'; // Выводит "2.718е+04".

cout.setf(ios::fixed); // Установить флаг fixed.

cout<< f<<'\n'; // Выводит "27182.8184".

return 0;

}

Манипуляторы

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

Ниже приводится сводка имеющихся манипуляторов, как простых, так и параметризованных. Они Перечислены в алфавитном порядке.

Таблица 9.2. Простые и параметризованные манипуляторы

Манипулятор | Описание
dec | Задает десятичную базу преобразования.
end1 | Передает в поток символ новой строки и сбрасывает поток.
ends | Передает в поток символ завершающего строку нуля.
flush | Сбрасывает выходной поток.
hex | Задает шестнадцатеричную базу преобразования.
lock(ios Sir) | Блокирует дескриптор файла потока ir.
oct | Задает восьмеричную базу преобразования.
resetiosflags(int f) | Сбрасывает флаги, биты которых установлены в f.
setbase(int b) | Устанавливает базу преобразования (0, 8, 10 или 16).
setiosflags(int f) | Устанавливает флаги, биты которых установлены в f.
setfill(int c) | Задает символ заполнения (аналогичен функции fiilO).
setprecision(long p) | Задает точность (аналогичен функции precision ()).
setw(iong w) | Задает ширину поля (аналогичен функции width ()).
lunlock(ios &ir) | Разблокирует дескриптор файла для потока ir.
ws | Исключает начальные пробельные символы.

Вот пример использования некоторых манипуляторов (мы создали один свой собственный):

Листинг 9.2. Форматирование с помощью манипуляторов

/////////////////////////////////////////////////

// Manip.cpp: Демонстрация некоторых манипуляторов.

//

#include <iomanip.h>

#pragma hdrstop

#include <condefs.h>

//////////////////////////////////////////////////

// Манипулятор, определенный пользователем - звонок.

//

ostream shell(ostream &os)

{

return os<< '\a';

#pragma argsused

int main(int argc, char* argv[])

{

cout “ bell; // Тестирование манипулятора bell.

//

// Манипуляторы базы преобразования.

//

long 1 = 123456;

cout<< "Hex: "<< hex<< 1<< end1

<<"Oct: "<< oct<< 1<< end1

<< "Dec: " << dec << 1 << end1;

//

// Параметризованные манипуляторы.

//

int h=12, m=5, s=0; // To же, что в примере

// Format.cpp. cout << "The time is " << setfill('0')

<< setw(2) << h << ':'

<< setw(2) << m << ':'

<< setw(2) << s << setfillC ') << end1;

return 0;

}

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

Примечание

Создать параметризованный манипулятор не так просто. Существуют различные способы сделать это, но наиболее очевидный из них — реализация манипулятора через класс эффектора. Идея состоит вот в чем. Нужно определить для манипулятора собственный класс с конструктором, принимающим нужные параметры, •и перегрузить для этого класса операцию передачи (извлечения) соответствующего потока. После этого конструктор можно вызывать в качестве параметризованного манипулятора. Создается временный объект, который выводится в поток перегруженной операцией и удаляется. Ниже показан манипулятор, который выводит в поток свой аргумент типа unsigned в двоичной форме.

#include <iostream.h>

// Класс эффектора.

class Bin {

int val;

public:

Bin(unsigned arg) { val = arg; }

friend ostream &operator“(ostreams. Bin);

};

// Вывод числа в двоичной форме.

ostream &ooerator<<(ostream &os. Bin b) {

int cb = 1; // Контрольный бит для отсчета циклов.

do {

if (b.val <0) // Если val < 0, то старший бит = 1. os << 1;

else

os<< 0;

} while (b.vai<<= 1, cb<<= 1) ;

return os;

}

int main ()

(

unsigned n = Ox00ff0f34;

cout<< "Some binary: "<< Bin(n)<< end1;

return 0;

}

Рис. 9.1 Манипулятор, выводящий свой аргумент в двоичной форме

Форматирующие флаги

Флаги управления форматированием являются битовыми полями, хранящимися в переменной типа fmtflags (псевдоним int). Для их чтения и/или модификации могут применяться следующие функции-элементы класса ics:

  • int flags (), int flags (int). Без параметра возвращает текущее состояние флагов. При указанном параметре устанавливает новые значения флагов и возвращает их прежнее состояние.
  • int setf(int), long setf(int, int). Первая форма устанавливает флаги, биты которых установлены в параметре. Вторая форма модифицирует флаги, биты которых установлены во втором параметре. Значения этих флагов задаются первым параметром. Возвращает прежнее состояние всех флагов.
  • void unsetf(int). Сбрасывает флаги, биты которых установлены в параметре.

Помимо функций, для управления флагами можно пользоваться манипуляторами setiosflags (аналог setf() с одним параметром) и reset-iosflags (аналог unsetf ()).

В таблице 9.3 описаны форматирующие флаги потоков.

Таблица 9.3. Форматирующие флаги класса ios

Флаг | Описание
internal | Если установлен, при выводе чисел знак выводится на левом краю поля вывода, а само число выравнивается по правому краю поля. Промежуток заполняется текущим символом заполнения.
dec | Устанавливает десятичное представление чисел. Принимается по умолчанию.
oct | Устанавливает восьмеричное представление чисел.
hex | Устанавливает шестнадцатеричное представление чисел.
showbase | Если установлен, то при восьмеричном и шестнадцатеричном представлении чисел выводит индикатор основания (0 для восьмеричных и Ох для шестнадцатеричных чисел).
showpoint | Если установлен, для вещественных чисел всегда выводится десятичная точка.
uppercase | Если установлен, шестнадцатеричные цифры от А до F, а также символ экспоненты Е выводятся в верхнем регистре.
boolalpfa | Если установлен, булевы значения выводятся как слова “true/false”. В противном случае они представляются соответственно единицей и нулем.
showpos | Выводит + для положительных чисел.
scientific | Если установлен, вещественные числа выводятся в научной (экспоненциальной) нотации.
fixed | Если установлен, вещественные числа выводятся в десятичном формате (с фиксированной точкой).
unitbuf | Если установлен, поток сбрасывается после каждой операции передачи.

Несколько замечаний относительно перечисленных в таблице флагов.
  • Флаги left, right и internal являются взаимоисключающими. В данный момент времени может быть установлен только один из них.
  • Взаимоисключающими являются также флаги dec, oct и hex.
  • При модификации базы представления в качестве второго параметра setf() можно использовать константу ios: :basefield.
  • При модификации выравнивания в поле можно аналогичным образом использовать константу ios: :adjustfield.
  • При модификации формы представления (нотации) чисел с плавающей точкой можно использовать константу ios : : floatfield. Ниже мы приводим листинг программы, демонстрирующей применение различных флагов форматирования.

Примечание

Имена перечисленных выше флагов и других констант принадлежат к области действия класса ios. Вне этого класса нужно либо воспользоваться разрешением области действия (ios : : scientific), либо обращаться к ним, как к элементам существующего объекта (cout. scientific). Мы поедпочитаем первый способ.

Листинг 9.3. форматирующие флаги потоков

////////////////////////////////////////////////////

// Flags.срр: Форматирующие флаги потоков.

//

#include <iostream.h>

#include <iomanip.h> #pragma hdrstop

#include <condefs.h>

#pragma argsused

int main(int argc, char* argv[])

{

//

// Демонстрация флага skipws. Если его сбросить, то при

// наличии начальных пробелов при вводе возникает ошибка.

//

long 1;

cout<< "Enter an integer: ";

cin.unsetf(ios::skipws);

cin >> 1;

if (cin) // При ошибке потока

cin == NULL. cout<< "You entered "<< 1<< endl;

else {

cout << "Incorrect input."<< endl;

cin.clear (); // Обнуление битов ошибки.

} cout<<endl;

//

// Демонстрация флагов основания и знака.

// Задается основание 16, вывод индикатора и знака +.

//

1 = 8191;

cout.setf(ios::hex, ios::basefield);

cout.setf(ios::showbase | ios::showpos);

cout << "hex: " <<1 << oct // Изменим основание

<< " oct: "<< 1 << dec // манипулятором.

<< " dec: " << 1 << endl;

cout << endl;

//

// Демонстрация флагов формата вещественных чисел.

//

double dl = 1.0е9, d2 = 34567.0;

cout <<"Default: " << dl << " "<<d2 << end1;

// Вывод десятичной точки. cout.setf(ios::showpoint);

cout << "Decimal: " << dl<< " " << d2 << endl;

// Нотация с фиксированной точкой.

// Заодно сбросим вывод знака +.

cout.setf(ios::fixed, ios::floatfield | ios::showpos);

cout << "Fixed: " << dl << " " << d2 << endl;

cout<< endl;

//

// Вывод булевых значений как "true/false".

//

bool b = true;

cout.setf(ios::boolalpha) ;

cout << "Boolean values: " << b << '' << !b “ endl;

return 0;

}

Рис. 9.2 Демонстрация флагов форматиоования потока

Состояние потока

Состояние объекта класса ios (и производных от него) содержится в его закрытом элементе _state в виде набора битов. Следующая таблица перечисляет имеющиеся биты состояния потока.

Таблица 9.4. Биты состояния потока

Бит | Описание
goodbit | С потоком все в порядке (на самом деле это не какой-то бит, а 0 — отсутствие битов ошибки).
eofbit | Показывает, что достигнут конец файла.
failbit | Индицирует ошибку формата или преобразования. После очистки данного бита работа с потоком может быть продолжена.
badbit | Индицирует серьезную ошибку потока, связанную обычно с буферными операциями или аппаратурой. Скорее всего, поток далее использовать невозможно.

Для опроса или изменения состояния потока в классе ios имеется ряд функций и операций.

  • int rdstate() ; Возвращает текущее состояние.
  • bool eof() ; Возвращает true, если установлен eofbit.
  • bool good () ; Возвращает true, если не установлен ни один из битов ошибки.
  • bool fail () ; Возвращает true, если установлен failbit или bad-bit.
  • bool bad() ; Возвращает true, если установлен badbit.
  • void clear (int =0); Сбрасывает биты ошибки (по умолчанию) или устанавливает состояние потока в соответствии с аргументом.
  • void setstate(int) ; Устанавливает состояние битов ошибки с соответствии с аргументом.
  • operator void*() ; Возвращает нулевой указатель, если установлен какой-либо из битов ошибки.
  • bool operator! () ; Возвращает true, если установлен какой-либо из битов ошибки.

Примечание

Функция operator void*() неявно вызывается, если поток сравнивается с нулем (как cin в примере из листинга),

Файловые потоки

Файловые потоки библиотеки ввода-вывода реализуют объектно-ориентированную методику работы с дисковыми файлами. Имеется три класса таких потоков:

  • ifstream специализирован для ввода из дисковых файлов.
  • of stream специализирован для записи дисковых файлов.
  • fstream управляет как вводом, так и записью на диск.

Эти классы выводятся соответственно из istream, ostream и iostream. Таким образом, они наследуют все их функциональные возможности (перегруженные операции << и>>” для встроенных типов, флаги форматирования и состояния, манипуляторы и т. д.).

Чтобы работать с файловым потоком, нужен, во-первых, объект потока, а во-вторых, открытый файл, связанный с этим объектом.

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

Каждый из трех классов файловых потоков имеет четыре конструктора.

  • Конструктор, создающий объект без открытия файла:

ifstream () ;

of stream();

fstream () ;

  • Конструктор, создающий объект, открывающий указанный файл и закрепляющий этот файл за потоком. Аргументами являются имя файла, режим открытия и режим защиты (в Windows не используется):

if stream(const char *name,

int mode = ios::in, long prot = 0666);

ofstream(const char *name,

int mode = ios::out, long prot = 0666);

fstream (const char *name, int mode, long prot = 0666);
  • Конструктор, создающий объект и связывающий с ним уже открытый файл. В качестве аргумента передается дескриптор файла:

ifstreamfint file);

ofstream(int file);

fstream (int file) ;

  • Конструктор, создающий объект и связывающий с ним уже открытый файл; объект ассоциируется указанным буфером:

ifstream(int file, char *buf, int len)

of stream(int file, char *buf, int len)

fstream (int file, char *buf, int len)

Режимы открытия файла

Параметр mode, который имеет вторая форма конструктора, задает режим открытия файла. Для значений параметра класс ios определяет символические константы, перечисленные в таблице 9.5.

Таблица 9.5. Константы класса ios для режимов открытия файла

Константа | Описание
арр | Открытие для записи в конец файла.
ate | При открытии позиционирует указатель на конец файла.
binary | Файл открывается в двоичном (не текстовом) режиме.
in | Файл открывается для ввода.
out | Файл открывается для вывода.
trunc | Если файл существует, его содержимое теряется.

Константы можно комбинировать с помощью поразрядного OR. Для конструкторов классов if stream и ofstream параметр mode имеет значения по умолчанию — соответственно ios : : in и ios : : out.

Закрытие файла

В классах файловых потоков имеется функция close (), которая сбрасывает содержимое потока и закрывает ассоциированный с ним файл.

Кроме того, деструктор потока автоматически закрывает файл при уничтожении объекта потока.

При ошибке закрытия файла устанавливается флаг failbit.

Примеры файловых потоков

Следующий пример (листинг 9.4) демонстрирует различные режимы и способы открытия потока.

Листинг 9.4. Примеры открытия файловых потоков

/////////////////////////////////////////////////////////

// Filemode.срр: Режимы открытия файлов.

//

#include <f stream .,h>

#include <string.h>

#pragma hdrstop

#include <condefs.h>

char *data[] = {"It's the first line of test data.",

"Second ,line.",

"Third line.",

"That's enough!"};

//

// Функция для распечатки содержимого файла. //

int Print(char *fn) {

char buf[80] ;

ifstream ifs(fn) ;

if (!ifs) {

cout <<fn<< " - Error reading file." << endl;

return -1;

} while (ifs) {

ifs.getline(buf, sizeof(buf)) ;

if (ifs)

cout << buf<< end1;

} return 0;

}

#pragma argsused

int main(int argc, char* argv[])

{

char name[]= "Newfile.txt";

fstream fs(name, ios::in);

if (fs) { // Файл уже существует. cout “ name “ " - File already exists." << endl;

} else { // Создать новый файл.

cout<< name<< " - Creating new file."<< endl;

fs.open(name, ios::out);

for (int i=0; i<3; i++) fs << data[i] << endl;

}

fs.close () ;

cout << end1;

//

// Файл либо уже существовал, либо мы его только что

// создали. Распечатаем его.

// Print(name);

cout << endl;

//

// Допишем строку в конец файла.

// fs.open(name, ios::app);

if (rs) {

fs M<< data[3]<< endl;

fs.close ();

} Print(name);

return 0;

}

Рис. 9.3 Результат работы программы Filemode

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

Бесформатный ввод-вывод

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

Однако в библиотеке C++ имеется немало функций бесформатного ввода-вывода, которые часто применяют для чтения и записи двоичных (не-текстовых) файлов.

Двоичный режим ввода-вывода

Двоичный режим открытия файла (с установленным битом binary) означает, что никакой трансляции данных при передаче из файла в поток и обратно производиться не будет. Речь здесь идет не о форматных преобразованиях представления данных. При текстовом режиме (он принимается по умолчанию) при передаче данных между файлом и потоком производится замена пар символов CR/LF на единственный символ LF (' \n ') и наоборот. Это происходит до преобразований представления, которые выполняются операциями извлечения/передачи. Двоичный ввод-вывод означает всего-навсего, что такой замены происходить не будет; тем не менее двоичный режим необходим при работе с сырыми данными, т. е. данными в машинной форме без преобразования их в текстовый формат.

Чтобы открыть файл в двоичном режиме, нужно, как уже упоминалось, установить в параметре mode конструктора потока или функции open() бит ios::binary.

Чтение и запись сырых данных

Чтение сырых данных производится функцией read () класса istream:

istream &read(char *buf, long len);

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

Запись сырых данных производится функцией write () класса ostream. Она выглядит точно так же, как функция read () :

ostream &write(char *buf, long len);

Здесь buf — адрес буфера, в котором содержатся данные, а len — число символов, которые должны быть записаны в поток.

Обе функции возвращают ссылку на свой объект-поток. Это означает, что возможны их цепные вызовы, т. е. выражения вроде

ostream os (...);

os.write(...).write (...).write(...) ;

Вот небольшой пример записи и чтения сырых данных:

#include <iostream.h>

#include <fstream.h>

int main(void) {

char name[] = "testfile.dat";

int i = 1234567;

double d = 2.718281828;

//

// Открытие выходного потока в двоичном режиме

//и запись тестовых данных.

//

ofstream ofs(name, ios::out | ios::binary);

if (ofs) {

ofs.write((char*)&i, sizeof(i)); // Целое.

ofs.write((char*)&d, sizeof(d)); // Вещественное.

ofs.write(name, sizeof(name)); // Строка. ofs.close ();

}

//

// Открытие входного потока в двоичном режиме.

// if stream ifs(name, ios::in | ios::binary) ;

i = 0; //

d = 0; // Уничтожить данные.

name[0] = '\0'; //

//

// Прочитать данные.

//

if (ifs) {

ifs.read((char*)&i, sizeof (i));

ifs.read((char*)&d, sizeof(d));

ifs.read(name, sizeof(name));

ofs.close () ;

} //

// Проверка - напечатать прочитанные данные. //

cout “ "Data read from file: i = " << i<< ", d = " << d

<< ", name = " << name << endl;

return 0;

}

Некоторые функции потоков

В классах istream и ostream есть ряд функций, которые позволяют выполнять над потоками разные полезные операции (в основном при бесформатном вводе-выводе). Здесь мы опишем наиболее часто употребляемые из них.

Класс istream

Следующие функции являются элементами класса istream:
  • istream &ignore(long n = 1, int t = EOF) ; Эта функция позволяет пропустить при вводе ряд символов. Она извлекает из потока максимум n символов, пока среди них не встретится ограничитель t. Ограничитель также извлекается из потока.
  • long gcount() ; Функция возвращает число символов, извлеченных из потока последней функцией неформатированного ввода. (Если после нее выполнялись какие-либо форматные процедуры, это число может быть модифицировано.)
  • int peek () ; Функция возвращает следующий символ потока, не извлекая его. Возвращает EOF, если установлен какой-либо из флагов состояния потока.
  • istream Sputback (char с) ; Возвращает во входной поток последний извлеченный символ.
  • istream &seekg(long pos) , istream &seekg(long ofs, int dir) ;

Устанавливает положение указателя потока. Для первой формы указывается абсолютная, для второй — относительная позиция указателя. Параметр dir может принимать следующие значения:

  • ios : :beg смещение (ofs) от начала файла;
    •
  • ios : : cur смещение от текущей позиции указателя;
  • ios : : end смещение от конца файла.
  • long tellg() ; Возвращает текущее положение указателя входного потока.

Класс ostream

Последним двум функциям из istream соответствуют аналогичные функции класса ostream:

  • ostream &seekp(long pos), ostream &seekp(long ofs, int dir) ; Аналогична see kg () . Выполняет абсолютное или относительное позиционирование выходного потока.
  • long tellp() ; Аналогична tellg() . Возвращает текущую позицию выходного потока.

Чтение символов и строк

Для чтения одиночных символов, а также строк применяется функция get класса istream. Эта функция перегружена следующим образом:

int get () ;

istream &get(char &c) ;

istream &get(char *buf, long len, char t = '\n');

Две первые формы функции предназначены для извлечения из потока одиночного символа. Функция int get() возвращает символ в качестве своего значения. Функция get (char &c) передает символ в параметре и возвращает ссылку на свой поток.

Вот, например, как можно было бы выполнить посимвольное копирование файлов:

ifstream ifs("infile.dat");

ofstream ofs("outfile.dat");

while (ifs & ofs)

ofs.put(ifs.get());

// put (char) передает в поток

// одиночный символ.

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

  • будет встречен ограничивающий символ t (по умолчанию ' \n ');
  • будет встречен конец файла;
  • в буфер будет записано len символов, включая завершающий строку 0. Имеется еще функция getline(), во всем аналогичная этой форме

get ():

istream Sgetline(char *buf, long len, char t = '\n');

Разница между этими двумя функциями состоит в том, что get line () извлекает из потока ограничивающий символ, в то время как get () этого не делает. Ни та, ни другая функция не записывает ограничивающий символ в буфер.

Пример использования getline () вы уже видели в листинге 9.4. Вот небольшой пример чтения строк с помощью get ():

#inciude <iostream.h>

int main(void) {

char name[9], ext[4];

cout << "Enter a filename with extension: ";

cin.get(name, 9, '.');

cin.ignore (80, '.'); // Удалить все оставшиеся

// до точки символы. cin.get(ext, 4) ;

cin.ignore(80, '\n'); // Удалить все, что осталось

// в строке.

cout<< "Name: "<< name << "; extension: " << ext << endl;

return 0;

}

Эта программа, как нетрудно догадаться, усекает произвольное имя файла до формата 8.3.

Ввод-вывод с произвольным доступом

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

Приведенная ниже программа открывает (создает новый или переписывает старый) свой файл как двоичный, и, кроме того, сразу для ввода и вывода. Она применяет функции позиционирования потока и функции бесформатного чтения-записи.

Листинг 9.5. Произвольный доступ к файлу

//////////////////////////////////////////////////

// Random.cpp: Демонстрация файла с произвольным доступом.

//

#include <fstream.h>

#include <iomanip.h>

#pragma hdrstop

#include <condefs.h>

const int NP = 10;

const int IS = sizeof(int);

#pragma argsused

int main(int argc, char* argv[])

{

int pt, i;

//

// Открытие файла для чтения/записи.

//

fstream fs("random.pts",

ios::binary | ios::in | ios::out | ios::trunc);

if (ifs) {

cerr << "Failed to open file." << endl;

return (1);

}

//

// Первоначальная запись файла.

//

cout << "Initial data:" << endl;

for (i=0; i<NP; i++){

pt = i;

fs.write((char*)&pt, IS);

cout << setw(4) << pt;

}

cout << endl << endl;

//

// Чтение файла от конца к началу.

//

cout << "Read from the file in reverse order:"<< endl;

for (i=0; i<NP; i++) {

fs.seekg(-(i + 1) * IS, ios::end);

fs.read((char*)&pt, IS);

cout “ setw(4)<< pt; . }

cout<< end1 << end1;

//

// Переписать четные индексы.

//

for (i=l; i<NP/2; i++) {

fs.seekg(2 * i * IS) ;

fs.read((char*)&pt, IS);

pt = -pt;

fs.seekg(fs.tellg () - IS); // Возврат на шаг.

fs.write((char*)&pt, IS);

}

//

// Распечатать файл.

//

cout << "After rewriting the even records:"<<endl;

fs.seekg(0) ;

for (i=0; i<NP; i++) {

fs.read((char*)&pt, IS);

cout << setw(4) << pt;

}

cout << endl;

fs.close ();

return 0;

}

Примечание

Когда эта программа открывает уже существующий файл, он усекается до нулевой длины (т. е. все его данные теряются). Если вы хотите работать с имеющимися в файле данными, нужно убрать бит ios: :trunc из режима открытия потока. Кстати, в примере это можно сделать безболезненно — данные файла все равно сразу переписываются заново.

В этом примере мы пользовались для позиционирования потока функцией seekg () . Но поскольку поток у нас типа f stream, и открыт он в режиме чтения-записи, то все равно, какую функцию применять для позиционирования — seekg () или seekp () .

Примечание

He следует упускать из виду, что при выполнении операций бесформатного чтения или записи (read/write) указатель потока сдвигается вперед на число прочитанных (записанных) байтов.

Вывод программы показан на рис. 9.4.

Рис. 9.4 Программа Random

Заключение

Аппарат потоковых классов библиотеки C++ довольно громоздок, если сравнивать его, например, с функциями языка С вроде printf (). Однако средства потоков C++ единообразны, надежны и расширяемы. Как вы узнали из этой главы, можно достаточно просто перегрузить операции извлечения и передачи, чтобы с точки зрения потоков ввода-вывода определенный вами тип выглядел бы подобно встроенным типам данных.

В следующей главе мы займемся шаблонами — средством C++, которое позволяет создавать “обобщенные” классы, служащие моделью некоторого множества конкретных классов.


Глава 10. Шаблоны

Продавай мобильный контент со своего сайта

  • Глава 10. Шаблоны
    • Шаблоны функций
      • Перегрузка шаблонов функций
      • Специализация шаблона функции
    • Шаблоны классов
      • Определение шаблона класса
      • Создание представителей шаблона
      • Разное
    • Порождение представителей шаблона
      • Установки проекта и ключи компилятора
      • Ключи компилятора
      • Явное создание представителя шаблона
      • Ключевое слово typename

    Глава 10. Шаблоны

    Подобно тому, как класс является схемой для создания своих представителей-объектов, шаблон класса в C++ является схемой для образования конкретных представителей-классов шаблона, или шаблонных классов. Шаблоны классов называют иногда параметризованными типами, поскольку действительный тип (класс) создается посредством спецификации конкретных параметров шаблона.

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

    Шаблоны функций

    Синтаксис определения шаблона функции имеет вид:

    template <список формальных типов>возвращаемый_тип имя_функции(список параметров) {

    тело функции }

    Список_формальных_типов состоит из спецификаций вида class формалъный_тип, разделенных запятыми. Формальный тип может обозначаться любым идентификатором, аналогично формальному параметру функции.

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

    template <class T> void Func1 (Та, Т b) { ... }

    template <class T> T Func2(Т a, int b) { ... }

    template <class Tl, class T2> long Func3(Tl a, T2 b) { ... }

    Как видите, определение шаблона функции отличается от обычной функции только наличием конструкции template <список_формальных_типов> в заголовке.

    Примечание

    В качестве примера шаблонов функций можно привести определение функций min () и max () из заголовочного файла sdlib.h. Определение это сводится к следующему:

    template <class T> inline const Т &min(const Т&t1, const T&t2)

    if (t1 < t2)

    return tl;

    else

    return t2;

    }

    template <class T>

    inline const T &max(const T &tl, const T &t2)

    {

    if (t1 > t2) return t1;

    else

    return t2;

    }

    Эти функции можно вызывать с аргументами любого типа (класса), в котором определены операции “больше-меньше”.

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

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

    Перегрузка шаблонов функций

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

    // Возвращает больший из двух аргументов.

    template <class Т> Т Мах(Т а, Тb) {

    return а > b? а : b;

    // Возвращает наибольший элемент массива. template <class Т> Т Мах(Т аrr[], size_t size)

    (

    Т maxVal = arr[0] ;

    for(int i=l; i<size; i++) if (arr[i] > maxVal) maxVal = arr[i];

    return maxVal;

    }

    Специализация шаблона функции

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

    Допустим, мы хотим, чтобы шаблон Мах()из последнего примера порождал функцию для двух аргументов-строк, которая возвращала бы большую из них (в смысле алфавитного порядка). Функция Мах (char*, char*), порожденная из первого шаблона, сравнивала бы адреса строк вместо их содержимого. Поэтому нужно определить отдельную функцию Мах (char*, char*):

    char *Max(char *a, char *b) {

    return strcmp(a, b) > 0? а : b;

    }

    Примечание

    Когда компилятор встречает вызов какой-то функции, для его разрешения он следует такому алгоритму:

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

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

    Листинг 10.1. Перегрузка и специализация шаблона

    ////////////////////////////////////////////////////////

    // Functemp.cpp: Шаблоны функций.

    //

    #include <string.h>

    #include <iostream.h>

    #pragma hdrstop

    #include <condefs.h>

    // Возвращает больший из двух аргументов.

    template <class Т> Т Мах(Т а, Т b) {

    return a > b? a : b;

    }

    // Возвращает наибольший элемент массива.

    template <class Т> Т Мах(Т аrr[], size_t size) {

    Т maxVal = arr[0] ;

    for(unsigned i=l; i<size; i++)

    if (arr[i] > maxVal)

    maxVal = arr[i] ;

    return maxVal;

    }

    // Возвращает большую из двух строк.

    char *Max(char *a, char *b)

    {

    return strcmp(a, b) > 0? а : b;

    }

    // Вызывается для целочисленных аргументов // различающихся типов. long Max(long a, long b)

    {

    return Max<long>(a, b);

    }

    #pragma argsused int main(int argc, char* argv[])

    {

    int il = 11, i2 = 22;

    float fl = 1.11, f2 = 22.2;

    char str1[] = "First string.";

    char str2[] = "Second string.";

    char с = 33;

    cout << "Max int: " << Max(il, i2) << endl;

    cout<< "Max float: " “ Max(fl, f2) << endl;

    cout << "Max element: "<< Max(strl, strlen(strl)) << endl;

    cout << "Max string: " << Max(strl, str2) << endl;

    cout << "Max(int,char): " << Max(i1, c) << endl;

    return 0;

    }

    Последнее определение—Max (long, long) —требует некоторых пояснений. Эта специализированная функция вызывает явным образом функцию шаблона для сравнения двух аргументов фактического типа long. Но какой в этом смысл?

    Если не определить такую функцию, компилятор вообще не сможет вызвать, например, Мах (int, char), как в последнем операторе вывода. (Подобные сравнения являются на самом деле “признаком дурного тона”.) Имеется только шаблон, два параметра которого имеют один и тот же тип, а как говорилось выше, компилятор использует шаблон только в том случае, если можно получить точное соответствие параметров типам аргументов в вызове. Однако благодаря определению специализированной функции компилятор может разрешить вызов, преобразовав char в long.

    На рисунке показан результат работы программы.

    Рис. 10.1 Программа Functemp

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

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

    template <список параметров шаблона> class имя шаблона {тело_класса };

    В списке_параметров_шаблона .могут присутствовать элементы двух видов:

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

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

    Определение шаблона класса предполагает:
    • определение собственно шаблона, синтаксис которого показан выше;
    • определение функций-элементов шаблона;
    • определение статических элементов данных.

    Определения функций-элементов, расположенные в теле шаблона, ничем не отличаются от определения встроенных функций-элементов обычного класса. Определения функций-элементов, располагаемые вне тела шаблона, имеют такой вид:

    template <список параметров шаблона> возвращаемый_тип имя шаблона< параметры_шаблона>::имя_функции(список_параметров) {тело_функции }

    Подобным же образом определяются статические элементы данных шаблона класса:

    template <список параметров шаблона>

    тип имя шаблона

    <параметры шаблона>::имя статического элемента[ =значение];

    Смысл всех синтаксических элементов определений будет ясен, если рассмотреть пример законченного шаблона класса:

    Листинг 10.2. Шаблон класса DataBase

    ///////////////////////////////////////////////////////

    // Deftmpl.h: Пример определения шаблона класса.

    //

    #ifndef _DEFTMPL_H

    #define _DEFTMPL_H

    template <class T, int numRec> class DataBase { protected:

    const int num.;

    bool err;

    T *base, *cp;

    public:

    DataBase (): num(numRec)

    {

    cp = base = new T[numRec];

    err = false;

    }

    ~DataBase () ( delete [] base;

    } bool Error () { return err; } T SRec(void) ;

    T &Rec(unsigned recno);

    };

    // Возвращает ссылку на текущую запись

    // и переходит к следующей.,

    template <class T, int numRec>

    Т &DataBase<T, numReO: :Rec (void)

    {

    if (cp - base == num) { err = true;

    return *(cp - 1) ;

    }

    else

    return *cp++;

    // Позиционирует указатель и возвращает ссылку

    // на текущую запись.

    template <class T, int numRec>

    T &DataBase<T, numRec>::Rec(unsigned recno)

    {

    if (recno < (unsigned)num) { err=false;

    return *(cp = base+recno);

    }

    else {

    err=true;

    return*(cp=base+num-1);

    }

    }

    #endif

    //_DEFTMPL_H

    Этот шаблон реализует примитивный “поток” или “базу данных”, являющуюся массивом записей, тип которых определяется аргументом шаблона. Класс содержит функции для обращения либо к записи с указанным индексом (Rec (unsigned)), либо к записи, на которую ссылается указатель “базы данных” (Re с (void)). В последнем случае указатель перемещается к следующей записи.

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

    Параметрами шаблона являются формальный тип записи Т и константа — число записей в массиве.

    Примечание

    Функции-элементы шаблона класса, определяемые вне тела шаблона, могут объявляться как встроенные с помощь, ключевого слова inline, подобно функциям-элементам обычных классов. Например, в приведенном выше определении шаблона можно было бы написать:

    template <class T,int numRec>

    inline T &DataBase<T, numRec>::Rec(void)

    {

    if (cp-base== num) {err= true/return*(cp-1) ;

    } else

    return *cp++;

    }

    Создание представителей шаблона

    Чтобы создать из шаблона представитель конкретного класса, нужно конструировать объект, указав для его типа имя шаблона с набором конкретных аргументов (типов и констант). Каждый формальный тип в списке параметров шаблона нужно заменить на имя действительного типа. Каждая формальная константа заменяется на константу указанного в шаблоне типа:

    // Шаблон класса. template <ciass T, int О class TmplClass { ... };

    // Создание представителей шаблонных классов.

    TmplClass<long, 100> IClassObj;

    TmplClass<float, 40> *fClassPtr;

    fClassPtr = new TmplClass<float, 40>;

    После того, как представитель шаблонного класса создан, с ним можно обращаться точно так же, как с любым объектом, принадлежащим к обыч-

    ному классу. Ниже показан пример программы, использующей определение шаблона из листинга 10.2.

    Листинг 10.3. Создание и использование представителя шаблонного класса

    ///////////////////////////////////////////////

    // Usetmpl.cpp: Использование шаблона класса. //

    #include <iostream.h>

    #pragma hdrstop

    #include <condefs.h>

    #include "Deftmpl.h"

    // Включить определение шаблона.

    // Класс записей, для которого будет создан шаблонный класс. class Record {

    char str[41] ;

    public:

    Record(void) { str[0] = 0; }

    void Set(const char *s)

    { strncpy(str, s, 40);}

    char *Get(void)

    { return str; } };

    #pragma argsused

    int main(int argc, char* argv[])

    {

    const int NumRec = 4;

    DataBase<Record, NumRec> db; // Объявление объекта

    // с 4-мя записями.

    // Инициализация массива.

    db.RecO .Set("First string.");

    db.Rec().Set("Second string.");

    db.RecO .Set("Third string.");

    db.Rec().Set("Fourth string.");

    cout.setf(ios::boolalpha);

    // Чтение с попыткой выхода за пределы массива.

    db.Rec(O); // Позиционирование на 0.

    for (int i=0; i<=NumRec; i++) {

    cout << db.RecO .Get() << " Error: ";

    cout << db.Error() << endl;

    } cout << endl;

    // Чтение с прямым указанием индекса.

    for (int i=NumRec-l; i>-l; i--) {

    cout << db.Rec(i).Get() << " Error: ";

    cout << db.Error() << endl;

    }

    return 0;

    }

    Вывод программы показан на рис. 10.2.

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

    Программа создает представитель шаблонного класса DataBase< Record, 4> и выполняет над ним различные действия — запись строк в “поток”, позиционирование, чтение.

    Рис. 10.2 Программа UsetmpI

    Примечание

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

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

    Для удобства работы с шаблонными классами можно воспользоваться определением typedef, например:

    template<class T> TmplClass { ... };

    typedef TmplClass<int> IClass;

    IClass iCIassObj; IClass *iCiassPtr; iCIassPtr = new IClass;

    Разное

    В этом параграфе мы расскажем о некоторых возможностях шаблонов, предусмотренных в стандартном C++, но не реализуемых компилятором C++Builder. Нам кажется, что о них необходимо рассказать, хотя бы для того, чтобы, читая другие книги по C++, вы не пытались осуществить в C++Builder методики, которые на нем осуществить невозможно.

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

    Если у вас есть Borland C++ 5 или более поздняя версия, и вы хотя бы немного умеете с ним работать, то можете при желании разобрать с его помощью приведенные ниже примеры.

    Примечание

    Специализация шаблона класса

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

    #include <iostream.h>

    #include <string.h>

    const int DefSize = 4;

    template <class T> Glass MyArray { protected:

    int size;

    int current;

    T *arr;

    public:

    MyArray (int n = DefSize) { size = n;

    current = 0;

    arr = new T[size];

    }

    ~MyArray ();

    void Insert(const T Sitem);

    T &Get(int idx) { return arr[idx]; } };

    // Общий шаблон Insert:

    template <class T> void MyArray<T>::Insert(const T Sitem)

    {

    if (current == size) return;

    arr[current++] = item;

    }

    // Специализированная Insert для параметра char*:

    void MyArray<char*>::Insert(char* const Sitem)

    {

    if (current == size) return;

    arr[current] = new char[strlen(item) + 1];

    strcpy(arr[current++], item);

    }

    // Общий деструктор:

    template <class T>

    MyArray<T>::-MyArray () ( delete[] arr; }

    // Специализированный деструктор:

    MyArray<char*>::-MyArray() (

    for (int i=0; i<size; i++)

    delete [ ] arr[i];

    delete [ ] arr;

    }

    А вот главная функция, тестирующая шаблон для “стандартного” типа int и для “специального” типа строк (т. е. char*):

    int main(void)

    {

    // Создание, заполнениеи вывод MyArray<int>.

    MyArray<int> *iArr;

    iArr = new MyArray<int>;

    int i;

    for (i=0; i<DefSize; i++) iArr->Insert (i);

    cout << "Integers: ";

    for (i=0; KDefSize; i++)

    cout << " " << iArr->Get(i);

    cout<< end1<< end1;

    delete iArr; // Уничтожение объекта.

    // Создание, заполнение и вывод MyArray<char*>.

    MyArray<char*> *sArr;

    sArr = new MyArray<char*>;

    for (i=0; KDefSize; i++) sArr->Insert("String!");

    cout << "Strings: ";

    for (i=0; KDefSize; i++)

    cout << " " << sArr->Get(i) ;

    cout << end1;

    delete sArr; // Уничтожение объекта.

    return 0;

    }

    Полная специализация шаблона

    Примечание

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

    #include <iostream.h>

    #include <string.h>

    const int DefSize = 4;

    // Общий шаблон:

    template <class T> class MyArray { protected:

    int size;

    int current;

    T *arr;

    public:

    MyArray(int n = DefSize) { size = n;

    current = 0;

    arr = new T[size];

    }

    ~MyArray() { delete[] arr; }

    void Insert(const T &item) {

    if (current == size) return;

    arr[current++] = item;

    }

    T &Get(int idx) { return arr[idx]; } } ;

    // Специализированный шаблон для char*:

    class MyArray<char*> { protected:

    int size;

    int current; char **arr;

    public:

    MyArray(int n = DefSize) { size = n;

    current = 0;

    arr = new char*[size];

    } ~MyArray() ;

    void Insert(char* const &item) { if (current == size) return;

    arr[current] = new char[strlen(item) + 1];

    strcpy(arr[current++], item);

    } char* &Get(int idx) { return arr[idx]; }

    };

    // Деструктор специализированного шаблона:

    MyArray<char*>::~MyArray() {

    for (int i=0; i<size; i++) delete [ ] arr[i] ;

    delete[] arr;

    }

    Заметьте, что все функции-элементы общего шаблона определены теперь как встроенные, а в специализированном только деструктор определен вне тела шаблона (его нельзя объявить как встроенный, поскольку он содержит оператор цикла). В общем, полное переопределение шаблона целесообразно в том случае, когда необходима специализация большинства его элементов-функций. Главная функция ничем не отличается от функции предыдущего примера.

    Функции, дружественные шаблону

    В качестве “друзей” класса чаще всего объявляют различные функции-операции, в которых участвуют объекты класса. Типичным примером может служить операция передачи объекта в поток. Для шаблона класса можно определить шаблон дружественной функции (не обязательно, конечно, операции). Такой шаблон будет порождать отдельную дружественную функцию для каждого генерируемого шаблонного класса. Вот пример шаблона дружественной функции (это модификация первого примера параграфа):

    #include <iostream.h>

    #include <string.h>

    const int DefSize = 4;

    template <class T> class MyArray { protected:

    int size;

    int current;

    T *arr;

    public: MyArray(int n = DefSize) { size = n; current = 0;

    arr = new T[size];

    } ~MyArray();

    void Insert(const T&);

    T &Get(int idx) { return arr[idx]; }

    friend ostream &operator“(ostream&, const MyArray<T>&);

    };

    // Шаблон дружественной функции-операции передачи объекта // в поток:

    template <class T>

    ostream &operator<<(ostream &os, const MyArray<T>&ma)

    {

    for (int i=0; i<ma.current; i++) os << " (" << ma.arr[i]<< "}";

    return os;

    }

    //

    // Здесь находятся общие и специализированные

    // функции-элементы... //

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

    int main(void)

    {

    MyArray<int> *iArr;

    iArr = new MyArray<int>;

    int i;

    for (i=0; KDefSize; i++) iArr->Insert (i) ;

    // Вывод объекта MyArray<int>:

    cout << "Integers: " << *iArr<< endl;

    cout << endl;

    delete iArr;

    MyArray<char*> *sArr;

    sArr = new MyArray<char*>;

    for (i=0; i<DefSize; i++) sArr->Insert("String!");

    // Вывод объекта MyArray<char*>:

    cout << "Strings: "<< *sArr << endl;

    delete sArr;

    return 0;

    }

    Результат работы программы показан на рис. 10.3.

    Рис. 10.3 Пример с шаблоном

    дружественной

    функции-операции

    Порождение представителей шаблона

    Этот раздел посвящен тому, каким образом C++Builder генерирует шаблонные классы и функции в программах, состоящих из нескольких модулей исходного кода. Несколько модулей проекта могут подключать один и тот же заголовочный файл с шаблоном, и создавать объекты одного и того же шаблонного класса (с одинаковым набором аргументов шаблона). В C++Builder имеются средства, позволяющие избежать дублирования кода в такой ситуации.

    Установки проекта и ключи компилятора

    В диалоге Project Options на странице C++ имеется флажок Templates: External (рис. 10.4). По умолчанию он сброшен, что означает оптимальное, или “интеллигентное”, порождение представителей шаблонов классов и функций.

    Рис. 10.4 Страница C++ диалога Project Options

    При сброшенном флажке External компилятор порождает глобальные представители шаблонов для всех модулей, где создаются объекты шаблонных классов (или вызываются шаблонные функции). Это возможно только в том случае, если компилятор при обработке модуля “видит” все определение шаблона, со всеми его функциями-элементами. Однако объектный код не обязательно генерируется для всех шаблонных методов (определенных как не-встроенные). По умолчанию код генерируется для методов:

    • действительно вызываемых в модуле;
    • виртуальных;
    • всех методов явным образом генерированного представителя шаблона класса.

    Что означает последний пункт, будет рассказано чуть ниже.

    После этого компоновщик ilink32.exe анализирует код объектных файлов и помещает в исполняемый файл только один экземпляр функции для каждой комбинации шаблон/аргументы.

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

    Ключи компилятора

    Сброшенному флажку External в диалоге Project Options соответствует ключ командной строки компилятора -Jgd, а установленному — -Jgx. Посредством директивы препроцессора #pragma option можно указать ту или иную опцию для конкретного файла. Не следует забывать о том, что компиляция и компоновка программы — два различных этапа ее создания. Компоновщик не знает, что вы задали в диалоге Project Options для компилятора. В любом случае компоновщик не будет включать в исполняемый код повторные определения функций шаблона.

    Явное создание представителя шаблона

    В подавляющем большинстве случаев прикладному программисту нужно только сбросить флажок External в диалоге Project Options и больше не беспокоиться о том, как написанные им шаблоны классов будут обрабатываться. Однако, если вы хотите создать, например, динамическую библиотеку на основе шаблона, которая будет содержать код всех его функций-элементов, то для генерирования полного представителя шаблона вам придется воспользоваться директивой template.

    Следующий пример включает в себя два исходных файла и один заголовочный, в котором определяется простой шаблон. Главный исходный модуль программы создает два шаблонных объекта для его аргументов int и float, но, поскольку модуль компилируется с директивой #pragma option -Jgx, то никакого кода для представителей шаблона в нем не создается. Вместо этого во втором исходном модуле (компилируемом с ключом -Jgx) явным образом генерируется полный представитель шаблона для аргумента float, а также неявно генерируется представитель для int, так как модуль ссылается на него.

    Листинг 10.4. Директива порождения представителя шаблона

    //////////////////////////////////////////////////

    // Simptmpl.h: Простой шаблон класса.

    //

    template <class T> class Simple {

    protected:

    int size;

    int current;

    T *arr;

    public:

    Simple(int) ;

    ~Simple () ;

    void Insert(T item)

    { if (current != size) arr[current++] = item; }

    T SGet(int) ;

    };

    template <class T> inline Simple<T>::Simple(int n): size(n),

    current (0), arr(new T[n]) {}

    template <class T> Simple<T>::~Simple() { delete[] arr; }

    template <class T> T SSimple<T>::Get(int idx) { return arr[idx]; }

    void Somefunc(int);

    /////////////////////////////////////////////////////////

    // Instance.cpp: Порождение представителей шаблона. //

    #pragma option -Jgx

    #include <iostream.h>

    #pragma hdrstop

    #include <condefs.h>

    #include "Simptmpl.h"

    USEUNIT("Somefunc.cpp") ;

    #pragma argsused

    int main(int argc, char* argv[])

    {

    const int Num = 9;

    Simple<int> ia(Num);

    Simple<float> fa(Num);

    fa.Insert(3.14) ;

    cout << "Float In main(): " << fa.Get(O) << endl;

    for (int i=0; i<Num; i++)

    ia.Insert(i * 2);

    cout << "From main(): ";

    for (int i=0; i<Num; i++)

    cout << ia.Get(i) << " ";

    cout << end1;

    Somefunc(10);

    cin.ignore();

    return 0;

    }

    //////////////////////////////////////////////////////////

    // Somefunc.cpp: Функция, использующая шаблон класса. //

    #pragma option -Jgd #include <iostream.h>

    #pragma hdrstop #include "Simptmpl.h"

    //

    // Следующая строка генерирует явный представитель шаблона

    // для типа float, хотя в данном модуле он не используется:

    // template class Simple<float>;

    void Somefunc(int n)

    {

    Simple<int> iArr(n);

    for (int i=0; i<n; i++) iArr.Insert (i);

    cout << "From Somefunc(): "<< n;

    for (int i=0; i<n; i++)

    cout<<" "<< iArr.Get(i);

    cout <<end1; ;

    }

    Вывод программы показан на рис. 10.5.

    Рис. 10.5 Результат работы программы Instance

    Ключевое слово typename

    Это ключевое слово может применяться в двух случаях. Во-первых, им можно заменять ключевое слово class в списке параметров шаблона. Такое дополнение сделано в ANSI C++ потому, что ключевое слово class в списке параметров не вполне отражает положение дел;

    параметром шаблона может быть любой тип, а не только класс, и стандартный синтаксис может вводить некоторых в заблуждение. Следующие две нотации эквивалентны:

    template <class T> class SomeClass {...};

    template <typename T> class SomeClass {...};

    Во-вторых, typename необходимо, если шаблон определяет некоторые объекты еще не объявленных типов. Рассмотрите такой шаблон функции:

    template <class T>

    void Func(Т Sargi, typename T::Inner arg2)

    {

    typename T::Inner tiObj;

    // Локальный объект

    // типа Т::Inner.

    // ...

    Примечание

    Нам еще как-то не приходилось говорить, что объявление класса может содержать вложенные объявления других типов, в том числе классов. Например:

    class One { public:

    class Two {

    // Элементы класса Two...

    } ;

    private:

    Two objOfTwo;

    // Другие элементы One... }

    Сослаться на вложенный тип можно либо через существующий объект, либо с помощью операции разрешения области действия с префиксом имени класса. Очень часто так объявляют перечисления и константы, пример чему вы могли видеть в классе ios, объявляющем перечислимые типы и их константы вида ios::fixed и т. п.

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

    typename.

    Заключение

    В этой главе вы увидели, насколько широкие и мощные возможности предоставляют программисту шаблоны функций и классов. Можно легко объявлять нужные вам классы, создавая представители уже имеющихся шаблонов; существует много больших библиотек, реализующих этот принцип. К их числу относятся, например, библиотека контейнерных классов Борланда и Стандартная библиотека шаблонов ANSI C++ (STL), о которой мы немного расскажем в следующей главе.


Глава 11. Библиотека стандартных шаблонов

Продавай мобильный контент со своего сайта

  • Глава 11. Библиотека стандартных шаблонов
    • Введение в библиотеку стандартных шаблонов
      • Состав библиотеки
      • Заголовочные файлы
      • Итераторы
      • Функции и функциональные объекты
    • Контейнеры
      • Векторы
      • Списки
      • Очереди deque
      • Множества и мультимножества
      • Битовые множества
      • Карты и мультикарты
      • Стеки
      • Очереди
      • Приоритетные очереди
    • Алгоритмы
      • Накопление
      • Подсчет
      • Поиск и замена
      • Удаление элементов
      • Перестановка
      • Лексикографическое сравнение
      • Сортировка
    • Стандартные строки
      • Создание строк
      • Характеристики строк
      • Ввод и вывод строк
      • Операции над строками
      • Функции строк

Глава 11.

Библиотека стандартных шаблонов

До сравнительно недавнего времени в языке C++ не было других стандартных средств программирования, кроме старой библиотеки стандартных функций С, которая совершенно не использовала мощных нововведений, таких, как классы, шаблоны, inline-функции и исключения. Библиотека стандартных шаблонов (Standard Template Library), разработанная в HP Laboratories, явилась в свое время весьма удачным шагом в решении проблемы стандартной библиотеки ANSI C++, в которую она теперь и входит.

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

Введение в библиотеку стандартных шаблонов

Состав библиотеки стандартных шаблонов можно разбить на две категории: алгоритмы и структуры данных. Из последних можно выделить стандартные строки, поскольку они реализованы совершенно независимо от остальных элементов библиотеки. Строки мы будем обсуждать отдельно и подробнее, чем все остальное.

Состав библиотеки

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

  • Итераторы, которые в некоторых отношениях подобны указателям. Это фундаментальное понятие STL; итераторы обеспечивают доступ к элементам данных контейнеров.
  • Контейнеры представляют собой структуры или наборы данных, такие, как списки, векторы, очереди, реализованные как шаблоны классов.
  • Уже упомянутые алгоритмы, представляющие собой шаблоны функций, оперирующих на данных контейнеров. Например, алгоритм может сортировать объекты, являющиеся элементами вектора или списка. Функции эти не обладают специфическими “знаниями” о типах и структурах, на которых они действуют.

Примечание

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

Вместе с STL в C++Builder предусмотрены различные дополнительные средства, например, класс комплексных чисел и средства обработки ошибок.

Заголовочные файлы

Стандартная библиотека C++ вводит новый стиль спецификации заголовочных файлов. Расширение .h опускается. Например, для подключения библиотеки алгоритмов нужно написать

#include <algorithm>

Компилятор автоматически укорачивает имя до восьми символов, добавляет .h и читает файл algorith.h из каталога $(BCB)\Include. На уровне исходного кода программы C++ получаются более мобильными, не привязанными к конкретной системе именования файлов.

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

Таблица 10.1. Контейнерные классы STL

Директива #include | Класс контейнера
<bitset> | bitset — множества как битовые наборы.
<deque> | deque — двусвязные очереди; имя является сокращением от “double-end queue”.
<iist> | list — списки.
<map> | map, multimap — карты; это структуры, подобные массиву, но в которых роль “индекса” могут играть не только целые числа, но любые упорядоченные типы.
<queue> | queue, priority queue — очереди, т. е. структуры, организованные по принципу “первым вошел, первым вышел”.
<set> | set, multiset — множества.
<stack> | stack — стеки, организованные по принципу “последним вошел, первым вышел”.
<vector> | vector, vector<bool> — векторы, во многом подобные обычным массивам.

Итераторы

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

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

Примечание

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

Основными операциями над итераторами- являются, как и в случае указателей, разыменование и инкремент. Если итератор i после конечного ряда приращений может стать равным итератору j, то говорят, что итератор j достижим из i. Если к итератору, достигшему верхней границы диапазона, применить операцию инкремента, он примет запредельное значение.

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

Типы итераторов

Существует пять основных форм итераторов:
  • Входной итератор обеспечивает доступ к контейнеру только для чтения в поступательном направлении (т. е. к итератору применима операция инкремента).
  • Выходной итератор обеспечивает доступ только для записи, также в поступательном направлении.
  • Поступательный итератор предоставляет доступ для чтения-записи в поступательном направлении.
  • Двунаправленный итератор допускает чтение и запись как в поступательном, так и реверсивном направлениях (к нему применимы как инкремент, так и декремент).
  • Итератор произвольного доступа предоставляет прямой доступ к данным для чтения-записи.

Итераторы, стоящие в этом списке ниже, выводятся из тех, что находятся выше. Это едва ли не единственный пример классовой иерархии в 8TL.

В следующей таблице показано, какими контейнерами стандартной библиотеки генерируются те ли иные итераторы.

Таблица 10.2. Итераторы, генерируемые стандартной библиотекой

Форма итератора | Контейнеры
входной итератор | istream iterator
выходной итератор | ostream iterator
двунаправленный итератор | List set и multiset map и multimap
итератор произвольного доступа | обычные указатели vector deque

Указатели как итераторы

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

#include <algorithm>

#include <iostream>

using namespace std;

#define SIZE 50 int iArr[SIZE] ;

int main() {

iArr[30] = 33;

int *ip = find(iArr, iArr + SIZE, 33);

if (ip != iArr + SIZE)

cout<< "Value "<< *ip<< " found at position "<< (ip - iArr)<< endl;

else

cout << "Value not found."<< endl;

return 0;

}

Прежде всего обратите внимание, что программа, применяющая стандартную библиотеку C++, должна специфицировать директивой using namespace пространство имен std.

В примере объявляется “контейнер” — обычный массив длиной в 50 элементов, одному из его элементов присваивается значение 33 и вызывается алгоритм find () для поиска этого значения.

Алгоритму find () передаются три аргумента. Два из них — итераторы, задающие диапазон поиска. Первый из них в данном случае указывает на начальный элемент массива, второй имеет запредельное значение iArr + SIZE, т. е. смещен на один элемент за верхнюю границу массива. Третий аргумент задает искомое значение.

Если find () находит его в заданном диапазоне, алгоритм возвращает соответствующий ему итератор; если нет, возвращается запредельное значение.

Итераторы контейнеров

Итераторы, генерируемые классами контейнеров, используются точно таким же образом, как указатели в показанном выше примере, но для получения граничных значений итератора вь1зываются обычно функции вроде begin () или end () конкретного контейнерного объекта. Вот совершенно аналогичный предыдущему пример для контейнера-вектора:

#include <algorithm>

#include <vector>

#include <iostream>

using namespace std;

#define SIZE 50 vector<int> iVect(SIZE);

int main() {

iVect[30] = 33;

vector<int>::iterator ii =

find (iVect. begin (), iVect.endO, 33);

if (ii != iVect.endO)

cout << "Value "<< *ii<< " found at position "

<< distance(iVect.begin(), ii) << endl;

else

cout << "Value not found." <<end1;

return 0;

Объявляемый в программе контейнер имеет тип vector<int>, а итератор — тип vector<int>: : iterator. Каждый стандартный контейнер объявляет свой собственный вложенный класс iterator.

Далее мы вкратце рассмотрим различные формы итераторов.

Входные, выходные и поступательные итераторы

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

К обеим этим формам итераторов можно применять, по меньшей мере, операцию неравенства (!=), разыменования (*) и инкремента (++).

Ниже показан пример копирования массива в вектор при посредстве выходного итератора и алгоритма copy (). Его последним параметром может быть любой выходной итератор. На самом деле тот же итератор здесь используется и как входной — в операторе вывода.

#include <algorithm>

#include <vector>

#include <iostream>

using namespace std;

double dArr[10] =

{1.0, 1.1, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9};

vector<double> dVect(lO);

int main()

{

vector<double>::iterator oi = dVect.begin ();

copy(dArr, dArr + 10, oi);

while (oi != dVect.endO) {

cout << *oi << endl;

oi++;

} return 0;

}

Итераторы потоков

Собственно, только входные и только выходные итераторы имеют смысл в основном при работе с потоками ввода-вывода, которые могут быть допускать либо только извлечение, либо только передачу данных. Любые контейнеры стандартной библиотеки генерируют более сложные, итераторы, которые, естественно, могут применяться и в качестве простых входных или выходных. / Вы уже хорошо знакомы со стандартными потоками cin и cout, извлечение и передача данных из которых производится операциями >> и <<. Однако возможен другой метод работы с этими потоками, при котором входной или выходной объект iostream преобразуется в итератор. Затем его можно передавать как аргумент стандартным алгоритмам.

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

#include <algorithm>

#include <vector>

#include <iostream>

using namespace std;

int main( ) {

vector<int> iVect(lO);

for (int i=0; i<10; i++) iVect[i] = i;

cout<< "The vector contents are: { ";

copy(iVect.begin (),

iVect.endf), ostream_iterator<int>(cout, " "));

cout << "}." << endl;

return 0;

}

Примечание

Последний параметр алгоритма copy () конструирует выходной итератор типа ostream iterator<int>. Параметрами конструктора являются выходной поток и строка - разделитель значений.

Поступательный итератор допускает как чтение, так и запись в контейнер. Однако, как и в случае двух предыдущих, возможен только инкремент, но не декремент итератора. Поступательные итераторы могут использоваться, например, в алгоритме replace (), который определяется так:

template <class Forwardlterator, class T>

void replace(Forwardlterator first,

Forwardlterator last,

const T &old_value,

const T &new_value);

Этот алгоритм заменяет все значения old_value, содержащиеся в контейнере, на new_value.

Двунаправленные итераторы

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

template <class Bidirectioriallterator>

void reverse(Bidirectionallterator first,Bidirectionallterator.last);

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

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

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

Алгоритм random_shuffle, также требующий итераторов произвольного доступа, случайным образом переставляет значения элементов в указанном диапазоне контейнера:

template <class RandomAccessIterator>

void random shuffle(RandomAccessIterator first, RandomAccessIterator last);

Итераторы вставки

Примечание

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

  • Начальные адаптеры, вставляющие объекты в начало контейнера, например, списка (при этом вставленные элементы данных оказываются расположенными в порядке, обратном исходному).
  • Конечные адаптеры, присоединяющие объекты в конец контейнера.
  • Адаптеры вставки, вставляющие данные перед произвольным элементом контейнера.

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

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

////////////////////////////////////////////////////////////////

// Inserter.срр: Демонстрация итераторов вставки. //

#include <algorithm>

#include <list>

#include <iostream>

#pragma hdrstop

#include <condefs.h>

using namespace.std;

int iArr[5] = (1, 2, 3, 4, 5);

//

// Функция вывода содержимого списка.

//

void Display(list<int> &1, const char *label)

(

cout << label<< ": { ";

copy (1 .begin (), 1.end(),

ostream_iterator<int>(cout, " "));

cout << "}" << endl;

}

int main(void) {

list<int> iLst; // Создание объекта списка.

// Копирование массива в список в обратном порядке:

copy(iArr, iArr + 5, front_inserter(iLst));

Display(iLst, "Before insertion");

// Поиск значения З:

list<int>::iterator i = find(iLst.begin(),

iLst.end(), 3) ;

// Вставка массива в список:

copy(iArr, iArr + 5, inserter(iLst, i));

Display(iLst, "After insertion ");

cin.ignore ();

return 0;

}

Рис. 11.1 показывает результат работы программы. Можно отметить различие между inserter (iLst, i-Lst. begin ()) и front inserter (iLst). Первый адаптер вставляет данные в контейнер в прямом, а второй — в обратном порядке.

Рис. 11.1 Демонстрация адаптеров

Функции итераторов

Имеются две функции, которые могут оказаться полезными при работе с итераторами. Это advance () и distance (.) .

Функция advance () выполняет инкремент или декремент итератора указанное число раз. Ей передается итератор и число, определяющее число повторений инкремента или декремента (при отрицательном аргументе). Допустим, вам требуется найти некоторый элемент списка и установить итератор на несколько позиций за ним. Вы можете написать:

list<int> :: iterator i = find (iLst .begin (), iLst.endO, 3);

advance(i, 2); // Сдвигает итератор на 2 позиции вперед.

С функцией distance () вы уже встречались в примере параграфа “Итераторы контейнеров”, где с ее помощью выяснялась позиция итератора по отношению к началу вектора. Эта функция определяет количество инкрементов, которые нужно выполнить для перехода от одного итератора к другому. Она перегружена:

template <class Forwardlterator> iterator_traits<Forward!terator>::

difference_type distance(Forwardlterator first, Forwardlterator last) ;

template <class Forwardlterator, class Distance>

void distance(Forwardlterator first,

Forwardlterator last. Distance &n) ;

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

int d = 0;

distance (iLst, i, d);

Функции и функциональные объекты

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

Функции и предикаты

Иногда нужно выполнить какое-то действие для каждого элемента контейнера. Упомянутый выше алгоритм for_each() позволяет сделать именно это. Функция, выполняющая необходимое действие для отдельного элемента, передается как третий аргумент алгоритма. Первые два задают диапазон. Вот пример:

void Square(int arg) { return arg * arg; }

int main() {

vector<int> iVect;

for_each(iVect.begin (), iVect.end(), Square);

}

Двухместные функции принимают два параметра. Часто они применяются к элементам различных контейнеров. Например, имеется два списка, и нужно что-то сделать с элементом первого списка в зависимости от значения соответствующего ему элемента во втором. Это делается с помощью алгоритма transform (), одна из форм которого имеет вид

template <class Inputlteratorl, class Inputlterator2,

class Outputlterator, class Binary0peration>_

Outputlterator transform(Inputlteratorl firsti,

Inputlteratorl lasti,

Inputlterator2 first2,

Outputlterator result,

BinaryOperation binary_func);

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

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

template <class Inputlterator, class Predicate>

Inputlterator find if(Inputlterator first,

Inputlterator last,

Predicate pred);

Функциональные объекты

Функциональный объект — это представитель класса, в котором определена операция вызова (скобки). Существуют различные ситуации, когда желательно передавать алгоритмам не функции, а функциональные объекты. Иногда это позволяет применить готовый функциональный объект стандартной библиотеки вместо новой функции; иногда — улучшить производительность благодаря генерированию встроенного кода. Кроме того, операция вызова может иметь доступ к информации, которая хранится в объекте — функциональный объект, в отличие от функции, обладает “памятью”.

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

Функциональный объект | Операция
Арифметические
plus | сложение х + у
minus | .вычитание х - у
multiplies | умножение х * у
divides | деление х / у
modulus | остаток х % у
negate | смена .знака -х
Отношения
equal to | равенство ==
not equal to | неравенство !=
greater | больше >
less | меньше <
greater equal | больше или равно >=
less equal | меньше или равно <=
Логические
logical and | логическое И &&
logical or | логическое ИЛИ | |
logical not | логическое отрицание !

Например, вызов алгоритма

transform(vec.begin(), vec.endf), vec.begin(),

negate<int> ());

меняет знак всех элементов вектора vec.

Можно привести примеры более сложных функциональных объектов с “памятью”, которые хранят между вызовами информацию о своем текущем состоянии. Определяемые пользователем функциональные объекты часто производятся от шаблонов классов unary_function и binary_function.

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

Вот программа, в которой реализуется совсем простой функциональный объект — генератор чисел Фибоначчи:

///////////////////////////////////////////////////

// FuncObj.cpp: Генератор чисел Фибоначчи.

//

#include <iostream>

#include <algorithm>

#include <vector>

#pragma hdrstop #include <condefs.h>

using namespace std;

class Fibo { // Класс функционального объекта.

int iCur, iNext;

public:

Fibo() { iNext = iCur =1; } // Инициализация состояния.

int operator ()() { // Операция вызова; возвращает

int temp = iCur; // следующий член ряда.

iCur = iNext; iNext = iCur + temp;

return temp;

} };

int main () {

//

// Сначала проверим вручную, как работает класс.

// Fibo fObj ;

cout << "Generated sequence of 16 numbers:" << endl;

for (int i=0; i<15; i++) cout << fObj () << ", ";

cout << f0bj() “ endl;

//

// Теперь генерируем вектор с помощью

// стандартного алгоритма.

//

vector<int> iVec(16);

generate (iVec .begin (), iVec.end(), Fibo());

cout << endl<< "Vector initialized by generate () algorithm:"<< endl;

copy (iVec .begin (), iVec.end(),ostream_iterator<int> (cout, " "));

return 0;

}

Программа выводит:

Generated sequence of 16 numbers:

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987

Vector initialized by generate() algorithm:

1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

Объекты-связки

Связка создает из двухместного функционального объекта одноместный функциональный объект, фиксируя значение одного из аргументов. В библиотеке шаблонов имеется два объекта-связки, bindlst и bind2nd. Они предназначены для фиксации соответственно первого или второго аргумента.

Следующий фрагмент кода подсчитывает число элементов в списке со значениями, большими 10:

int count = 0;

count_if (aList .begin ()., alist.end(),

bind2nd(greater<int>(), 10), count);

cout<< "Number of elements greater than 10 = " << count<< endl;

Здесь связка bind2nd применяется к функциональному объекту greater, задавая его второй аргумент равным 10 (если применить связку bindlst, то будут подсчитаны элементы, меньшие 10.

Негаторы

Негатор создает из функционального объекта другой объект с прямо противоположным действием, т. е. служит выражением отрицания. В библиотеке есть два негатора, noti и not2, применяемые соответственно к одноместным и двухместным функциональным объектам. Например,

noti(bind2nd(greater<int>(), 10))

означает “не больше 10”. Конечно, такое отношение можно записать и без негатора (имеется функциональный объект less_equal), но все равно негаторы часто оказываются полезны.

Контейнеры

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

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

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

Векторы

Вектор — это своего рода массив с “интеллектом”, помнящий свой размер и поддерживающий свою целостность. Они, среди прочих контейнеров, отличаются эффективностью доступа к элементам. К недостаткам следует отнести сложность вставки элементов в середину вектора, поскольку при этом приходится перемещать часть его содержимого.

Создание векторов

Объявления векторов могут выглядеть примерно так:

#include <vector>

vector<int> vint;

vector<double> vdouble (100);

vector<string> vstr(10);

vector<MyClass> myvect(20);

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

vector<int> vint1(24, -1);

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

  • конструктор по умолчанию
  • конструктор копии
  • деструктор
  • операцию взятия адреса
  • операцию присваивания.

Примечание

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

Естественно, потребуются еще какие-то механизмы присваивания и извлечения значений элементов данных создаваемого объекта и т. п. Но это вопрос внешний по отношению к функционированию контейнера.

Если вы хотите выполнять поиск или сортировку, потребуются операции равенства (==) и “меньше” (<).

Скелет класса, пригодного для помещения в вектор, может иметь такой вид:

class Coord {

int x, у;

public:

Coord() : x(0), у(0) {}

void set(int _x, int y) { x = x; у = _y; }

void get(int &_x, int &_y) { _x = x, _y = y; ) };

Для доступа к данным вектора применяется индексация:

vstr[3] = "Third value: ";

vdouble[10] = 3.3333333333;

cout<< vstr[l]<< vdouble[10] << endl;

Можно конструировать вектор с помощью итераторов, например:

int iArr[16] ;

vector<int> iVectl(iArr, iArr + 16);

vector<int> iVect2(iVect!.begin(), iVect!.end());

(Для обычного массива, как мы уже говорили, итератором является простой указатель. Второй из векторов конструируется из первого спецификацией его начального и конечного итераторов.)

Действия над векторами

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

Размер и вместимость вектора можно получить с помощью его методов size() и capacity():

cout <<"Size: "<< vdouble.size ()<<"Capacity: " << vdouble .capacity () <<end1;

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

Функция resize () позволяет изменить размер массива. Функция is_empty () возвращает true, если вектор пуст (размер равен 0). Вместимость вектора можно изменить его функцией reserve (). Заблаговременное резервирование позволяет избежать частых автоматических выделений памяти:

vdouble.reserve (1000);

Функция clear () удаляет все элементы вектора.

Функция assign () присваивает указанное значение первым n элементам:

vdouble.assign (100, 1.0);

Альтернативная форма функции позволяет присвоить элементам значения из диапазона другого контейнера:

void assign (Input-Iterator first, Inputlterator last);

Функции front() и back () возвращают значения соответственно первого и последнего элементов вектора.

Наиболее часто используемыми функциями векторов являются, вероятно, erase () и insert (). Они служат для удаления и вставки элементов. При удалении элемента (или нескольких элементов) из вектора все последующие сдвигаются к началу. Эти функции перегружены; мы приводим только две формы insert ():

iterator erase (iterator position) ;

iterator erase (iterator first, iterator last) ;

iterator insert(iterator pos, const T& x);

void insert(iterator pos,

Inputlterator first, Inputlterator last) ;

Эти функции позволяют соответственно удалить элемент, удалить диапазон, вставить элемент и вставить диапазон элементов из другого контейнера.

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

Вот небольшая иллюстрация:

#include <iostream>

#include <vector>

#pragma hdrstop

#include <condefs.h>

using namespace std;

//

// Вспомогательная функция для вывода содержимого контейнера

//

template<class T> void Display(const Т &с) {

cout<< "( " ;

copy(с.begin (), c.end(),

ostream_iterator<T::value_type> (cout, " "));

cout << "} Size: " << c.size() << endl;

}

int main ()

(

int iarr[5] = (1, 2, 3, 4, 5);

vector<int> vintl(iarr, iarr + 5);

cout << "Initial vector : ";

Display(vinti);

vector<int>::iterator p =

find(vinti.begin (), vintl.end(), 3);

vinti.erase (p);

cout << "After erasing 3: ";

Display(vinti);

vinti.insert (p, iarr, iarr + 3);

cout << "After insertion: ";

Display(vinti);

cout << "Pop and push : ";

vinti.pop_back();

Display(vinti);

vinti.push back(33);

cout << " Display(vinti);

vinti.pop_back ();

return 0;

}

Программа выводит:

Initial vector : {12345} Size: 5

After erasing 3: {1245} Size: 4

After insertion: {1212345} Size: 7

Pop and push : {121234} Size: 6

{ 1 2 1 2 3 4 33 } Size: 7

Полный список функций-элементов вектора вы можете найти в оперативной справке C++Builder'a.

Списки

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

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

Существуют различные способы конструирования списков.

#include <list>

list<int>ilist;

list<double>dlist(20, 1.0);

list<MyType>mtlist(10) ;

Эти объявления имеют тот же смысл, что и соответствующие объявления для векторов. Так же как и вектор, список можно конструировать, инициализировав содержимым другого контейнера:

int iarr[5] = {1, 2, 3, 4, 5};

…

list<int> linti(iarr, iarr + 5);

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

Действия над списками

Поскольку список занимает ровно столько памяти, сколько необходимо, для него не имеет смысла понятие вместимости. Поэтому у списков нет функций capacity () и reserve (). Невозможно и обращение к элементам по индексу.

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

Помимо известных вам уже методов push back() и pop back (), имеются функции push_front () и pop_front () для добавления или удаления элемента в начале списка.

Функция remove () удаляет из списка все элементы с указанным значением.

Функция unique () удаляет все повторяющиеся элементы (стоящие;

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

Функция reverse () обращает порядок элементов в списке.

Функция sort () (без аргумента) производит сортировку списка в соответствии с операцией “меньше”, т. е. в восходящем порядке. Можно задать в качестве аргумента функциональный объект, реализующий отношение, в соответствии с которым нужно сортировать список:

#intlude <functional> linti.sort(greater_equal<int>());

Примечание

Функция sort() сохраняет относительный порядок следования повторяющихся элементов. Такого рода сортировку называют стабильной.

Функция merge () выполняет слияние сортированного списка с другим сортированным списком. Элементы второго списка удаляются. Как и в случае sort (), можно задать второй аргумент — функциональный объект, определяющий отношение сортировки.

Примечание

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

Функция splice () является специальным вариантом вставки. Она удаляет вставляемый элемент или элементы списка, из которого производится вставка.

Описанные функции частично иллюстрирует следующая программа.

#include <iostream>

#include <list>

#pragma hdrstop

#include <condefs.h>

using namespace std;

//

// Операция передачи списка в поток.

//

template<class T>

ostream &operator“(ostream &os, const list<T> &c)

{

cout << "{ ";

copy (c. begin (), c.end(),

ostream_iterator<T> (os, " "));

os << "} Size: " “ c.size();

return os;

}

int main() {

int iarrl[5] = {5, 7, 3, 1, 9};

list<int> lintl(iarr1, iarr1 + 5);

cout<< "Initial list : "<< linti << endl;

linti.sort (); // Сортировка.

cout << "After sort : "<< lint1 << end1;

int iarr2[6] = {6, 2, 4, 8, 2, 6};

list<int> lint2(iarr2, iarr2 + 6);

cout << "Second list : " << lint2 << end1;

lint2.sort () ;

lint2.unique(); // Удаление повторов.

cout<< "Sorted unique: " << lint2 “ endl;

linti.merge(lint2); // Слияние.

cout <<"After merge : " << lint1 “ end1;

linti.reverse (); // Обращение порядка.

cout << "After reverse: "<< lint1 << end1;

return 0;

}

Программа выводит:

Initial list : {57319} Size: 5

After sort : {13579} Size: 5

Second list : {624826} Size: 6

Sorted unique:(2468) Size: 4

After merge :{123456789} Size:9

After reverse:{987654321}Size:9

Очереди deque

Контейнеры deque (формально это сокращение означает “двусторонняя очередь”) комбинируют в себе свойства списков и векторов. Они допускают прямой доступ к элементам, но эффективно работают при вставках и удалениях.

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

Множества и мультимножества

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

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

Элементы множества всегда сортированы. Поэтому поиск нужного ключа очень прост и эффективен.

Что касается последовательных операций и прямого доступа, то тут множества далеки от совершенства. Набор функций-элементов у множеств невелик по сравнению с другими контейнерами.

Создание множеств

Объявляются множества несколько сложнее, чем рассмотренные до сих пор контейнеры, так как при этом необходимо указать функциональный объект, который будет использоваться при упорядочении элементов:

set<double, less<double> > dset;

Примечание

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

Удобно переименовать представитель шаблона:

typedef set<double, less<double> > set_type;

set type dset;

Множество, как и другие контейнеры, можно создать из диапазона элементов другого контейнера:

double darr[6] = (1.0, 2.0, 2.5, 4.5, 3.5, 2.5};

set_type dset(darr, darr + 6) ;

В каком бы порядке ни следовали элементы в исходном контейнере, в множестве они окажутся сортированными.

Примечание

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

Действия над множествами

Как я сказал, функций у множеств сравнительно немного. Функции insert () и erase () имеют дополнительную форму с одним параметром, специфицирующим ключ, который нужно добавить или удалить из множества:

dset.insert (3.14);

dset.erase(3.5);

Функции lower bound () и upper bound () возвращают соответственно итератор элемента, который больше или равен, и элемента, который больше указанного ключевого значения. Пример использования этих функций показан в приведенной ниже программе.

Функция count () возвращает 'число вхождений в множество указанного ключа. В set функция может возвращать только 0 или 1. Вызов этой функции — простейший способ определить, входит ли ключ в множество.

Следующая программа иллюстрирует эти функции множеств.

#include <iostream>

#include <set>

#pragma hdrstop

#include <condefs.h>

using namespace std;

// Дать имя типу множества;

typedef multiset<double, less<double> > set_type;

//

// Операция передачи множества в поток.

//

ostream &operator“(ostream Sos, const set_type &c)

(

cout<< "{ ";

copy(c.begin (), c.end(),

ostream_iterator<set_type::value_type>(os, " "));

os << "} Size: "<< c.sizeO;

return os;

}

int main() {

set type dset;

cout << "Inserting... ";

for (int i=8; i>0; i--) { // Ввести элементы

dset.insert (i); //в множество.

cout << i << " ";

} cout<< end1;

cout.setf(ios::fixed);

cout.precision (1) ;

cout << "Initial set : " << dset<< end1;

dset.erase (2.0); // Удалить 2.0,если есть.

cout << "2.0 erased :" << dset<< endl;

dset.insert(4); // Добавить лишние четверки.

dset.insert (4); //

cout << "4's inserted : " << dset << endl;

cout<< "Count of 4.0 :"<< dset.count (4.0)<<endl;

// Сосчитать их.

set type::iterator pi =dset.lower_bound(2.5),

p2 =dset.upper bound(6.5);

dset.erase (pi, p2); // Найти диапазон значений

// и удалить его.

cout << "Erase 2.5-6.5: " << dset<< end1;

return 0;

}

Программа выводит:

Inserting. ..87654321

Initial set : ( 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 } Size: 8

2.0 erased : {1.0 3.0 4.0 5.0 6.0 7.0 8.0 ) Size: 7

4's inserted : { 1.0 3.0 4.0 4.0 4.0 5.0 6.0 7.0 8.0 }Size: 9

Count of 4.0 : 3

Erase 2.5-6.5: { 1.0 7.0 8.0 } Size: 3

Битовые множества

Битовое множество представляет собой контейнер, в котором могут храниться битовые последовательности фиксированной длины. Можно сказать, что оно служит представлением подмножеств фиксированного множества (в математическом смысле), каждому элементу которого соответствует один бит в определенной позиции. Единичный бит означает, что элемент принадлежит подмножеству, нулевой — что он не входит в данное подмножество. Подобным образом организованы множества языка Pascal.

Биты в bitset плотно упакованы, так что информация хранится очень экономно. Однако минимальный физический размер bitset равен 4 байтам — размеру int.

Создание битовых множеств

При создании битового множества указывается его размер (как аргумент шаблона). Можно инициализировать bitset строкой, состоящей из нулей и единиц:

bitset<32> bset1;

bitset<8> bset2(string ( "01011011"));

Примечание

Вторая форма конструктора объявлена как explicit, с аргументом типа string, поэтому приходится делать явное преобразование литеральной строки в стандартную.

Действия над bitset

У класса bitset нет итераторов, и обращение к его элементам осуществляется по индексу.

Примечание

Порядок следования элементов в bitset с точки зрения индексации является обратным расположению нулей и единиц в инициализирующей строке. (Это соответствует семантике двоичных чисел — слева стоит самый старший бит, которому обычно приписывают наибольший номер.)

Функция-элемент test() позволяет проверить состояние бита с указанным индексом. Функция апу() возвращает true, если хотя бы один из битов множества установлен.

Функции set () и reset () служат соответственно для установки и сброса битов множества. При указании индекса в качестве аргумента функция устанавливает/сбрасывает соответствующий бит; при вызове функции без аргумента устанавливаются/сбрасываются все биты множества.

Функция flip () инвертирует состояние указанного бита или всех битов множества.

К битовым множествам можно применять обычные логические поразрядные операции и сдвиги (~, &, |, ^, <<, >>).

Для битовых множеств определена операция передачи в поток (<<).

Вот небольшой пример, иллюстрирующий возможности битовых множеств:

#include <iostream>

#include <bitset>

#pragma hdrstop

#include <condefs.h>

using namespace std;

int main() {

bitset<8> bsetl, bset3;

bitset<8> bset2(string ("01011011"));

bsetl.set (0);

bsetl [7] = 1;

cout<< "First bitset : "<< 'bsetl<< endl;

cout << "Second bitset: " << bset2 << endl;

bsetl.flip();

cout<< "Flip first : " << bsetl << endl;

bset3 = bsetl ^ bset2;

cout << "First^second : " << bset3 << end1;

bset3 <<= 4;

cout << "Shifted by 4:" << bset3 << end1;

return 0;

}

Этот код выводит:

First bitset : 10000001

Second bitset: 01011011

Flip first : 01111110

First^second : 00100101

Shifted by 4 : 01010000

Карты и мультикарты

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

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

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

Создание карт

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

typedef map<string, double, less<string> > map_type;

Ключи такого контейнера будут строками, а ассоциированные объекты — значениями типа double. Сортирована карта будет в соответствии с алфавитным порядком ключей.

Кроме того, иногда бывает удобно объявить имя для типа элемента карты (т. е. по сути структуры, состоящей из ключа и ассоциированного типа объекта; подобные типы определяются с помощью шаблона pair<Tl, Т2>):

typedef map type::value_type val_type;

Обычно создается пустая карта, а затем в нее вводятся элементы функцией insert () . Возможно также конструирование новой карты из части уже существующей. При этом конструктору передаются, как обычно, два итератора.

Действия над картами

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

///////////////////////////////////////////////////////

// Multimap.срр: Макет телефонной книжки на основе multimap.

//

#include <iostream>

#include <iomanip>

#include <map>

#pragma hdrstop using namespace std;

struct Phone {

long pn;

Phone(long n = 0): pn(n) {} };

typedef multimap<string. Phone, less<string> > map_type;

typedef map type::value type val type;

typedef map_type::iterator i_type;

//

// Выводит элемент multimap (т.е. пару (string. Phone)).

//

ostreams operator“(ostream& os, const val_type& v)

{

os << setiosflags(ios::left)<< setfill('.')

<< setw(20) << v.first

<<resetiosflags(ios::left) << setfill('0')

<< setw(3) << v.second.pn / 10000 << "-"

<< setw(4)<< v.second.pn % 10000 << setfill(' ');

return os;

}

//

// Выводит "структуру" Phone.

//

ostreamS operator“(ostreamS os. Phone p)

(

os << setw(20) << "" << setfill('0')

<< setw(3) << p.pn / 10000 << "-"

<< setw(4)<< p.pn % 10000<< setfill(' ');

return os;

}

//

// Распечатка всех номеров, относящихся к одному имени.

// Возвращает итератор, ссылающийся на первую запись с

// другим именем. Обратите внимание на функцию

// equal range(), возвращающую структуру pair.

//

i_type Retrieve(ostreams os, map_type& mp, string name)

{

pair<i__type, i type>

r = mp.equal_range(name);

// Временная карта, конструированная по паре итераторов:

map_type b(r.first, r.second);

if (b.empty())

cout << "*** No such name! ***" << endl;

else {

i type p = b. begin ();

os << *p++ << endl; // Распечатать ключ и номер.

while (p != b.end())

os << (p++)->second << endi; // Для остальных

// только номер.

} return r.second;

}

//

// Распечатка всей карты.

//

void Printout'(ostream& os, map_type& mp, i_type from)

{

while (from != mp.endO) // Если не пустая,

// распечатать

from = Retrieve (os, mp, from->first);

// все номера

// первого ключа

//и перейти к

// следующему.

os << "*** End of the book ***" << endl;

}

ostreamS operator<<(ostreamb os, map_type& v) {

Printout (os, v, v.begin ());

return os;

}

int main() {

map type book;

// Попробуем распечатать пустую карту...

cout << "Contents of a new book:" << end1;

cout << book << endl;

book.insert(val_type("Petrov", 1653318));

book.insert(val_type("Ivanov", 2640535));

book.insert(val_type("Sidorov", 2340711));

book.insert(val_type("Ivanov", 4720415));

book.insert(val_type("Petrov", 1770212));

book.insert(val_type("Pavlov", 5551703));

book.insert(val_type("Ivanov", 4722306)) ;

// Распечатка.

cout << "Contents of the phone book:" << endl;

cout << book << end1;

// Поиск отдельных имен.

cout << "Searching Petrov... " << endl;

Retrieve(cout, book, "Petrov");

cout << "Searching Kozlov... " << end1;

Retrieve(cout, book, "Kozlov");

return 0;

}

Поиск нужного имени в этой программе выполняется функцией equal_range (), которая возвращает пару итераторов (структуру pair), ссылающихся на начало и конец диапазона с одним и тем же заданным ключом (если он присутствует).

Результат запуска программы показан на рис. 11.2.

Рис. 11.2 Вывод программы Multimap

Стеки

Стек — очень простая структура данных. В STL можно организовать три разновидности стеков: на основе вектора, на основе списка и на основе deque. Функционально они не отличаются друг от друга.

Создание и действия со стеками

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

#include <stack>

#include <vector>

stack<int, vector<int> > iStack;

Функция push () помещает указанное значение на вершину стека;

функция pop () удаляет из стека верхнее значение. Получить значение с вершины стека можно функцией top ():

for (int i=0; i<10; i++) iStack.push(i) ;

while (!iStack.empty()) {

cout<< iStack.topO << endl;

iStack.pop();

}

Очереди

Очередь отличается от стека порядком извлечения элементов: если в стеке операция pop () удаляет самый последний из помещавшихся в него элементов, то в очереди там же операция удаляет наиболее “старый” элемент. Получить значение этого элемента можно, вызвав функцию front ().

Очередь может быть конструирована на основе либо списка, либо deque. Вот пример, аналогичный примеру со стеком из предыдущего параграфа:

#include <queue>

#include <list>

queue<int, list<int> > iQueue;

for (int i=0; i<10; i++) iQueue.push (i);

while (!iQueue.empty ()) {

cout << iQueue.front() << end1;

iQueue.pop ();

}

Приоритетные очереди

Наконец, последний из рассматриваемых здесь контейнеров стандартной библиотеки — это приоритетная очередь. Она строится на основе вектора или deque. От обычной очереди она отличается тем, что вне зависимости от порядка размещения элементов первым будет извлекаться наиболее “критический” из них. Критичность, или приоритет, элемента определяется заданным функциональным объектом отношения (по умолчанию — “меньше”). Наиболее приоритетный (наибольший) элемент помещается на вершину очереди (его значение доступно посредством функции top ()) и удаляется первым (функция pop ()).

Создание и действия с приоритетной очередью

При конструировании очереди в общем случае указывается тип элементов, тип контейнера-основы и функциональный объект, определяющий приоритеты. Контейнером-основой для приоритетной очереди может быть вектор или deque.

Примечание

Нужно сказать, что шаблоны стеков и очередей имеют аргументы по умолчанию. Тип контейнера и отношение (для приоритетной очереди) указывать, вообще говоря, не обязательно. Так, для стека и очереди тип контейнера по умолчанию — deque, для приоритетной очереди — vector, а операция отношения — “меньше”.

Вот маленький пример, моделирующий составление списка неотложных дел в порядке их важности:

////////////////////////////////////////////////////

// Priority.срр: Демонстрация приоритетной очереди.

//

#include <iostream>

#include <string>

#include <queue>

#include <deque>

#pragma hdrstop

using namespace std;

class ToDo {

int priority;

string doit;

public:

ToDo(int p = 0, string d = ""): priority(p), doit(d) {}

bool operator<(const ToDo &arg) const { return priority < arg.priority; }

friend ostream &operator<<(ostreams, const ToDo&);

};

ostream &operator<<(ostream &os, const ToDo &t) {

os << t.priority << " - " << t.doit;

return os;

}

int main() {

priority_queue<ToDo, deque<ToDo>, less<ToDo> > todo;

// Разместим некоторые неотложные дела... todo.push(ToDo(3, "Finish the program you started yesterday."));

todo.push(ToDo(7, "Write a letter to X."));

todo.push(ToDo(4, "Buy some food for dinner."));

todo.push(ToDo(1, "Call your publisher."));

// Распечатать список в порядке срочности. while (!todo.empty()) {

cout << todo.top() << endl;

todo-pop() ;

)

return 0;

)

Программа выводит:

7 - Write a letter to X.

4 - Buy some food for dinner.

3 - Finish the program you started yesterday.

1 - Call your publisher.

Алгоритмы

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

Чтобы можно было вызывать эти алгоритмы, нужно включить в программу заголовок algorithm:

#include <algorithm>

using namespace std;

(Не забывайте указывать пространство имен std, когда пользуетесь новой нотацией включаемых файлов!)

Некоторые алгоритмы вы уже видели (например, find ()), так что здесь мы показываем в основном те, с которыми вы еще не встречались.

Накопление

Накопление, или аккумуляция — это перебор заданного диапазона контейнера с суммированием (иди перемножением, или какой-то иной комбинацией) элементов в некоторой итоговой переменной. По умолчанию выполняется суммирование:

#include <numeric> double sum = accumulate(v.begin (), v.end(), 0.0);

Примечание

Замечание: Шаблон accumulate () находится в заголовочном файле numeric, а не algorithm.

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

#include <numeric>

#include <functional>

double product = accumulate(v.begin(), v.end(),

1.0, multiplies<double> ());

Подсчет

Алгоритм count () осуществляет подсчет числа элементов контейнера с указанным значением. Алгоритм count_if() выполняет подсчет элементов, для которых выполняется условие заданного предиката:

void count(Inputlterator first, Inputlterator last,

const T& value, Size& count) ;

void count if(Inputlterator first, Inputlterator last,

Predicate p, Size& count);

Результат подсчета возвращается в четвертом параметре.

Поиск и замена

С алгоритмом поиска вы уже встречались не раз:

Inputlterator

find(Inputlterator first, Inputlterator last,

const T& value);

Inputlterator

find(Inputlterator first, Inputlterator last,

Predicate pred) ;

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

Алгоритмы замены replace () и replace_if() позволяют заменять существующие значения контейнера новыми:

void replace(Forwardlterator first, Forwardlterator last,

const T& value, const T& new_value) ;

void replace_if(Forwardlterator first, Forwardlterator last,

Predicate pred, const T& new_value) ;

Удаление элементов

Удаление элементов контейнера с указанным значением выполняется алгоритмами remove () и remove_if:

Forwardlterator

remove(Forwardlterator first, Forwardlterator last,

const T& value) ;

Forwardlterator

remove if(Forwardlterator first, Forwardlterator last,

Predicate pred) ;

Необходимо заметить, что эти алгоритмы не уменьшают числа элементов в контейнере. Они только сдвигают элементы, которые должны остаться в новом наборе, к его началу, и возвращают итератор конца нового набора элементов. Чтобы действительно удалить ненужные элементы, нужно применить метод контейнера erase ():

array.erase(remove(array.first(), array.end(), value),

array.end());

Алгоритм unique () удаляет из контейнера все элементы с повторяющимися значениями, следующие друг за другом, оставляя только первый из них:

Forwardlterator remove(Forwardlterator first, Forwardlterator last);

Алгоритм возвращает итератор конца нового набора элементов.

Перестановка

Алгоритм random_shuffle () производит случайную перестановку элементов контейнера:

void random_shuffle(RandomAccessIterator first,

RandomAccessIterator last);

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

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

Лексикографическое сравнение

Такое страшное название алгоритма означает всего-навсего, что он выполняет сравнение содержимого двух контейнеров, аналогичное сравнению текстовых строк. Элементы контейнеров могут быть любого типа, лишь бы для них была объявлена операция “меньше” (или какая-либо функция, задающая отношение сравнения):

bool

lexicographical_compare (Inputlteratorl first1,

Inputlteratorl last1,

Inputlterator2 first2,

Inputlterator2 last2);

bool lexicographical compare(Inputlteratorl first1,

Inputlteratorl last1,

Inputlterator2 first2,

Inputlterator2 last2,

Compare comp);

Алгоритм возвращает true, если содержимое первого контейнера меньше, чем второго.

Сортировка

С сортировкой мы уже встречались, правда, в виде функции контейнера (при изучении списков — их нельзя сортировать по-другому), а не отдельного алгоритма. Сортировка по умолчанию производится в восходящем порядке (используется операция < для сравнения элементов):

void sort(RandomAccessIterator first, RandomAccessIterator last);

void sort(RandomAccessIterator first,

RandomAccessIterator last. Compare comp);

Стандартные строки

Под стандартными строками понимают объекты, принадлежащие шаблону basic_string, чаще всего его классам-представителям string или wstring. В повседневном .программировании применяется почти исключительно класс string.

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

Чтобы можно было работать со стандартными строками, необходимо включить в программу заголовок string. При этом, кстати, автоматически подключается заголовок С string, h, так что вы можете при этом пользоваться стандартными функциями С для строк, ограниченных нулем.

Создание строк

Конструировать строки можно многими способами; можно создать пустую строку (конструктор по умолчанию), можно инициализировать строку литералом, присваиванием литерала или копированием, можно зарезервировать указанное число символов, инициализировав в то же время строку литералом меньшей длины, можно создать строку из стандартного контейнера (например, вектора), содержащего символы, и т. д. Ниже приводится ряд примеров конструирования строк.

string sEmpty;

string sLiteral("A string from literal.");

string sAssign = "A string by assign.";

string sCopy(sLiteral);

string sPart(sCopy, 14, 7);

string sFill(32, '#') ;

Пояснений, вероятно, требуют только два последних конструктора. Предпоследний создает строку из уже существующей, выделяя ее подстроку длиной 7 символов, начиная с индекса 14. Последний конструктор создает строку длиной 32 символа, заполняя ее символами ' # '.

Характеристики строк

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

Функция | Возвращаемый тип | Описание
size () | size type | Возвращает текущий размер строки.
length() | size type | Длина строки (то же, что и size).
capasity() | size type | Возвращает вместимость строки.
max size() | size type | Возвращает максимально возможный размер.
resize(n) | void | Изменение размера (может урезать строку).
reserve(n) | void | Резервирование по крайней мере n символов.
empty () | bool | Возвращает true, если строка пуста.

Функции resize () и reserve () могут выбрасывать исключение length_error, если запрашиваемый размер больше максимально возможного (он обычно определяется размером наибольшего свободного блока памяти).

Ввод и вывод строк

Наиболее прост вывод строк с помощью операции <<:

string s("This is a string!");

cout << s << end1;

По форме это ничем не отличается от вывода строки С. Ввод строки вроде бы тоже выглядит очень просто:

string s;

cin >> s;

Однако такой оператор считывает в строку только одно “слово” до первого пробельного символа. Чтобы прочитать всю введенную строку вплоть до ограничителя, нужно воспользоваться функцией getline ():

string s;

getline(cin, s, '\n');

Ограничитель ' \n ' можно было бы и не указывать, так как он принимается по умолчанию.

Операции над строками

Для стандартных строк перегружен ряд операций.

Операция присваивания позволяет присвоить стандартной строке другую строку, строку С (или литерал), отдельный символ. Все показанные ниже присваивания допустимы:

char с = ' С ';

char cs[20] = "С string.";

string sOld("Source string.");

string sNew;

sNew = sOld;

sNew = cs;

sNew = "Literal string.";

sNew = c;

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

string si("First"), s2("Second");

string s3;

s3 = si + " " + s2;

si += s2;

Строки можно индексировать. При обычной нотации индексации проверки диапазона не делается. Однако можно применить функцию at (), также возвращающую ссылку на символ строки с указанным индексом. В этом случае при выходе за текущую длину строки выбрасывается исключение out_of_range:

string s("A short string.");

try {

cout<< s.at(30) << endl;

) catch(out_of_range e) {

cout << "Range error: "<< end! << e.what() << endl;

}

Этот фрагмент кода выводит:

Range error:

position beyond end of string in function:

basic_string::at(size_t)

index: 30 is greater than max index: 15

Наконец, для стандартных строк перегружен весь набор операций отношений: равенство, неравенство, “больше”, “меньше” и т. д. Операции < и > производят лексикографическое сравнение в соответствии с алфавитным порядком.

Функции строк

Класс string (точнее, basic_string) имеет богатый набор функций, выполняющих обработку строк. Мы расскажем только о некоторых.

Присваивание и присоединение

Функции assign () и append () делают в общем-то то же, что и операции присваивания и сложения, однако обладают большими возможностями благодаря дополнительным параметрам. Можно, например, скопировать из одной строки в другую определенное количество символов, начиная с некоторой позиции:

string si("Some String already exists."), s2;

s2.assign (si, 5, 6); // Скопирует только слово "String".

Функции assign () и append () имеют аналогичные перегруженные формы и различаются только тем, что первая полностью заменяет текущее содержимое строки новым текстом, а вторая дописывает тот же текст в конец строки. Приведем все имеющиеся формы этих функций, чтобы дать читателю представление о том, что там вообще есть (возвращаемый тип пропущен — все они возвращают basic_string&):

append (const basic strings s);

append (const. basic_string& s, size type pos, size_type npos);

append (const charT* s, size type n);

append (const charT* s);

append (size_type n, charT с );

append (Inputlterator first, Inputlterator last);

assign (const basic strings s);

assign (const basic_string& s,

size__type pos, size_type n);

assign (const charT* s, size type n);

assign (const charT* s);

assign (size_type n, charT с);

assign (Inputlterator first, Inputlterator last);

Вставка и удаление

Функции insert () и erase () производят соответственно вставку указанного текста в заданное место строки и удаление фрагмента строки. Третья функция, replace (), является их комбинацией: делается вставка, а затем часть старого содержимого строки удаляется.

Приведем всего один пример, простейший:

string s1 ("First string.");

string s2(" and second");

si.insert(s1.find(' '),s2);

В данном случае insert () вставляет содержимое второй строки в первую перед найденным в ней пробелом. Следующий оператор удалит только что вставленную строку:

sl.erase(sl.find(' '), 11);

Есть восемь перегруженных вариантов функции insert () и десять вариантов replace () . Для erase () , правда, имеются всего три формы. Все это при желании читатель может найти в оперативной справке С++Вuilder.

Поиск в строках

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

Функция find () наиболее проста; она ищет первое вхождение строки, строки С или одиночного символа, начиная с указанного места, по умолчанию от начала, и возвращает позицию найденного фрагмента:

int i = s1.find("and");

Примечание

Если указанный элемент текста не найден, функции поиска возвращают -1. Правда, возвращаемое ими значение имеет, по-видимому, беззнаковый тип (у меня не было желания в этом разбираться); во всяком случае, если это значение выводить на терминал без приведений, печатается беззнаковый эквивалент минус единицы.

Функция find_first_of () ищет первое вхождение в строку символа из указанного набора. По умолчанию поиск начинается от начала.

Функция find first not of() ищет первое вхождение символа, не входящего в указанный набор.

Функции find_last_of() и find_last_not_of() работают аналогично двум предыдущим функциям, но поиск начинается по умолчанию с конца строки и идет в направлении к ее началу.

Вот простейший пример подобного поиска:

string s("13:30:00 11/03/2000");

int k=0;

k=s.find_first_of(" :/",k) ;

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

Преобразование в строки С

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

Можно также сразу скопировать содержимое строки (или его часть, что бывает удобно) в символьный буфер функцией copy ():

char buf[40];

string s = "abcdefghijklmnopqrstuvwxyz";

s.copy(buf 10 3);

buf[10]='\0';

Этот фрагмент копирует 10 символов из строки s в символьный буфер, начиная с 3-й позиции (т. е. с буквы d). Функция copy (), правда, не записывает в буфер конечный нуль, так что его приходится добавлять вручную.

Заключение

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


Глава 12. Управление исключениями

Продавай мобильный контент со своего сайта

  • Глава 12. Управление исключениями
    • Основные синтаксические конструкции
      • Блок try
      • Блок catch
      • Оператор throw
      • Спецификации исключений
    • Механика исключений
      • Исключения и стек
      • Поиск обработчика и неуправляемые исключения
    • Исключения и классы
      • Исключения, конструкторы и деструкторы
      • Классы исключений
      • Предопределенные исключения
      • Информация об исключении
      • Установки компилятора
    • О системных исключениях
      • SЕН в С++
      • Процессорные исключения

Глава 12.

Управление исключениями

Грамотно организованная, устойчивая программа должна справляться с нестандартными ситуациями, встречающимися в реальной работе с реальными данными. Такой нестандартной ситуацией может быть, например, ошибка пользователя при вводе данных или нарушение структуры некоторого файла. В языках, предшествующих C++, подобные проблемы решались с помощью глобальных “флагов ошибки” или приписыванием определенному значению, возвращаемому функцией, специального смысла “индикатора ошибки”. Язык C++ вводит понятие управления исключениями, т. е. специальных средств изменения программного потока управления с целью обработки нестандартных, непредвиденных или ошибочных ситуаций, возникающих в процессе работы.

В главе 4 мы вкратце упоминали о т. н. структурированной обработке исключений (SEH) в языке С; она реализуется в C++ Builder при посредстве нестандартных ключевых слов _try, _except и _finally. Ее принципы заимствованы, на самом деле, из операционной системы Windows NT; в NT это средства, встроенные в систему.

В C++ средства обработки исключений встроены непосредственно в язык. Ключевые слова, связанные с данным аспектом языка, следующие: try, catch и throw.

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

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

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

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

Основные синтаксические конструкции

Общий синтаксис обработки исключения в C++ такой:

try { // Начало "пробного блока".

throw выражение; / / "Выбрасывание" исключения.

} catch(тип переменная) { // Заголовок обработчика для <типа>.

тело_обработчика) [catch ...] // Возможно, обработчики других типов.

Теперь мы в деталях рассмотрим элементы этой конструкции.

Блок try

Ключевое слово try начинает пробный блок операторов, показывая, что данный блок может генерировать исключение. Тело блока заключается в фигурные скобки. Оно может содержать вызовы функций, тело которых при этом тоже будет рассматриваться как принадлежащее пробному блоку. Другими словами, весь код, могущий прямо или косвенно исполняться при входе в блок, принадлежит пробному блоку:

try {

cout << "Входим в пробный блок..."<< end.1;

DangerousFunc(); // Вызов процедуры, способной

// генерировать исключение.

}

// Конец try-блока.

Блоки try могут быть вложенными.

Блок catch

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

try {

}

catch(int. i) { // Перехватывает исключения типа int.

} catch(char* str) { // Перехватывает char*.

} catch (...) { // Перехватывает все остальное.

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

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

Оператор throw

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

Throw с операндом

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

Листинг 12.1. Программа, демонстрирующая простейшие исключения

////////////////////////////////////////////////////////////////////

// SimpTypes.срр: Перехват простых исключений.

//

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

int main () (

double d = 1.0;

for (int i=0; i<4; i++) { . try {

cout << endl<< "Entering the try-block..." <<end1;

switch (i) { case 0:

throw "Throwing an exception of char*"; // Выбросить

// строку. case 1:

throw 5; // Выбросить

// целое.

default:

throw d; // Выбросить double. }

// Следующий оператор исполняться не будет

// из-за исключений.

cout<< "In the „try-block after all exceptions..." << endl;

} // Конец try-блока.

catch(int 1) { // Обработчик int.

cout << "Int thrown: " << 1 << endl;

} catch(char* str) { // Обработчик char*.

cout << "String thrown: " << str << endl;

} catch (...) { // Для всего остального.

cout << "An unknown type thrown."<< "Program will.terminate." << endl;

cin.ignore () ;

return -1; // Завершить программу. }

cout<< "End of the loop."<< endl;

} // Конец цикла.

cout << "The End." << endl; // Эти операторы не исполняются cin.ignore (); // никогда, т.к. третье

// исключение

return 0; // завершает программу. }

Вывод программы показан на рис. 12.1

Давайте разберемся, что здесь происходит.

В программе организован цикл, который должен выполниться четыре раза. В нем находится пробный блок, генерирующий исключения различных типов — int, char* и double в зависимости от значения счетчика цикла. На первом проходе оператор throw выбрасывает строку, которая перехватывается вторым по счету обработчиком. Так как обработчик не выполняет никаких действий, кроме вывода сообщения, выполнение про-

Рис. 12.1 Простая программа с исключениями

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

На третьем проходе цикла выбрасывается переменная типа double, для которого обработчика не предусмотрено. Однако имеется “всеядный” третий обработчик. Он исполняет оператор return, завершающий программу. Поэтому цикл for в четвертый раз не выполняется и вообще программа не доходит до своего “нормального” конца.

Обратите внимание, что последний оператор пробного блока (вывод сообщения) не будет выполняться никогда, так как мы в любом случае выбрасываем исключение в предшествующем ему блоке switch.

Порядок следования catch-обработчиков

Примечание

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

  • либо тип выброшенного объекта совпадает с типом, ожидаемым обработчиком (если выброшен объект типа Т, то годятся обработчики для Т, const Т, Т& или const T&); ,
  • либо тип обработчика является открытым базовым классом для типа объекта;
  • либо типы обработчика и выброшенного объекта являются указателями, второй из которых может быть приведен к первому согласно общим правилам преобразования указателей.

Процедура поиска не ищет “наилучшего соответствия” типов, а просто берет первый по порядку следования подходящий обработчик. Например, у вас есть два класса исключения, причем второй является производным от первого. Если в списке обработчиков первым будет стоять тот, что предназначен для исключений базового класса, он будет перехватывать все исключения — как базового, так и производного классов. Или рассмотрите такой пример:

int main() {

try {

throw "Throwing char*"; // Выбрасывает char*. }

catch(void*) ( // Ловит void*.

cout<< "Void* caught." << endl;

return -1;

}

catch(char*) { // Ловит char*.

cout << "Char* caught." << endl;

return -1;

}

return 0;

}

Здесь обработчики исключений расположены в неправильном порядке, так как обработчик для void* будет перехватывать все исключения, предназначенные для обработчика char*.

Throw без операнда

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

Спецификации исключений.

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

<тип> FuncName(<список параметров>) throw([<тип>

[, <тип> ...]])

{

<тело функции>

}

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

Обработка непредвиденных исключений

Однако то, какие исключения функция прямо или косвенно выбрасывает на самом деле, выясняется только во время выполнения. Компилятор не выдает никаких ошибок или предупреждений на этот счет. Если функция, снабженная спецификацией исключений, выбрасывает непредвиденное, т. е. не указанное в спецификации, исключение, вызывается функция unexpected () . По умолчанию последняя просто вызывает terminate () . Вы можете, тем не менее, указать свою собственную функцию, которая должна активироваться при появлении непредвиденных исключений, вызвав set_unexpected (). Прототип ее находится в файле except.h (не обращайте внимания на _RTLENTRY; он расширяется в _cdecl):

typedef void (_RTLENTRY *unexpected_function)();

…

unexpected_function _RTLENTRY set_unexpected(unexpected_function);

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

Механика исключений

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

Исключения и стек

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

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

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

Приведем небольшую иллюстрацию. Ниже показана программа, состоящая из main () и двух функций FuncA () и FuncB () . Главная функция создает объект класса S и передает его FuncA (), которая его модифицирует и передает в FuncB (). Затем управление возвращается к main () .

Листинг 12.2. Работа стека при вызовах функций

///////////////////////////////////////////////////

// Stack.срp: Работа стека.

//

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

struct S // Простой класс. {

int s;

S(int ss): s(ss) // Конструктор (преобразования из int).

{

cout << "Constructor for "<< s << endl;

} S (const S& src) // Конструктор копии.

{

s = src.s;

cout << "Copy constructor for " << s << endl;

}

~S() // Деструктор.

{

cout << "Destructor of " << s << endl;

} };

void FuncB(S obj)

{

cout << "In FuncB: got << obj.s endl;

cout << "Exiting FuncB..." << endl;

}

void FuncA(S obj)

{

cout << "In FuncA: got"<< obj.s << endl;

obj.s = 22; // Модифицирует полученную копию объекта и...

FuncB(obj); // ...передает ее FuncB().

cout << "Exiting FuncA..." << end1;

}

int main() {

S mainObj = 11; // Локальный объект.

cout << "In main..." << endl; FuncA(mainObj);

cout << "Exiting main..." << endl;

return 0;

}

Программа выводит следующие сообщения:

Constructor for 11

In main...

Copy constructor for 11

In FuncA: got 11

Copy constructor for 22

In FuncB: got 22

Exiting FuncB...

Destructor of 22

Exiting FuncA...

Destructor of 22

Exiting main...

Destructor of 11

Здесь видно, как создается копия объекта при передачи параметра (по значению) и как она удаляется при возврате из функции.

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

Можно слегка модифицировать предыдущий пример, организовав пробный блок в main() и заставив FuncB() выбрасывать исключение в виде строки:

…

void FuncB(S obj)

{

cout << "In FuncB: got " << obj.s << endl;

cout << "Throwing exception..." << endl;

throw "Exception!";

cout << "Exiting FuncB..." << endl;

}

int main() {

S mainObj = 11; // Локальный объект.

cout << "In main..." << endl;

try {

FuncA(mainObj);

} catch(char* str) {

cout << "Gaught in main: " << str << end1;

} cout << "Exiting main..." << endl;

return 0;

}

Теперь программа выводит:

Constructor for 11

In main...

Copy constructor for 11

In FuncA: got 11

Copy constructor for 22

In FuncB: got 22

Throwing exception...

Destructor of 22

Destructor of 22

Caught in main: Exception!

Exiting main...

Destructor of 11

Временные копии объекта уничтожаются по-прежнему, хотя возврата из функции в обычном смысле слова не происходит.

Поиск обработчика и неуправляемые исключения

Если не удается найти подходящий обработчик исключения в списке текущего пробного блока, происходит переход на более высокий уровень, т. е. к списку обработчиков try-блока, -непосредственно включающего текущий. Если такой, конечно, имеется.

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

Можно установить свою собственную процедуру завершения с помощью функции set_terminate () ; прототип ее находится в except, h:

typedef void(_RTLENTRY *terminate_function) ();

terminate_function _RTLENTRY set_terminate(terminate function);

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

Следующая программа демонстрирует некоторые моменты вышесказанного. Ее вывод показан на рис. 12.2.

Листинг 12.3. Поиск обработчиков и неуправляемые исключения

///////////////////////////////////////////////////////////

//Unhandled. срр: Прохдедура для неуправляемых исключений.

//

#include <iostream.h>

#include <except.h>

#pragma hdrstop

#include <condefs.h>

class Dummy {}; // Пустой класс исключения.

void FuncB(int f) {

if (!f) { cout << "FuncB: throwing int..." << endl;

throw 7;

} else {

cout<< "FuncB: throwing char*..."<< endl;

throw "Exception!";

} }

void FuncA(int f)

{

try {

FuncB(f);

} catch(char* str) { // Обработчик выбрасывает Dummy.

cout << "FuncA: char* caught. Rethrowing Dummy..."<< endl;

Dummy d;

throw d;

} }

void MyTerminate() // Новая процедура завершения. {

cout << "Termination handler called..." << endl;

abort ();

}

int main() {

set_terminate(MyTerminate); // Установка процедуры

// завершения.

for (int j=0; j<2; j++) { try {

FuncA(j) ;

} catch(int k) {

cout << "Main: int caught - " << k << endl;

} }

// Следующие операторы исполняться не будут... cout “ "Exiting main..." “ endl;

return 0;

}

Рис.12.2 Выполнение программы Unhandled

Тело пробного блока в main () выполняется два раза. Имеется вложенный пробный блок в FuncA () . На первом проходе FuncB () выбрасывает int, для которого нет обработчика во внутреннем блоке и потому перехватываемое во внешнем пробном блоке, т. е. в main О . На втором проходе выбрасывается строка, которая перехватывается в FuncA () . Обработчик сам выбрасывает исключение Dummy — неуправляемое, поэтому вызывается установленная пользователем процедура завершения.

Исключения и классы

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

Исключения, конструкторы и деструкторы

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

Локальные (автоматические) объекты

Когда выброшено исключение, начинается разматывание стека с вызовом необходимых деструкторов. Однако деструкторы в этом случае вызываются только для полностью конструированных локальных объектов. Это означает, что если исключение выброшено в конструкторе объекта, для самого этого объекта деструктор вызван не будет. Будут вызваны только деструкторы его элементов-объектов и базовых классов. Поэтому, если объект содержал уже к этому времени указатели, например, на выделенную динамическую память, она освобождаться не будет. Возникнет утечка памяти.

Рассмотрите такой пример:

Листинг 12.4. Исключение в конструкторе

/////////////////////////////////////////////////////

// Construct.срр: Исключение в конструкторе. //

#inciude <stdio.h>

#include <stdlib.h>

#include <string.h>

#pragma hdrstop

#include <condefs.h>

void* operator new[](size_t size)

// Глобальная new[].

{

printf("Global new[].\n");

return malloc(size);

}

void operator delete[](void *p) // Глобальная delete[].

{

printf("Global delete[].\n");

free (p) ;

}

class Hold { // Класс, содержащий динамический массив char. char *ptr;

public:

Hold(char *str) // Конструктор преобразования из char*.

{

printf("Constructor.\n") ;

ptr = new char[strlen(str)+1] ;

strcpy(ptr, str) ;

// printf("Constructor: throwing exception...\n");

// throw "Exception!";

} ~Hold() // Деструктор.

{

printf("Destructor.\n") ;

delete [ ] ptr;

}

void Show() // Распечатка строки.

{

printf("My contents: %s\n", ptr);

} };

int main() {

try {

Hold h = "Some string."; // Попытка конструировать

// объект. h.Show() ;

} catch(char *str) {

printf("Message caught: %s\n", str);

}

printf("Exiting main...\n");

return 0;

}

Программа создает локальный в try-блоке объект класса Hold. Строка в конструкторе, выбрасывающая исключение, пока закомментирована, и программа выводит:

Constructor.

Global new[].

My contents: Some string.

Destructor.

Global delete [].

Exiting main...

Примечание

Вопрос на сообразительность: почему мы для вывода сообщений пользовались в этом примере функцией библиотеки С printf (), а не потоковыми операциями C++?

Если же раскомментировать строку, будет выброшено исключение, причем, поскольку деструктор не полностью конструированного объекта не вызывается, операция delete [ ] для уже выделенной строки выполнена не будет:

Constructor.

Global new[].

Constructor: throwing exception...

Message caught:Exception!

Exiting main...

Отсюда можно сделать полезный методический вывод: ресурсы, подобные выделяемой памяти (например, графические объекты Windows), следует оформлять как классы, которые будут входить в целевой класс в качестве его элементов. Класс из предыдущего примера можно модифицировать примерно так:

class Hold { // Класс, содержащий динамический

// массив char. struct IChar { // Вложенный класс, инкапсулирующий

// массив. char *ptr;

IChar(char *str) {

printf("IChar: constructor.\n");

ptr = new char[strlen(str)+1];

strcpy(ptr, str) ;

}

~IChar() {

printf("IChar: destructor.\n") ;

delete [] ptr;

}

} iStr; // Элемент - объект IChar. public:

Hold(char *str) // Конструктор преобразования из char*.

iStr(str) // Инициализатор элемента iStr. {

printf("Constructor: throwing exception ...\n");

throw "Exception!";

} ~Hold() // Деструктор - ничего не делает.

{

printf("Destructor.\n");

} void Show() // Распечатка строки.

{

printf("My contents: %s\n", iStr.ptr);

} };

Как видите, действия по выделению и освобождению памяти возложены теперь на класс IChar. Он, конечно, не обязан быть вложенным, как я сделал здесь (зачем, и сам не знаю). Программа выводит:

IChar: constructor.

Global new[].

Constructor: throwing exception...

IChar: destructor.

Global delete [].

Message caught: Exception!

Exiting main...

Хотя, как видите, деструктор Hold и не вызывается, деструктор его полностью конструированного элемента iStr вызывается правильно и память освобождается.

Динамические объекты

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

Хотя деструктор не вызывается, память объекта (не та, на которую он может ссылаться посредством указателей, а его собственная) автоматически удаляется. По сути, то же происходит и с локальными объектами, только там не полностью конструированный объект просто удаляется со стека, здесь же вызывается операция класса delete. Вот пример:

Листинг 12.5. Исключение в конструкторе динамического объекта

////////////////////////////////////////////////

// Dynamic.срр: Исключение при операции класса new.

//

#include <iostream.h>

#include <string.h>

#pragma hdrstop

#include <condefs.h>

const int MaxLen = 80;

class AClass {

char msg[MaxLen];

public:

AClass () // Конструктор, выбрасывающий исключение.

{ {

cout << "AClass: constructor." << endl;

cout << "Throwing exception..." << endl;

throw "Exception!";

}

~AClass() // Деструктор.

{

cout << "AClass: destructor."<< endl; }

void *operator new (size t size) // new класса.

{

cout<< "AClass: new." << endl;

return ::new char[size];

}

void operator delete(void *p) // delete класса.

{

cout << "AClass: delete." << endl;

::delete[] p;

}

};

int main() {

AClass *ap;

try {

ар = new AClassO; // Попытка выделить, объект.

}

catch(char *str) {

cout << "Caught a sring: " << str << endl;

)

return 0;

}

Эта программа выводит:

AClass: new.

AClass: constructor.

Throwing exception...

AClass: delete.

Caught a string: Exception!

Таким образом, при исключении память объекта освобождается операцией класса delete.

Классы исключений

Часто для обработки исключительных ситуаций используются классы, специально предназначенные для этой цели. Главное в управлении исключениями — отыскать нужный обработчик, а это делается путем сопоставления типа выброшенного объекта с типами, объявленными в обработчиках. Поэтому иногда для исключений определяют совершенно “пустые” классы, однако с уникальными именами.

Классы исключений программы могут быть организованы в иерархическую структуру. Схожие типы исключений объявляются в качестве производных одного и того же базового класса, являющегося их обобщением. Используя полиморфные механизмы, можно перехватывать только указатель или ссылку на базовый класс; полиморфизм обеспечит адекватную обработку исключения любого производного класса. Вот примерная схема:

class GenericFault { // Обобщенная ошибка.

public: virtual void Report ();

//

// Конкретные типы ошибок...

//

class OpenError: public GenericFault {

public:

void Report();

}

class BadHeader: public GenericFault { public:

void Report ();

class BadRecord: public GenericFault ( public:

void Report () ;

}

int main() {

try {

}

catch(GenericFault &err) { err.Report () ;

}

return 0;

}

Предопределенные исключения

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

xmsg

Класс xmsg предназначен для передачи строковых сообщений об исключениях. Он объявлен в заголовке except, h:

class EXPCLASS xmsg : public std::exception

{ public:

xmsg(const std::string &msg);

xmsg(const xmsg &);

virtual ~xmsg() throw ();

xmsg & operator=(const xmsg &) ;

virtual const char * what() const throw ();

const std::string & why() const;

void raise () throw(xmsg);

private:

std::string *str;

};

Применять класс xmsg очень просто:

#include <iostream.h>

#include <except.h>

#pragma hdrstop

#include <condefs.h>

int main() {

try {

xmsg X("Exception!");

throw X;

// или

//

X.raise () ;

}

catch(xmsg Smsg) {

cout << "Caught in main: " << msg.why() << end1;

}

return 0;

}

В классе имеется функция-элемент raise (), посредством которой объект выбрасывает сам себя. Ею можно воспользоваться вместо оператора throw. Функция why () возвращает стандартную строку с сообщением, записанным в объект.

Примечание

Класс xmsg считается устаревшим. Теперь в стандартной библиотеке C++ определяется ряд классов (производных от exception, как и xmsg), организованных иерархически. По сути они ничем друг от друга не отличаются; данные им имена ничего особенного не значат. Вот эти классы: class logic_error public exception class domain_error public logic_error class invalid argument public logic_error class length_error public logic_error class out_of_range public logic_error class runtime error public exception class range error public runtime error class overflow_error public runtime error class underflow error public runtime error

Как видите, logic_error и runtime_error — производные от exception, а все остальные — от первых двух. Эти классы имеют конструктор, принимающий ссылку на стандартную строку, виртуальный деструктор и виртуальную функцию-элемент what (), которая возвращает указатель на константную строку С. Вот пример:

#include <iostream>

#include <stdexcept>

using namespace std;

static void f() ( throw runtime_error("a runtime error");

}

int main ()

{

try

{ f();

}

catch (const exceptions e) {

cout << "Got an exception: " << e.what() << endl;

} return 0;

}

Программа печатает:

Got an exception: a runtime error

Иерархия и полиморфизм этих классов делают их весьма гибким средством организации обработки ошибок.

bad_alloc

Если операция new не может выделить запрашиваемую память, она выбрасывает исключение bad_alloc. Этот класс также является производным от exception:

class bad alloc : public exception {

public:

bad_alloc () throw() : exception () { ; }

bad_alloc(const bad_alioc&) throw()

{ ; }

bad_alloc& operator=(const bad_alloc&) throw()

{ return *this; }

virtual ~bad_alloc () throw ();

virtual const char * what () const throw()

{

return _RWSTD::_rw_stdexcept_BadAilocException;

} };

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

int main () {

try {

throw bad_alloc();

} catch(const exception &e) (

cout << "Caught something: "<< e.what() “ endl;

}

return 0;

}

Функция what () объекта bad_alloc возвращает строку "bad alloc exception thrown".

Примечание

В прежних версиях языка new при ошибке выбрасывала xalloc, класс, производный от xmsg:

class _EXPCLASS xalloc : public xmsg

{

public:

xalloc(const std::string &msg, _SIZE_T size);

_SIZE_T requested () const.;

void raise () throw(xalloc);

private:

_SIZE_T siz;

};

Теперь этим классом пользоваться нельзя, и он остался в библиотеке C++Builder только для обеспечения совместимости со старыми библиотеками. Еще раньше операция new при отказе просто возвращала NULL. Такое поведение операции восстановить очень просто, вызвав функцию set_new_hand-ler():

#include <new.h>

set new handler (0);

Прототип set_new_handler () имеет вид

typedef void (*new_handler)();

new_handler set__new_handler(new_handler rny_handler);

Функция предназначена для установки пользовательского обработчика ошибок глобальных операций new или new [ ]. Обработчик my handler может:

  • освободить память (удалить какой-то мусор) и возвратить управление;
  • вызвать функцию abort () или exit ();
  • выбросить исключение bad_alloc или производного типа.

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

В следующей главе мы встретимся еще с двумя предопределенными классами исключений — bad_typeid и bad_ typeinfo.

Информация об исключении

Имеются три глобальные переменные, в которых хранится информация о текущем исключении (они объявлены в заголовке except, h):

  • _throwExceptionName содержит указатель на строку с именем типа исключения;
  • _throwFileNane содержит указатель на строку с именем файла, где произошло исключение;
  • _throwLineNumber — целое без знака, представляющее номер строки файла, где было выброшено исключение.

Чтобы эта информация стала доступной, на странице C++ диалога Project Options нужно установить флажок Location information в группе Exception handling (по умолчанию выключен). Он соответствует ключу командной строки -хр.

Вот пример:

////////////////////////////////////////////////////

// Loclnfo.cpp: Информация о точке выброса исключения.

//

#include <iostream.h>

#include <stdexcept>

#pragma hdrstop

#include <condefs.h>

void f() {

throw invalid_argument("Exception from f(): ");

}

int main() {

try { f () ;

}

catch(const exception &e) { cout << e.what()

<< _throwExceptionName << end1

<< " in file " << _throwFileName << end1

<< " line " << _throwLineNumber << end1;

}

return 0; }

Программа печатает:

Exception from f(): invalid argument

in file C:\Projects\Chl2\LocInfo\LocInfo.cpp line 8

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

Помимо упоминавшегося выше флажка Location information, в группе Exception handling страницы C++ диалога установок проекта находятся следующие флажки:

  • Enable exceptions — при сброшенном флажке исключения запрещены, и компилятор генерирует сообщение об ошибке, если встречает try-блок; соответствует ключу командной строки -х и по умолчанию установлен.
  • Destructor cleanup — если установлен, то при разматывании стека вызываются деструкторы локальных автоматических объектов; по умолчанию установлен. Соответствующий ключ командной строки — -xd.
  • Fast exception prolog — при установленном флажке ряд служебных функций, ответственных за обработку исключений, расширяются как inline; по умолчанию сброшен. Ключ командной строки -xf.

О системных исключениях

Системные или процессорные исключения вроде деления на ноль невозможно обработать, пользуясь только механизмом исключений языка C++. Эти исключения сразу перехватываются операционной системой. Тем не менее, C++Builder поддерживает уже упоминавшееся в 4-й главе структурированное управление исключениями (SEH — Structured Exception Handling}, реализованное первоначально в качестве интегрированной части Windows NT и позволяющее работать с процессорными исключениями.

SEH в C++

Как вы, возможно, помните, в языке С (расширенном) имеются две структуры управления исключениями. Это обработка кадра (__try/_except) и обработка завершения (_try/_finally).

Обработки завершения мы здесь касаться не будем и вообще не будем в подробностях рассматривать работу со структурированными исключениями в C++, поскольку нас прежде всего интересуют здесь системные исключения, не поддерживаемые стандартным C++. Вместо ключевого слова _try в коде C++ принято писать просто try, как и для встроенных исключений языка. Таким образом, мы будем пользоваться только конструкцией try/_except.

Итак, синтаксис кадровой структуры управления имеет вид:

try

{

<тело защищенного блока> }

_except(<выражение-фильтр>) {

<блок обработки кадра>. }

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

Фильтры

Фильтрующее выражение должно принимать одно из трех значений:

EXCEPTION_EXECUTE_HANDLER

EXCEPTION_CONTINUE_SEARCH

EXCEPTION_CONTINUE_EXECUTION

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

В выражении фильтра можно вызвать функцию GetExceptionInformation() или GetExceptionCode () . Первая из них возвращает указатель на структуру EXCEPTION_POINTERS, которая содержит детальную ин-

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

Примечание

Сама функция фильтра не может вызывать GetExceptionInformation () или GetExceptionCode (). Значения, возвращаемые этими функциями, должны передаваться ей в аргументах.

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

Вот пример простейшей функции фильтра:

int MyFilter(int code) {

if (code == EXCEPTION_ACCESS_VIOLATION) return EXCEPTION_EXECUTE_HANDLER;

else

return EXCEPTION_CONTINUE_SEARCH;

}

try {

}

_except(MyFilter(GetExceptionCode())) {

}

Процессорные исключения

Наибольший интерес для нас представляют исключения, связанные с процессором. Их, как уже говорилось, нельзя обрабатывать стандартными средствами C++. Для этих исключений в заголовке winbase.h определен ряд символических констант. Вот некоторые из них:

EXCEPTION_ACCESS_VIOLATION

EXCEPTION_ARRAY_BOUNDS_EXCEEDED

EXCEPTION_FLT_DENORMAL_OPERAND

EXCEPTION_FLT_DIVIDE_BY_ZERO

EXCEPTION_FLT_INEXACT_RESULT

EXCEPTION_FLT_INVALID_OPERATION

EXCEPTION_FLT_OVERFLOW

EXCEPTION_FLT_STACK_CHECK

EXCEPTION_FLT_UNDERFLOW

EXCEPTION_INT_DIVIDE_BY_ZERO

EXCEPTION_INT_OVERFLOW

EXCEPTION_PRIV_INSTRUCTION

EXCEPT ION_IN_PAGE_ERROR

EXCEPTION_ILLEGAL_INSTRUCTION

EXCEPTION_NONCONTINUABLE_EXCEPTION

EXCEPTION_STACK_OVERFLOW

EXCEPTION_INVALID_DISPOSITION

EXCEPTION_GUARD_PAGE

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

///////////////////////////////////////////////////

// Access.cpp: Применение SEH для перехвата

// системных исключений.

//

#include <except.h>

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

static int xfilter(EXCEPTION_POINTERS *info)

{

if (info->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)

return EXCEPTION_EXECUTE_HANDLER;

else

return EXCEPTION_CONTINUE_SEARCH;

}

int main () {

try (

int *p = NULL;

*P = -1;

}

_except(xfilter(GetExceptionInformation())) { cerr << "Exception Access Violaton caught..." << endl;

exit(l);

} cout<<"Normal exit..." << endl;

return 0;

}

Для справки приведем описание структуры exception_pointers:

struct EXCEPTION_POINTERS {

EXCEPTION_RECORD *ExceptionRecord;

CONTEXT *Context;

};

Struct EXCEPTION_RECORD { DWORD ExceptionCode;

DWORD ExceptionFlags;

struct EXCEPTION_RECORD *ExceptionRecord;

void *ExceptionAddress;

DWORD NumberParameters;

DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];

Как уже говорилось, структуры управления исключениями SEH и C++ могут быть вложены друг в друга. Более того, except-обработчик может выбрасывать исключение C++, которое будет далее перехватываться catch-обработчиком. Можно в целях удобства и единообразия заключить все критические участки кода, могущие возбудить процессорные исключения (или вообще всю программу) в блоки try/ except, выбрасывающие исключения C++, и затем обрабатывать их наравне с другими исключениями. Рассмотрите такой пример:

////////////////////////////////////////////////

// SehPlus.cpp: Переход от SEH к C++.

//

#include <iostream.h>

#include <excpt.h>

#include <stdexcept>

#pragma hdrstop

#include <condefs.h>

static EXCEPTIONJRECORD eRec;

static int xfliter(EXCEPTION_POINTERS *xp) {

eRec = *(xp->ExceptionRecord);

return EXCEPTION_EXECUTE_HANDLER;

}

int main () {

double d = 10000;

try { try {

for (int i=5; i>=0; i-) { d = d / i;

cout << i << "... ";

} }

_except- ixfliter(GetExceptionInformation ())) ( if (eRec.ExceptionCode ==

EXCEPTIOM_FLT_DIVIDE_BY_ZERO) throw runtime error(

"Floating point divide by zero!");

else

throw runtime_error(

"Unknown processor exception.");

} }

catch(const exception &e) { cout << e.what() << end1;

} return 0;

}

Программа выводит:

5... 4... 3...2... I... Floating point divide by zero!

Примечание

Целесообразно было бы предусмотреть для процессорных исключений специальный класс, производный от, например, runtime_error, дополнив его структурой EXCEPTION RECORD. Тогда вообще вся обработка осуществлялась бы средствами C++.

Заключение

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


Глава 13. RTTI и приведение типов

Продавай мобильный контент со своего сайта

  • Глава 13. RTTI и приведение типов
    • RTTI
      • Операция typeid
      • type_info
      • bad_typeid
    • Специальные операции приведения типа
      • reinterpret_cast
      • const_cast
      • static_cast
      • dynamic_cast

Глава 13. RTTI и приведение типов

Аббревиатура RTTI означает RunTime Type Identification, т. е. “Идентификация типа времени выполнения”. Это механизм, позволяющий определить тип объекта во время выполнения программы, что очень полезно в иерархии типов, где указатель или ссылка базового класса может ссылаться на представитель любого производного класса. Полиморфные механизмы, конечно, хороши, но выглядят снаружи подобно “черному ящику”. Вы вызываете виртуальные методы, но не знаете, к чему, собственно, они применяются. Иногда требуется точно знать тип объекта. Если же можно с уверенностью идентифицировать типы, открывается возможность безопасного их приведения.

В этой главе рассматриваются RTTI и усовершенствованные операции приведения типа C++.

RTTI

В этом разделе описан синтаксис и даются примеры использования RTTI.

Операция typeid

Для получения информации о типе во время выполнения программы применяется операция typeid:

typeid(имя_ типа) typeid(выражение)

Ее операндом является либо имя типа, либо выражение, оцениваемое как некоторый тип. Операция возвращает константную ссылку на объект класса type_info, объявленный в заголовке typeinfo.h.

Если операция не может определить тип своего операнда, она выбрасывает исключение типа bad_typeid.

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

type info

Класс type_info объявлен следующим образом:

class _TIDIST _rtti type_info {

public:

tpid * tpp;

private:

cdecl type_info(const type info FAR &);

type info & cdecl operator=(const type_info _FAR &);

public:

virtual _cdecl ~type_info() ;

bool cdecl operator==(const type info FAR &) const;

bool cdecl operator!=(const type info FAR &) const;

bool _cdecl before(const type_info _FAR &) const;

const char _FAR *_cdecl name() const;

protected:

cdecl type_info(tpid * tpp) { tpp = tpp; } };

Примечание

Ключевое слово _rtti перед именем класса гарантирует, что информация о типе для него будет генерироваться вне зависимости от состояния флажка Enable RTTI на странице C++ диалога Project Options (ему соответствует ключ компилятора -rt).

Открытые элементы класса представлены операциями сравнения на равенство и неравенство, а также функциями name () и before (). Первая возвращает указатель на символьную строку с именем типа. Вторая возвращает true, если класс ее объекта является базовым по отношению к классу аргумента.

Вот пример с использованием операции typeid и класса type_info:

Листинг 13.1. Операция typeid

//////////////////////////////////////////////////////

// Typeinfo.срр: Операция typeid.

//

#include <typeinfo.h>

#include <iostream.h>

#include <string.h>

#pragma hdrstop

#include <condefs.h>

class Base { // Базовый класс.

public:

virtual ~Base (){} };

class Derived: public Base { // Производный класс.

char *str;

public:

Derived(const char *s) {

str = new char[strien(s)+1];

strcpy(str, s);

}

~Derived() { delete [] str;}

const char *Get() ( return str;}};

int main() {

Derived d("Derived class' string.");

Base &bRef = d; // Базовая ссылка на производный объект.

cout << "Typeinfo of bRef: " << typeid(bRef).name() << end1;

if (typeid(bRef) == typeid(Derived))

cout << "Contents of bRef: "<< ((Derived 6)bRef).Get() << endl;

else

cout << "Cannot cast safely bRef to Derived." << endl;

return 0;

Здесь демонстрируется операция typeid, сравнение типов и функция name () класса type_inf о. Программа выводит:

Typeinfo of bRef: Derived

Contents of bRef: Derived class' string.

Сравнение типов объекта bRef и Derived показывает, что они совпадают, и программа приводит ссылку к производному типу. Если закомментировать виртуальный деструктор класса Base, он станет неполиморфным, и typeid уже не сможет определить тип объекта, на который в действительности ссылается bRef:

Typeinfo of bRef: Base

Cannot cast safely bRef to Derived.

Примечание

Эта программа является примером того, как не следует поступать. Идентифицировать класс, затем привести ссылку к нужному типу — это попытка поставить RTTI на место виртуального механизма. Гораздо проще сделать функцию Get() виртуальной:

class Base { public:

virtual ~Base () { }

virtual const char *Get() { throw MyExcpt; } };

try {

cout << "Contents of bRef: " << bRef.Get() << endl;

} catch(MyExcept) {

}

При недопустимом типе объекта выбрасывается исключение (возможны и другие решения).

RTTI следует применять только в тех случаях, когда тип объекта не известен во время компиляции и нецелесообразно применение других средств C++ вроде позднего связывания.

bad_typeid

Если typeid не может определить тип объекта, выбрасывается исключение bad_typeid. Это происходит, например, при попытке определить тип, на который ссылается нулевой указатель:

//////////////////////////////////////////////

// BadType.cpp: Исключение bad_typeid.

//

#include <iostream.h> #include <typeinfo.h>

#pragma hdrstop

#include <condefs.h>

class Base { public:

virtual ~Base() {} };

class Derived: public Base {};

int main() {

try {

Base *bp = NULL;

cout<< "Typeid of bp: " << typeid(*bp).name() << endl;

} catch(bad_typeid) {

cout << "Bad typeid caught!"<< endl;

} return 0;

}

Специальные операции приведения типа

Стандарт ANSI определяет специальный синтаксис операций приведения типа, позволяющий программисту воспользоваться преимуществами RTTI и, кроме того, указать точно, что он хочет получить в результате таких операций. Новых операций приведения четыре: dynamic_cast, static cast, reinterpret cast и const_cast.

Здесь нужно вспомнить, для чего вообще может служить приведение типа. Можно назвать следующие случаи:

  • Чтобы изменить действительное представление данных либо поведение объекта, на который ссылается некоторый указатель. Простейшее приведение такого рода — преобразование целого типа в вещественный.
  • Чтобы изменить лишь интерпретацию компилятором некоторых данных, не меняя их действительного (физического) представления. Таково, например, приведение типа int к типу unsigned и наоборот.
  • Чтобы снять ограничения на возможные манипуляции с объектом, накладываемые модификатором const.

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

Мы начнем с более простых и традиционных приведений.

reinterpret_cast

Синтаксис данной формы операции приведения таков:

reinterpret_cast<Целевой_тиn> (аргумент)

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

Целевой_тип может быть типом ссылки, указателя, целым, перечислимым или вещественным типом.

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

Операция возвращает значение целевого типа.

Возможно, например, явное преобразование указателя в целый тип, равно как и обратная операция. Можно приводить указатель на функцию одного •типа к указателю на функцию другого типа или на некоторый объект, при условии, что он (указатель на объект) имеет достаточную разрядность.

Вот пример преобразования указателя в целое и наоборот:

////////////////////////////////////////////////

// Reinterpret.срр: Демонстрация reinterpret_cast

//

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

int main () {

int i = 7;

int *ip = Si;

int temp = reinterpret cast<int>(ip);

cout.setf(ios::showbase) ;

cout << "Pointer value is"<< ip << end1;

cout << "Representation of a pointer as int is " << hex << temp << endl;

cout << "Convert it back and dereference:"<<*reinterpret_cast<int*>(temp) << endl;

return 0;

}

Эта программа выводит:

Pointer value is 0065FEOO

Representation of a pointer as int is Ox65fe00

Convert it back and dereference: 0х7

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

const_cast

Операция cons't_cast имеет ту же форму, что и предыдущая:

соnst_сonst<целевой_тип>(аргумент)

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

Вот пример инициализации динамической константной строки:

/////////////////////////////////////////////

// ConstCast.срр: Подавление модификатора const.

//

#include <string.h>

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

int main ()

cons с char *ip;

ip = new char[20];

strcpy(const_cast<char*>(ip), "New const string,");

cout << ip << end1; delete [] ip;

return 0;

static_cast

Операция статического приведения типа

static саst<целевой тип> (аргумент)

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

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

Если некоторый указатель может быть приведен к типу Т*, то объект этого типа может быть приведен к типу Т&.

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

Листинг 13.2. Нисходящее приведение указателей и ссылок

//////////////////////////////////////////////////////

// StatCast.срр: Статическое нисходящее приведение типа.

//

#include <iostream.h>

#pragma hdrstop

#include <condefs.h>

class A {} ;

class B: public A { public:

int i;

B(int ii): i(ii) {}

B(A&): i(11) {

cout << "Derived conversion constructor... ";

} };

int main() {

В b(22), *pb = &b;

A Sra = static cast<A&>(b); // Ссылка на b как

// базовый объект.

А *ра = static_cast<A*>(pb); // Указатель на b как

// базовый объект.

cout << "Derived object: " << b.i << endl;

cout << "Downcasting pointer to pointer: "

<< static_cast<B*>(pa)->i << endl;

// Приведение

// указателей.

cout <<"Downcasting referense to referense: "

<< static cast<B&>(га).i<< endl;

// Приведение

// к ссылке.

cout << "Downcasting reference to object: ";

cout << static cast<B>(ra).i<< endl;

// Приведение

// к объекту.

return 0;

}

Вот что выводит этот код:

Derived object: 22

Downcasting pointer to pointer: 22

Downcasting referense to referense: 22

Downcasting reference to object: Derived conversion

constructor... 11

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

dynamic_cast

Операция динамического приведения типа

dynamic сast<целевой_тип>(аргумент)

не имеет аналогов среди операций, выполняемых .с применением “классической” нотации приведения. Операция и проверка ее корректности при известных условиях происходит во время выполнения программы.

Примечание

Динамическое приведение типа опирается на механизм RTTI, поэтому необходимо установить флажок Enable RTTI в диалоге Project Options (страница C++). Если этот флажок сброшен, программа компилироваться не будет.

Целевой тип операции должен быть типом указателя, ссылки или void*. Если целевой тип — тип указателя, то аргументом должен быть указатель на объект класса; если целевой тип — ссылка, то аргумент должен также быть соответствующей ссылкой. Если целевым типом является void*, то аргумент также должен быть указателем, а результатом операции будет указатель, с помощью которого можно обратиться к любому элементу “самого производного” класса иерархии, который сам не может быть базовым ни для какого другого класса.

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

При попытке произвести некорректное преобразование операция возвращает нуль, если целевой_тип — указатель. Если ссылка, операция выбрасывает исключение типа bad_cast.

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

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

Листинг 13.3. Нисходящее и перекрестное приведение типа

//////////////////////////////////////////////////

// Dynamic.срр: Динамическое приведение типа.

//

#include <iostream.h>

#include <typeinfo.h>

#pragma hdrstop

#include <condefs.h>

class Bl { // Полиморфный базовый класс.

public:

virtual ~B1() {} } ;

class B2 {}; class D:

public Bl,

public B2 {}; // Производный класс.

int main () {

D d;

Bl bl;

Bl &rbl = d;

try { //

// Нисходящее приведение. //

cout <<"Downcasting from Bl; object ID: "

<< typeid(rbl).name() << endl;

D &rd = dynamic_cast<DS>(rbl);

cout << "OK..."<< endl;

//

// Перекрестное приведение.

//

cout << "Cross-castind from Bl to B2; object ID: "

<< typeid(rbl).name() << endl;

B2 &rb2 = dynamic_cast<B2&> (rbl);

cout << "OK..." << endl;

//

// Попытка недопустимого приведения.

//

Bl &rrbl = bl;

cout << "Try invalid cross-casting; object ID:"

<< typeid(rrbl).name() << endl;

B2 &rrb2 = dynamic_ca3t<B2&>(rrbl);

cout << "OK..." << endl;

} catch(bad_cast) {

cout << "Cast failed." << endl;

} catch(bad_typeid) {

cout << "Typeid failed." << end1;

}

return 0;

}

Вывод программы:

Downcasbing from Bl; object. ID: D

OK. . .

Cross-castind from Bl to B2; object ID: D

OK. . .

Try invalid cross-casting; object ID: Bl

Cast failed.

Перекрестное приведение типа, т. е. такое, при котором классы хотя и относятся к одной иерархии, но находятся на разных ее “ветвях”, допускается и для обычных приведений, но в этом случае вряд ли вы получите сколько-нибудь осмысленный результат.

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

Листинг 13.4. Приведение от виртуального базового класса

/////////////////////////////////////////////////

// VBaseCast.срр: Приведение виртуального базового класса.

//

#include <iostream.h>

#include <typeinfo.h>

#pragma hdrstop

#include <condefs.h>

class VBase { // Виртуальный базовый класс. public:

virtual ~VBase() {} 1;

class Bl: public virtual VBase {};

class B2: public virtual VBase {};

class D: public Bl, public B2 {}; // Производный класс.

//

// Вспомогательная функция, аргументом которой

// может' быть любой класс данной иерархии.

//

void Report(VBase *pvb)

{

try {

cout << " ... Object ID: "

<< typeid (*pvb).name() << endl;

}

catch(bad_typeid) {

cout << " ...'Bad typeid in Report()."<< endl;

}

xin-. ilia in ( ) {

D d;

Bl bl;

{

Base *pvb = &d;

cout << "Original class: " << typeid(*pvb).name();

//не корректное приведение - pvb ссылается на объект //Производного класса.

//

Report (dynamic_cast<D*> (pvb) ) ;

pvb = o.al;

cout<< "Original class: " << typeid(*pvb).name();

//

// Следующее приведение не удается, поскольку объект,

// на который ссылается pvb, не является D. В Report()

// выбрасывается.bad_typeid, т.к. аргумент нулевой.

//

Report(dynamic cast<D*>(pvb));

} catch(bad__typeid) {

cout << " ... Bad typeid in main()." << end1;

}

return 0;

}

Программа выводит:

Original class: D ... Object ID: D

Original class: B1 ... Bad typeid in Report ().

Примечание

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

Заключение

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

Этой главой мы заканчиваем описание стандартного языка C++. Следующая часть книги посвящена визуальным средствам C++Builder и связанными с ними особенностям языка этой системы.


Часть III. Визуальное программирование в C++Builder

Продавай мобильный контент со своего сайта

  • Часть III. Визуальное программирование в C++Builder
    • Глава 14. Введение в визуальное программирование
      • Простой пример
        • Установка свойств компонентов
        • Ввод кода событий
        • Класс формы
      • Компоненты, свойства и события
        • Компоненты
        • Поля и методы
        • Свойства
        • События
      • Еще один пример
        • Форма
        • Код

Часть III. Визуальное программирование в C++Builder

  • Введение в визуальное программирование
  • Создание визуальных компонентов

В этой заключительной, очень небольшой части книги мы обсудим некоторые аспекты “визуального программирования”, как обычно называют этот род деятельности. Другое название — Rapid Application Development (RAD), “ускоренная разработка приложений”. В отличие от описаний стандартного языка C/C++ в первых двух частях книги изложение будет носить совершенно неформальный характер. Мы будем ориентироваться в основном на разбор конкретных примеров. Я настоятельно рекомендую читателю побольше экспериментировать, изучать материалы оперативного справочника (кстати, сейчас существует, кажется. Help для C++Builder'a на русском языке) и вообще, так сказать, творчески подходить к делу. Визуальное программирование, при всей его кажущейся легкости, для рядового программиста оказывается своего рода искусством с неизбежным атрибутом в виде метода проб и ошибок, поскольку C++Builder и его библиотеки визуальных компонентов представляют собой очень большую и сложную систему.

Глава 14. Введение в визуальное программирование

Вы, конечно, уже знакомы с визуальным программированием в С+ + Builder, когда на экране перед вами находится фирма, на нее накладываются различные компоненты, реализующие элементы управления графического интерфейса, а затем пишется код для событий этих компонентов, использующий свойства последних. Возможно, вы работали раньше с Visual Basic или даже с Delphi. Все эти системы внешне очень похожи друг на друга, a Delphi — вообще двойник C++Builder'a и опирается на ту же самую библиотеку компонентов.

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

Начнем с очень простого примера. Создайте новое приложение (File New Application в главном меню или значок Application в диалоге New Items). На экране появится пустая форма. Уменьшите ее размер и, руководствуясь рис. 14.1, разместите на ней следующие компоненты: календарь (CCalendar) и кнопку прокрутки (CSpinButton) со страницы Samples палитры компонентов, а также командную кнопку и две метки со страницы Standard. (Я не буду приводить детальных пошаговых инструкций, поскольку вы и так наверняка умеете все это делать, а если не умеете, то полезно будет разобраться самому, да это и не сложно.)

Рис. 14.1 Форма программы в режиме проектирования

Установка свойств компонентов

Для дальнейшей работы вам потребуется инспектор объектов (если его нет на экране, откройте его через меню View). Чтобы проще было писать

код, назовите для краткости календарь Са1, а кнопку прокрутки — Spin (т. е. измените в инспекторе свойство Name этих компонентов).

Введите надписи (свойство Caption) для командной кнопки и второй метки (той. что справа, у меня она называлась Label2) — соответственно “Выход” и “Месяц”. Для первой метки (внизу) установите желаемый размер и, если хотите, гарнитуру шрифта, чтобы текст легко читался и выглядел прилично.

На этом этап визуального проектирования нашего приложения закончен. (Сейчас самое время сохранить проект, присвоив имена модулю формы, который по умолчанию называется Unitl.cpp, и главному модулю Ргоjectl.cpp, по имени которого будет назван весь проект и конечный исполняемый файл.) Теперь нужно написать код, который придаст существованию расположенных на форме компонентов какую-то осмысленность.

Этот простой проект содержит следующие основные файлы:

  • Ргор.Ьрг — файл проекта;
  • Ргор.срр — главный исходный модуль, с ним редко приходится иметь дело;
  • PropU.cpp — модуль исходного кода, связанного с формой;
  • PropU.dfm — файл визуальных ресурсов формы.
  • PropU.h — заголовочный файл с определением класса формы.

В проекте может быть несколько форм, и каждой из них будет соответствовать свой исходный модуль. Для каждой формы генерируется также заголовочный файл с расширением .h. Проект может включать в себя и модули исходного кода, не связанные непосредственно с какими-либо формами. Если вы создаете именно модуль (значок Unit в диалоге New Items), а не просто срр-файл, то C++Builder автоматически создаст и h-файл с тем же именем.

Ввод кода событий

Откройте страницу Events инспектора объектов. Общая методика написания кода такова: вы выбираете компонент (на форме или из выпадающего списка инспектора объектов) и дважды щелкаете кнопкой мыши на нужном событии в правой колонке страницы событий инспектора. C++Builder автоматически генерирует оболочку обработчика события и переключает фокус на окно редактора кода. Текстовый курсор стоит прямо там, куда вы должны ввести свой код.

Начните с кнопки “Выход”, которая должна закрывать форму, завершая тем самым приложение. Обработчик должен вызывать метод формы Close ().

Листинг 14.1. Файлы программы Prop — PropU.h и PropU.cpp

//---------------------------------------

// PropU.h: Заголовок для PropU.срр.

//---------------------------------------

#ifndef PropUH #define PropUH

//---------------------------------------

#include <Classes.hpp>

#include <Controls.hpp>

#include <StdCtrls.hpp>

#include <Forms.hpp>

#include "CCALENDR.h"

#include <Grids.hpp>

#include "CSPIN.h"

//---------------------------------------

class TFormI : public TForm

{

_published: // IDE-managed Components

TButton *Buttonl;

TCCalendar *Cal;

TLabel *Labell;

TCSpinButton *Spin;

TLabel *Label2;

void _fastcall ButtonlClick(TObject *Sender);

void_fastcall CalChange(TObject *Sender);

void _fastcall SpinDownClick(TObject *Sender);

void _fastcall SpinUpClick(TObject *Sender) ;

void _fastcall FormCreate(TObject *Sender) ;

private: // User declarations

public: // User declarations

_fastcall TFormI(TComponent* Owner) ;

};

//---------------------------------------

extern PACKAGE TFormI *Forml;

//---------------------------------------

#endif

//---------------------------------------

// PropU.cpp: Исходный модуль примера с календарем.

//---------------------------------------

#include <vcl.h>

#pragma hdrstop #include <stdio.h>

#include "PropU.h"

//---------------------------------------

#pragma package(smart_init)

#pragma link "CCALENDR"

#pragma link "CSPIN"

#pragma resource "*.dfm"

TFormI *Forml;

//---------------------------------------

fastcall TFormI::TFormI(TComponent* Owner)

: TForm(Owner) {

}

//---------------------------------------

void_fastcall TFormI::ButtonlClick(TObject *Sender) {

Close () ;

}

//---------------------------------------

void_fastcall TFormI::CalChange(TObject *Sender)

{

char s[40] ;

sprintf(s, "Новая дата %d/%d/%d",

Cal->Day, Cal->Month, Cal->Year);

Labell->Caption = s;

}

//---------------------------------------

void_fastcall TFormI::SpinDownClick(TObject *Sender)

{

Cai->Day = 1;

if (Cal->Month != 1)

Cal->Month-;

else {

Cal->Month = 12;

Cal->Year-;

}

}

//---------------------------------------

void _fastcall TFormI::SpinUpClick(TObject *Sender) (

Cal->Day = 1;

if (Cal->Month != 12) Cal->Month++;

else {

Cal->Month = 1;

Cal->Year++;

}

//---------------------------------------

void _fastcall TFormI::FormCreate(TObject *Sender) {

char s[40] ;

sprintf(s, "Текущая дата %d/%d/%d",

Cal->Day, Cal->Month, Cal->Year);

Labell->Caption = s;

}

//---------------------------------------

В соответствии с листингом файла PropU.cpp введите код для тела обработчиков следующих событий:

  • OnCreate формы;
  • OnClick командной кнопки;
  • OnUpClick и OnDownClick кнопки прокрутки;
  • OnChange календаря.

Примечание

Если вы случайно, щелкнув лишний раз мышью, создали ненужный обработчик какого-нибудь события, лучше не удалять его вручную (не забывайте также, что одновременно с оболочкой обработчика создается его объявление в классе формы в h-файле). При компиляции программы он будет удален автоматически; C++Builder сам удаляет все пустые обработчики событий.

На этом этап написания кода закончен. Все остальное, что вы видите в листинге, генерирует C++Builder, в том числе определение класса формы в файле PropU.h. Для компиляции и запуска приложения нажмите кнопку Run. Кнопка прокрутки позволяет менять текущий месяц (и год), который отображается календарем. В календаре можно выбрать мышью текущее число. Получившаяся дата отображается меткой Labell.

На рис. 14.2 показано окно программы сразу после запуска (вверху) и после некоторых манипуляций ее органами управления.

Рис.14.2 Запущенная программа

Как вы понимаете, ничего полезного эта программа не делает; она просто отображает (по событию OnChange календаря) выбранную пользователем дату. В настоящем приложении событие OnChange и извлеченная из компонента календаря дата могли бы, например, управлять чем-нибудь вроде ежедневника. Вся действительная работа с данными происходила бы “внутри” обработчика этого события в том смысле, что возврат из него означал бы переход программы в состояние ожидания выбора новой даты. Возможны, правда, и другие варианты организации потока управления, однако все равно — в приложении C++Builder любой программный код должен прямо или косвенно вызываться из некоторого обработчика события.

Класс формы

Определение класса формы создается в h-файле с тем же именем, что и имя модуля. Открыть в редакторе этот файл можно, выбрав в контекстном меню пункт Open Source/Header File. Как видите, класс содержит указатели на объекты размещенных на форме компонентов, а также объявления обработчиков событий. Все элементы класса формы (кроме открытого конструктора) объявлены в разделе с меткой _published (опубликованные). Объявление элемента класса в этом разделе эквивалентно объявлению в разделе public за исключением того, что опубликованные свойства и методы доступны в режиме проектирования через инспектор объектов. Например, поле выбранного события в правой колонке инспектора представляет собой комбинированный выпадающий список. При нажатии на стрелку отображаются доступные обработчики события с подходящим набором параметров. Если переместить, например, обработчик ButtonlClick из раздела _published в раздел public, в этих списках вы его больше не увидите, однако вы можете ввести его имя вручную или, скажем, установить обработчик программно.

Как вы, без сомнения, также заметили, все функции-элементы класса формы объявлены со спецификацией _fastcall. Этот спецификатор протокола вызова описывался в 4-й главе. Он означает, что аргументы при вызове такой функции должны по возможности передаваться в регистрах процессора.

В библиотеке VCL применяется исключительно соглашение о вызове _fastcall. Функции, написанные вами, не обязаны иметь эту спецификацию, однако если функция является методом формы или компонента, разумным будет объявить ее именно так. Во-первых, этот вызов действительно быстрее вызова С или stdcall, а во-вторых, просто ради единообразия.

Компоненты, свойства и события

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

Компоненты

Под компонентами в C++Builder понимаются объекты или классы объектов, являющиеся, в некотором смысле, объектами “реального мира”. Вы непосредственно видите их на экране, их можно передвигать мышью, они реагируют на нажатие ее кнопок и т. д. Компоненты VCL инкапсулированы в классах языка Object Pascal, однако вполне возможно и написание компонентов на расширенном варианте C++, реализуемом в C++Builder.

Компоненты календаря и кнопки прокрутки, которые мы взяли для вышеприведенного примера, включены в палитру компонентов именно в качестве образцов такого рода. На самом деле стандартный календарь из VCL со страницы Win32 уже имеет все то, что мы реализовали в своем примере.

Чтобы можно было подключать библиотеку VCL к программам на C++, она сопровождается заголовочными файлами C++, моделирующими разделы интерфейса модулей языка Pascal. Эти заголовки содержат “параллельные” определения классов VCL. Заголовки VCL имеют по большей части расширение .hpp.

VCL расшифровывается как “библиотека визуальных компонентов”. Однако среди ее компонентов попадаются и не визуальные. Не визуальным компонентом является, например, таймер (класс TTimer). Компоненты стандартных диалогов тоже, как это ни странно, не визуальны.

Поля и методы

Поле — это просто другое название для элемента данных класса. Соответственно метод — синоним для функции-элемента класса. Для классов компонентов в C++Builder применяются именно эти термины.

Свойства

На первый взгляд свойство компонента не отличается от обычного элемента данных класса (поля). Действительно, в приведенном выше листинге мы видим выражения вроде

Cal->Month =12;

Cal->Year++;

Свойству можно присваивать значение, извлекать из него значение и вообще вроде бы делать с ним все то, что делают с простым полем. Действительно, свойство, как правило, имеет ассоциированное с ним поле, и иногда операции над свойством не означают ничего, кроме соответствующих операций над его полем. Но в примере из предыдущего раздела вы могли видеть, как простое присваивание значения, например, свойству Month календаря сразу меняет все его содержимое. Другими словами, свойство может иметь непосредственный коррелят в “реальном мире” компьютерного экрана. Механизм свойств обеспечивает компонентам ту их реальность, о которой мы говорили чуть выше.

Все это означает, что изменение значения свойства должно сопровождаться некоторыми побочными действиями, и потому присваивание значения свойству реализуется посредством вызова некоторой функции. Такие функции называют обычно set-функциями, а функции для чтения свойств — get-функциями. И те и другие называются функциями доступа.

Наш пример с календарем позволит нам исследовать некоторые аспекты свойств. Откройте окно обозревателя классов, если оно еще не открыто (View Explorer в контекстном меню редактора). Найдите в нем узел TCCalendar и дважды щелкните на нем кнопкой мыши. В редакторе откроется файл ccalendr.h, и курсор будет установлен на начало определения класса календаря. В разделе published вы можете видеть несколько типичных объявлений свойств, например:

_property TDayOfWeek StartOfWeek =(read=FStartOfHeek, write=SetStartOfWeek, defauit=l);

В разделе private объявлено ассоциированное поле:

TDayOfWeek FStartOfWeek;

TDayOfWeek — это просто short. В фигурных скобках записан список атрибутов свойства, который означает, что:

  • чтение свойства осуществляется посредством прямого доступа,
  • для установки значения свойства вызывается функция SetStartOfWeek();
  • свойство имеет значение по умолчанию, равное 1.

Set-функция, как и поле свойства, объявлена в разделе private:

void _fastcall SetStartOfWeek(TDayOfWeek Value);

Объявление свойства Month несколько сложнее:

property Integer Month = {read=GetDateElement,

write=SetDateElement,

stored=false,

index=2,

nodefault};

Атрибуты stored и nodefault относятся к так называемым спецификаторам хранения. Атрибут index показывает, что функции доступа должны вызываться с дополнительным (первым) аргументом, равным 2. Вот объявление set-функции:

void _fastcall SetDateElement(int Index, int Value);

На самом деле календарь сохраняет дату в единственном поле FDate типа TDateTime. Свойства Year, Month, Day не имеют собственных полей, а их функции доступа (они одни и те же, только с разными индексами) оперируют полем FDate.

Исходный модуль с кодом календаря ccalendr.cpp вы можете найти в папке ...\CBuilder5\Examples\Controls\Source.

События

С точки зрения определения класса компонента события являются просто свойствами определенного типа. В том же классе календаря:

private:

TNotifyEvent FOnChange;

published:

property TNotifyEvent OnChange = {read=FOnChange,

write=FOnChange};

Тип свойства-события должен быть так называемым замыканием (closure), о котором мы подробнее расскажем в следующей главе. Пока имейте в виду, что это специального вида указатель на функцию-элемент.

TNotifyEvent — простейший тип события, не имеющий дополнительных параметров кроме указателя па пославший событие объект:

typedef void _fastcall

(closure *TNotifyEvent)(System::TObject* Sender);

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

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

При всяком изменении состояния календарь вызывает свой метод Change ():

void _fastcall TCCalendar::Change() {

if(FOnChange)

FOnChange(this);

}

Метод, в свою очередь, вызывает процедуру, указатель на которую записан в поле свойства OnChange.

Еще один пример

В заключение главы мы покажем еще один пример, который не продемонстрирует ничего особенно нового, но послужит основой для разработки нашего собственного компонента в следующей главе. Пример выводит на форму “бегущую строку”.

Форма

Проектирование формы сводится к установке подходящего (небольшого) ее размера и размещению всего трех командных кнопок с надписями “Старт”, “Стоп” и “Выход”. Вы можете руководствоваться рис. 14.5, на котором показана запущенная программа.

Код

После этого нужно ввести код программы. Помимо кода обработчиков OnClick для кнопок и OnPaint для формы, вам потребуется написать:

  • код тела конструктора формы;
  • деструктор формы;
  • объявления полей bm, started, position и -interval класса формы;
  • методы Setup () и Loop ().

Код программы показывает следующий листинг.

Листинг 14.2. Файлы RunningU.h и RunningU.cpp

//---------------------------------------

// RunningU.h

//---------------------------------------

#ifndefRunningUII

#define RunninqUH

//---------------------------------------

#include <Classes.hpp>

#include <Controls.hpp>

#include <StdCtrls.hpp>

#include <:Forms.hpp>

#include <ExtCtrls.hpp>

//---------------------------------------

class TFormI : public TForm {

_published: // IDE-managed Components

TButton *Buttonl;

TButton *Button2;

TButton *Button3;

void _fastcall Button3Click(TObject *Sender);

void _fastcail Button1Click(TObject *Sender);

void _fastcall Button2Click(TObject *Sender);

void _fastcall FormPaint(TObject *Senders-private:

// User declarations

Graphics::TBitmap *bm;

inL position;

bool started;

void fastcall Setup();

void _fastcall Loop();

public: // User declarations

int interval;

_fastcall TFormI(TComponent* Owner);

_fastcail ~TForml<);

};

//---------------------------------------

extern PACKAGE TFormI *Forml;

//---------------------------------------

#endif

//---------------------------------------

// RunningU.cpp: Исходный модуль программы

//с "бегущей строкой".

//---------------------------------------

#include <vcl.h>

#pragma hdrstop

#include <sys\timeb-:h>

#include "RunningU.h"

//---------------------------------------

#pragma package (smart_init)

#pragma resource "*.dfm"

TFormI *Forml;

char tent[80] = "Тестирование довольно длинной бегущей

строки...";

//---------------------------------------

// Конструктор формы - выделяет и инициализирует

// битовую матрицу.

//

_fast-call TFormI: :TForml (TComponent* Owner)

: TForm(Owner) {

position = Width;

interval = 10;

started = false;

bm = new Graphics::TBitmap;

Setup 0;

}

//---------------------------------------

// Деструктор формы - удаляет битовую матрицу.

//

_fastcall TFormI::--TFormI() (

delete bm;

}

//---------------------------------------

//Инициализирует битовую матрицу образом текстовой строки.

void _fastcall TFormI:: Setup ()

bm->Canvas->Font->Name = "Comic.Sans MS";

bm->Canvas->Font->Size = 16;

bm->Height = bm->Canvas->TextHeight(text);

bm->Width = bm->Canvas->TextWidth(text) + 1;

bm->Canvas->Brush->Color = clBtnFace;

bm->Canvas->FillRect(Rect(0, 0, bm->Width, bm->Height)) ;

bm->Canvas->TextOut (0, 0, text) ;

}

//---------------------------------------

// цикл бегущей строки. Организует таймер с помощью

//Функциии API GetTickCount () .

//

void_fastcall TFormI::Loop() {

unsigned long nextTick = GetTickCount();

while (started) {

Application->ProcessMes sages ();

if (!started) return; if (GetTickCount () > nextTick)

{

//

// Копировать битовую матрицу с текстом

// на канву формы.

//

Canvas->Draw(--position, 12, bm);

if (position < -bm->Width) position = Width;

nextTick += interval;

} }

//---------------------------------------

// Кнопка "Выход".

//

void fastcall TFornil::Button3Click(TObject *Sender)

{ —

started = false;

Close () ;

}

//---------------------------------------

// Кнопка "Старт".

//

void_fastcall TFormI::ButtonlClick(TObject *Sender)

{

if (started) return;

started = true;

Loop() ;

} //-------------------------------------

// Кнопка "Стоп".

//

void _fastcall TFormI::Button2Click(TObject *Sender)

{

started = false;

}

//---------------------------------------

// Событие OnPaint.

// Обеспечивает обновление изображения

// при остановленной строке.

//

void _fastcall TFormI::FormPaint(TObject *Sender)

{

if (!started)

Canvas->Draw(position, 12, bm) ;

}

Для определения элементов любого класса можно воспользоваться услугами ClassExplorer. Щелкните правой кнопкой на узле нужного класса. Контекстное меню имеет пункты New Field..., New Property... и New Method... Эти пункты меню вызывают соответствующие диалоги, автоматически генерирующие необходимый код, по крайней мере его основу. Например, New Method вводит в класс объявление метода и создает в исходном модуле его оболочку.

Обратите внимание на методы Setup () и Loop (). В первом из них используется свойство Canvas битовой матрицы, созданной в конструкторе формы, во втором — свойство Canvas формы. Это свойство имеется у очень многих компонентов и инкапсулирует контекст графического устройства

Windows (DC). Подсвойства Canvas (такие, как Font, Brush и т. гг.) представляют различные графические объекты. Методы класса TCanvas позволяют рисовать основные графические формы, выводить текст, копировать изображения с одной канвы на другую и выполнять другие операции, связан Метод Loop (), вызываемый при нажатии кнопки “Старт”, содержит цикл ожидания, из которого программа не выходит до тех пор, пока не будет нажата кнопка “Стоп”. На каждом проходе цикла вызывается функция ProcessMessages () объекта Application. Эта функция передает управление системе Windows, чтобы последняя могла обработать находящиеся в очереди сообщения.

Метод использует также функцию API GetTickCount (), которая возвращает число миллисекунд, прошедшее с момента запуска системы. Цикл ожидания проверяет, достигло ли это число маркера времени, хранящегося в переменной nextTick, и если достигло, выводит текст на экран в следующей позиции, сдвигая при этом маркер времени дальше на заданный интервал.

На рисунке показана работающая программа.

Рис. 14.3 Программа бегущей строки

В библиотеке VCL имеется таймер (класс TTimer), который может периодически вырабатывать событие OnTimer. Применение в этой программе таймера позволило бы не прибегать к методике такого, не очень красивого, цикла ожидания. Беда в том, что компонент таймера не может обеспечить достаточно короткий интервал генерирования события. Хотя интервал таймера задается в единицах миллисекунд, на самом деле он оказывается кратным 55 ms, т. е. частота срабатывания таймера не может быть выше 18 герц. (По крайней мере, так обстоит дело в Windows 98, с которой я работаю.) Частота это определяется аппаратным таймером системы. Нам же нужна частота порядка сотни герц, чтобы получилась приемлемая скорость сдвига строки.

Заключение

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


Глава 15. Создание визуальных компонентов

Продавай мобильный контент со своего сайта

  • Глава 15. Создание визуальных компонентов
    • Расширение набора ключевых слов
      • _classic
      • _closure
      • _property
      • _published
      • _declspec
      • _thread
    • Пример создания компонента
      • Начало разработки
      • Код компонента
      • Разбор кода
      • Тестирование компонента
      • Установка компонента
      • Окончательное тестирование
      • Пример применения компонента

Глава 15. Создание визуальных компонентов

Одним из показателей квалификации программиста является то, может ли он разрабатывать свои собственные компоненты для визуальной среды программирования вроде C++Builder. Пользовательский компонент должен ничем не отличаться от “настоящего” в том смысле, что его можно будет найти в палитре компонентов, можно будет устанавливать его свойства и события в инспекторе объектов и т. п. Эта глава посвящена написанию пользовательских визуальных компонентов и некоторым сопутствующим вопросам.

Расширение набора ключевых слов

Реализация визуальной среды программирования требует расширения языковых средств, поскольку невозможно на стандартном C++ осуществить, например, механизм свойств. Кроме того, разработчики C++Builder ориентировались на уже существующую библиотеку VCL, разработанную для Delphi и написанную на языке Object Pascal, и потому необходимо было ввести в язык дополнения, необходимые для моделирования чуждых C++ языковых конструкций. В этом разделе мы вкратце опишем ключевые слова C++Builder, так или иначе связанные с визуальным программированием.

_classid

Это ключевое слово обозначает операцию, выполняемую во время компиляции. Синтаксис:

_classid(имя_класса)

Стандартный C++, в отличие от Object Pascal, не может оперировать классами как таковыми. Он оперирует представителями классов, конструированными объектами. Операция _classid позволяет получить, как говорят, указатель на метакласс для специфицированного класса (класс TMetaClass).

Указатель на класс как таковой, безотносительно к его представителям, необходим, например, при регистрации класса компонента. При создании нового компонента C++Builder автоматически генерирует в его модуле такой код:

namespace Cticktape Х

void __fastcail PACKAGE ReaisterO

{

TComponentClass classes[1]=

{_classid(CTickTape)};

RegisterComponents("Samples", classes, 0);

}

}

Операция _classid редко используется непосредственно. C++Builder генерирует ее автоматически, как, например, в приведенном фрагменте кода.

_closure

Ключевое слово _closure позволяет объявить специальный указатель (замыкание) на функцию-элемент. В отличие от стандартного указателя на функцию-элемент, объявленного, например, как

void (AClass::*funcPtr)(int);

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

void (_closure *aClosure) (int) ;

SomeClass *some0bj = new SomeClass;

aClosure = someObj->Func;

aClosure (1);

Замыкание комбинирует указатель на функцию-элемент с указателем на конкретный объект, и последний передается в качестве this при вызове функции через замыкание.

Типы событий в классах компонентов являются замыканиями:

typedef void fastcall ( closure *TPassCompleteEvent)

(System::TObject *Sender, bool& stop);

published:

_property TPassCompleteEvent OnPassComplete =

{ read=FOnPassComplete, write=FOnPassComplete };

_property

Ключевое слово _property служит для объявления свойств и спецификации их атрибутов. Синтаксис:

property тип свойства имя = { список атрибутов свойства };

Спецификации атрибутов имеют вид атрибут [= значение] и отделяются друг от друга запятыми. Возможны следующие атрибуты:

read = поле \ get-функция

write = поле \ set-функция

stored = true | false | поле get-функция

index = целая константа

default = значение

nodefault

Для атрибутов read/write может указываться либо ассоциированное со свойством поле (тогда говорят о прямом доступе к свойству), либо имя метода доступа. Если спецификатор write опущен, то свойство можно только читать. Можно, конечно, объявить и свойство только для записи, но трудно придумать случай, когда это имело бы смысл.

Обычным является объявление прямого доступа для чтения и set-функции — для записи:

_property bool Started = {read=FStarted, write=SetStarted};

Атрибут index специфицирует целое число, которое должно передаваться в качестве единственного аргумента get-функции (обычно последняя параметров не имеет) и первого аргумента set-функции (которая обычно имеет один параметр — значение для свойства):

private:

int FCoords [4];

int _fastcall GetCoords(int Index) {

return FCoords[Index];

void _fastcall SetCoords (int Index, int value) FCoords[Index] = value;

}

public:

_property int Left = {read=GetCoords,

write=SetCoords, index=0};

_property int Top = (read=GetCoords,

write=SetCoords, index=l);

_property int Right = (read=GetCoords,

write=SetCoords, index=2} ;

_property int Bottom = {read=GetCoords,

write=SetCoords, index=3};

Атрибуты stored, default и nodefault yназываются спецификаторами хранения

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

Атрибут default позволяет указать для свойства значение по умолчанию

_property int TickRate = { read=FTickRate,

write=SetTlcKRate, default=10 );

Значение по умолчанию, заданное атрибутом default, относится только к значению свойства, отображаемому инспектором объектов и не присваивается свойству автоматически при запуске программы. Это значение нужно явно присвоить в конструкторе компонента.

Атрибут nodefault отменяет унаследованное значение свойства по умолчанию.

Атрибуты default и nodefault поддерживаются только для целых типов, перечислений и множеств (класс Set библиотеки VCL).

В C++Builder приняты определенные соглашения об именах свойств, их полей и методов доступа. Этим соглашениям, в частности, следует ClassExplorer при автоматическом генерировании кода для свойств. Если имя свойства, скажем, PropName, то именем поля будет FPropName, именем get-функции GetPropName и именем set-функции — SetPropName.

Опубликование унаследованных свойств

Базовый класс создаваемого компонента может объявлять некоторое свойство как открытое или защищенное. В производном классе компонента можно переобъявить такое свойство:

  • Если свойство базового класса — защищенное, в производном классе его можно сделать открытым или опубликованным.
  • Если базовое свойство открытое, его можно сделать опубликованным.

Кроме того, при переобъявлении свойства в производном классе можно указать новое значение по умолчанию или, специфицировав nodefault, отменить унаследованное значение, не задавая нового.

При переобъявлении свойства указывается только его имя и, возможно, спецификаторы хранения, например:

_published:

_property OldProp = {nodefault};

_published

В разделе класса _published объявляются опубликованные свойства. Этот раздел могут иметь только классы, производные от TObject.

В качестве спецификатора доступа _published эквивалентно public. Разница между ними в том, что опубликованные свойства доступны и в режиме проектирования через инспектор объектов, в то время как к открытым свойствам можно обращаться только программно во время выполнения.

В разделе _published нельзя объявлять конструкторы и деструкторы, а также свойства-массивы. Объявляемые в нем поля должны принадлежать к классовому типу.

_declspec

Применение этого ключевого слова не ограничено визуальным программированием, однако также участвует в поддержке кода VCL.

Общее применение _declspec

Выражение вида _declspec (аргумент) является модификатором, который может применяться к функциям или переменным. Аргументы dllexport, dilimport и thread формируют модификаторы, соответствующие обычным export, _import и _thread. Разница между ними заключается в том, что обычные модификаторы должны всегда непосредственно предшествовать имени объявляемой переменной или функции, в то время как модификаторы _declspec можно помещать в любом месте объявления:

void _declspec(dllexport) f(void);// Верно.

_declspec(dllexport) void f(void);// Верно.

_export void f(void) // Ошибка: должно быть

// void export.

Другими аргументами _declspec общего применения являются:

  • naked: Применяется к определению функции. Подавляет генерацию кода пролога/эпилога вызова, позволяя программисту написать свой собственный код, используя встроенный ассемблер.
  • noreturn: Сообщает компилятору, что функция не возвращает управления вызывающей программе. Обычно, когда компилятор обнаруживает, что при некоторых условиях функция, объявленная с типом возвращаемого значения, ничего не возвращает, он выдает предупреждение. Однако если эта ситуация связана с вызовом другой функции, не возвращающей управления, то, объявив последнюю с модификатором _declspec (noreturn), можно подавить такое предупреждение. Вот пример:

_declspec(noreturn) void finish(){

...

throw "No return";

}

int RetInt(){

if(....)

return 1;

else

finish(); // Без noreturn генерировалось бы

// предупреждение.

  • nothrew: Соответствует спецификации исключения без аргументов. Следующие объявления эквивалентны:

__declspec(nothrow) void f();

void f() throw();

  • novtable: Применяется к классам, для которых никогда не будет создаваться представителей. В большинстве случаев этот модификатор подавляет генерирование vtaDie и, соответственно, кода всех функции,на которые она ссылается.
  • property: Позволяет реализовать в классе не статические “виртуальные элементы данных”, нечто вроде свойств. Синтаксис:

_declspec(property(get = get-функция,

put = put-функция)) объявление_ элемента;

Одна из спецификаций функций доступа может быть опущена. Компилятор транслирует обращения к “элементу данных” в вызовы функций доступа. Возможно также объявление “массива” с одним или несколькими “индексами”:

declspec(property(get=GetVal, put=PutVal)) int vArr[];

Число индексов в обращении к виртуальному массиву должно соответствовать числу параметров у функций доступа. (У простой виртуальной переменной get-функция не имеет ни одного, а put-функция имеет один параметр — присваиваемое значение.) Индексы не обязательно должны быть целыми, они могут быть, например, строками.

  • selectany: Применяется к глобальным переменным. В ANSI C/C++ принята концепция пробного определения (tentative definition). Если встречается объявление глобальной переменной без модификаторов класса хранения и без инициализации, оно рассматривается как пробное определение. Если тот же идентификатор появляется позднее в другом файле, предыдущее пробное определение считается объявлением external.

Однако инициализация глобальной переменной должна производиться только в одном месте. Если, например, глобальная переменная объявляется и инициализируется в заголовочном файле, подключаемом несколькими файлами проекта, при компоновке возникнет ошибка. Спецификация seiectany решает эту проблему:

_declspec(selectany) int gint = 10;

// Может появляться в

// нескольких файлах проекта.

selectany не может применяться к неинициализируемым переменным и переменным, недоступным за пределами текущего файла (т. е. глобальным статическим, например, глобальным константам C++ без спецификатора external).

  • uuid: Присоединяет GUID к объявляемому классу. Применяется только к СОМ-классам. Синтаксис:

_Declspec (uuid("GUID_COM-объекта") )

объявление/определение класса

Применение _declspec с VCL

Перечисленные ниже аргументы _declspec, служащие для поддержки VCL, редко применяются непосредственно. Они используются в макросах, определяемых файлом vcl\sysmac.h.

  • delphiclass: Может применяться для объявления классов, производных от TObject. Для классов, объявленных с этим аргументом _declspec, обеспечивается совместимость с VCL no RTTI, поведению конструкторов/деструкторов и обработке исключений. Накладываются также некоторые ограничения: класс не может быть сложным производным, его представители могут создаваться только динамически, он обязан иметь деструктор и для него не генерируются автоматически конструктор копии и операция присваивания по умолчанию.
  • delphireturn: Только для внутреннего использования VCL в C++Builder. Служит для поддержки встроенных типов ObjectPascal, для которых не существует эквивалентов в C++. Применяется в реализациях классов C++Builder Currency, AnsiString, Variant, TDateTime и Set.
  • dynamic: Служит для объявления динамических функций. По своему поведению они не отличаются от виртуальных, но реализуются по-другому. Они включаются в виртуальную таблицу только того класса, который их определяет. Тем самым экономится память, но падает эффективность, так как иногда приходится производить поиск в виртуальных таблицах, возможно, нескольких базовых классов. Аргумент может применяться только к функциям классов, производных от TObject.
  • hidesbase: В языке Object Pascal виртуальные функции базового класса могут появляться в производном как функции, не имеющие никакого отношения к одноименным функциям базового класса (это имеет место, если у производной функции не указан спецификатор override). Применение hidesbase к объявлению функции производного класса моделирует эту семантику Object Pascal.
  • package: Показывает, что код класса может компилироваться в пакете. При создании пакетов спецификатор генерируется компилятором автоматически.
  • pascalimplementation: Показывает, что код класса реализован на Object Pascal. Применяется в заголовочных файлах .hpp, моделирующих интерфейс Pascal-классов VCL.

_thread

Это ключевое слово имеет отношение к VCL лишь постольку, поскольку последняя включает в себя средства для работы с нитями (threads) кода В программе может инициироваться несколько потоков управления исполняющих параллельно. Эти потоки и называются нитями.

Обычно все параллельные нити используют одни и те же глобальные

переменные. Это кстати, позволяет эффективно реализовать взаимодействие между нитями. Объявление глобальной переменной с модификатором _thread приводит к тому, что для каждой нити будет создана своя копия этой переменной.

Модификатор не может применяться к объектам, требующим инициализации во время выполнения (например, переменной — объекту класса с определенным пользователем конструктором).

Пример создания компонента

В этом разделе мы продемонстрируем, как в C++Builder создается визуальный компонент. Это будет компонент телетайпной ленты, отображающий “бегущую строку”. Нечто подобное вы видели в предыдущей главе.

Начало разработки

Разработка специального компонента начинается с создания файлов исходного модуля и заголовка. Это можно сделать, выбрав либо значок Component в диалоге New Items, либо пункт Component | New Component... в меню. Появится диалог, показанный на рис. 15.1.

Рис. 15.1 Диалог New Component

В этом диалоге нужно выбрать из выпадающего списка Ancestor class базовый класс компонента. Мы возьмем в качестве базового TGraphicControl. Далее нужно указать имя нового класса. (Обычно имена классов компонентов начинаются с Т, но мы назвали наш компонент CTickTape.) После этого осталось указать местоположение и имя модуля в поле Unit file name. Нажмите кнопку с многоточием, и появится стандартный диалог Save.

Все поля заполнены (убедитесь, что в поле Palette page указано Samples), и можно нажимать кнопку ОК. В редакторе кода откроется файл CTickTape. срр. Сохраните компонент (кнопкой SaveAll).

Кстати, визуальные компоненты бывают оконные (базовый класс TWinControl) и графические (базовый класс TGraphicControl). Последние, к которым относится и наш компонент, не имеют собственного окна Windows. Они располагаются в пространстве окна своего родительского объекта. Компоненты оконные, как, например, командная кнопка, имеют свое собственное окно.

Код компонента

Создание визуального компонента — процесс сам по себе не визуальный. Все сводится к написанию кода для свойств и методов класса компонента. Здесь вам может очень помочь ClassExplorer, автоматически или, скорее, полуавтоматически генерирующий базовый код. С двумя его диалогами — для полей и методов — вы уже познакомились в предыдущей главе. Правда, чтобы можно было вводить в класс компонента поля, методы и свойства средствами обозревателя классов, нужно сначала создать проект, который будет этот компонент использовать. Создайте новое приложение и присоедините к его проекту модуль CTickTape.cpp (это делается либо в контекстном меню менеджера проектов, либо выбором Project | Add to Project... в главном меню). Созданное приложение станет потом тестовой программой для проверки работоспособности нашего компонента. Пока же с ним ничего делать не нужно.

Registei-() и ValidCtrCheck()

C++Builder уже создал заготовку компонента — файл CTickTape.cpp. Он содержит на данный момент пустой конструктор и две функции — Registerf) и ValidCtrCheck():

static inline void ValidCtrCheck(CTickTape *)

{

new cricKTapenull);

}

//-----------------------

_fastcall CTickTape::CTick(TComponent*Ownet)

: TGraphicControl(Owner) { }

//----------------------------------------

namespace Cticktape {

void _fastcall PACKAGE Register()

(

TComponentClass classes[1] =

{_classid(CTickTape)};

RegisterComponents("Samples", classes, 0);

}

}

Процедура Register () вызывается при регистрации компонента в палитре C++Builder. Первый ее оператор создает массив компонентов (метаклассов), состоящий в нашем случае из единственного элемента.

С помощью ValidCtrCheck () C++Builder проверяет, можно ли создать представитель класса компонента, т. е. не является ли класс абстрактным. Если класс компонента содержит не определенные чистые виртуальные функции, компонент зарегистрирован не будет.

Ввод пользовательского кода

Завершенный код компонента показан в нижеприведенных листингах 15.1 и 15.2.

Листинг 15.1. Класс компонента телетайпной ленты — CTickTape.h

//------------------------------

#ifndef CTickTapeH

#define CTickTapeH

//-------------------------------

#include <SysUtils.hpp>

#include <Controls.hpp>

#include <Classes.hpp>

#include <Forms.hpp>

//-------------------------

typedef void _fastcall (_closure *TPassCoitipleteEvent)

(System::TObject *Sender, boolS stop);

class PACKAGE CTickTape : public TGraphicControl { private:

int FTickRate;

unsigned long FNextTick;

bool FStarted;

bool needsFill;

int storeWidth;

int position;

Graphics::TBitmap* bm;

TPassCompleteEvent POnPassComplete;

void _fastcall SetStartcd(bool value);

void fastcall SetTickRate(int value);

void _fastcall CMChanged(Messages::TmeasageS message) ;

protected:

virtual void _fastcall Setup ();

virtual void_fastcall DoDrawText(TRect rect, long flags);

virtual void fastcall Paint();

virtual void fastcall PassComplete();

public:

_fastcall CTickTape(TComponent* Ownersvirtual void fastcall Tick();

_fastcall -CTickTape();

_property unsigned long NextTick = {

read=FNextTick, write=FNextTick };

_property bool Started =

{ read=FStarted, write=SetStarted };

_published:

property Caption;

property Font;

_property ParentFont;

_property int TickRate = { read=FTickRate,

write-SetTickRate, default=10};

_property TPassCompleteEvent OnPassComplete = { read=FOnPassComplete, write^FOnPassComplete };

BEGIN_MESSAGE_MAP

VCL_MESSAGE_HANDLER(CM_FONTCHANGED,

TMessage, CMChanged) ;

VCL_MESSAGE_HANDLER(CM_TEXTCHANGED,

TMessage, CMChanged) ;

END_MESSAGE_MAP(TGraphicControl) ;

};

//---------------------------------

#endif

Листинг 15.2. Модуль компонента — CTickTape.cpp

//-----------------------------------

// CtickTape.срр: Модуль компонента "Бегущая строка".

//

#include <vcl.h>

#pragma hdrstop

#include "CTickTape.h"

#pragma Package (smart_init)

//-----------

//VolidCtrCheck is used to assure that the components

//createddonot have any pure virtual functions.

static in void ValidCtrCheck (CTickTape *)

{

new CTickTape (NULL);

}

//------------------

_fastcall CTickTape: :CTickTape (TComponent* Owner)

:TGraphicControl (Owner)

{

Width = 100;

FTickRate = 10;

ControlStyle << csOpaque;

bm = new Graphics::TBitmap ;

Setup ();

}

_fastcall CTickTape::~CTickTape () {

delete bm;

}

//----------------------------

namespace Cticktape {

void _fastcall PACKAGE Register() {

TComponenfcClass classes[1] =

{_classid(CTickTape)};

RegisterComponents("Samples", classes, 0) ;

} I

//------------------------------------

// Установка параметров битовой матрицы.

//

void _fastcall CTickTape::Setup() {

AnsiString text = Caption;

bm->Canvas->Font = Font;

Height = bm->Canvas->TextHeight(text);

storeWidth = Width;

bm->Width = bm->Canvas->TextWidth(text) + 1;

bm->Height = Height;

bm->Canvas->Brush->Color = Color;

bm->Canvas->TextOut(0, 0, text + " ");

if (ComponentState.Contains(csDesigning))

position = 0;

else

position =г Width;

needsFill = true;

}

void_fastcall CTickTape::DoDrawText(TRect rect, long flags) {

if (Width != storeWidth) Setup() ;

if (needsFill) {

Canvas->Brush->Color = Color;

Canvas->FillRect(rect);

needsFill = false; .

} Canvas->Draw(position, 0, bm) ;

}

void _fastcall CTickTape::Paint() {

TRect rect - ClientRect;

DoDrawText(rect, 0);

}

//---------------------------------

// Сдвиг строки на пиксел, если истек

// очередной интервал ожидания.

//

void _fastcall CTickTape::Tick()

{

if ( ! FStarted || (GetTickCount () < FNextTicK) ) return;

FNextTick += FTickRate;

Repaint () ;

if (~position < - bm->Width) {

position = Width;

PassComplete ();

}

}

//-------------------------------------------

// Set-функции.

//

void _fastcall CTickTape::SetStarted(bool value)

{

if (value == FStarted) return;

FStarted = value;

if (value)

FNextTick = GetTickCount ();

}

void _fastcall CTickTape::SetTickRate (int value)

{

if (value > 0) FTickRate = value;

}

//--------------------------------------------------------

// Отработка сообщений FontCbanged и TextChanged.

//

void _fastcall CTickTape::CMChanged(Messages::

TMessage& message)

{

Setup () ;

Repaint () ;

}

//----------------------------------

// Событие OnPassComplete.

//

void_fastcall CTickTape:: PaseComplite()

{

if (OnPassComplete) { bool stop = false;

OnPassComplete (this", stop);

Started = [stop];

} }

Если вы хотите собственноручно повторить все этапы создания этого компонента, то, воспользовавшись услугами обозревателя классов и руководствуясь листингами, введите в класс следующие элементы:

  • поля needsFill, storeWidth, position и bm;
  • методы Setup(),DoDrawText(),Paint(),PassComplete(),CMChanged (), Tick () и деструктор;
  • свойства NextTick, Started, TickRate и OnPassComplete (последнее будет событием компонента).

Не забудьте также ввести код тела конструктора.

В файл CTickTape.h введите typedef замыкания TPassComplete-Event и четыре строки с макросами, которые вы видите в самом конце определения класса. Переобъявите в разделе _published свойства Caption, Font и ParentFont. Это защищенные свойства класса TControl. Если подходить к делу написания компонента совсем серьезно, можно и должно было бы переобъявить и другие унаследованные свойства и, возможно, ввести еще какие-то другие, но для демонстрации этих трех вполне достаточно. Даже ParentFont, пожалуй, лишнее.

Вот, кажется, и все. Сохраните компонент (оба файла). Теперь мы рассмотрим, как он работает.

Разбор кода

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

Конструктор и деструктор

Конструктор создает операцией new битовую матрицу и устанавливает значения некоторых полей и свойств (Width — унаследованное и уже опубликованное свойство). После этого он вызывает функцию Setup ().

Примечание

Обратите внимание на строку со свойством ControlStyie. Оно относится к классу Set, который моделирует встроенный тип множеств языка Pascal. Перегруженная операция “ вводит элемент в множество. Стиль компонента определяет, в частности, как будет выполняться его перерисовка. Если не установить csOpaque, весь компонент перед перерисовкой, т. е. вызовом функции Paint (), о которой речь пойдет ниже, будет очищаться и текст строки будет заметно мерцать.

Деструктор удаляет выделенную в конструкторе битовую матрицу.

Метод Setup()

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

Метод копирует свойство компонента Font в соответствующее свойство канвы битовой матрицы, определяет ширину и высоту образа текстовой строки и устанавливает по ним размеры матрицы. Устанавливается также цвет фона, и строка выводится на битовую матрицу методом ее канвы TextOut () . Образ строки готов. Справа от текста в битовой матрице имеется по крайней мере одна колонка пикселов, закрашенных фоновым цветом. Благодаря этому обеспечивается очистка пространства компонента, “появляющегося” из-под конца строки при движении ее влево.

Наконец, устанавливается начальная позиция вывода битовой матрицы в графическое пространство компонента. Проверяется свойство Component-State (это множество). Оно индицирует различные аспекты статуса компонента, в частности, участвует ли компонент в визуальном проектировании приложения или он функционирует в работающей программе. Если компонент находится в режиме проектирования, начальная позиция образа строки устанавливается равной 0, т. е. строка будет выведена начиная от его левого края и в конструкторе формы будет виден установленный в инспекторе объектов текст. В противном случае позиции присваивается значение, равное ширине компонента, что помещает строку за его правым краем.

Методы DoDrawTextQ и PaintQ

DoDrawText () производит вывод битовой матрицы на канву компонента, вызывая ее метод Draw () . Попутно он проверяет, не была ли измена ширина компонента и, если это так, вызывает Setup () и очищает компонент, вызывая FillRect (). Методу передается прямоугольник, задающих координаты экранной области компонента (области клиента). Setup(), конечно, производит в данном случае массу лишних операций, но ради простоты логики можно поступиться его изяществом и эффективностью, которая в данном случае не важна. Изменение ширины — единичное редкое действие.

Примечание

Кстати об эффективности. Вывод образа текста на экран с буферизацией в битовой матрице был выбран потому, что копирование матрицы должно происходить очень быстро, по идее, быстрее, чем непосредственный вывод текста методом Text0ut(). Однако я этого не проверял. Если читатель захочет поэкспериментировать, он может сравнить эффективность методов Draw (), TextOut () и Text?ect (). Это полезное упражнение.

DoDrawText () вызывается из метода Paint (). Виртуальный метод Paint () вызывается, в свою очередь, по сообщениям Windows WM PAINT, a также в результате вызова Refresh () , Repaint (), Update () или Invalidate (). Наш компонент никогда не перерисовывает себя, непосредственно вызывая Paint () . Перед вызовом Paint () производится инициализация канвы компонента и ей выделяется контекст устройства Windows. Без этого наша функция DoDrawText () работать не будет.

Set-функции

Функции SetStartedf) и SetTickRate () устанавливают значения соответствующих свойств. Свойство Started управляет статусом строки — бежит она или стоит на месте. Если его значение меняется с false на true, инициализируется поле маркера времени, свойства NextTick.

Свойство TickRate задает интервал срабатывания “таймера” строки. Set-функция проверяет, не присваивается ли свойству отрицательное число или ноль. Проверка допустимости присваиваемых данных является еще одним важным аспектом механизма свойств.

Метод Tick()

Это единственный, не считая конструктора и деструктора, открытый метод компонента. Именно он обеспечивает “бег” строки по экрану. Если строка остановлена или время нарисовать ее в следующей позиции еще не подошло, метод немедленно возвращает управление. Если же строка запущена и текущий счетчик миллисекунд сравнялся или перешел за маркер времени NextTick, вызывается Repaint () , маркер времени сдвигается на интервал TickRate и определяется следующая позиция строки.

Если строка вышла за пределы компонента, восстанавливается ее начальная позиция и вызывается функция PassComplete () .

Функция PassCompleteQ

Наш компонент может генерировать событие OnPassComplete. Инициирует событие функция PassComplete (). При вызове этой функции из метода Tick() она проверяет, присвоено ли значение (указатель-замыкание) свойству OnPassComplete. Если присвоено, то через свойство вызывается установленная пользователем процедура обработки события со вторым аргументом — переменной stop, в которой пользовательский обработчик может передать true, чтобы остановить строку. Другими словами, если строка ушла с экрана, обработчик события может немедленно запретить возобновление цикла, модифицировав свой параметр-ссылку.

Обработка сообщений

Наш компонент может обрабатывать сообщения cm_fontchanged и CM_TEXTCHANGED, передаваемых ему при изменении соответственно свойств Font и Caption. Это внутренние сообщения приложения С++Вuider, не связанные с сообщениями Windows.

Для обработки обоих этих сообщений мы предусмотрели единственную функцию CMChange() Она вызывает Setup () для обработки изменившихся свойств и затем Repaint () , немедленно перерисовывающую компонент.

Чтобы привязать данные конкретные сообщения к данной процедуре обработки, в классе должен быть реализован виртуальный метод Dis?ат:сЬ (), распределяющий сообщения по назначенным для них обработчикам. В конце определения класса в файле CTickTape.h вы видите четыре макроса “таблицы сообщений”. Они генерируют метод Dispatch () примерно такого вида:

void _fastcali CTickTape::Dispatch(void* Message)

{

switch (((TMessage*)Message)->Msg) { case CM_TEXTCHANGED:

case CM_FOMTCHANGED:

CMChanged(*(TMessage*)Message) ;

breaks; default :

TGraphicControl::Dispatch(Message) ;

} }

Я его несколько “оптимизировал”, поскольку для обоих событий вызывается, одна и та же функция и нет смысла дублировать код для двух меток case. Оператор switch сравнивает идентификатор сообщения Msg с пунктами “таблицы” и вызывает в случае совпадения CMChanged. Если сообщение не опознано, вгдзывается Dispatch () базового класса.

Тестирование компонента

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

Заготовка тестового приложения у вас уже есть. Нужно спроектировать его форму и написать код.

Форма тестера показана на рис. 15.2. Она содержит несколько кнопок и поле редактирования (компонент TEdit). Метка “Текст” слева от поля редактора, собственно, не нужна.

Рис. 15.2 Форма текстового приложения

Что должно тестироваться? В первую очередь, конечно, что строка движется и может быть остановлена и вновь пущена установкой значения свойства Started. Затем нужно проверить, как компонент реагирует на изменения свойств и на сообщение wm_paint, которое посылается приложению, например, при открытии его окна, ранее заслоняемого окном другой программы. Наконец, нужно проверить, генерирует ли компонент предусмотренные события.

Листинг 15.3 показывает код файлов TapeU.h и TapeU.cpp (проект тестера мы назвали Таре).

Листинг 15.3. Файлы программы-тестера для CTickTape

//-------------------------------

// TapeU.h

//

#fndef TapeUH

#define TapeUH

//--------------------------------------------

#include <Classes .hpp>

#include <Controis .hpp>

#include <StdCtrls.hpp>

#include <Forms.hpp>

#include <Dialogs.hpp>

#include "CTickTape.h"

//----------------------------------

class TFormI : public TForm

{

_published:

// IDE-managed Components

TButton *Buttonl;

TLabel *Labell;

TEdit *Editl;

TButton *Buttun2;

TButton *Button3;

TButton *Button4;

TButton *Button5;

TFontDialog *FontDialogl;

TButton *Button6;

CTickTape *Tape;

void _fastcall ButtonlClick(TObject *Sender);

void _fastcall Button2Click(TObject *Sender);

void _fastcall Button3Click (TObject *Sender);

void _fastcall Button4Click(TObject *Sender);

void _fastcall Button5Click(TObject *Sender);

void _fastcall Button6Click(TObject *Sender);

void _fastcall TapePassComplete(TObject *Sender,buul Sstop),

private: // User declarations

public: // User declarations

_fastcall TFormI(TComponent* Owner) ;

// _fastcall -TFormlO;

};

//-------------------------------------

extern PACKAGE TFormI *Forml;

//-------------------------------------

#endif

//-------------------------------------

// TapeU.cpp: Модуль тестера бегущей строки.

//

#include <vcl.h>

#pragma hdrstop

#include "TapeU.h"

//--------------------------------------

#pragma package(smart_init)

#pragma link "CTickTape"

#pragma resource "*.dfm"

TFormI *Forml;

char *strings[3] = ("Первая строка...", "Вторая строка...", "Третья строка...");

//---------------------------------------

_fastcall TFormI::TForm1(TComponent* Owner) : TForm (Ow.ner)

{

/*

Tape = new CTickTape(this);

Tape->Parent = this;

Tape->Left = 20;

Tape->Top = 10;

Tape->OnPassComplete = TapePassComplete;

*/

}

/*

_fastcall TFormI::-TFormI()

{

delete Tape;

}

*/

//-----------------------

//кнопка "Выход".

//

_fastcall TFormI::ButtonlClick(TObject *Sender) Tape->Started = false;

Close();

}//------------------

//Кнопка"Ввести".

//

void _fastcall TFormI::Button2Click(TObject *Sender)

{

Tape->Caption = Editl->Text;

}

//--------------------------------

//

void_fastcall TFormI : : Button3Click (TObject *Sender)

{

Tape->Started - true;

while (Tape->Started) {

Application->ProcessMessages() ;

Tape->Tick() ;

}

//-----------------------------------------------

// Кнопка "Стоп".

//

void _fastcall TFormI::Button4Click(TObject *Sender)

{

Tape->Started = false;

} //----------------------

// Кнопка "Ширина".

//

void_fastcall TFormI::Button6Click(TObject *Sender)

{

Tape->Width = Tape->Width % 300 + 50;

}

//----------------------------------------

//----------------------------------------

// Выбор шрифта.

//

void_fastcall TFormI::ButtonSClick(TObject *Sender)

{

FontDialogl->Font = Tape->Font;

if (FontDialogl->Execute ())

Tape->Font = FontDialogl->Font;

//------------------------------------------

// Обработчик события OnPassComplete.

//

void _fastcall TFormI::TapePasaComplete(TObject* Sender,

bool& stop)

static int i = 0;

if (i > 2) {

i = 0;

stop = true;

} Tape->Caption =strings[i++] ;

} //----------------------------------------

В модуле TapeU.cpp вы видите закомментированный код (и одну строку в TapeU.h), выделенный жирным шрифтом, который нужно в данный момент раскомментировать. Показанный в листинге вариант программы (с исключенным деструктором формы и кодом тела конструктора) соответствует окончательному тестированию компонента, установленного в палитру к размещенному на форме тестера во время ее проектирования.

Код программы-тестера

Конструктор формы динамически создает компонент CTickTape и задает начальные значения некоторых свойств, в том числе указатель на процедуру обработки события OnPassComplete. Указатель на компонент, объявленный в классе формы, назван Таре.

Чтобы запустить бегущую строку, нужно нажать кнопку “Старт”. Обравитчик события кнопки устанавливает свойство компонента Started и входит в цикл, который будет прерван только при нажатии кнопки “Стоп”; в общем случае тогда, когда свойство Started окажется равным false.

Код кнопки “Старт” исполняет неопределенный цикл, вызывающий функцию приложения ProcessMessaghes () и метод компонента Tick () . Метод Tick () сам знает, когда нужно будет перерисовать компонент.

Остальная часть программы довольно очевидна. Для управлением шрифтом мы разместили на форме компонент TFontDialog. Компонент этот невизуальный, на форме отображается только его значок.(на рис. 15.3 его можно видеть в левом нижнем углу). Настоящий диалог выбора шрифта заводится на экран, когда вызывается его метод Execute () . Это делает функция Button5Click () (кнопка “Шрифт”).

Тестируется также способность компонента реагировать на ширину своего поля. Размер компонента можно менять, устанавливая свойство Width. Такое действие не генерирует никакого сообщения, однако вызывает функцию Paint ().

Окно редактора и кнопка “Ввести” позволяют присвоить любой текст свойству компонента Label.

Кнопка “Ширина” устанавливает горизонтальный размер компонента.

Пример тестирует также событие компонента OnPassComplete. Как только текущая строка ушла с экрана, по этому событию свойству Caption аписваивается друругая строка. Когда все наличные строки исчерпаны, обработчик события останавливает строку.

Работающий компонент показан на рис. 15.3.

Рис. 15.3 Программа-тестер,

работающая с компонентом CtickTape

Установка компонента

Чтобы установить компонент в палитру компонентов, выберите в меню Component | Install Component... В появившемся диалоге вам нужно будет задать только имя модуля компонента. По умолчанию ваш компонент будет введен в пакет dclusr50.b.pk. Компонент окажется на странице палитры Samples. Если вы не предусматриваете для него специального значка, он будет представлен значком по умолчанию.

Значок компонента

Если вы хотите, чтобы ваш компонент, как и “настоящие”, был представлен в палитре компонентов индивидуальным значком, вы, во-первых, должны предоставить для регистрации сам значок, а также сообщить C+4-Builder сведения о его местоположении. Значок (пиктограмма) должен иметь размер 24Х24 пиксела. “Прозрачным” цветом в C++Builder считается оливковый.

Значок должен находиться в компилированном файле ресурсов с расширением .res или .dcr и иметь идентификатор ресурса, соответствующий имени класса компонента, в нашем случае CTICKTAPE.

Файл ресурсов должен быть присоединен к проекту пакета, в котором будет размещаться компонент. Если ваш компонент имеет стандартное имя, начинающееся с Т, то он может быть загружен автоматически, если файл ресурсов имеет расширение .dcr, находится в том же каталоге, что и модуль компонента и имеет то же имя, что и класс, но без Т. В нашем случае имя класса нестандартное, так что придется либо присоединить значок к проекту пакета с помощью менеджера проекта, либо вручную открыть файл CBuilder5\Lib\dclusr50.bpk и ввести в него примерно такую строку:

USERES("C:\Ppojects\Chl5\TickTape\CTickTape.dcr") ;

Как вы понимаете, все это нужно сделать до того, как вы станете устанавливать компонент.

Окончательное тестирование

Теперь, когда компонент находится в палитре, можно модифицировать программу-тестер (закомментировать код конструктора и деструктор) и разместить компонент на форме визуальным образом, а также установить нужные его свойства в инспекторе объектов. На рис. 15.5 показан размещенный на форме компонент и инспектор объектов, показывающий имеющиеся опубликованные свойства.

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

Мы хотим здесь продемонстрировать, что наш компонент очень прост в использовании. Форма имеет три кнопки — для запуска, остановки и выхода из программы. Также на ней размещаются три компонента TTickТаре. Для каждого из них можно задать свой размер и шрифт. Свойства TickRate были установлены в инспекторе объектов равными 10, 7 и 15. Код модуля приведен в листинге 15.4.

Рис. 15.4 Компонент конструкторе форм

Листинг 15.4. Файл ThreeU.cpp.

//-------------------------

#include<vcl.h>

#pragma hdstop

#include<ThreeU.h"

//-------------------------

#pragma packege(smart_init)

#pragma link "CTickTape"

#pragma resourse "*.dfm"

TForm1 *Form1;

//---------------------------

_fastcall TFormI: :TForml (.TComponent* Owner) :Form(Owner)

{

}

//---------------------

void _fastcalll TFormI: :Button2Click(TObject *Sender)

{

if(run)return;

CTickTapel->Started = true

CTickTape2->Started = true CTickTape3->Started = true

run = true;

while (run) (

Application->ProcessMessages() ;

if (!run) break;

CTickTapel->Tick()

CTickTape2->Tick()

CTickTape3->Tick() }

} //-----------------------------

void_fastcall TFormI::Button3Click(TObject *Sender) {

if (run) { run = false;

CTickTapel->Started = false;

CTickTape2->Started = false;

CTickTape3->Started = false;

}

}

//----------------------------------

void_fastcall TFo'rmI: :ButtonlClick (TObject *Sender) {

Button3Click(this) ;

Close () ;

} //------------------------------------

В обработчике кнопки “Старт” находится цикл ожидания:

run = true;

while (run) {

Application->ProcessMessages() ;

if (!run) break;

CTickTapel->Tick() ;

CTickTape2->Tick() ;

CTickTape3->Tick() ;

}

При нажатии этой кнопки три строки начинают бежать по экрану, каждая со своей скоростью. Как видите, все, что требуется от программиста — это достаточно часто вызывать метод Tick() строки. Время компонент будет отмерять сам. На рис. 15.5 показано запущенное приложение.

Рис. 15.5 Программа с тремя бегущими строками

Заключение

На этом наше знакомство с C++Builder заканчивается. В этой главе мы рассмотрели один из довольно сложных аспектов визуального программирования — создание визуальных компонентов. Созданный нами компонентов конечно, нельзя считать завершенным. Его нужно было бы дополнить свойствами класса TComponent, такими, как Visible, Enabled и т. д., может быть, придумать и реализовать какие-то специфические свойства и события. Скажем, можно было бы определить событие для двойного щелчка, который бы останавливал и снова запускал строку. Но все это мы оставляем на усмотрение читателя.


Приложение. Классы AnsiString и Set

Продавай мобильный контент со своего сайта

  • Приложение. Классы AnsiString и Set
    • Класс AnsiString
      • Методы
    • Класс Set

Приложение. Классы AnsiString и Set

В VCL для представления текстовых строк используются не символьные массивы и не шаблон стандартной библиотеки, а строки Object Pascal. Кроме того, многие свойства компонентов представлены множествами этого языка. Для моделирования таких объектов C++Builder реализует классы AnsiString и Set. В этом приложении мы даем краткое описание этих классов.

Класс AnsiString

Класс AnsiString определяется в заголовке dstring.h. Конструктор этого класса перегружен, так что строки можно инициализировать различными способами — другой строкой, строкой С (указателем на char), а также целыми типами. В последнем случае строка будет содержать текстовое представление числа.

Методы

Мы не будем приводить здесь полное описание всех методов AnsiString, а расскажем только о важнейших.

  • int _fastcall AnsiCompare(const AnsiStringfi rhs) const.; Этот метод производит сравнение строки с указанной строкой. Сравнение производится в соответствии с текущим локалом Windows, т. е., например, в русифицированной системе сравнение строк с русским текстом должно давать правильный результат. (Честно говоря, я этого не проверял.) Соответственно результат сравнения может отличаться от результата операций сравнения, которые производятся исходя из ASCII-значений символов.
  • int _fastcall AnsiPos(const AnsiStringS subStr) const; Метод возвращает позицию указанной подстроки. Первому символу строки соответствует 1. Нулевой результат означает, что подстрока не найдена.
  • char* _fastcall с str() const; Возвращает указатель на ограниченную нулем строку С (символьный массив). Если строке ничего не присвоено, возвращается указатель на пустую строку.
  • const void* fastcall data() const { return Data; } Аналогичен предыдущему, но возвращает нулевой указатель в случае неприсвоенной строки.
  • AnsiString& _fastcall Delete(int index, int count); Удаляет заданное число символов начиная с указанной позиции. Позиция 1 соответствует первому символу строки.
  • enum TStringFloatFormat

(sffGeneral, sffExponent, sffFixed,

sffMumber, sffCurrency };

static AnsiString _fastcall FloatToStrF(long double value, TStringFloatFormat format, int precision, int digits) ;

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

  • AnsiString& _fastcall Insert(const AnsiString& sir, int index) ; Вставляет строку в указанной позиции (1 — перед первым символом). Если индекс меньше 1, он считается единицей.
  • static AnsiString fastcall IntToHex(int value,int digits) ; Преобразует число в строку с представлением его в виде щестнадцате-ричных цифр. Второй параметр указывает минимальное число цифр. Аналогичен Pos, но допускает многобайтовые символы.
  • bool _fastcall IsDelimiter (const AnsiString& delimiters, int index) const; Проверяет, входит ли символ в позиции index в указанную строку delimiters.
  • bool _fastcall IsEmptyO const; Возвращает true, если строка пустая.
  • int _fastcall LastDelimiter (const AnsiString& delimiters) const; Возвращает позицию последнего из символов строки, входящих в указанную строку ограничителей.
  • int _fastcall Length() const; Возвращает число байтов в строке.
  • static AnsiString fastcall LoadStr(int ident) ; Загружает строку строковым ресурсом исполняемого файла. Если ресурса с указанным идентификатором не существует, возвращается пустая строка.
  • AnsiString _fastcall Lowercase() const; Возвращает строку, преобразованную в нижний регистр в соответствии С Текущим ЛОКалом Windowa. He хюмвняет исходную строку.
  • int _fastcall Pos(const AnsiString& subStr) const; Возвращает позицию указанной подстроки.
  • int _cdecl printf(const char* format, ...); Формирует строку в соответствии со стандартным форматом С. Возвращает длину строки-результата.
  • AnsiString& _fastcall SetLength(int newLength) ; Устанавливает новую длину строки, перераспределяя память под ее символьный массив. В новый массив копируется исходное содержимое строки. Если новая длина меньше исходной, строка усекается.
  • AnsiString& cdecl sprintf(const char* format, ...); To же, что и printf (), но возвращается ссылка на модифицированную строку (*this).
  • StringOfChar
  • AnsiString fastcall Substring(int index, int count) const; Возвращает подстроку с указанной позицией и длиной.
  • AnsiString _fastcall Substring(int index, int count) const; Преобразует строку в число двойной точности. Если строка не содержит корректного представления числа с плавающей точкой, выбрасывается исключение EConvertError.
  • int _fastcall Tolnt() const; Преобразует строку в целое. Если строка не содержит корректного представления числа, выбрасывается исключение EConvertError.
  • int _fastcall ToIntDef(int defaultValue) const; Преобразует строку в целое. Если строка не содержит корректного представления числа, возвращается указанное значение по умолчанию.
  • AnsiString fastcall Trim() const; Возвращает строку с удаленными начальными и конечными пробелами и управляющими символами.
  • AnsiString fastcall TrimLeftO const; Возвращает строку с удаленными начальными пробелами и управляющими символами.
  • AnsiString _fastcall TrimRight() const; Возвращает строку с удаленными конечными пробелами и управляющими символами.
  • Unique
  • AnsiString _fastcall Uppercase() const;

Возвращает строку, преобразованную в верхний регистр в соответствии с текущим локалом Windows. He изменяет исходную строку. В классе AnsiString реализованы операции присваивания, сложения (возвращает конкатенацию строк), индексации (возвращает символ с указанным индексом) и отношений (сравниваются ASCII-значения входящих В строку символов).

Если перед dstring.h включается заголовок iuatrcam кла директива

#define vcl_iostream

то строки можно использовать в операторах ввода/вывода потоков C++ с операциями “ и ”.

Класс Set

Set — это шаблон, определенный в заголовке sysset.h:

tempiate<class T, unsigned char minEl, unsigned char maxEl>

class _declspec(delphireturn) Set;

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

Set<int, 0, 31> a5et0f32;

typedef Set<char, 'A', 'Z'> SetOfUppercase;

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

Множества можно складывать, умножать (объединение и пересечение) и вычитать (пересечение c дополнением). Операции “ и ” соответственно вводят или удаляют элемент из множества. Эти же операции могут использоваться для извлечения и передачи множеств в поток (в виде последовательностей нулей и единиц), если перед sysset.h включается заголовок iostream или директива

#define VCL_IOSTREAM

Множества Set имеют два метода:

  • Sets _fastcall Clear (); Удаляет из множества все элементы.
  • boo1 _fastcall Contains(const T el) const; Возвращает true, если множество содержит указанный элемент.