C++ Программирование в среде С++ Builder 5
Немного теории и терминологии
Что собой представляет персональный компьютер? Если говорить о его устройстве, то в нем имеется центральное процессорное устройство (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.
О библиотеках
Выше мы уже упоминали о библиотеках, не объясняя, впрочем, что они собой представляют. Конкретнее, мы имели в виду библиотеки объектных модулей; такая библиотека является просто собранием модулей объектного кода, скомпонованных в одном файле с расширением lib, своего рода архивом. На рис. 1.1 показана гипотетическая библиотека пользователя Addon.lib. Она могла бы быть создана из нескольких объектных модулей путем обработки их библиотекарем tlib32.exe.

На рисунке показаны также код запуска и исполнительная библиотека языка C/C++. Это необходимые элементы компоновки любой программы на С. Код запуска исполняется перед тем, как управление будет передано на входную точку вашей программы (функцию main, WinMain и т. п.). Среди задач, им выполняемых, можно указать следующие:
Исполнительная библиотека содержит разнообразные процедуры общего назначения, которые вы можете вызывать в своей программе. В частности, библиотека обеспечивает:
Что касается динамически присоединяемых библиотек — DLL, — то во многих случаях они, с точки зрения программиста, мало чем отличаются от обычных, статически компонуемых библиотек объектных модулей. Функция, которая находится в DLL, вызывается так же, как и всякая другая функция. Правда, “снаружи” динамическая библиотека выглядит скорее как исполняемый файл; эти библиотеки создаются не библиотекарем, а компоновщиком. Вообще-то вопросы разработки и использования DLL выходят за рамки этой книги, но в следующей главе мы приведем пример простейшей DLL, чтобы читатель получил представление о том, как это делается в C++Builder.
Каждая библиотека, как правило, сопровождается своим заголовочным файлом (их может быть и несколько), который определяет интерфейс ее функций и других элементов. Исходный код библиотеки может быть и недоступен для программиста. Но в ее заголовочном файле (или файлах) имеется все необходимое для использования библиотеки в прикладной программе.
Заключение
В этой очень короткой главе мы попытались обрисовать “классический” процесс создания прикладной программы на языке C/C++. По сути наше изложение было ориентировано на то, чтобы дать читателю представление о терминологии, которую мы будем постоянно применять, и создать тем самым некоторый первоначальный контекст, для дальнейшего обсуждения. То, что осталось непонятным, будет проясняться в дальнейшем.
Проблема раздельной компиляции
Когда-то давно программа для машины вроде БЭСМ-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
#include "myfile.h"
Препроцессор заменяет директиву #include содержимым указанного файла; после завершения препроцессорной обработки полученный текст передается компилятору, который транслирует его в объектный код.

Во многих системах программирования на C/C++ препроцессор составляет единое целое с компилятором (это верно и для C++Builder). Тем самым ускоряется компиляция исходных файлов, и нет необходимости создавать промежуточный файл, содержащий обработанный препроцессором код. Однако в C++Builder имеется отдельный препроцессор срр32.ехе, запускаемый из командной строки. Вы можете им воспользоваться, если нужно просмотреть текст, получаемый в результате препроцессорной обработки; это может быть полезно, например, при отладке макросов препроцессора. Директивы препроцессора будут подробно рассматриваться в 4-й главе.
Один и тот же заголовок можно подключать ко многим исходным файлам. Таким образом, отдельно компилируемые модули получают доступ к необходимой информации о процедурах и прочих элементах программы, определяемых в других модулях. Кроме того, это гарантирует, что размещенное в заголовочном файле объявление типа или функции будет одним и тем же во всех компилируемых файлах, и любые внесенные в него изменения будут автоматически воздействовать на все модули программы.
Процесс построения программы
В этом разделе мы опишем “классический” процесс подготовки и трансляции программы на языке высокого уровня (в нашем случае это C++') в исполняемый файл, содержащий машинные инструкции и все остальное, что необходимо для работающей программы системы Windows. В C++Builder, как мы увидим в дальнейшем, детали этого процесса в основном скрыты от программиста и, кроме того, имеются дополнительные моменты, обусловленные спецификой визуального подхода к программированию. Создание программы на языке C++ выглядит примерно так. Прежде всего, программист с помощью того или иного текстового редактора готовит файлы исходного кода на C/C++. После этого происходит построение программы, в котором можно выделить такие этапы:
Компоновку ресурсов (ресурсы включают в себя битовые матрицы, курсоры, строковые таблицы, пиктограммы и т.п.). Это завершающий этап, на котором формируется конечный ехе-файл, запускаемый на выполнение. Этот процесс иллюстрируется рис. 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) сразу выполняют компоновку объектных файлов с ресурсами, совмещая два последних этапа. Что касается первого шага — компиляции — то здесь возникает одна проблема, которую стоит обсудить.
Hello World — консольное приложение
Консольное приложение Windows похоже на программу DOS, но только внешне. Оно работает в “окне MS-DOS”, которое на самом деле в 32-битных системах Windows таковым не является. Консольное приложение — 32-битное, и запустить его в обычной 16-битной DOS невозможно. Однако, подобно примитивной программе DOS, оно ориентировано на символьный ввод-вывод, что делает консольные приложения полезными при изучении стандартных функций ввода-вывода языка С и классов стандартных потоков C++.
Чтобы создать в C++Builder консольное приложение, выполните следующие действия:

Рис. 2.4 Диалог New Items
#pragma hdrstop
#include
//--------------------------
#pragma argsused
int main(int argc, char* argv[ ])
{
return 0;
}
#pragma hdrstop
#include
#include
#include
//------------------------------------------
#pragma argsused int main(int argc, char* argv[])
{
printf("Hello World from Console!\n"), printf("Press any key...");
getch() ;
return 0;
}
Вот и все — консольное приложение готово и работает. Для этого вам потребовалось ввести вручную пять строк кода на С. Первые две из них — директивы ^include, включающие в исходный код два заголовочных файла стандартной библиотеки. Файл stdio.h содержит прототипы общеупотребительных функций буферизованного ввода-вывода (мы здесь использовали функцию prinf() — форматируемый вывод на консоль). Файл соnio.h необходим потому, что для ожидания нажатия клавиши мы применили низкоуровневую функцию ввода символа getch() ; символ, возвращаемый функцией, мы игнорировали.

Рис. 2.5 Окно программы HWConsole.exe
Функция main () присутствует (иногда неявно) в каждой программе C/C++ и является ее входной точкой. Именно этой функции передается управление после загрузки и инициализации программы.
Директивы препроцессора #pragma мы будем обсуждать в 4-й главе. Пока скажем, что они здесь не имеют принципиального значения и их можно было бы удалить. Но это привело бы к выдаче предупреждений при компиляции, а в некоторых случаях замедлило ее.
Строки, начинающиеся с двойной дробной черты, содержат комментарии и не принимаются во внимание при компиляции файла.

Написанная программа является по сути программой на языке С. В 5-й версии компилятора, если вы хотите написать “честную” программу на С и компилировать ее как такую, проблем никаких — исходный язык задается прямо в панели консольного мастера. В 4-й версии дела обстоят несколько сложнее. Дело в том, что в C++Builder 4 главный исходный файл консольного приложения должен обязательно быть срр-файлом и определять функцию main. Если же вы хотите использовать в качестве главного файла программы именно с-файл, придется пойти на некоторые ухищрения. Сделайте следующее:
#pragma hdrstop
#include
#define main
//---------------------------------
#pragma hdrstop
#include
#include
//----------------------------
main ()
{
printf("Hello World - С main file.\n");
getch();
return 0;
}
Еще раз подчеркнем, что сказанное относится только к 4-й версии. C++Builder 5 проделывает практически то же самое, но автоматически.
Теперь мы покажем, как вывести на экран сакраментальное “Hello World”, используя графический интерфейс системы Windows. Другими словами, мы применим технологию визуального программирования, реализованную в C++Builder.
Hello World — приложение GUI
Процедура создания приложения с графическим интерфейсом пользователя даже проще, чем в случае консольного приложения, поскольку C++Builder предназначен именно для этого. Выполните такие действия:
Теперь разместите на форме необходимые компоненты — две командных кнопки и метку, в которой будет отображаться требуемый текст.
Вы наверняка заметили, что при перемещении компонента или изменении размеров положение его фиксируется узлами сетки, показанной на форме маленькими точками. Шаг сетки можно изменять или вообще отменить .привязку к сетке.

Рис. 2.6 Форма программы в процессе проектирования
К данному моменту мы практически завершили то, что называют этапом визуального проектирования программы. Теперь нужно написать программный код, который будет решать требуемую задачу, в данном случае — вывод (по команде) на экран строки текста “Hello World”.
void _fastcall TFormI::ButtonlClick(TObject *Sender) {
Labell->Caption = "Hello World from GUI!";
}
void _fastcall TFormI::Button2Click(TObject *Sender) {
Forml->Close () ;
}
Вот и все. Нажмите кнопку Message. Будет выведена строка сообщения. Кнопка Exit завершает работу программы, закрывая ее главное окно (его можно закрыть и системной кнопкой в правом верхнем углу — это то же самое). Ниже показана запущенная программа.

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

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

Рис. 2.8 Console Wizard 4-й версии с установками для оконного приложения и 5-й версии для консольного приложения на С
#include
#pragma hdrstop
#include
//--------------------------------
#pragma argsused
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
MessageBox(NULL, "Hello World from Windows!",
"Simple Greeting", MB_OK | MB_ICONEXCLAMATION);
return 0;
}

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

Эта панель отображается функцией API MessageBox (), аргументы которой задают текст сообщения, заголовок панели и ее вид (тип кнопок и значок). Главной функцией программы Windows является, как видите, не main (), а WinMain () .
Приведенные примеры показывают, каким образом в C++Builder можно экспериментировать с различными языковыми конструкциями, которые мы будем изучать. Можно написать “честное” (консольное) приложение C/C++, пользуясь в качестве интерактивных средств функциями ввода с клавиатуры и символьного вывода в окно MS-DOS. Либо с тем же (и даже большим) успехом можно воспользоваться методикой визуального программирования и общаться с тестовым приложением в рамках GUI Windows. Правда, получаемые таким способом программы не являются в строгом смысле программами на C++, поскольку язык визуально-компонентной модели содержит некоторые расширения; однако в плане изучения стандартных элементов языка это несущественно. Существенно то, что стандарт ANSI C++ поддерживается в полном объеме.
Напоследок мы продемонстрируем пример группы проектов C++Builder, т. е. совместного создания нескольких целевых модулей. Мы построим простейшую DLL и тестирующее ее приложение.
Hello World
По традиции изучение любого языка программирования начинается с того, что новичку предлагают написать программу, которая выводит на экран слова “Hello World”. Мы не будем отступать от этой традиции и покажем, как реализовать такую программу в C++Builder. Мы составим даже две программы; одну — с выводом на консоль, а другую — с графическим интерфейсом пользователя (GUI), т. е. настоящее приложение Windows.
Интегрированная среда разработки C++Builder
На рис. 2.1 показан C++Builder сразу после запуска. То, что вы видите — это интегрированная среда разработки (IDE), включающая в себя четыре основных элемента. Наверху находится главное окно. Оно содержит обычную линейку меню, инструментальную панель (слева) и палитру компонентов (многостраничная панель справа).

Рис. 2.1 C++Builder 5 с пустой формой
Правее инспектора объектов располагается конструктор форм. При запуске C++Builder конструктор отображает пустую форму. Форма — это центральный элемент визуального программирования. Она может представлять главное окно программы, дочернее окно, диалоговую панель. На ней вы размещаете различные элементы управления (типичный и самый распространенный — командная кнопка), называемые визуальными компонентами. Существуют также и невизуальные компоненты, например, таймеры и компоненты связи с базами данных. В инспекторе объектов вы сопоставляете событиям компонентов написанные вами процедуры обработки. Это, по существу, и есть визуальное программирование, базирующееся на компонентной модели. Подробнее мы расскажем о нем в части III этой книги.
Наконец, под конструктором форм находится окно редактора кода (на рисунке оно почти полностью закрыто формой). О нем мы поговорим отдельно.
Пример DLL
В следующем примере мы покажем, как присоединить к программе функцию из библиотеки. Причем библиотека эта будет не простая, а динамическая. Наиболее существенной особенностью динамических библиотек (DLL) является то, что содержащиеся в них процедуры, функции и ресурсы могут совместно использоваться несколькими приложениями. Это экономит, во-первых, место на диске (поскольку исключается дублирование кода), а во-вторых, оперативную память, когда в нее одновременно загружено несколько программ (по той же самой причине).
Последовательность действий в этом примере будет несколько сложнее, чем в предыдущих, так как мы здесь будем создавать сразу два программных модуля, связанных друг с другом: библиотеку и исполняемый модуль, который ее вызывает. Предлагаю вам проделать следующее:
Как я уже, кажется, говорил, желательно сохранять файлы каждого проекта в отдельном пустом каталоге.
Файл теперь должен иметь такой вид:
#include
#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() ;
}
#include
#include
#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;
}
Рядом с пунктом контекстного меню Make (собрать) имеется пункт Build (построить). Если вы откроете Project в главном меню, то увидите там эквивалентные пункты Make SHello и Build SHello. Обе команды выполняют построение текущего проекта (его целевого файла). Разница в том, что Build компилирует и компонует заново все входящие в проект файлы, в то время как Make производит те или иные действия лишь при необходимости, например, при модификации кода в одном исходном файле будет компилироваться только этот файл, после чего будет выполнена компоновка программы.

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

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

То, что мы сейчас проделали, называется статической загрузкой динамической библиотеки. Суть ее в том, что DLL (если она еще не загружена) автоматически загружается в память при запуске главной программы. Библиотека импорта необходима именно для статической загрузки, при которой все ссылки на DLL разрешаются еще на этапе компоновки.
Возможна и динамическая загрузка DLL, которая, однако, выполняется вручную, т. е. специально написанным для этого программным кодом; достоинство ее в том, что она может производиться лишь при необходимости. При этом разрешение ссылок на DLL происходит уже во время выполнения, — другими словами, программе приходится “на ходу” как-то искать нужную функцию в DLL (например, по имени).
Вот, собственно, и все о DLL. Хотя динамические библиотеки (да и всякие другие) выходят за рамки тематики этой книги и в дальнейшем изложении нам встречаться больше не будут, нам показалось полезным дать читателю некоторое представление о том, как создание и использование этих библиотек выглядит в C++Builder.
Заключение
В этой главе мы показали типичные действия, выполняемые при разработке приложений в интегрированной среде C++Builder. Конечно, представленные здесь программы совершенно ничтожны, однако они работают, а заставить что-то заработать в Windows — это не шутка. В следующих главах мы переходим уже собственно к вопросам программирования и изучению различных конструкций языка C/C++.
Редактор кода
Окно редактора кода, показанное на рис. 2.2, является основным рабочим инструментом программиста. Его функции не ограничиваются редактированием исходного текста программы.
Нужно сказать, что почти все инструментальные окна C++Builder являются стыкуемыми окнами. Вы, наверное, не раз встречались в различных программах со стыкуемыми инструментальными панелями. Их масса в Microsoft Word, в котором я пишу этот текст. Такая панель может быть плавающей, а может быть состыкована с главным окном программы. В этом случае она становится обычной инструментальной линейкой.
При первоначальном запуске C++Builder к левой стороне редактора кода пристыковано окно обозревателя классов. Это не просто инструмент просмотра структуры классов. При некотором навыке можно использовать его с большой выгодой, поскольку он позволяет автоматически вводить в описание класса новые элементы (функции, данные и свойства).

Рис. 2.2 Окно редактора кода с обозревателем классов
Инструментальное окно C++Builder может быть состыковано с другим окном в одном из пяти его портов стыковки: либо вдоль какой-либо стороны окна, либо по центру. Если стыковка производится вдоль стороны окна, получается что-нибудь подобное показанному на рис. 2.2. В случае стыковки по центру окно становится многостраничным, с закладками, позволяющими переключаться между страницами.
Советую вам поэкспериментировать со стыковкой окон. Сделайте, например, такое упражнение:
В редакторе можно открывать сразу несколько файлов исходного кода. При этом он также становится многостраничным окном с закладками. Надписи на закладках отражают имена файлов.
Рис. 2.3 показывает окно редактора кода с двумя открытыми файлами и пятью пристыкованными инструментальными окнами.
Может показаться, что все эти экзерсисы со стыковкой не более чем игра. Отнюдь нет. В процессе работы над программой, особенно во время отладки, вам понадобятся три-четыре вспомогательных окна, а может быть, и больше. Это небольшие окна, и они легко могут потеряться за более крупными. Если же все необходимые окна будут сосредоточены в одном месте, получить доступ к нужному инструменту очень просто. Любой из них находится, как говорят, “на расстоянии двух щелчков мыши” от вас.

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

Рис. 2.3 Редактор кода с пристыкованными окнами различных инструментов
Ну а теперь лучше всего перейти, наконец, прямо к делу и написать пару работающих программ на С.
Блоки и локальные переменные
Поскольку при описании управляющих конструкций мы попутно ввели понятие блока, нужно сделать одно уточнение касательно объявлений и области действия локальных переменных. На самом деле локальные переменные могут объявляться не только в начале тела функции, но и в любом другом блоке (if, while и т. д.). Областью действия переменной является блок, в котором она объявлена; она скрывает любую переменную с тем же именем, объявленную вне данного блока. За пределами блока переменная недоступна.
Циклы
В языке С структуры многократного повторения реализуются тремя разновидностями операторов цикла. Это циклы 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. Глубина рекурсии ограничивается только размером стека программы.
Факториал, конечно, вообще никто никогда не вычисляет, во всяком случае, подобным образом. Однако существуют задачи, для которых метод рекурсии оказывается естественным решением и реализуется намного проще, чем это можно
было бы сделать с помощью циклов. Таковы, прежде всего/ различные операции над древовидными структурами данных.
Не все языки программирования допускают рекурсию. Мы напомнили о ней просто для того, чтобы подчеркнуть, что С — один из тех языков, в которых рекурсия возможна.
Директивы #include
Директива # include заменяется препроцессором на содержимое указанного в ней файла. Обычно это заголовочные файлы с расширением .h. Они содержат информацию, обеспечивающую раздельную компиляцию файлов исходного кода и корректное подключение различных библиотек. Имя файла может быть заключено либо в угловые скобки (о, меньше—больше), либо в обычные двойные кавычки (""). Эти случаи различаются порядком поиска включаемых файлов; если использованы угловые скобки, поиск будет сначала производиться в стандартных каталогах C++Builder, если кавычки — в текущем каталоге.
Директивы # pragma
Строки исходного кода, начинающиеся со знака #, являются, как правило, директивами препроцессора, т. е. управляют обработкой текста программы еще до его передачи собственно компилятору (сюда относятся текстовые подстановки, вставка содержимого других файлов и некоторые специальные операции). Директивы #pragma в этом смысле являются исключением, поскольку они адресованы непосредственно компилятору и служат для передачи ему различных указаний. Например, #pragma argsused говорит компилятору, что следует подавить выдачу предупреждающего сообщения о том, что параметры функции main () никак в ней не используются.

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

Мы уже не раз пользовались подобной формой описания синтаксиса. Это нечто вроде метаязыка (первый из них назывался формой Бэкуса-Наура), которые широко используются для формального определения языков программирования. Надеемся, что смысл написанного достаточно ясен. Курсивом_без_ пробелов обозначаются синтаксические элементы, имеющие самостоятельное значение. Например, список_параметров является отдельной синтаксической единицей, хотя он обладает собственной внутренней структурой. Можно было бы раскрыть его определение примерно так:
Список__параметров:
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 () и другими функциями ввода-вывода стандартной библиотеки С.
Функция main()
После всех директив в программе расположено определение функции па in () . Как уже говорилось, в строгом смысле любая программа на С содержит эту функцию, которая является ее входной точкой. Однако в среде Windows вместо main () часто используется WinMain () .
Функция main () — это, конечно, частный случай функции вообще. Функции являются основными “строительными блоками” программы, или подпрограммами. Они, в свою очередь, строятся из операторов, составляющих тело функции. Каждый оператор оканчивается точкой с запятой (;). В общем виде функция определяется таким образом:
Возвращаемый_ тип_ имя функции(список_ параметров)
{
// В фигурных скобках заключено тело функции,
// составленное из отдельных операторов.
тело_функции
}

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

Параметры функции main () служат для передачи программе аргументов командной строки, т. е. имен файлов, ключей, опций и вообще всего, что вы вводите с клавиатуры после подсказки DOS, запуская программу. Конечно, программа не обязана воспринимать какие-либо команды, указываемые в строке запуска, однако в любом случае функции main () передаются два параметра — число аргументов/включая имя, под которым запущена программа (argc), и массив указателей (argv) на отдельные аргументы (выделенные элементы командной строки). Забегая вперед, приведем пример, который распечатывает по отдельности все “аргументы” строки, введенной пользователем при запуске:
#include
int main(int argc, char *argv[])
{
int i;
for (i=0; i
return 0;
}
Вы сможете вернуться к этому примеру, когда мы изучим массивы, строки и циклы. Теперь мы займемся более последовательным изучением основ языка С.
Элементы простой программы
Давайте немного поближе познакомимся со строением консольной программы Hello World, написанной в предыдущей главе:
#pragma hdrstop
#include
#include
#include
//---------------------------------------------
#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(); // Ожидание нажатия клавиши.
В данном случае комментарий размещен в конце строки и поясняет смысл расположенного в ней оператора.
Литералы
Прежде всего, данные могут присутствовать непосредственно в тексте программы. В этом случае они представляются в виде литеральных констант. Эти литералы могут быть числовыми, символьными и строковыми. В программе 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

При обработке исходного кода препроцессором выполняется просто текстовая подстановка: каждое вхождение имени макроса заменяется соответствующим ему литералом. Макросы называют также символическими константами (не путайте с символьными).
Массивы и указатели
Массивы и указатели довольно тесно связаны между собой. Имя массива можно разыменовывать, как указатель. В свою очередь, указатель можно индексировать, как массив, если это имеет смысл. Поэтому мы рассматриваем массивы и указатели в одном разделе.
Массивы
Массив по существу является совокупностью однотипных переменных (элементов массива), объединенных под одним именем и различающихся своими индексами. Массив объявляется подобно простой переменной, но после имени массива указывается число его элементов в квадратных скобках:
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
#include
#include
/********************************************************
** Процедура сортировки
*/
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 Робота программы сортировки
Объединения
Объединения, определяемые с помощью ключевого слова union, похожи по своему виду на структуры:
union этикетка {список_элементов] [переменные];
Отличие состоит в том, что все элементы объединения занимают одно и то же место в памяти, они перекрываются. Компилятор отводит под объединение память, достаточную для размещения наибольшего элемента.

Объединения полезны, когда требуется обеспечить своего рода “полиморфное поведение” некоторого объекта. Например, вы хотите определить тип, реализующий представление различных геометрических фигур — прямоугольников, окружностей, линий, многоугольников. В зависимости от того, чем конкретно является данная фигура, для ее описания необходимы различные наборы значений. Круг описывается иначе, чем многоугольник и т. п. И вот из структур, описывающих различные фигуры, можно в свою очередь составить обобщенный тип-объединение, который будет обрабатываться различно в зависимости от значения специального поля, определяющего род фигуры. В то же время на все фигуры можно будет ссылаться через указатели одного и того же типа, что, в частности, позволит составлять динамические связанные списки из любых фигур.
Доступ к элементам объединения (их иногда называют разделами) осуществляется так же, как и в структурах, — посредством точки или стрелки, за которыми следует имя раздела.
В заключение разговора о типах, определяемых пользователем, приведем пример законченной программы. В ней определяется тип структуры, способной хранить данные различных “графических объектов”. В программе реализованы всего два их вида — прямоугольник и текстовая метка.
Листинг 3.5. Демонстрация работы со структурами
/*
** Struct.с: Структуры и объединения.
*/
#pragma hdrstop
#include
#include
#include
/* Тип для определения вида объекта */
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. Такие расширения, связанные в основном с особенностями процессора и операционной системы, почти всегда имеются в любой реализации языка.
О них также будет говориться в следующей главе.
Область действия переменных и связанные с ней понятия
Теперь, когда мы более-менее разобрались с принципами функциональной организации программы, следует обсудить некоторые весьма важные вопросы относительно переменных.
Переменные в С могут быть локальными и глобальными, статическими и автоматическими, регистровыми, внешними и даже нестабильными. Они различаются своей областью действия, видимостью и временем жизни. Попробуем как-то сориентироваться во всем этом многообразии.
Область действия
Область действия — это та часть программы, где переменная в принципе доступна для программного кйда (что означает это “в принципе”, выяснится чуть позже). По своей области действия переменные делятся на локальные и глобальные.
Локальные переменные объявляются внутри функции и вне ее тела недоступны. Вернитесь к последнему примеру. Там программа состоит из двух функций. В main () объявляются переменные number, s и name (две последних — не простые переменные, а массивы, но это несущественно). В функции Convert объявлены grp1, grp2 и grp3.
Все эти переменные являются локальными. К каждой из них можно обращаться только в пределах объявляющей ее функции. Поэтому, кстати, имена локальных переменных не обязаны быть уникальными. Если две функции описывают переменную с одним и тем же именем, то это две совершенно различные переменные и никакой неоднозначности не возникает.

Параметры в определении функции (формальные параметры) можно рассматривать как локальные переменные, инициализируемые значениями аргументов при ее вызове.
В противоположность локальным глобальные переменные не относятся ни к какой функции и объявляются совершенно независимо. В пределах текущего модуля имя глобальной переменной, естественно, должно быть уникальным. Областью действия глобальных переменных является по умолчанию вся программа.
Довольно интересная проблема возникает, казалось бы, когда имя локальной переменной функции совпадает с именем некоторой глобальной переменной. Это вполне допустимая ситуация, и одноименные переменные здесь на самом деле различны. Если мы входим внутрь определения функции, то оказываемся в области действия сразу двух переменных. Однако локальная переменная в этом случае скрывает, как говорят, глобальную переменную с тем же именем. Тут речь идет о их области видимости, которая не совпадает с областью действия. Эти два понятия часто путают. Вот пример кода, иллюстрирующий вышесказанное.
/***************************************************
** Область действия и видимость переменных.
*/
#include
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, в С имеются и другие модификаторы, применяемые к объявлениям переменных. Опишем их вкратце.
Модификатор volatile
Об этом модификаторе следует сказать отдельно. Он применяется для объявления переменных, которые можно назвать нестабильными. Модификатор volatile сообщает компилятору, что значение переменной может изменяться как бы само по себе, например, некоторым фоновым процессом или аппаратно. Поэтому компилятор не должен пытаться как-либо оптимизировать выражения, в которые входит переменная, — предполагая, например, что ее значение не менялось с предыдущего раза и потому выражение не нужно заново пересчитывать.
Есть и другой момент. В программе могут быть так называемые критические участки кода, во время исполнения которых изменение значения нестабильной переменной приведет к абсурдным результатам. (Возьмите случай оценки “А или не-А”, если А нестабильно!) Для таких критических участков компилятор должен создавать копию, например, в регистре, и пользоваться этой копией, а не самой переменной.

Можно написать такое объявление:
volatile const int vciVar = 10;
Другими словами, “нестабильная константа” типа int. В этом нет никакого противоречия — компилятор не позволит вашей программе изменять переменную, но и не будет предполагать ее значение известным априори, так как оно может меняться в силу внешних причин.
Обзор языка С
Как уже говорилось, хорошее понимание языка С представляется необходимым для успешного программирования на C++. C++Builder в полной мере поддерживает стандарт ANSI С и, кроме того, некоторые другие версии языка (Керниган & Ричи, Unix V). Мы в основном будем ориентироваться на стандарт ANSI, который, кстати, является в какой-то мере результатом “обратного воздействия” языка C++.
В этой главе мы, опираясь на короткие примеры, расскажем об элементах программы на С и синтаксисе различных его конструкций.
Операции и выражения
Как все знают, из переменных, функций и констант в алгебре можно составлять формулы. Точно так же и в языке 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 | слева направо |
Оператор выбора 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
#include
#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.
Операторы прерывания блока
Часто бывает необходимо “досрочно” выйти из некоторого цикла, до того, как будет удовлетворено условие его завершения (говоря точнее, до того, как условие продолжения станет ложным). Например, вы просматриваете массив на предмет поиска заданного значения. Как только нужный элемент массива найден, выполнять цикл далее нет необходимости. Для досрочного завершения циклов в С применяются операторы break, return и continue. С оператором break вы уже встречались — помимо циклов, он используется в блоках switch.

Разумеется, операторы прерывания циклов должны выполняться условно, т. е. должны входить в блок if или else некоторого условного оператора и исполняться только при наступлении условий досрочного завершения цикла.
Эти два оператора эквивалентны следующим конструкциям с goto:
// Эквивалент break:
while (...) {
…
goto brkLabel;
…
} // Закрывающая скобка блока. brkLabel:
// Метка следующего за блоком оператора.
// Эквивалент continue:
while (...) (
…
goto cntLabel;
…
CntLabel:; // Пустой помеченный оператор.
} // Закрывающая скобка блока.

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

Если функция “возвращает” тип void, выражение опускается. Если код функции при ее вызове не исполняет ни одного оператора return, подразумевается, что return присутствует в качестве последнего оператора тела функции. Возвращаемое значение при этом не определено.
Не следует ставить операторы return где попало. Вернитесь и посмотрите на листинг 3.3. Это пример того, как не надо программировать. По правилам “хорошего тона” оператор возврата должен быть только один (от силы два) и он должен располагаться в конце тела функции.
Перечислимые типы
Ключевое слово 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. Это обычная практика.)
Переименование типов
Любому типу в С можно присвоить простое имя или переименовать его. Это делается с помощью ключевого слова 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 является просто средством упрощения записи операторов объявления переменных.
Переменные
Итак, отдельная единица данных должна обязательно иметь определенный тип. Для ее хранения во время работы программы мы должны, во-первых, отвести соответствующее место в памяти, а во-вторых, идентифицировать ее, присвоив некоторое имя. Именованная единица памяти для-хранения данных называется переменной.
Переменные создаются с помощью оператора объявления переменных, в котором указывается тип, имена переменных и (при необходимости) начальные значения, которыми переменные инициализируются. Вот несколько примеров:
short i;
// Объявление короткой целой
// переменной.
char quit = 'Q';
// Инициализация символьной
// переменной.
float fl, factor = 3.0, f2;
// Три переменных типа float,
// одна из которых
// инициализируется.
Синтаксис оператора объявления можно описать примерно так:
тип имя_переменной [= инициализирующее_значение][, ...];
Как и любой другой оператор С, он оканчивается точкой с запятой.

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

Объявление переменной должно предшествовать ее использованию в программе. Обычно все объявления размещают в начале тела функции или блока, до всех исполняемых операторов.
Представление данных в С
Любая программа так или иначе обрабатывает данные. Наша маленькая программа обрабатывает свои данные — строку сообщения “Hello World”, выводя ее на экран. Рассмотрим, какие возможны варианты представления информации в С.
Пример функции
Теперь мы напишем небольшую программу, которая проиллюстрирует все существенные моменты создания функции; в программе применяются некоторые из функций для работы со строками, описанных выше.
Пользователю предлагается ввести имя (в произвольной форме — только имя, имя и фамилию и т. п.), а затем номер телефона, просто как 7-значное число без пробелов или дефисов. После этого программа распечатывает полученные данные, выводя номер телефона в более привычном формате (рис. 3.2).
Листинг 3.2. Пример создания функции
/*
** Convert.с: Пример функции, преобразующей число
** в строку специального вида.
*/
#pragma hdrstop
#include
#include
#include
/* Прототип функции */
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 () .

Подытожим некоторые правила относительно прототипов и определений функций:
Семантика операций
Несколько слов об операциях, перечисленных в таблице (всего их получилось что-то около пятидесяти). Смысл некоторых из них будет проясняться в дальнейшем при изучении массивов, структур и указателей; здесь же мы вкратце расскажем об операциях, относящихся в основном к арифметике.
Арифметические операции
К арифметическим мы отнесем те операции, которые перечислены в таблице под рубриками “Мультипликативные” и “Аддитивные”. Нужно сказать, что только эти операции (да и то за исключением взятия по модулю) имеет смысл применять к вещественным операндам (типам 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.
Операция-запятая применяется довольно редко, обычно только в управляющих выражениях циклов.
Структуры
Массивы позволяют обращаться с набором логически связанных однотипных элементов как с единым целым. Если же требуется хранить набор разнородных, но логически связанных данных, описывающих, например, состояние некоторого объекта реального мира, используются структуры. Синтаксис структуры имеет такой вид:
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. Доступ к битовым полям осуществляется так же, как и к регулярным элементам структуры. Если имя_поля отсутствует, место под поле отводится, но оно остается недоступным. Это будут просто “заполняющие” биты.

Битовые поля применяются либо там, где необходима плотная упаковка информации (как это бывает при передаче функции некоторого набора логических флагов), либо, например, для отображения регистров внешнего устройства, которые часто бывают организованы как совокупность небольших полей и отдельных битов.
Типизированные константы
Разновидностью переменных являются типизированные константы. Это переменные, значение которых (заданное при инициализации) нельзя изменить. Создание типизированной константы ничем не отличается от инициализации переменной, за исключением того, что перед оператором объявления ставится ключевое слово const:
const тип имя_константы = значение [, ...];
Например:
const double Pi = 3.14159265;
Ранее мы демонстрировали определение символической константы:
#define PI 3.14159265

Чем этот макрос отличается от показанной выше типизированной константы? Здесь следует иметь в виду два момента. Во-первых, типизированная константа по своему смыслу относится к конкретному типу данных, поэтому компилятор генерирует совершенно определенное представление для ее значения. Представление символической константы не определено.
Во-вторых, имя символической константы значимо только на этапе препроцессор ной обработки исходного кода, поэтому компилятор не включает ею в отладочную информацию объектного модуля. Вы не можете использовать это имя в выражениях при отладке. Напротив, типизированные константы являются по существу переменными, и их имена доступны отладчику. В силу этих причин предпочтительнее применять для представления постоянных величин типизированные константы, а не макросы #define.
Типы, определяемые пользователем
Встроенные типы данных, указатели и массивы образуют основу для представления и обработки информации на языке С. Подлинная же сила языка состоит в том, что он позволяет пользователю (под “пользователем” в подобного рода выражениях понимается программист) самому определять наиболее подходящие для конкретной задачи типы, способные адекватно представлять сложно структурированные данные реального мира. Эти средства С мы начнем изучать со сравнительно простого вспомогательного оператора typedef.
Указатели и массивы
Между указателями и массивами в С существует тесная связь. Имя массива без индекса эквивалентно указателю на его первый элемент. Поэтому можно написать:
int iArray[4] ;
int *piArr;
piArr = iArray; // piArr указывает на начальный элемент // iArray.
Последнее эквивалентно
piArr = &iArray[0];
И наоборот, указатель можно использовать подобно имени массива, г. е. индексировать его. Например, piArr [3] представляет четвертый элемент массива iArray [ ] .
К указателю можно прибавлять или отнимать от него целочисленные выражения, применять операции инкремента и декремента. При этом значение указателя изменяется в соответствии с размером объектов, на которые он указывает. Так, (piArr + 2) указывает на третий элемент массива. Это то же самое, что и & iArray [ 2 ]. Когда мы прибавляем к указателю единицу (piArr++) , адрес, который в нем содержится, в действительности увеличивается на 4 — размер типа int.

Короче говоря, в выражениях с указателями и массивами можно обращаться одинаково. Следует только помнить, что объявление массива выделяет память под соответствующее число элементов, а объявление указателя никакой памяти не выделяет, вернее, выделяет память для хранения значения указателя — некоторого адреса. Компилятор по-разному рассматривает указатели и массивы, хотя внешне они могут выглядеть очень похоже.
Однако возможности, которые раскрываются перед программистом благодаря указателям, выявляются в полной мере лишь при работе с динамическими структурами данных, о которых мы поговорим в следующем разделе.
Указатели
Указатель — это переменная, которая содержит адрес другого объекта. Этим объектом может быть некоторая переменная, динамический объект или функция. Говорят, что указатель ссылается на соответствующий объект. Хотя адрес, по существу — 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");
Соотношение между указателями и функциями примерно такое же, как между указателями и массивами, о чем говорится в следующем разделе.
Управляющие конструкции С
Программа, операторы которой исполняются строго последовательно — так, как они записаны, — обладает весьма скромными возможностями. Именно такими и были приводимые нами до сих пор примеры. Настоящая программа должна уметь “принимать решения”, т. е. изменять последовательность исполнения своих операторов в зависимости от текущей ситуации — входных данных, результатов вычислений, сообщений 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.
Встроенные типы данных
Однако данные могут не только вписываться в текст программы, но и храниться в памяти во время ее выполнения. С физической точки зрения любая информация в памяти машины выглядит одинаково — это просто последовательности нулей и единиц, сгруппированных в байты. Поэтому наличные в памяти данные должны как-то интерпретироваться процессором; этой интерпретацией управляет, естественно, компилятор С. Любая информация рассматривается компилятором как принадлежащая к некоторому типу данных. В языке имеется несколько встроенных, или простых, типов (возможны и другие типы данных, например, определяемые пользователем). Простые типы перечислены в следующей таблице.
Таблица 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. О таких вещах не следует забывать, особенно если вы занимаетесь переносом программ на машину с другой разрядностью.
Ввод и вывод в С
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
#include
#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 | Вертикальная табуляция | Переход на несколько строк вниз. |
| \\ | Выводит обратную дробную черту. | |
| \' | Выводит апостроф (одинарную кавычку). | |
| \" | Выводит кавычку (двойную). |
| \000 | От одной до трех восьмеричных цифр после esc-символа. |
| \хНН или \ХНН | Одна или две шестнадцатеричных цифры после esc-символа. |
В языке С для ввода имеется “зеркальный двойник” 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.
Директива #error
С помощью этой директивы можно вывести сообщение об ошибке при компиляции.
#error сообщение
Сообщение представляет собой любую строку (не заключенную в кавычки!), которая может содержать макросы, расширяемые препроцессором. Обычно директива применяется, чтобы обеспечить определение некоторого идентификатора:
#ifndef WIN_VERSION
terror He определена версия Windows!
#endif
Директива # include
Об этой директиве мы уже достаточно говорили в прошлой главе. Напомним вкратце, что препроцессор заменяет директиву содержимым указанного в ней файла. Есть две ее формы:
#include
В первом случае поиск нужного файла производится только в стандартных каталогах включаемых файлов; во втором случае этому предшествует поиск в текущем каталоге.
Директива # 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) мы расскажем в разделе об особенностях компилятора.
Директивы препроцессора
Препроцессорная обработка представляет собой первую фазу того процесса, что именуется компиляцией программы на 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) ((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; и т.д.
Особенности 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.
Несколько слов о стеке. На стеке сохраняется состояние процессора при прерываниях, распределяется память для автоматических (локальных) переменных, в нем сохраняется адрес возврата и передаются параметры процедур. Адресация стека (в 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),
па — число аргументов,
а — указатель на первый элемент массива аргументов.
Страница Advanced Compiler
Эта страница (рис. 4.2) позволяет управлять деталями генерации объектного кода.

Рис. 4.2 Страница Advanced Compiler диалога Project Options
при сброшенном флажке все преобразования выполняются в строгом соответствии с правилами ANSI С. Correct Pentium FDIV генерирует код, исключающий возможность ошибки из-за дефекта в ранних версиях процессора Pentium. Соответствующие ключи командной строки — -f-, -ff и -fp.
Страница Compiler
Эта страница диалога показана на рис. 4.1.
В нижней части страницы вы видите две кнопки: Full debug и Release. Первая из них выполняет все установки параметров, позволяющие в полной мере использовать возможности отладчика C++Builder; вторая запрещает генерацию какой-либо отладочной информации и оптимизирует код для получения максимальной скорости выполнения. При изучении языка вам лучше всего воспользоваться кнопкой Full debug и не задумываться больше об установках, влияющих на отладку и эффективность кода.

Рис. 4.1 Страница Compiler диалога Project Options
Коротко о разделах страницы Compiler.
Объем кода заголовочных файлов, включаемых в модуль исходный модуль, может достигать тысяч, если не десятков и сотен тысяч строк. К тому же часто эти заголовочные файлы включаются в каждый модуль проекта. Поскольку при разработке программы заголовочные файлы приходится изменять сравнительно редко (а стандартные заголовки вообще не меняются), имеет смысл компилировать все необходимые заголовки один раз и создать файл специального вида, который будет содержать всю необходимую “заголовочную” информацию в форме, обеспечивающей максимально быстрый доступ к ней. Компилятор C++Builder может генерировать такие файлы (с расширением .csm), •во много раз ускоряющие повторное построение проектов. Недостатком их можно считать разве что весьма большой размер — типичный файл прекомпилируемых заголовков может занимать от пяти до десяти мегабайт.

Если вы хотите отлаживать программу, то должны убедиться, что флажок Create debug information на странице Linker также установлен.
Страница 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, чем мы и займемся в следующей главе.
Типичное применение препроцессорных директив
В этом коротком разделе мы покажем только некоторые из наиболее распространенных случаев, когда препроцессорные директивы могут оказаться полезны.
Предотвращение включения файлов
Иногда при использовании заголовков может происходить дублирование кода из-за повторного включения некоторого файла. (Допустим, у вас имеется исходный файл 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(!someError) ;
Если перед включением 41айла assert.h определить символ ndebug, операторы assert () будут “закомментированы”.
Управление компилятором
В этом разделе мы рассмотрим установки диалога Project Options, имеющие отношение к программам на С. В основном это будет касаться страниц Compiler и Advanced Compiler этого диалога. Он открывается выбором Project | Options в главном меню.
Условная компиляция
Можно производить выборочную компиляцию различных участков кода в зависимости от оценки некоторого константного выражения или определения идентификатора. Для этого служат директивы #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.
Другие инструменты отладки
В 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 и системой могут происходить странные вещи.
Элементы отладки
Наиболее общими приемами отладки являются установка контрольных точек, наблюдение за переменными и пошаговое исполнение кода.
Контрольные точки
Программа, запущенная под управлением отладчика 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. В окне наблюдений будет показано имя переменной и ее текущее значение либо сообщение, показывающее, что переменная в данный момент недоступна или наблюдение отключено (

Рис. 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, которое описано ниже.
Отладочные пункты меню
При отладке вам понадобится обращаться в основном к трем меню; это каскадное меню 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 в главном |
Пошаговое исполнение кода
Одной из важнейших и самых очевидных операций при отладке является пошаговое исполнение кода. Когда программа приостановлена в контрольной точке, вы можете наблюдать значения различных переменных. Но чтобы найти конкретный оператор, ответственный за неправильную работу программы, нужно видеть, как эти значения меняются при исполнении отдельных операторов. Таким образом, выполняя операторы программы по одному, можно определить момент, когда значения каких-то переменных оказываются совсем не теми, что ожидались. После этого уже можно подумать, почему это происходит и как нужно изменить исходный код, чтобы устранить обнаруженную ошибку.
Эти команды могут выполняться из главного меню 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, отладка все равно остается чем-то вроде искусства. А любое искусство требует, во-первых, овладения техникой, т. е. знания имеющихся в вашем распоряжении средств, а во-вторых, опыта, приобретаемой в результате практической работы с отладчиком. Поэтому я советую вам не жалеть времени на изучение отладчика и побольше экспериментировать.
Предварительные шаги
Прежде всего, для исследования отладчика нам понадобится программа. В листинге 5.1 показана тестовая программа, вызывающая функции DoSomeMath () и DoSort () . Первая из них довольно бессмысленна и включена в программу только для того, чтобы продемонстрировать плавающую арифметику и соглашение _pascal. Вторая представляет собой вариант пузырьковой сортировки, частично реализованный на языке ассемблера.

При создании нового модуля debugC.C автоматически создается заготовка включаемого файла debugC.h, причем C++Builder сразу вводит в него директивы защиты от повторных включений, о которых мы говорили в прошлой главе. Кстати, главный исходный файл консольного проекта называется в 5-й версии Debug, bpf, а не Debug.срр
Листинг 5.1. Тексты программы Debug
/**********************************************************
* * Debug.срр: Главный файл проекта.
*/
#pragma hdrstop
#include
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
#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
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. Правда, компоновка отладочных библиотек может значительно замедлить компиляцию, поэтому не стоит прибегать к этому средству без необходимости.
Теперь я предлагаю вам посмотреть на различные меню, имеющие отношение к отладке.
Анонимные объединения
C++ допускает объявление объединений специального вида, называемых анонимными. Они не имеют этикеток и не являются именованными объектами. Последнее означает, что при обращении к разделам такого объединения не нужна операция-точка.
Глобальные анонимные объединения должны объявляться как static.
Аргументы по умолчанию
В прототипе функции язык C++ позволяет задать значения по умолчанию для некоторых параметров. Тогда при вызове функции соответствующие аргументы можно будет опустить, и компилятор подставит на их место указанные в прототипе значения. Правда, здесь существуют определенные ограничения.
Если вы задаете для какого-то параметра значение по умолчанию, то должны предусмотреть значения по умолчанию для всех последующих параметров функции. Таким образом, параметры со значениями по умолчанию должны стоять после всех остальных параметров в списке. Соответственно, если вы опускаете в вызове функции некоторый аргумент, то нужно опустить и все последующие аргументы. Короче говоря, в списке аргументов не должно быть пропусков.
Вот небольшая иллюстрация:
//////////////////////////////////////////////////
// Defaults.срр: Аргументы по умолчанию.
//
#include
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 можно не указывать, если требуется посчитать сумму всего двух или трех чисел.

Значения по умолчанию в прототипе не обязаны быть обязательно константами. Это могут быть, например, глобальные переменные или даже значения, возвращаемые функцией.
Дополнительные обозначения операций
Для ряда операций, в основном логических, в 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.
Глобальные константы
В С глобальная константа, т. е. инициализированная глобальная переменная с модификатором const, имеет своей областью действия всю программу (доступна для внешней компоновки), как и любая переменная без const. Другими словами, ее имя заносится в список глобальных символов объектного модуля и поэтому к ней можно обращаться из любого другого исходного файла программы.
В C++ областью действия глобальных констант является текущий файл, аналогично глобальным переменным static. Для того, чтобы к глобальной константе можно было обращаться из других компилируемых модулей, она должна быть определена как extern const, например:
extern const double eConst = 2.718281828;
В модулях, которые будут обращаться к такой константе, она, как и в С, должна быть объявлена внешней:
extern const double eConst;
Такое поведение глобальных констант C++ упрощает их объявление. Можно поместить определение константы (без extern) в заголовочный файл и включать его во все исходные модули, где она используется. Тем самым будет генерированы отдельные константы для каждого модуля, с одним именем и одинаковыми значениями. В С такие объявления вызывали бы ошибку дублирования определения.
Имена-этикетки
В языке 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, таким образом, будет настоящим именем типа.
Модификатор const
В языке С модификатор const означает, что значение переменной после инициализации не может быть изменено. В C++ переменные с const рассматриваются как истинные константные выражения. Поэтому в отличие от С в C++ допускается их использование в объявлении массивов:
const int aSize = 20 * sizeof(int);
char byteArray[aSize];
Объявления переменных
Локальные переменные в С должны объявляться в начале блока, т. е. до всех исполняемых операторов. В C++ переменные можно объявлять где угодно. Это предоставляет программисту определенные удобства и уменьшает возможность ошибок, позволяя размещать объявления переменных ближе к тому месту, где они используются. Вот один пример:
#include
int main(void) {
int n = 10;
printf("Hello! i =") ;
int i;
for (i=0; i
printf("%4d", i);
}
printf("\nAfter loop i = %d\n", i);
return 0;
Счетчик i объявляется непосредственно перед заголовком цикла for, а не в начале блока.

Можно объявлять переменную счетчика прямо в заголовке цикла, как это часто и делается:
for (int i=0; i
Операции распределения памяти
В языке C++ для управления динамической памятью введены операции new и delete (для массивов new [ ] и delete [ ]). В С для этого применялись в C++, однако новые операции имеют некоторые преимущества.
Переопределение операций new и delete
Стандартные (глобальные) версии операций new и delete можно переопределить или перегрузить, чтобы придать им некоторые дополнительные свойства или обеспечить возможность передачи им дополнительных аргументов. Это бывает полезно при отладке, когда требуется проследить все выделения и освобождения динамической памяти:
#include
#include
////////////////////////////////////////////////////
/ / Переопределение операций 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
#include
#include
void MyHandler(void)
{
prir-tf("No memory!\n");
exit(l) ;
}
int main(void) {
set_new_handler (MyHandler) ; //Установка обработчика.
return 0;
}
Обработчик ошибок new должен:
Заключение
Теперь вы знаете о ряде конструкций языка C++, которые можно рассматривать в качестве модификаций старого С, не вносящих ничего принципиально нового. Принципиально новое начнется со следующей главы, где мы перейдем к классам и объектно-ориентированному программированию.
Операция разрешения области действия
В языке С локальная переменная скрывает глобальную с тем же именем. Другими словами, если функция объявляет переменную, одноименную глобальной переменной, то, хотя мы и остаемся в области действия последней, она оказывается недоступна для функции. Чтобы разрешить конфликт имен, компилятор считает, что все ссылки на данное имя относятся к локальной переменой.
В C++ имеется операция разрешения области действия, позволяющая в такой ситуации обращаться к глобальной переменной, предваряя ее имя символами “ : : ”. Вот пример, поясняющий разрешение области действия:
#include
int main(void)
{
int aVar == 222; // Локальная переменная.
printf("Local aVar is %d.\n", aVar);
printf("Global aVar is, %d.\n", ::aVar);
return 0;
}
Отличия C++ от ANSI С
Есть несколько случаев, когда написанный на С код оказывается не корректным, если его компилировать в режиме C++.
Перегруженные функции
В программировании то и дело случается писать функции для схожих действий, выполняемых над различными типами и наборами данных. Возьмите, например, функцию, которая должна возвращать квадрат своего аргумента. В C/C++ возведение в квадрат целого и числа с плавающей точкой — существенно разные операции. Вообще говоря, придется написать две функции — одну, принимающую целый аргумент и возвращающую целое, и вторую, принимающую тип double и возвращающую также double. В С функции должны иметь уникальные имена. Таким образом, перед программистом встает задача придумывать массу имен для различных функций, выполняющих аналогичные действия. Например, Square-Int() и SquareDbl() .
В C++ допускаются перегруженные имена функций (термин взят из лингвистики), когда функции с одним именем можно тем не менее идентифицировать по их списку параметров, контексту, так сказать, в котором имя употребляется.
Рассмотрите следующий пример с вышеупомянутыми квадратами. Мы предусмотрели еще “возведение в квадрат” строки, когда результатом функции должна быть строка, в которой любые символы, кроме пробелов, удваиваются.
#include
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++ решает эту проблему, позволяя разбить проект на сегменты с различными пространствами имен (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 иденгификатор_пространства_имен::имя;
Элемент указанного пространства имен, идентифицируемый именем, объявляется как принадлежащий локальной области действия. После этого все не квалифицированные ссылки на имя будут рассматриваться как относящиеся к данному элементу. Вот пример:
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
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;
Расширяемые функции
Определение функции транслируется компилятором в отдельный и относительно автономный блок объектного кода. При вызове функции в вызывающей процедуре (программе) генерируется инструкция CALL с предшествующим ей кодом, ответственным за передачу параметров, и последующим кодом очистки стека. Инструкция CALL передает управление коду функции, являющемуся внешним по отношению к вызывающей процедуре. Все это вы можете увидеть воочию в окне CPU отладчика.
По своем завершении функция возвращает управление инструкцией RET. Выполнение вызывающей процедуры возобновляется с инструкции, следующей за call.
Однако, если определить функцию с модификатором inline, компилятор, по крайней мере в принципе, генерирует встроенное расширение функции на месте вызова. Это напоминает расширение макроса. Тем самым из кода исключаются инструкции вызова, возврата, передачи параметров и т. д., что, естественно, уменьшает общее время выполнения. Но следует иметь в виду, что код функции будет воспроизводиться в исполняемом файле столько раз, сколько имеется операторов вызова в исходной программе. Если функция велика и вызывается из многих мест, объем исполняемого файла может заметно возрасти.
Вот простейший пример расширяемой функции:
#include
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++ не может расширять встроенные функции также в следующих случаях:
В качестве иллюстрации рассмотрите такой пример:
/////////////////////////////////////////////////
// Inline.срр: Расширение inline-функций.
//
#pragma hdrstop
#include
#include
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.

Лучше всего размещать определения расширяемых функций в заголовочных файлах (для обычной, определенной где-то вовне функции файл включал бы ее прототип).
Символьные константы
Типом символьной константы (символ, заключенный в апострофы) в С является int, В языке C++ символьная константа имеет тип char. Конечно, это различие практически несущественно, но можно придумать случай, когда из-за него код С окажется несовместимым с C++.
Символьные типы
В языке С тип 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 эквивалентны.
Ссылки
В C++ имеется модифицированная форма указателей, называемая ссылкой (reference). Ссылка — это указатель, который автоматически разыменовывается при любом обращении к нему. Поэтому ссылке, как таковой, нельзя присвоить значение, как это можно сделать с обычным указателем. Ссылку можно только инициализировать.
Ссылки как псевдонимы переменных
Переменная-ссылка объявляется со знаком “&”, предшествующим ее имени; инициализирующим выражением должно быть имя переменной. Рассмотрите такой пример:
///////////////////////////////////////////////////////////////////
// Alias.cpp: Ссылка как псевдоним.
//
#include
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
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
#include
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
printf("iArray[%02d] = %4d\n'\ i, refltem(i));
refltem(i) = 0; // Попытка выхода за пределы
// массива.
return 0;
}
В первом из операторов for функция refltem() вызывается в левой части присваивания. Во втором for она возвращает значение, которое передается функции printf (). Обратите внимание, что, во-первых, массив iArray[] объявлен как статический локальный в refltem(), благодаря чему непосредственное обращение к нему вне этой функции невозможно. Во-вторых, refltem() попутно проверяет допустимость переданного ей индекса. Ссылка позволяет на элементарном уровне организовать механизм, который мы в следующей главе назовем сокрытием или абстрагированием данных.
Тип 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.
Указатели типа void*
В языке С значение типа void* можно непосредственно присваивать переменной-указателю любого типа, без каких-либо приведений. В C++ это не допускается. Другими словами, код вроде
SomeStruct *sPtr;
void AllocBuffer(int n)
{
sPtr = malloc(n * sizeof(SomeStruct));
}
вызовет ошибку в C++. Необходимо явное приведение типа указателя:
sPtr = (SomeStruct *)malloc(n * sizeof(SomeStruct));
“Улучшенный С”
В этом разделе мы рассмотрим те нововведения C++, которые можно считать усовершенствованиями С, поскольку они не выходят за рамки классической структурной модели программирования.
Класс
Класс — это множество объектов, имеющих одинаковую структуру. Класс в программировании является аналогом понятия, или категории. В то время как объект представляет собой конкретную сущность, класс является абстракцией сущности объекта. Конкретный объект является представителем, или (не совсем грамотно) экземпляром класса.
Другими словами, структура характеристик объекта и все потенциальные отношения между объектами заложены в классе. Однако классы могут находиться еще и в специфических отношениях между собой.
Наследование
Если существенными видами отношений между объектами являются связь и агрегация, то фундаментальное отношение между классами — это наследование. Один класс может наследовать другому. В C++ в таком случае говорят, что один класс является базовым, а другой (который наследует первому) — производным. Еще их называют соответственно классом-предком и классом-потомком. Наследование может быть прямым, когда один класс является непосредственным предком (потомком) другого, или косвенным, когда имеют место промежуточные наследования.
Производный класс наследует всю структуру характеристик и поведение базового, однако может дополнять или модифицировать их. Если класс В является производным по отношению к А, то с логической точки зрения “В есть А”. Например, понятие, или класс, “автомобиль” (В) является производным по отношению к понятию “средство передвижения” (А). По Л. Н. Толстому, В есть А.
Как и в логике, здесь существует взаимосвязь между “содержанием” и “объемом” понятия. Производный класс имеет большее содержание, но меньший объем, чем базовый.
Наследование может быть простым, когда производный класс имеет всего одного непосредственного предка, и сложным, если в наследовании участвуют несколько базовых классов.
Полиморфизм
Полиморфизм, наряду с наследованием, является фундаментальной концепцией объектной модели программирования. Без него объектно-ориентированное программирование потеряло бы значительную долю своего смысла. Суть полиморфизма в том, что с объектами различных классов, имеющих один и тот же базовый класс, можно при определенных условиях обращаться, как с объектами базового класса; однако объект, являющийся, по видимости, объектом базового класса, будет вести себя по-разному в зависимости от того, что он такое на самом деле, т. е. представитель какого из производных классов.
В C++ полиморфное поведение объектов обеспечивается механизмом виртуальных функций-элементов. В одной из предыдущих глав мы на самом деле уже приводили пример “полиморфизма”, реализованного на языке С. Допустим, программа должна в числе всего прочего выводить на экран различные геометрические фигуры. Она определяет класс “фигура”, в котором предусмотрен виртуальный метод “нарисовать” (в C++ это был бы абстрактный класс). От данного класса можно произвести несколько классов — “точка”, “линия”, “круг” и т. д., — каждый из которых будет по-своему определять этот метод.
Указатель на класс “фигура” может ссылаться на объект любого из производных классов (поскольку все они являются фигурами), и для указываемого им объекта можно вызвать метод “нарисовать”, не имея представления о том, что это на самом деле за фигура.
Абстракция и инкапсуляция
Об этих принципах объектного подхода мы уже упоминали. На самом деле это принципы программирования, присущие не только объектно-ориентированной модели. Но хотелось бы несколько уточнить их в отношении к организации классов.
Чтобы абстрагировать объект, он должен быть сравнительно “слабо связан” с окружающим миром. Он должен обладать сравнительно небольшим набором (существенных) свойств, характеризующих его отношения с другими объектами. С другой стороны, выделение класса как некоторого понятия, охватывающего целый ряд различных объектов, также является моментом абстракции.
Поэтому абстрагирование, как таковое, имеет два аспекта: выделение общих и в тоже время существенных свойств, описывающих поведение ряда схожих предметов.
С абстракцией неразрывно связан принцип инкапсуляции. Инкапсуляция — это сокрытие второстепенных деталей объекта. Для этого нужно выделить сначала существенные его свойства. Но чтобы выделить существенные свойства, нужно сначала отвлечься от второстепенных. Так что в действительности речь может идти только о едином акте, в котором можно лишь отвлеченно выделить два отдельных момента.
С технической точки зрения абстракция и инкапсуляция выражаются в том, что классы состоят из интерфейса и реализации. Интерфейс представляет абстрагированную сущность объектов. Реализация скрыта в своих деталях от пользователя класса.
Сделав такие предварительные замечания о понятиях объектно-ориентированного подхода, перейдем к конкретному рассмотрению классов, как они реализованы в языке C++.
Об объектном подходе к программированию
Существует разные подходы к программированию. Любому из них присущ свой собственный способ абстрагирования сущностей, с которыми он работает. Так, процедурно-ориентированный подход оперирует абстракцией алгоритма. С, кстати, — типичный процедурный язык, хотя на нем возможно писать программы, напоминающие по стилю объектно-ориентированные. Логико-ориентированный имеет в виду цели, выражаемые на языке логических предикатов (таков язык Prolog). Наконец, объектно-ориентированное программирование абстрагирует классы и объекты. В чем же состоит его суть?
Известный теоретик Грейди Буч так определяет этот подход:
“Объектно-ориентированное программирование — это методология программирования, основанная на организации программы, в виде совокупности объектов, каждый из которых является представителем определенного класса, а классы образуют иерархию наследования.”

В “строго” объектно-ориентированных языках объектами (или их составными частями) являются решительно все элементы программы, в том числе она сама. В языке Java, например. Язык C++ таковым, кстати, не является хотя бы потому, что он сохраняет все возможности процедурного языка С. В C++ можно создать, например, совершенно отдельно стоящую глобальную переменную, да и сама функция main)) — совершенно “внеклассовая”. Такая универсальность C++ может быть как преимуществом, так и недостатком, если ею злоупотреблять. У программиста, впервые применяющего объектный подход, всегда имеется тенденция мыслить старыми, процедурными категориями.
Итак, центральным элементом абстракции объектно-ориентированной методологии является, очевидно, объект.
Объект
Объекты нашего программного мира моделируют объекты реального или воображаемого мира (например, мира некоторой игры, хотя это мир тоже мыслимый, а стало быть, реальный в смысле логики). Как модель, программный объект представляет собой некоторую абстракцию объекта реального, что предполагает выделение существенных свойств последнего и игнорирование тех, что безразличны с насущной, “сиюминутной”, точки зрения.
Как нечто, как единичность, объект (реальный или программный) отличен от всего остального. Он обладает индивидуальностью, или самотождественностью, если применить опять же термин логики. Понятие индивидуальности программного объекта определить сложно, пожалуй, даже сложнее, чем для реальных объектов, и о нем редко вообще задумываются. Однако интуитивно ясно, что объект, как бы его ни переименовывать и как бы ни менялось его состояние, остается той же самой единичной сущностью с момента создания и до своего уничтожения.
Наконец, заметим, что о программном объекте можно говорить точно так же, как о реальном. Мы можем не только говорить о нем, но и описывать на специальном формальном языке — языке программирования.
В общем, как видно, объект в программе мало чем отличается от предмета реального мира. Он является моделью последнего. Поэтому, в известном смысле, пока не важно, о каких объектах пойдет речь — реальных или программных.
Состояние объекта
Объект, прежде всего, характеризуется своим состоянием. Возьмем, к примеру, базу данных. Она — объект, поскольку есть нечто цельное. К числу моментов, определяющих ее состояние, относится текущее число записей. Каждая запись тоже, очевидно, является объектом. Отдельные поля существующей записи могут модифицироваться, и их совокупность определяет текущее состояние записи, а вместе с ней и состояние всей базы данных.
По-видимому, состояние программного объекта полностью определяется некотором набором (структурой) характеристик и их текущими значениями. Эти характеристики называют полями или (в C++) элементами данных объекта. Но не все моменты состояния объекта должны быть непосредственно видимы извне. Классический пример, который неизменно приводят американские авторы, — автомобиль. С точки зрения водителя у него есть приборный щиток, отражающий скорость, обороты, температуру двигателя и т. д., и органы управления. Но в автомобиле масса частей, спрятанных под капотом, состояние которых не исчерпывается показаниями приборов.
На самом деле состояние, как таковое, может быть вообще скрыто от внешнего взгляда. Оно, как говорят, инкапсулировано в объекте. Однако в зависимости от своего состояния объект по-разному взаимодействует со своим окружением, что приводит нас к следующему понятию.
Поведение объекта
Поведение — это то, как объект взаимодействует с окружением (другими объектами). Объект может подвергаться воздействию окружения или сам воздействовать на него. Объект может рассматриваться как аналог предмета, а поведение — как реакция на манипуляции с ним или действия, инициированные самим объектом. В некоторых объектных системах (например, OLE 2) говорят о глаголах, т. е. действиях, которые могут связываться с объектом. Для каждого объекта существует определенный набор возможных действий над ним.
Действия в отношении к объектам иногда называют передачей сообщений между ними. В языках, подобных Object Pascal, операции над объектами называют обычно методами. В C++ благодаря его “процедурному наследству” чаще говорят о функциях-элементах объекта. Эти функции являются структурными элементами определения класса, к которому принадлежит объект.
Отношения между объектами
Естественно, программа, т. е. цельная система, реализуется только во взаимодействии всех ее объектов. Здесь можно выделить в основном два типа взаимодействий, или отношений: связь и агрегацию.
Связь является довольно очевидной разновидностью взаимодействий — один объект может воздействовать на другой, являющийся в известном смысле автономной сущностью. Тут существует отношение подчинения — “А использует В”. Один объект является активным, другой — пассивным. Понятно, что в системе один и тот же объект может выступать как в активной, так и в пассивной роли по отношению к различным объектам.
Другой тип отношений — агрегация, когда один объект является составной частью, т. е. элементом другого — “А содержит В”. Агрегация может означать физическое вхождение одного объекта в другой; в C++ это соответствует описанию первого объекта в качестве элемента данных другого объекта. Но это не обязательно. Например, в Windows есть понятие дочернего окна. Здесь имеет место отношение агрегации, хотя на физическом уровне родительское и дочернее окна автономны.
Определение класса
Приведенный ниже код определяет два класса, которые могли бы применяться в графической программе. Это классы точек и линий.
////////////////////////////////////////////////////////////
// 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, являются классами. Отличия их сводятся к следующему:
Я никогда не видел, чтобы в C++ применяли объединения. Хотя это, возможно, и имело бы смысл в некоторых ситуациях, когда требовалось бы объединить несколько разнородных классов в один тип.
Элементы данных и элементы-функции
Элементы данных класса совершенно аналогичны элементам структур в С, за исключением того, что для них специфицирован определенный тип доступа. Объявления элементов-функций аналогичны прототипам обычных функций.
Конструктор и деструктор
В классе могут быть объявлены две специальные функции-элемента. Это конструктор и деструктор. Класс Line в примере объявляет обе эти функции.
Конструктор отвечает за создание представителей данного класса. Его объявление записывается без типа возвращаемого значения и ничего не возвращает, а имя должно совпадать с именем класса. Конструктор может иметь любые параметры, необходимые для конструирования, т. е. создания, нового представителя класса. Если конструктор не определен, компилятор генерирует конструктор по умолчанию, который просто выделяет память, необходимую для размещения представителя класса.
Деструктор отвечает за уничтожение представителей класса. Это происходит либо в случае, когда автоматический объект данного класса выходит из области действия, либо при удалении динамических объектов, созданных операцией new.
Деструктор объявляется без типа возвращаемого значения, ничего не возвращает и не имеет параметров. Если деструктор не определен, генерируется деструктор по умолчанию, который просто возвращает системе занимаемую объектом память.
Заключение
В этой главе мы постарались познакомить вас, весьма конспективно, с основными понятиями и терминологией объектно-ориентированного программирования. После этого мы перешли к элементарному введению в классы C++. В следующей главе мы продолжим изучение классов, уже на более серьезном уровне.
Введение в классы С++
Сначала мы приведем простейший пример определений классов, которые можно было бы разместить в заголовочном файле. Так обычно и делается, если классы должны быть доступны нескольким модулям программы. Следует напомнить, что класс — это расширение понятия типа данных, а точнее, понятия структуры. В C++ принято говорить просто о типах; представитель класса уже нельзя считать просто данными, поскольку ему присуще некоторое поведение.
После этого мы, опираясь на показанный пример, объясним элементарные моменты строения класса в C++.
Чисто виртуальные функции и абстрактные классы
Виртуальная функция-элемент некоторого класса может быть объявлена чистой. Это выглядит так:
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()
{
// . . .
}
//...
// И т.д.
Деструктор
Деструктор является противоположностью конструктора. Он вызывается при уничтожении объектов и должен производить необходимую очистку объекта перед освобождением занимаемой им памяти
Именем деструктора должно быть имя класса, которому предшествует тильда (~). Свойства деструкторов таковы:
Доступ к базовым классам
Ключ доступа определяет “внешний” доступ к элементам базового класса через объекты производного. Что касается доступа самого производного класса к элементам базового класса, то ключ доступа на него не влияет. Для производного класса доступны разделы protected и public базового класса; раздел private строго недоступен вне области действия базового класса.
Для доступа к элементам базового класса через производный можно сформулировать такое правило: права доступа, определяемые для них базовым классом, остаются неизменными, если они такие же или строже, чем специфицировано ключом доступа. В противном случае права доступа определяются ключом в определении производного класса.
Например, при наследовании с ключом public права доступа к элементам базового класса остаются неизменными; при закрытом наследовании (ключ private) все элементы базового класса будут недоступны за пределами производного класса.
При закрытом наследовании можно сделать некоторые открытые функции базового класса открытыми в производном, если переобъявить их имена в производном классе:
class First { public:
void FFunc(void) ;
//... }
class Second: private First { public:
First::FFunc; // First::FFunc() открыта в классе Second.
//.. .
}
Нужно сказать, что в прикладном программировании применяется почти исключительно открытое наследование.
Доступ к элементам данных
Поскольку функции-элементы класса находятся в его области действия, они могут обращаться к элементам данных непосредственно по имени, как можно видеть в последнем примере. Обычные функции или функции-элементы других классов могут обращаться к элементам существующего представителя класса с помощью операций “.” или “->”:
class Time { public:
int hour;
int min;
} ;
int main()
{
Time start; // Локальный объект Time.
Time *pTime = Sstart; //Указатель на локальный объект.
start.hour = 17; // Операция доступа к элементу.
pTime->min = 30; // Косвенный доступ к элементу.
return 0;
}
“Друзья”
Спецификаторы доступа позволяют указать, к каким элементам класса могут обращаться функции, в него не входящие. Однако могут быть случаи, когда целесообразно разрешить некоторому классу или функции обращаться к закрытым или защищенным элементам данного класса. Это можно сделать с помощью ключевого слова friend.
“Друзьями” класса могут быть объявлены другие классы или отдельные функции, как обычные, так и являющиеся элементами некоторых классов. Друзья могут объявляться в любом из разделов определения класса (закрытом, защищенном или открытом), — в каком именно, не имеет значения. В любом случае дружественный класс или функция будет иметь полный доступ к элементам класса.
Вот пример объявления Друзей класса:
class SomeClass (
friend class AnotherClass;
friend void regularFunc (int);
friend void OtherClass::MemFunc(double);
//...
};
Следует иметь в виду такие правила:
Функции преобразования
Объекты класса могут быть преобразованы в другие типы (или созданы из других типов) с помощью операций приведения типа или конструкторов преобразования.
Конструкторы преобразования
Если конструктор класса А имеет единственный параметр типа В, то объект В может быть неявно преобразован в класс А с помощью такого конструктора. Другими словами, компилятор может сам вызывать такой конструктор, чтобы “из В сделать А”. Возьмите пример из предыдущего раздела. Локальный объект можно было бы инициализировать по-другому:
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 имя_нового_типа();
Процедуры преобразования характеризуются следующими правилами:
Вот пример процедуры преобразования:
#include
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;
Элементы данных
Суммируем и дополним то, что говорилось об элементах данных в предыдущей главе. Элементы данных аналогичны элементам структур языка С. Стоит добавить следующее:
Элементы-функции
Функция-элемент класса объявляется внутри определения класса. Там же может быть расположено и оределение тела функции. В этом случае функцию-элемент называют встроенной и компилятор будет генерировать ее встроенное расширение на месте вызова. Если определение функции располагается вне тела класса, то к ее имени добавляется префикс, состоящий из имени класса и операции разрешения области действия. В этом случае функцию-элемент также можно определить как встроенную с помощью ключевого слова inline. Вот несколько модифицированный класс Point из предыдущей главы вместе с его реализацией:
#include
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++'вводится новый вид области действия — класс. Имена элементов класса расположены в области действия класса, и к ним можно обращаться из функций-элементов данного класса. Кроме того, получить доступ к элементам класса можно в следующих случаях:
Классы С++
В прошлой главе мы показали, как определяются простейшие классы C++. То, что содержится в приведенном коде — это интерфейс класса. В самом определении класса объявляются обычно лишь прототипы функций-элементов. Чтобы класс стал работоспособным, необходима еще его реализация. Реализация класса, располагаемая часто в отдельном файле, содержит код его функций-элементов, а также некоторые элементы данных, называемые статическими.
Мы переходим теперь к подробному изучению классов, включая, естественно, и аспекты их реализации.
Ключевое слово 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, а к содержимому строки, на которую он ссылается).
Константные объекты и функции-элементы
Можно создать представитель класса с модификатором const. Тем самым гарантируется, что после инициализации содержимое объекта нельзя будет изменить. Компилятор C++Builder выдает предупреждение в случае, если для объекта вызывается функция-элемент, не объявленная как 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;
}
Конструктор копии
Конструктор копии является конструктором специального вида, который принимает в качестве параметра ссылку или константную ссылку на объект данного класса. Он автоматически вызывается компилятором, когда вновь создаваемый объект инициализируется значениями существующего объекта:
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;
}
Если вы не предусмотрели в классе конструктор копии, компилятор генерирует конструктор копии по умолчанию, который производит простое копирование данных объекта в новый представитель класса. Если класс содержит какие-то указатели или ссылки, то такое копирование скорее всего будет бессмысленным или опасным.
Иногда, когда копирование объектов класса в принципе не может привести ни к чему хорошему, имеет смысл объявить конструктор копии (это может быть просто “пустышка”) в закрытом разделе определения класса. Тогда пользователь класса не сможет создавать копии существующих объектов.
Конструктор
Конструктор имеет то же имя, что и класс. Он вызывается компилятором всегда, когда создается новый представитель класса. Если в классе не определен никакой конструктор, компилятор генерирует конструктор по умолчанию (не имеющий параметров).,Относительно конструкторов имеют место такие правила:
Конструктор не может быть объявлен как const, volatile, virtual или static.
Поскольку конструктор не возвращает значений, то для сигнализации об ошибке при инициализации объекта, если требуется, нужно применять механизм управления исключениями, о котором мы будем говорить в 10-й главе.

Можно вызвать конструктор для инициализации уже существующего объекта, если перегрузить глобальную операцию new таким образом, чтобы она принимала дополнительный аргумент — указатель типа void*. Это называют размещающей формой операции; мы о ней уже упоминали в прошлой главе. Такая методика иногда применяется для глобальных представителей класса, если их нужно инициализировать после выполнения каких-то предварительных действий. Вот пример:
#include
// Операция 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 или как ссылки (а также закрытым элементам базовых классов).
Конструкторы, деструкторы и наследование
Конструкторы не наследуются. Это утверждение требует некоторых пояснений. Оно означает, что если в базовом классе имеются конструктор с некоторыми параметрами, он не будет вызываться автоматически, если вы . попробуете создать объект производного класса с такими параметрами. Для этого нужно написать конструктор производного класса, в котором конструктор базового класса будет вызываться через посредство списка инициализации. О нем мы уже говорили выше в связи с инициализацией элементов данных класса. Базовые классы в смысле инициализации ничем от них не отличаются.
Если в списке инициализации конструктора отсутствует вызов какого бы то ни было конструктора базового класса, компилятор все равно вызовет для последнего конструктор по умолчанию, т. е. конструктор без параметров. В примере предыдущего параграфа объявлен конструктор базового класса, который может вызываться без параметров, поскольку для него определены аргументы по умолчанию. Если вы не поленитесь пройти по этому примеру в отладчике, то увидите последовательность вызовов при создании объекта производного класса в функции 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++ может наследовать элементы-данные и элементы-функции от одного или нескольких базовых классов. Сам класс называется в этом случае производным по отношению к базовым классам или классом-потомком. В свою очередь, производный класс может являться базовым по отношению к другим классам.
Принцип наследования, или порождения новых классов, позволяет абстрагировать (инкапсулировать) некоторые общие свойства и поведение в одном базовом классе, которые будут наследоваться всеми его потомками.
Наследование позволяет также модифицировать поведение базового класса. Производный класс может переопределять некоторые функции-элементы базового класса, оставляя основные свойства класса в неприкосновенности .
Синтаксис производного класса следующий:
class имя класса: ключ доступа имя_базового класса [, ...] {
тело_объявления_класса } ;
Ключ_доступа — это одно из ключевых слов private, protected или public.
Некоторые замечания
При перегрузке операций полезно помнить следующее:
Операции класса new и delete
Класс может определять свои собственные операции new и delete (new[] и delete [] для массива объектов):
Ниже приведен довольно длинный пример, демонстрирующий определение операций класса new и delete, а также глобальных new [ ] и delete []. Вывод программы позволяет проследить порядок вызовов конструкторов/деструкторов и операций new/delete при создании автоматических и динамических объектов.
Листинг 8.1. Определение операций класса new и delete
//////////////////////////////////////////////
// NewDel.cpp: Операции класса new и delete.
//
#pragma hdrstop
#include
#include
#include
#include
#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

Пример заодно поясняет, зачем нужен деструктор. Здесь он удаляет динамически созданную строку, адрес которой хранится в объекте. Если не определить деструктор, то генерированный компилятором деструктор по умолчанию удалит сам объект, но не строку, которая окажется “потерянной”. Подобные упущения являются распространенной причиной утечек памяти.
Операция присваивания
Операция присваивания — это функция-элемент класса с именем 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 имя_класса&. Последнее предпочтительнее, так как простая ссылка на класс не позволяет копировать константные объекты.
Если класс содержит указатели или ссылки, может быть целесообразным, как и в случае конструктора копии, запретить присваивание объектов, объявив операцию присваивания в закрытом разделе класса.
Операция вызова объекта
Перегрузка операции вызова 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;
}
Перегрузка функций-элементов
Функции-элементы класса могут быть перегружены подобно обычным функциям; несколько функций-элементов могут иметь одно и то же имя, если их можно однозначно идентифицировать по списку аргументов. Вы уже встречались в этой главе с перегруженным конструктором. Это весьма распространенная ситуация. Вот еще подобный пример:
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 операция ([операнды]) ;

Если функция является элементом класса, то первый операнд соответствующей операции будет самим объектом, для которого вызвана функция-операция. В случае одноместной операции список параметров будет пуст. Для двухместных операций функция будет иметь один параметр, соответствующий второму операнду. Если функция-операция не является элементом класса, она будет иметь один параметр в случае одноместной операции и два — в случае двухместной.
Для перегрузки операций существуют такие правила:
Ниже мы приводим два примера
Ниже мы приводим два примера классов с перегруженными операциями. Первый из них определяет рудиментарный класс строк, допускающих конкатенацию с помощью знака сложения. Второй пример показывает перегрузку индексации.
Листинг 8.2. Перегрузка операции сложения
//////////////////////////////////////////////////////
// StrAdd.cpp: Демонстрация перегрузки сложения для строк.
//
#pragma hdrstop
#include
#include
#include
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
#include
#include
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
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
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
Простое наследование
При простом, наследовании производный класс порождается всего одним базовым классом. Вот пример:
#include
#include
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;
}
Реализация виртуального механизма
Для реализации виртуальных свойств классов нужно обеспечить выбор нужной функции на этапе выполнения программы. Это называют поздним связыванием (объекта с его методами). Компилятор не может заранее разрешить обращение к виртуальной функции-элементу объекта, если последний представлен указателем или ссылкой. На этапе компиляции действительный тип объекта неизвестен. Поэтому компилятор делает примерно следующее:
Как видите, виртуальный механизм, как и все хорошее в этом мире, связан с некоторыми издержками, как в плане занимаемой памяти (виртуальная таблица), так и в плане времени выполнения (дополнительные косвенные ссылки при вызове). Однако эти издержки, как это ни удивительно, очень малы.
Заключение
Содержание данной главы практически исчерпывает объектно-ориентированные аспекты C++. Однако в языке еще немало различных возможностей, среди которых можно назвать управление исключительными ситуациями и шаблоны. Одним из таких аспектов C++ являются классы потоков ввода-вывода, изучением которых мы займемся в следующей главе. Реализация ввода-вывода может служить примером использования ряда объектно-ориентированных концепций, описанных выше.
Сложное наследование
Язык C++ допускает не только простое, но и сложное наследование, т. е. наследование от двух и более непосредственных базовых классов. Это позволяет создавать классы, комбинирующие в себе свойства нескольких независимых классов-предков.
Это чаще всего имеет смысл, когда у вас есть некоторый набор понятий, которые могут более или менее независимо комбинироваться с различными элементами другого набора понятий. Простейший пример. Имеется понятие “растение”. Бывают “культурные растения”, и бывают “дикорастущие растения”. С другой стороны, растение может иметь или не иметь “товарной ценности”, т. е. быть полезным или бесполезным с коммерческой точки зрения. Если говорить о товарной ценности, то тут у растений бывают “цветы” и “плоды” и т. д. Все это образует довольно развернутую структуру, которая может порождать понятия вроде “дикое растение, цветы которого можно продавать на рынке”. (Возможно, кстати, и такое: “дикое растение, цветы которого имеют товарную ценность, но которые нельзя продавать на рынке”!) А можно, с некоторыми модификациями, говорить то же самое не о растениях, а о животных или веществах, минералах. И есть не только “товарные” сущности, но и сорняки, вредители. И так далее.
Очевидно, здесь существует ряд довольно независимых категорий — “растение”, “товар”, “культурность” и прочее. Подобная структура — отличный кандидат на реализацию в виде иерархии классов со сложным наследованием.
Кстати, в языке Object Pascal, реализованном в сходном продукте Борланда — Delphi, — нет сложного наследования, что в ряде случаев может значительно ограничить его полезность в сравнении с C++.
Для иллюстрации сложного наследования возьмем последний пример с “сообщениями таймера”. Понятия времени и понятие сообщения — независимые, и, возможно, в программе будут другие классы, родственные “времени” и “сообщению”. Поэтому вполне разумным будет определить для них отдельные классы и породить от них третий класс, применив методику сложного наследования:
#include
#include
//////////////////////////////////////////////////////////////////////
// Базовый класс - время.
//
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 с теми же параметрами.
Специальные функции-элементы класса
Специальными функциями-элементами называют функции, которые могут вызываться компилятором неявно. Это может происходить при создании и уничтожении представителей класса, при их копировании и преобразовании в другие типы. К таким функциям относятся:
Статические элементы данных
Статический элемент данных является по существу глобальной переменной с областью действия в классе и разделяется всеми представителями класса. Он только один, вне зависимости от того, сколько представителей имеет класс. На самом деле статический элемент данных существует даже в том случае, когда никаких представителей класса не создано.
Помимо объявления в определении класса, статический элемент данных должен еще и определяться:
class SomeClass
{
static int iCount;
// Объявление статического
// элемента.
//.. .
};
int SomeClass::iCount = 0;
// Определение статического
// элемента.

Обращаться к открытым статическим элементам класса можно либо через любой его представитель операциями “.” и “->”, либо с помощью операции разрешения области действия (SomeClass : : iCount). Последний способ предпочтительнее, так как ясно показывает, что элемент не связан с конкретным объектом.
Статические элементы-функции
Функция класса, объявленная с модификатором static, не связывается ни с какими его конкретными представителями. Другими словами, ей не передается указатель this в качестве скрытого параметра. Это означает, что:

Статические функции-элементы класса могут передаваться процедурам API Windows в качестве возвратно-вызываемых, поскольку не предполагают наличия на стеке параметра this. Обычные функции-элементы для этого не годятся.
Статические элементы класса
Можно объявить элемент класса (данные или функцию) как статический.
Указатель 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, справа налево).
Виртуальные функции
Функции-элементы класса могут объявляться в C++ как виртуальные. Ключевое слово virtual заставляет компилятор генерировать для класса некоторую дополнительную информацию 6 функции. Происходит следующее: если виртуальная функция переопределяется в производном классе, и если имеется указатель или ссылка на базовый класс (которые могут с тем же успехом ссылаться на производный класс, поскольку производный объект есть в то же время и объект базового класса), то при обращении к функции через указатель (ссылку) будет вызвана правильная функция-элемент (т. е. соответствующая типу действительного объекта) — базового или одного из производных классов, в зависимости от типа конкретного объекта.
Не хочу показаться занудой, но, как мне кажется, стоит повторить, чем косвенное обращение к объекту (указатель или ссылка) в данном отношении отличается от прямого. Можно недвусмысленно объявить объект базового и объект производного классов. Потом можно присвоить объект производного класса переменной базового типа. Не требуется даже никаких приведений, потому что, как я уже говорил, производный объект является объектом базового класса. “Автомобиль” есть “средство передвижения”. Однако при этом будет потеряна всякая специфика “автомобиля”, отличающая его от всех других средств передвижения, наземных, водных или воздушных. Но применение указателей или ссылок в объектно-ориентированных языках типа C++ приводит к тому, что объект сам может помнить, к какому типу он относится, и указатель на базовый тип может быть в данном случае снова приведен
Следующий пример покажет вам разницу между виртуальным и не виртуальным переопределением функции.
//////////////////////////////////////////////////////////
// Virtual.cpp: Демонстрация виртуальной функции.
//
#pragma hdrstop
#include
#include
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() { /* ... */}
Вызов функций-элементов класса
Совершенно аналогично тому, что имеет место в случае элементов-данных, функции-элементы класса могут вызываться функциями-элементами того же класса просто по имени. Обычные функции и элементы других классов могут вызывать функции-элементы данного класса для существующих его представителей с помощью операций “ . ” или “->” (через указатель). Приведенный ниже пример это иллюстрирует.
#include
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;
}
Бесформатный ввод-вывод
До сих пор речь у нас шла почти исключительно о вводе-выводе с использованием операций извлечения/передачи данных. Эти операции перегружены для всех встроенных типов и выполняют соответствующие преобразования из внутреннего представления данных в текстовое и из текстового во внутреннее (машинное).
Однако в библиотеке C++ имеется немало функций бесформатного ввода-вывода, которые часто применяют для чтения и записи двоичных (не-текстовых) файлов.
Чтение и запись сырых данных
Чтение сырых данных производится функцией 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
#include
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;
}
Чтение символов и строк
Для чтения одиночных символов, а также строк применяется функция 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, пока не произойдет одно из следующих событий:
get ():
istream Sgetline(char *buf, long len, char t = '\n');
Разница между этими двумя функциями состоит в том, что get line () извлекает из потока ограничивающий символ, в то время как get () этого не делает. Ни та, ни другая функция не записывает ограничивающий символ в буфер.
Пример использования getline () вы уже видели в листинге 9.4. Вот небольшой пример чтения строк с помощью get ():
#inciude
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.
Двоичный режим ввода-вывода
Двоичный режим открытия файла (с установленным битом binary) означает, что никакой трансляции данных при передаче из файла в поток и обратно производиться не будет. Речь здесь идет не о форматных преобразованиях представления данных. При текстовом режиме (он принимается по умолчанию) при передаче данных между файлом и потоком производится замена пар символов CR/LF на единственный символ LF (' \n ') и наоборот. Это происходит до преобразований представления, которые выполняются операциями извлечения/передачи. Двоичный ввод-вывод означает всего-навсего, что такой замены происходить не будет; тем не менее двоичный режим необходим при работе с сырыми данными, т. е. данными в машинной форме без преобразования их в текстовый формат.
Чтобы открыть файл в двоичном режиме, нужно, как уже упоминалось, установить в параметре mode конструктора потока или функции open() бит ios::binary.
Файловые потоки
Файловые потоки библиотеки ввода-вывода реализуют объектно-ориентированную методику работы с дисковыми файлами. Имеется три класса таких потоков:
Эти классы выводятся соответственно из istream, ostream и iostream. Таким образом, они наследуют все их функциональные возможности (перегруженные операции << и>>” для встроенных типов, флаги форматирования и состояния, манипуляторы и т. д.).
Чтобы работать с файловым потоком, нужен, во-первых, объект потока, а во-вторых, открытый файл, связанный с этим объектом.
Форматирование
Библиотека ввода-вывода предусматривает три способа форматирования: посредством вызова форматирующих функций-элементов, с помощью манипуляторов или путем установки или сброса флагов потока.
Форматирующие флаги
Флаги управления форматированием являются битовыми полями, хранящимися в переменной типа fmtflags (псевдоним int). Для их чтения и/или модификации могут применяться следующие функции-элементы класса ics:
Помимо функций, для управления флагами можно пользоваться манипуляторами 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 | Если установлен, поток сбрасывается после каждой операции передачи. |
Несколько замечаний относительно перечисленных в таблице флагов.

Имена перечисленных выше флагов и других констант принадлежат к области действия класса ios. Вне этого класса нужно либо воспользоваться разрешением области действия (ios : : scientific), либо обращаться к ним, как к элементам существующего объекта (cout. scientific). Мы поедпочитаем первый способ.
Листинг 9.3. форматирующие флаги потоков
////////////////////////////////////////////////////
// Flags.срр: Форматирующие флаги потоков.
//
#include
#include
#include
#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<
//
// Демонстрация флагов основания и знака.
// Задается основание 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 << " "<
// Вывод десятичной точки. 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 и перегружены таким образом, чтобы можно было либо читать, либо устанавливать значение соответствующего атрибута потока. Если аргумент в вызове отсутствует, функция возвращает текущее значение атрибута. Если аргумент указан, функция устанавливает новое и возвращает предыдущее значение атрибута.
long width(long)
Эта функция предназначена для чтения или установки атрибута ширины поля.
char fill(char)
Функция позволяет прочитать или установить текущий символ заполнения.
По умолчанию символ заполнения — пробел.
long precision(long)
Эта функция позволяет прочитать или установить значение атрибута точности, определяющего либо общее число выводимых цифр, либо число цифр дробной части.
Пример
Ниже приводится программа, демонстрирующая форматирование потока с помощью функций-элементов класса ios.
Листинг 9.1. Демонстрация форматирующих функций потока
///////////////////////////////////////////////
// Format.срр: Форматирующие функции-элементы ios.
//
#include
#pragma hdrstop
#include
#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;
}
Классы потоков
К классам потоков относятся следующие:
Классы istrstream и ostrstream управляют резидентными потоками (форматированием строк в памяти). Это устаревшая методика, оставшаяся в C++Builder в качестве пережитка.
Для работы с потоками вам потребуется включить в программу заголовочный файл iostream.h. Кроме того, может потребоваться подключить файлы fstream.h (файловый ввод-вывод), iomanip.h (параметризованные манипуляторы) и strstream.h (форматирование ь памяти).
Конструирование объекта потока
Каждый из трех классов файловых потоков имеет четыре конструктора.
ifstream () ;
of stream();
fstream () ;
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)
Манипуляторы
Манипуляторы потоков являются по существу функциями, которые можно вызывать непосредственно в цепочке операций извлечения или передачи в поток. Различают простые и параметризованные манипуляторы. У простых манипуляторов аргументы отсутствуют. Параметризованные манипуляторы имеют аргумент.
Ниже приводится сводка имеющихся манипуляторов, как простых, так и параметризованных. Они Перечислены в алфавитном порядке.
Таблица 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
#pragma hdrstop
#include
//////////////////////////////////////////////////
// Манипулятор, определенный пользователем - звонок.
//
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
// Класс эффектора.
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 Манипулятор, выводящий свой аргумент в двоичной форме
Некоторые функции потоков
В классах istream и ostream есть ряд функций, которые позволяют выполнять над потоками разные полезные операции (в основном при бесформатном вводе-выводе). Здесь мы опишем наиболее часто употребляемые из них.
Класс istream
Следующие функции являются элементами класса istream:
Устанавливает положение указателя потока. Для первой формы указывается абсолютная, для второй — относительная позиция указателя. Параметр dir может принимать следующие значения:
Класс ostream
Последним двум функциям из istream соответствуют аналогичные функции класса ostream:
Операции извлечения и передачи в поток
Основными классами ввода-вывода C++ являются istream и ostream. Первый из них перегружает операцию правого сдвига (>>), которая служит в нем для ввода данных и называется операцией извлечения из потока. Класс ostream перегружает соответственно операцию левого сдвига (<<); она применяется для вывода и называется операцией передачи в поток.

Нужно сказать, что стандартной русской терминологии как таковой в C++ не существует. Каждый изобретает свою собственную; иногда удачно, иногда — нет.
Вот простейшие операторы ввода и. вывода на стандартных потоках:
#include
int main()
{
char name [ 8.0] ;
cout<< "Enter your name: ";
cin>> name;
cout <<"Hello " << name << "!";
return 0;
}
Как видите, при действиях над потоками возможно последовательное сцепление операций, подобно последовательному присваиванию. Как вы уже знаете, такая форма записи обеспечивается благодаря тому, что функции-операции извлечения и передачи возвращают ссылку на свой объект.
Перегруженные операции для встроенных типов
Классы istream и ostream перегружают операции соответственно извлечения и передачи в поток для всех встроенных типов. Это позволяет единообразно применять эти операции для чтения и вывода символов, целых, вещественных чисел (т. е. с плавающей точкой) и строк. Вот небольшая иллюстрация, где попутно показан еще простейший прием проверки на ошибку при вводе:
#include
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
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;
}
Предопределенные потоки
Библиотека ввода-вывода C++ предусматривает четыре предопределенных объекта-потока, связанных со стандартными входным и выходным устройствами. Ниже дана сводка этих объектов.
Таблица 9.1. Предопределенные объекты-потоки C++
| Имя | Класс | Описание | |||
| cin | istream | Ассоциируется со стандартным вводом (клавиатурой). | |||
| cout | ostream | Ассоциируется со стандартным выводом (экраном). | |||
| cerr | ostream | Ассоциируется со стандартным устройством ошибок (экраном) без буферизации. | |||
| clog | ostream | Ассоциируется со стандартным устройством ошибок (экраном)с буферизацией. |
Примеры файловых потоков
Следующий пример (листинг 9.4) демонстрирует различные режимы и способы открытия потока.
Листинг 9.4. Примеры открытия файловых потоков
/////////////////////////////////////////////////////////
// Filemode.срр: Режимы открытия файлов.
//
#include
#include
#pragma hdrstop
#include
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 <
} 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 () , которая будет подробно описана чуть позже.
Режимы открытия файла
Параметр mode, который имеет вторая форма конструктора, задает режим открытия файла. Для значений параметра класс ios определяет символические константы, перечисленные в таблице 9.5.
Таблица 9.5. Константы класса ios для режимов открытия файла
| Константа | Описание |
||
| арр | Открытие для записи в конец файла. | ||
| ate | При открытии позиционирует указатель на конец файла. | ||
| binary | Файл открывается в двоичном (не текстовом) режиме. | ||
| in | Файл открывается для ввода. | ||
| out | Файл открывается для вывода. | ||
| trunc | Если файл существует, его содержимое теряется. |
Константы можно комбинировать с помощью поразрядного OR. Для конструкторов классов if stream и ofstream параметр mode имеет значения по умолчанию — соответственно ios : : in и ios : : out.
Состояние потока
Состояние объекта класса ios (и производных от него) содержится в его закрытом элементе _state в виде набора битов. Следующая таблица перечисляет имеющиеся биты состояния потока.
Таблица 9.4. Биты состояния потока
| Бит | Описание |
||
| goodbit | С потоком все в порядке (на самом деле это не какой-то бит, а 0 — отсутствие битов ошибки). | ||
| eofbit | Показывает, что достигнут конец файла. | ||
| failbit | Индицирует ошибку формата или преобразования. После очистки данного бита работа с потоком может быть продолжена. | ||
| badbit | Индицирует серьезную ошибку потока, связанную обычно с буферными операциями или аппаратурой. Скорее всего, поток далее использовать невозможно. |
Для опроса или изменения состояния потока в классе ios имеется ряд функций и операций.
bool fail () ; Возвращает true, если установлен failbit или bad-bit.

Функция operator void*() неявно вызывается, если поток сравнивается с нулем (как cin в примере из листинга),
Ввод-вывод с произвольным доступом
Понятие произвольного доступа к файлу подразумевает два, или даже три, различных момента. Во-первых, оно означает, что можно произвольно обращаться к любой записи или любому байту в файле, в противоположность последовательному доступу, когда данные извлекаются или передаются в поток строго по очереди. Во-вторых, предполагается, что на открытом файле можно произвольно чередовать операции чтения и записи. И, наконец, из сказанного вытекает, что ввод-вывод с произвольным доступом является по преимуществу бесформатным.
Приведенная ниже программа открывает (создает новый или переписывает старый) свой файл как двоичный, и, кроме того, сразу для ввода и вывода. Она применяет функции позиционирования потока и функции бесформатного чтения-записи.
Листинг 9.5. Произвольный доступ к файлу
//////////////////////////////////////////////////
// Random.cpp: Демонстрация файла с произвольным доступом.
//
#include
#include
#pragma hdrstop
#include
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
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
fs.read((char*)&pt, IS);
cout “ setw(4)<< pt; . }
cout<< end1 << end1;
//
// Переписать четные индексы.
//
for (i=l; i
fs.read((char*)&pt, IS);
pt = -pt;
fs.seekg(fs.tellg () - IS); // Возврат на шаг.
fs.write((char*)&pt, IS);
}
//
// Распечатать файл.
//
cout << "After rewriting the even records:"<
fs.seekg(0) ;
for (i=0; 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++, которое позволяет создавать “обобщенные” классы, служащие моделью некоторого множества конкретных классов.
Закрытие файла
В классах файловых потоков имеется функция close (), которая сбрасывает содержимое потока и закрывает ассоциированный с ним файл.
Кроме того, деструктор потока автоматически закрывает файл при уничтожении объекта потока.
При ошибке закрытия файла устанавливается флаг failbit.
Явное создание представителя шаблона
В подавляющем большинстве случаев прикладному программисту нужно только сбросить флажок External в диалоге Project Options и больше не беспокоиться о том, как написанные им шаблоны классов будут обрабатываться. Однако, если вы хотите создать, например, динамическую библиотеку на основе шаблона, которая будет содержать код всех его функций-элементов, то для генерирования полного представителя шаблона вам придется воспользоваться директивой template.
Следующий пример включает в себя два исходных файла и один заголовочный, в котором определяется простой шаблон. Главный исходный модуль программы создает два шаблонных объекта для его аргументов int и float, но, поскольку модуль компилируется с директивой #pragma option -Jgx, то никакого кода для представителей шаблона в нем не создается. Вместо этого во втором исходном модуле (компилируемом с ключом -Jgx) явным образом генерируется полный представитель шаблона для аргумента float, а также неявно генерируется представитель для int, так как модуль ссылается на него.
Листинг 10.4. Директива порождения представителя шаблона
//////////////////////////////////////////////////
// Simptmpl.h: Простой шаблон класса.
//
template
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
current (0), arr(new T[n]) {}
template
template
void Somefunc(int);
/////////////////////////////////////////////////////////
// Instance.cpp: Порождение представителей шаблона. //
#pragma option -Jgx
#include
#pragma hdrstop
#include
#include "Simptmpl.h"
USEUNIT("Somefunc.cpp") ;
#pragma argsused
int main(int argc, char* argv[])
{
const int Num = 9;
Simple
Simple
fa.Insert(3.14) ;
cout << "Float In main(): " << fa.Get(O) << endl;
for (int i=0; i
ia.Insert(i * 2);
cout << "From main(): ";
for (int i=0; i
cout << ia.Get(i) << " ";
cout << end1;
Somefunc(10);
cin.ignore();
return 0;
}
//////////////////////////////////////////////////////////
// Somefunc.cpp: Функция, использующая шаблон класса. //
#pragma option -Jgd #include
#pragma hdrstop #include "Simptmpl.h"
//
// Следующая строка генерирует явный представитель шаблона
// для типа float, хотя в данном модуле он не используется:
// template class Simple
void Somefunc(int n)
{
Simple
for (int i=0; i
cout << "From Somefunc(): "<< n;
for (int i=0; i
cout<<" "<< iArr.Get(i);
cout <
}
Вывод программы показан на рис. 10.5.

Рис. 10.5 Результат работы программы Instance
Ключевое слово typename
Это ключевое слово может применяться в двух случаях. Во-первых, им можно заменять ключевое слово class в списке параметров шаблона. Такое дополнение сделано в ANSI C++ потому, что ключевое слово class в списке параметров не вполне отражает положение дел;
параметром шаблона может быть любой тип, а не только класс, и стандартный синтаксис может вводить некоторых в заблуждение. Следующие две нотации эквивалентны:
template
template
Во-вторых, typename необходимо, если шаблон определяет некоторые объекты еще не объявленных типов. Рассмотрите такой шаблон функции:
template
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), о которой мы немного расскажем в следующей главе.
Ключи компилятора
Сброшенному флажку External в диалоге Project Options соответствует ключ командной строки компилятора -Jgd, а установленному — -Jgx. Посредством директивы препроцессора #pragma option можно указать ту или иную опцию для конкретного файла. Не следует забывать о том, что компиляция и компоновка программы — два различных этапа ее создания. Компоновщик не знает, что вы задали в диалоге Project Options для компилятора. В любом случае компоновщик не будет включать в исполняемый код повторные определения функций шаблона.
Определение шаблона класса
Определение шаблона класса предполагает:
Определения функций-элементов, расположенные в теле шаблона, ничем не отличаются от определения встроенных функций-элементов обычного класса. Определения функций-элементов, располагаемые вне тела шаблона, имеют такой вид:
template <список параметров шаблона> возвращаемый_тип имя шаблона< параметры_шаблона>::имя_функции(список_параметров) {тело_функции }
Подобным же образом определяются статические элементы данных шаблона класса:
template <список параметров шаблона>
тип имя шаблона
<параметры шаблона>::имя статического элемента[ =значение];
Смысл всех синтаксических элементов определений будет ясен, если рассмотреть пример законченного шаблона класса:
Листинг 10.2. Шаблон класса DataBase
///////////////////////////////////////////////////////
// Deftmpl.h: Пример определения шаблона класса.
//
#ifndef _DEFTMPL_H
#define _DEFTMPL_H
template
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
Т &DataBase
if (cp - base == num) { err = true;
return *(cp - 1) ;
}
else
return *cp++;
// Позиционирует указатель и возвращает ссылку
// на текущую запись.
template
T &DataBase
{
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
inline T &DataBase
{
if (cp-base== num) {err= true/return*(cp-1) ;
} else
return *cp++;
}
Перегрузка шаблонов функций
Шаблоны функций можно перегружать точно так же, как обычные функции. Два шаблона могут иметь одно и то же имя, если их можно различить по списку параметров, например:
// Возвращает больший из двух аргументов.
template
return а > b? а : b;
// Возвращает наибольший элемент массива. template
(
Т maxVal = arr[0] ;
for(int i=l; i
return maxVal;
}
Порождение представителей шаблона
Этот раздел посвящен тому, каким образом C++Builder генерирует шаблонные классы и функции в программах, состоящих из нескольких модулей исходного кода. Несколько модулей проекта могут подключать один и тот же заголовочный файл с шаблоном, и создавать объекты одного и того же шаблонного класса (с одинаковым набором аргументов шаблона). В C++Builder имеются средства, позволяющие избежать дублирования кода в такой ситуации.
Разное
В этом параграфе мы расскажем о некоторых возможностях шаблонов, предусмотренных в стандартном C++, но не реализуемых компилятором C++Builder. Нам кажется, что о них необходимо рассказать, хотя бы для того, чтобы, читая другие книги по C++, вы не пытались осуществить в C++Builder методики, которые на нем осуществить невозможно.
В конце концов, C++Builder не является универсальным инструментом. Он ориентирован на визуальное программирование, а те моменты, о которых мы будем здесь говорить, второстепенны с этой, да и, пожалуй, с любой другой точки зрения.
Если у вас есть Borland C++ 5 или более поздняя версия, и вы хотя бы немного умеете с ним работать, то можете при желании разобрать с его помощью приведенные ниже примеры.

Специализация шаблона класса
Подобно шаблону функции, шаблон класса может быть специализирован для специфического набора его аргументов. Для этого нужно написать явные реализации тех или иных методов шаблона для конкретных типов. Вот, например, шаблон, который генерирует класс массива объектов, в том числе символьных строк, для которых отдельно реализуется функция добавления в массив и деструктор:
#include
#include
const int DefSize = 4;
template
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
{
if (current == size) return;
arr[current++] = item;
}
// Специализированная Insert для параметра char*:
void MyArray
{
if (current == size) return;
arr[current] = new char[strlen(item) + 1];
strcpy(arr[current++], item);
}
// Общий деструктор:
template
MyArray
// Специализированный деструктор:
MyArray
for (int i=0; i
delete [ ] arr[i];
delete [ ] arr;
}
А вот главная функция, тестирующая шаблон для “стандартного” типа int и для “специального” типа строк (т. е. char*):
int main(void)
{
// Создание, заполнениеи вывод MyArray
MyArray
iArr = new MyArray
int i;
for (i=0; i
cout << "Integers: ";
for (i=0; KDefSize; i++)
cout << " " << iArr->Get(i);
cout<< end1<< end1;
delete iArr; // Уничтожение объекта.
// Создание, заполнение и вывод MyArray
MyArray
sArr = new MyArray
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
#include
const int DefSize = 4;
// Общий шаблон:
template
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
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
for (int i=0; i
delete[] arr;
}
Заметьте, что все функции- элементы общего шаблона определены теперь как встроенные, а в специализированном только деструктор определен вне тела шаблона (его нельзя объявить как встроенный, поскольку он содержит оператор цикла). В общем, полное переопределение шаблона целесообразно в том случае, когда необходима специализация большинства его элементов-функций. Главная функция ничем не отличается от функции предыдущего примера.
Функции, дружественные шаблону
В качестве “друзей” класса чаще всего объявляют различные функции-операции, в которых участвуют объекты класса. Типичным примером может служить операция передачи объекта в поток. Для шаблона класса можно определить шаблон дружественной функции (не обязательно, конечно, операции). Такой шаблон будет порождать отдельную дружественную функцию для каждого генерируемого шаблонного класса. Вот пример шаблона дружественной функции (это модификация первого примера параграфа):
#include
#include
const int DefSize = 4;
template
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
};
// Шаблон дружественной функции-операции передачи объекта // в поток:
template
ostream &operator<<(ostream &os, const MyArray
{
for (int i=0; i
return os;
}
//
// Здесь находятся общие и специализированные
// функции-элементы... //
Определенный таким образом шаблон функции-операции реализует передачу в поток всего объекта, в противоположность предыдущим примерам, где объекты шаблонных классов выводились поэлементно. Главная функция:
int main(void)
{
MyArray
iArr = new MyArray
int i;
for (i=0; KDefSize; i++) iArr->Insert (i) ;
// Вывод объекта MyArray
cout << "Integers: " << *iArr<< endl;
cout << endl;
delete iArr;
MyArray
sArr = new MyArray
for (i=0; i
// Вывод объекта MyArray
cout << "Strings: "<< *sArr << endl;
delete sArr;
return 0;
}
Результат работы программы показан на рис. 10.3.

Рис. 10.3 Пример с шаблоном
дружественной
функции-операции
Шаблоны функций
Синтаксис определения шаблона функции имеет вид:
template <список формальных типов>возвращаемый_тип имя_функции(список параметров) {
тело функции }
Список_формальных_типов состоит из спецификаций вида class формалъный_тип, разделенных запятыми. Формальный тип может обозначаться любым идентификатором, аналогично формальному параметру функции.
Список _параметров функции должен включать в себя параметры типов, перечисленных в списке формальных _типов, и еще, возможно, какие-то другие. Возвращаемый_тип также может быть одним из формальных типов. Например:
template
template
template
Как видите, определение шаблона функции отличается от обычной функции только наличием конструкции template <список_формальных_типов> в заголовке.
В качестве примера шаблонов функций можно привести определение функций min () и max () из заголовочного файла sdlib.h. Определение это сводится к следующему:
template
if (t1 < t2)
return tl;
else
return t2;
}
template
inline const T &max(const T &tl, const T &t2)
{
if (t1 > t2) return t1;
else
return t2;
}
Эти функции можно вызывать с аргументами любого типа (класса), в котором определены операции “больше-меньше”.
Когда компилятор встречает вызов функции шаблона, он автоматически порождает представитель шаблона, подставляя вместо формального типа конкретный тип аргумента, с которым вызывается функция.
Шаблоны функций размещают чаще всего в заголовочных файлах, подобно определениям макросов и inline-функций.
Шаблоны классов
Шаблон класса является обобщенным определением некоторого семейства классов, имеющих схожую структуру, но различных в смысле используемых типов или констант. Синтаксис шаблона класса следующий:
template <список параметров шаблона> class имя шаблона {тело_класса };
В списке_параметров_шаблона .могут присутствовать элементы двух видов:
Создание представителей шаблона
Чтобы создать из шаблона представитель конкретного класса, нужно конструировать объект, указав для его типа имя шаблона с набором конкретных аргументов (типов и констант). Каждый формальный тип в списке параметров шаблона нужно заменить на имя действительного типа. Каждая формальная константа заменяется на константу указанного в шаблоне типа:
// Шаблон класса. template
TmplClass
TmplClass
fClassPtr = new TmplClass
После того, как представитель шаблонного класса создан, с ним можно обращаться точно так же, как с любым объектом, принадлежащим к обыч-
ному классу. Ниже показан пример программы, использующей определение шаблона из листинга 10.2.
Листинг 10.3. Создание и использование представителя шаблонного класса
///////////////////////////////////////////////
// Usetmpl.cpp: Использование шаблона класса. //
#include
#pragma hdrstop
#include
#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
// с 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
typedef TmplClass
IClass iCIassObj; IClass *iCiassPtr; iCIassPtr = new IClass;
Специализация шаблона функции
Несколько напоминает перегрузку шаблонов ситуация, когда определяется обычная функция, имя которой совпадает с именем шаблона и список параметров которой соответствует шаблону с некоторым специфическим набором фактических типов. Такую функцию называют специализированной функцией шаблона. Этот прием применяют, когда для некоторого типа или набора типов общий шаблон работать не будет.
Допустим, мы хотим, чтобы шаблон Мах()из последнего примера порождал функцию для двух аргументов-строк, которая возвращала бы большую из них (в смысле алфавитного порядка). Функция Мах (char*, char*), порожденная из первого шаблона, сравнивала бы адреса строк вместо их содержимого. Поэтому нужно определить отдельную функцию Мах (char*, char*):
char *Max(char *a, char *b) {
return strcmp(a, b) > 0? а : b;
}

Когда компилятор встречает вызов какой-то функции, для его разрешения он следует такому алгоритму:
Ниже приводится полный пример, иллюстрирующий различные аспекты перегрузки и специализации шаблонов.
Листинг 10.1. Перегрузка и специализация шаблона
////////////////////////////////////////////////////////
// Functemp.cpp: Шаблоны функций.
//
#include
#include
#pragma hdrstop
#include
// Возвращает больший из двух аргументов.
template
return a > b? a : b;
}
// Возвращает наибольший элемент массива.
template
Т maxVal = arr[0] ;
for(unsigned i=l; i
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
}
#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
Установки проекта и ключи компилятора
В диалоге Project Options на странице C++ имеется флажок Templates: External (рис. 10.4). По умолчанию он сброшен, что означает оптимальное, или “интеллигентное”, порождение представителей шаблонов классов и функций.

Рис. 10.4 Страница C++ диалога Project Options
При сброшенном флажке External компилятор порождает глобальные представители шаблонов для всех модулей, где создаются объекты шаблонных классов (или вызываются шаблонные функции). Это возможно только в том случае, если компилятор при обработке модуля “видит” все определение шаблона, со всеми его функциями-элементами. Однако объектный код не обязательно генерируется для всех шаблонных методов (определенных как не-встроенные). По умолчанию код генерируется для методов:
Что означает последний пункт, будет рассказано чуть ниже.
После этого компоновщик ilink32.exe анализирует код объектных файлов и помещает в исполняемый файл только один экземпляр функции для каждой комбинации шаблон/аргументы.
Если же флажок External будет установлен, то компилятор вообще не будет генерировать никакого кода для не-встроенных функций класса, а будет рассматривать их вызовы как внешние ссылки. Такой вариант может иметь смысл, если, допустим, вы используете в своем проекте библиотеку, в заголовочных файлах которой определяются некоторые шаблоны и в которой уже имеется компилированный код для всех имеющих смысл представителей шаблонов.
Алгоритмы
Алгоритмы стандартной библиотеки выполняют разные распространенные действия на наборах данных, представленных стандартными контейнерами. Среди этих действий можно назвать сортировку, поиск, замену и т. д. Ниже мы вкратце опишем имеющиеся в библиотеке алгоритмы, не вдаваясь в подробности и не приводя развернутых примеров. Применение стандартных алгоритмов достаточно очевидно и, как правило, не вызывает никаких затруднений.
Чтобы можно было вызывать эти алгоритмы, нужно включить в программу заголовок algorithm:
#include
using namespace std;
(Не забывайте указывать пространство имен std, когда пользуетесь новой нотацией включаемых файлов!)
Некоторые алгоритмы вы уже видели (например, find ()), так что здесь мы показываем в основном те, с которыми вы еще не встречались.
Библиотека стандартных шаблонов
До сравнительно недавнего времени в языке C++ не было других стандартных средств программирования, кроме старой библиотеки стандартных функций С, которая совершенно не использовала мощных нововведений, таких, как классы, шаблоны, inline-функции и исключения. Библиотека стандартных шаблонов (Standard Template Library), разработанная в HP Laboratories, явилась в свое время весьма удачным шагом в решении проблемы стандартной библиотеки ANSI C++, в которую она теперь и входит.
Это весьма обширное собрание структур данных и алгоритмов общего назначения, которое позволяет решать самые различные задачи обработки наборов данных.
Битовые множества
Битовое множество представляет собой контейнер, в котором могут храниться битовые последовательности фиксированной длины. Можно сказать, что оно служит представлением подмножеств фиксированного множества (в математическом смысле), каждому элементу которого соответствует один бит в определенной позиции. Единичный бит означает, что элемент принадлежит подмножеству, нулевой — что он не входит в данное подмножество. Подобным образом организованы множества языка 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
#include
#pragma hdrstop
#include
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
Функции и функциональные объекты
Некоторые алгоритмы стандартной библиотеки C++ требуют функций в качестве параметров. Простейший пример — алгоритм for each (), который вызывает переданную ему функцию для каждого элемента контейнера. В этом разделе мы рассмотрим вопросы, связанные с функциональными параметрами алгоритмов.
Функции и предикаты
Иногда нужно выполнить какое-то действие для каждого элемента контейнера. Упомянутый выше алгоритм for_each() позволяет сделать именно это. Функция, выполняющая необходимое действие для отдельного элемента, передается как третий аргумент алгоритма. Первые два задают диапазон. Вот пример:
void Square(int arg) { return arg * arg; }
int main() {
vector
for_each(iVect.begin (), iVect.end(), Square);
}
Двухместные функции принимают два параметра. Часто они применяются к элементам различных контейнеров. Например, имеется два списка, и нужно что-то сделать с элементом первого списка в зависимости от значения соответствующего ему элемента во втором. Это делается с помощью алгоритма transform (), одна из форм которого имеет вид
template
Outputlterator transform(Inputlteratorl firsti,
Inputlteratorl lasti,
Inputlterator2 first2,
Outputlterator result,
BinaryOperation binary_func);
Первые два параметра задают диапазон первого контейнера. Параметр first2 указывает начало второго контейнера. Контейнер, куда будет записан результат, начинается с result. Последний параметр — указатель на двухместную функцию преобразования.
Особо можно выделить функции- предикаты. Предикат — это функция, возвращающая булево значение. Они используются с алгоритмами типа find_if () , который находит в указанном диапазоне значение, для которого предикат истинен:
template
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
меняет знак всех элементов вектора vec.
Можно привести примеры более сложных функциональных объектов с “памятью”, которые хранят между вызовами информацию о своем текущем состоянии. Определяемые пользователем функциональные объекты часто производятся от шаблонов классов unary_function и binary_function.
Типичным примером функциональнь1х объектов, сохраняющих свое состояние, являются различные генераторы, как, например, генератор случайных чисел.
Вот программа, в которой реализуется совсем простой функциональный объект — генератор чисел Фибоначчи:
///////////////////////////////////////////////////
// FuncObj.cpp: Генератор чисел Фибоначчи.
//
#include
#include
#include
#pragma hdrstop #include
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
generate (iVec .begin (), iVec.end(), Fibo());
cout << endl<< "Vector initialized by generate () algorithm:"<< endl;
copy (iVec .begin (), iVec.end(),ostream_iterator
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
cout<< "Number of elements greater than 10 = " << count<< endl;
Здесь связка bind2nd применяется к функциональному объекту greater, задавая его второй аргумент равным 10 (если применить связку bindlst, то будут подсчитаны элементы, меньшие 10.
Негаторы
Негатор создает из функционального объекта другой объект с прямо противоположным действием, т. е. служит выражением отрицания. В библиотеке есть два негатора, noti и not2, применяемые соответственно к одноместным и двухместным функциональным объектам. Например,
noti(bind2nd(greater
означает “не больше 10”. Конечно, такое отношение можно записать и без негатора (имеется функциональный объект less_equal), но все равно негаторы часто оказываются полезны.
Функции строк
Класс 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 (), правда, не записывает в буфер конечный нуль, так что его приходится добавлять вручную.
Заключение
В этой главе вы познакомились с библиотекой стандартных шаблонов, то есть с контейнерами, строками и всем, что с ними связано — итераторами, функциональными объектами и т. п. Сила стандартных контейнеров в том, что они представляют собой готовые динамические структуры данных, которые сами следят за выделением и удалением памяти под свои данные. Программисту остается только грамотно написать базовый класс для хранящихся в контейнере объектов, снабдив его необходимыми конструкторами, деструкторами и перегруженными операциями. Все остальное сделает контейнер, обеспечив поддержание корректности структуры данных в процессе работы программы.
Характеристики строк
Как и контейнеры, строки характеризуются своим размером и вместимостью. Вот сводка функций-элементов, позволяющих манипулировать различными характеристиками строк. Они аналогичны соответствующим функциям контейнеров:
| Функция | Возвращаемый тип | Описание | |||
| 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, если запрашиваемый размер больше максимально возможного (он обычно определяется размером наибольшего свободного блока памяти).
Итераторы
Итераторы, как было замечено выше, являются центральным механизмом, обеспечивающим работу с данными контейнеров. Они являются аналогом указателей и делают возможным циклический перебор всех элементов контейнера. Существуют разные виды итераторов, поскольку различные алгоритмы по-разному обращаются к данным. Каждый класс контейнера может порождать итераторы, необходимые для работы адекватных ему алгоритмов.
Подобно указателю, итератор может ссылаться на единственный элемент данных; пара итераторов может задавать определенный диапазон контейнера; итератор может иметь т. н. запредельное значение, аналогичное NULL и означающее, что его нельзя разыменовывать.

Следует упомянуть, что при вызове различных алгоритмов для диапазона, заданного парой итераторов, второй из них соответствует не последнему значению итератора в диапазоне, а следующему за ним.
Основными операциями над итераторами- являются, как и в случае указателей, разыменование и инкремент. Если итератор i после конечного ряда приращений может стать равным итератору j, то говорят, что итератор j достижим из i. Если к итератору, достигшему верхней границы диапазона, применить операцию инкремента, он примет запредельное значение.
Сделав такие предварительные замечания, мы перейдем теперь к конкретному изучению итераторов библиотеки стандартных шаблонов.
Типы итераторов
Существует пять основных форм итераторов:
Итераторы, стоящие в этом списке ниже, выводятся из тех, что находятся выше. Это едва ли не единственный пример классовой иерархии в 8TL.
В следующей таблице показано, какими контейнерами стандартной библиотеки генерируются те ли иные итераторы.
Таблица 10.2. Итераторы, генерируемые стандартной библиотекой
| Форма итератора | Контейнеры |
| входной итератор | istream iterator |
| выходной итератор | ostream iterator |
| двунаправленный итератор | List set и multiset map и multimap |
| итератор произвольного доступа |
обычные указатели vector deque |
Указатели как итераторы
Чтобы продемонстрировать, как применяются итераторы, мы рассмотрим простейший вид итератора — обычный указатель. Следующая программа вызывает стандартный алгоритм find() для поиска значения в обычном массиве.
#include
#include
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
#include
#include
using namespace std;
#define SIZE 50 vector
int main() {
iVect[30] = 33;
vector
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." <
return 0;
Объявляемый в программе контейнер имеет тип vector
Далее мы вкратце рассмотрим различные формы итераторов.
Входные, выходные и поступательные итераторы
Простейший из итераторов — входной. Он может перемещаться по контейнеру только в поступательном направлении и допускает только чтение данных. Первые два параметра алгоритма find (), например, должны быть входными итераторами. Выходной итератор отличается от входного правом доступа. Он допускает только запись данных в контейнер.
К обеим этим формам итераторов можно применять, по меньшей мере, операцию неравенства (!=), разыменования (*) и инкремента (++).
Ниже показан пример копирования массива в вектор при посредстве выходного итератора и алгоритма copy (). Его последним параметром может быть любой выходной итератор. На самом деле тот же итератор здесь используется и как входной — в операторе вывода.
#include
#include
#include
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
int main()
{
vector
copy(dArr, dArr + 10, oi);
while (oi != dVect.endO) {
cout << *oi << endl;
oi++;
} return 0;
}
Итераторы потоков
Собственно, только входные и только выходные итераторы имеют смысл в основном при работе с потоками ввода-вывода, которые могут быть допускать либо только извлечение, либо только передачу данных. Любые контейнеры стандартной библиотеки генерируют более сложные, итераторы, которые, естественно, могут применяться и в качестве простых входных или выходных. / Вы уже хорошо знакомы со стандартными потоками cin и cout, извлечение и передача данных из которых производится операциями >> и <<. Однако возможен другой метод работы с этими потоками, при котором входной или выходной объект iostream преобразуется в итератор. Затем его можно передавать как аргумент стандартным алгоритмам.
Например, ниже показано, как можно применить выходной итератор для вывода на экран содержимого контейнера.
#include
#include
#include
using namespace std;
int main( ) {
vector
for (int i=0; i<10; i++) iVect[i] = i;
cout<< "The vector contents are: { ";
copy(iVect.begin (),
iVect.endf), ostream_iterator
cout << "}." << endl;
return 0;
}
Последний параметр алгоритма copy () конструирует выходной итератор типа ostream iteratorПоступательный итератор допускает как чтение, так и запись в контейнер. Однако, как и в случае двух предыдущих, возможен только инкремент, но не декремент итератора. Поступательные итераторы могут использоваться, например, в алгоритме replace (), который определяется так:
template
void replace(Forwardlterator first,
Forwardlterator last,
const T &old_value,
const T &new_value);
Этот алгоритм заменяет все значения old_value, содержащиеся в контейнере, на new_value.
Двунаправленные итераторы
Двунаправленные итераторы допускают чтение и запись данных и к ним можно применять операции как инкремента, так и декремента. Такие итераторы могут быть, например, аргументами алгоритма reverse () , который меняет порядок элементов контейнера на обратный:
template
void reverse(Bidirectionallterator first,Bidirectionallterator.last);
Такой алгоритм может быть полезен, если, скажем, из контейнера, уже сортированного в восходящем порядке, вы хотите получить контейнер с нисходящей сортировкой.
Итераторы произвольного доступа
Эти итераторы являются наиболее универсальными с точки зрения возможностей доступа. Их можно использовать для произвольного чтения и записи данных контейнера. (Обычные указатели принадлежат, кстати, к этому виду итераторов.) Такие итераторы участвуют в алгоритмах сортировки, входящих в стандартную библиотеку.
Алгоритм random_shuffle, также требующий итераторов произвольного доступа, случайным образом переставляет значения элементов в указанном диапазоне контейнера:
template
void random shuffle(RandomAccessIterator first, RandomAccessIterator last);
Итераторы вставки

Для вставки значений в контейнер применяются итераторы вставки. Их называют также адаптерами, поскольку они преобразуют контейнер в итератор, т. е. адаптируют его к специальному использованию в алгоритмах вроде copy (). Имеется три вида итераторов вставки:
При вставке данных в контейнер может произойти перемещение уже находившихся там данных, из-за чего некоторые итераторы станут недействительными. Это может случиться в случае вектора, но не списков, в которых данные при вставке не смещаются.
В типичном случае адаптер вставки применяется после поиска некоторого значения, как показано в следующем примере.
////////////////////////////////////////////////////////////////
// Inserter.срр: Демонстрация итераторов вставки. //
#include
#include
#include
#pragma hdrstop
#include
using namespace.std;
int iArr[5] = (1, 2, 3, 4, 5);
//
// Функция вывода содержимого списка.
//
void Display(list
(
cout << label<< ": { ";
copy (1 .begin (), 1.end(),
ostream_iterator
cout << "}" << endl;
}
int main(void) {
list
// Копирование массива в список в обратном порядке:
copy(iArr, iArr + 5, front_inserter(iLst));
Display(iLst, "Before insertion");
// Поиск значения З:
list
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
advance(i, 2); // Сдвигает итератор на 2 позиции вперед.
С функцией distance () вы уже встречались в примере параграфа “Итераторы контейнеров”, где с ее помощью выяснялась позиция итератора по отношению к началу вектора. Эта функция определяет количество инкрементов, которые нужно выполнить для перехода от одного итератора к другому. Она перегружена:
template
difference_type distance(Forwardlterator first, Forwardlterator last) ;
template
void distance(Forwardlterator first,
Forwardlterator last. Distance &n) ;
В упомянутом примере функция применялась в своей первой, несколько пугающей, форме, но на деле она проще второй, устаревшей. Она возвращает расстояние между итераторами как свое значение. Вторая форма функции передает расстояние в третьем параметре. Функция аккумулирует значение в этом параметре, поэтому его требуется перед вызовом инициализировать:
int d = 0;
distance (iLst, i, d);
Карты и мультикарты
Карты являются ассоциативными контейнерами и очень похожи на множества за исключением того, что в картах с ключами можно связать объекты произвольного типа. Ключ, таким образом, может служить своего рода индексом, по которому можно получить доступ к ассоциированному объекту. Соответственно, на картах определена операция индексации.
Ключевые значения в картах должны быть уникальны, в мультикартах они могут повторяться. Данные хранятся сортированными по ключевым значениям.
В общем, работа с картами практически ничем не отличается от работы с множествами. Только объявление карты выглядит несколько по-другому, поскольку в нем нужно указать дополнительно тип ассоциированных объектов.
Создание карт
В объявлении карты (мультикарты) требуется указать три аргумента шаблона: тип ключа, тип ассоциированного значения и класс функционального объекта, которым будет определяться способ упорядочения ключей. Обычно для простоты типу карты присваивается новое имя:
typedef map
Ключи такого контейнера будут строками, а ассоциированные объекты — значениями типа double. Сортирована карта будет в соответствии с алфавитным порядком ключей.
Кроме того, иногда бывает удобно объявить имя для типа элемента карты (т. е. по сути структуры, состоящей из ключа и ассоциированного типа объекта; подобные типы определяются с помощью шаблона pair
typedef map type::value_type val_type;
Обычно создается пустая карта, а затем в нее вводятся элементы функцией insert () . Возможно также конструирование новой карты из части уже существующей. При этом конструктору передаются, как обычно, два итератора.
Действия над картами
Важнейшие действия, выполняемые с картами и мультикартами — это поиск и извлечение данных по заданному ключу. Вообще-то карты в этом смысле практически полностью аналогичны множествам и имеют те же функции-элементы. Ниже приводится программа, демонстрирующая вариант “записной книжки” на основе мультикарты, которая может хранить несколько телефонных номеров для одного и того же имени-ключа. Нечто подобное мы уже делали в главе 8, когда говорили о перегрузке операции индексации, но индекс должен быть уникальным, а мультикарта позволяет иметь повторяющиеся ключи.
///////////////////////////////////////////////////////
// Multimap.срр: Макет телефонной книжки на основе multimap.
//
#include
#include