Урок на тему Інформатика Тест

Про матеріал
Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот ААшот Ашот Ашот Ашот Ашот Ашот шот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот ААшот Ашот Ашот Ашот Ашот Ашот шот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот ААшот Ашот Ашот Ашот Ашот Ашот шот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот Ашот ААшот Ашот Ашот Ашот Ашот Ашот шот Ашот
Перегляд файлу

Герберт Шилдт:                     С++     базовый      курс

В   этой   книге описаны       все      основные    средства      языка            С++     -           от элементарных       понятий        до супервозможностей.            После            рассмотрения основ            программирования        на       C++     (переменных, операторов, инструкций управления,           функций,     классов        и          объектов)    читатель освоит           такие более            сложные      средства      языка,           как     механизм обработки   исключительных  ситуаций (исключений),           шаблоны,    пространства имен,            динамическая       идентификация     типов, стандартная         библиотека шаблонов    (STL), а          также            познакомится        с          расширенным       набором ключевых    слов,  используемых       в          .NET-программировании.         Автор справочника          общепризнанный            авторитет     в          области программирования        на       языках          C          и          C++,   Java    и          C#       включил в          текст  своей книги и          советы          программистам,   которые       позволят повысить эффективность          их        работы.

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

Примеры          программ    работают     со        всеми            компиляторами    C++,     включая       Visual C++

Оглавление

Глава           1.            Из           истории               создания             C++

Глава           2.            Обзор   элементов         языка    C++

Глава           3.            Основные          типы     данных

Глава           4.            Инструкции       управления

Глава           5.            Массивы             и             строки

Глава           б.            Указатели

Глава           7.            Функции,            часть     первая: основы

Глава           8.            Функции,            часть     вторая: ссылки,                перегрузка        и             использование       аргументов       по           умолчанию

Глава           9.            Еще       о             типах    данных и             операторах

Глава           10.          Структуры          и             объединения

Глава           11.          Введение           в              классы

Глава           12.          О             классах подробнее

Глава           13.          Перегрузка        операторов

Глава           14.          Наследование

Глава           15.          Виртуальные    функции              и             полиморфизм

Глава           16.          Шаблоны

Глава           17.          Обработка         исключительных            ситуаций

Глава           18.          С++        -              система               ввода-вывода

Глава           19.          Динамическая идентификация               типов    и             операторы        приведения      типа

Глава           20.          Пространства   имен     и             другие темы

Глава           21.          Введение           в              стандартную     библиотеку       шаблонов

Глава           22.          Препроцессор C++

Приложение           А.            С-ориентнрованная      система               ввода-вывода

Приложение           Б.            Использование               устаревшего     С++-компилятора

Приложение           В.            .NET-расширения           для        C++

Предметный           указатель

Об   авторе

Герберт Шилдт          (Herbert       Schildt)         —        признанный           авторитет     в области программирования   на       языках          С,        C++     Java    и          С#, профессиональный         Windows-программист, член комитетов   ANSI/ISO, принимавших        стандарт      для     языков          С          и          C++.   Продано      свыше 3 миллионов          экземпляров          его      книг.  Они    переведены           на       все самые           распространенные          языки мира.           Шилдт          —        автор таких бестселлеров,        как     Полный        справочник по       С,        Полный        справочник по C++,   Полный        справочник по       С#,      Полный        справочник по       Java   2,        и многих          других          книг, включая:      Руководство         для     начинающих          по C++,   С#:      A         Beginner’s    Guide и          Java   2:        A Beginner’s            Guide. Шилдт—      обладатель степени        магистра      в          области        вычислительной техники (университет     шт.      Иллинойс). Его      контактный             телефон       (в консультационном          отделе):       (217) 586-4683.

Введение

Цель       этой   книги —        научить        писать           программы на       C++     —     самом           мощном       языке программирования       наших           дней. Для     освоения     представленного здесь материала   никакого предыдущего     опыта            в          области        программирования        не       требуется.   Мы     начнем         с          азов,  знание которых    позволит      читателю     осилить     сначала        фундаментальные           понятия        языка,           а          затем и          его ядро.      Изучив          базовый       курс,  вы       справитесь  и          с          более     сложными  темами,        освоение     которых даст         вам    право            считать     себя   вполне          сложившимся        программистом    на       C++.

Язык       C++     —        это      ключ  к          современному      объектно-ориентированному         программированию. Он          создан          для     разработки высокопроизводительного     программного      обеспечения          и чрезвычайно популярен  среди            программистов.   Сегодня        быть  профессиональным программистом    высокого     класса           означает      быть  компетентным      в          C++.

Этот         язык  не       просто          популярен. Он      обеспечивает концептуальный  фундамент,            на который опираются  другие          языки программирования        и          многие         современные         средства обработки данных.        Не       случайно     ведь   потомками C++     стали такие почитаемые языки,           как     C# и    Java.

Поскольку        язык  C++     предназначен        для     профессионального программирования,       для изучения         он       не       самый           простой;       тем     не менее,          C++     —        самый           лучший         язык  для     изучения. Освоив            C++, вы       сможете       писать           профессиональные         высокопроизводительные программы. Кроме         того,  вы       сможете       легко изучить         такие языки программирования,       как     C#       и          Java, поскольку    они     используют            тот же       базовый       синтаксис    и          те        же       принципы   разработки.

Что      нового          в          третьем    издании

За время,          прошедшее            с          момента      выхода         предыдущего издания       этой   книги,           язык  C++     не претерпел         никаких        изменений. Однако         изменилась            вычислительная   среда.           Например, доминирующее положение в          Web-программировании          занял язык  Java,   появилась   система .NET    Framework  и          язык  С#.      Но       мощь C++     за        прошедшие несколько   лет      ничуть           не       убавилась. C++     был,   есть    и          еще    долго будет основным   языком         "классных" программистов.

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

Кроме    того,  добавлено  два     приложения.          В          одном           описаны определенные      компанией Microsoft     ключевые    слова,           которые используются        для     создания     управляемого       кода, предназначенного          для среды           .NET   Framework. В          другом         разъясняется,        как     адаптировать код, приведенный           в          этой   книге,            к          более            старым         и нестандартным     компиляторам.

Наконец,           все      примеры     программ    были протестированы   с использованием  таких компиляторов,     как     Visual Studio            .Net    (Microsoft)   и C++     Builder           (Borland).

О            версии          C++

         Материал    этой     книги     описывает     Standard      C++.      Эта     версия    C++     определена

Американским      национальным     институтом стандартов  (American    National Standards     Institute —  ANSI) и          Международной  организацией        по стандартизации    (International          Standards Organization   —        ISO)    в          качестве стандарта    для     C++,   который       поддерживается  практически всеми известными            компиляторами.  Поэтому,      используя   эту      книгу,            вы можете         быть  уверены в   том,   что      освоенное   вами  сегодня        непременно будет применено завтра.

Как        работать  с          этой книгой

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

Если      вы      работаете            под     управлением         Windows

Если        на       вашем           компьютере           установлена           система        Windows, и          ваша  цель   —        создание Windows-ориентированных        программ,  вы сделали       правильный           выбор,          решив           изучать         C++, поскольку     C++ —        это      родной         язык  для     Windows.     Однако         ни       в          одном           из примеров    этой   книги не       используется         Windows-интерфейс       GUI     (graphical user    interface       —        графический          интерфейс пользователя).       Все приведенные         здесь примеры     являются     консольными,       т.е. приложениями, запускаемыми        из        командной строки           (поскольку  не имеют           графического         интерфейса).          Дело в          том,   что      GUI-программы отличаются высокой       сложностью           и          большим     размером.  Кроме того, они     используют            множество  методов,      которые       напрямую   не       связаны с          языком         C++. Поэтому         они     не       совсем          подходят     для     обучения языку            программирования.       Однако         это      не мешает   использовать Windows-ориентированный    компилятор            для     компиляции           программ    из этой    книги,           поскольку   он       автоматически      создаст         консольный сеанс,            позволяющий выполнить        программу.

Освоив   C++,   вы       сможете       применить  свои   знания          к          Windows-программированию. Windows-программирование     на       основе          C++ позволяет    использовать         библиотеки            классов, например          MFC   или .NET   Framework, которые       значительно           упрощают   разработку Windowsпрограмм.

Программный         код     —        из        Web-пространства

Помните,        что    исходный    код     всех    программ,  приведенных         в этой книге,           можно загрузить           с          Web-сайта с          адресом: http://www.osborne.com.         Загрузка      кода  избавит      вас     от необходимости вводить      текст          программ   вручную.

Что      еще    почитать

Книга      C++:   базовый       курс   —        это      ваш    "ключ"          к          серии книг   по       программированию, написанных    Гербертом   Шилдтом.   Ниже перечислены         те        из        них,    которые       могут представлять для            вас интерес.

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

■  Полный        справочник             по       C++

■  Руководство           для     начинающих          по       C++

■  Освой            самостоятельно    C++     за        21       день

■  STL      Programming          from   the      Ground         Up

■  Справочник            программиста       пo       C/C++

Тем,        кого   интересует  программирование        на       языке            Java,   мы     рекомендуем        такие издания. ■  Java    2:        A         Beginner’s    Guide

■  Полный        справочник             по       Java    2

■  Java    2:        Programmer’s         Reference

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

■  С#:      A         Beginner’s    Guide

■  Полный        справочник             по       C#

Тем,        кто      интересуется          Windows-программированием,        мы      можем предложить           такие книги            Шилдта.

■  Windows      98       Programming          from   the      Ground         Up

■  Windows      2000  Programming          from   the      Ground         Up

■  MFC   Programming          from   the      Ground         Up

■  The     Windows      Annotated    Archives

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

■  Полный        справочник             по       С

■  Освой            самостоятельно    С          за        21       день

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

Ждем   ваших           отзывов!

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

Мы          ждем ваших            комментариев       и          надеемся     на       них.    Вы можете         прислать      нам    бумажное или      электронное           письмо         либо просто          посетить       наш    Web-cepвep            и          оставить       свои   замечания там. Одним          словом,        любым,        удобным     для     вас      способом    дайте нам знать,            нравится      или     нет вам        эта      книга,            а          также выскажите  свое   мнение         о          том,   как     сделать        наши книги более интересными         для     вас.

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

E-mail:    info@williamspublishing.com

WWW:   http:   //www.williamspublishing.     com Адреса            для     писем:

из России:         115419,     Москва,        а/я      783 из     Украины:     03150,     Киев, а/я      152

от         редактора FB2

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

   <<<тут  должно        быть  3          квадрата...  если   их        нет      то        код будет сдвинут        вправо          в нужных     местах           (ваше            устройство поддерживает       юникод).     Если   вы       их        видите          то        в          местах сдвига (там   где     они     отображаются)      пишите         пару   пробелов     (или   игнорируйте их)

Так           же       обращайте  внимание    на       построчный            коментрарий:        // на       маленьком экране          он может     разбиться    на       2          строки!

код          программы //коментарий

коментарий    может           разбиться    на       две     строки

1-я     строка-ком 2-я     строка-ентарий

в   этом   случае           компилятор            будет ругаться       на       команду      ентарий     в          компиляторе         он       будет

отображаться         другим         цветом          нежели

//ком

в   итоге программа небудет        запускаться.           Лечится        это      перемещением ентарий       на       строку           // ком

-> 1-я      строка           //коментарий

Настройка        отображения         кода: (для   того    чтобы            было удобее     читать           код     программ) AlReader2:

меню=> настройки=>          стили текста=>       код=>            настройте: выравнивание=К  левому         краю, цвет=(выберите цвет),           отступ слева=нет  отступа,    отступ           справа=нет           отступа,    отступ начала абзаца=        (убрать       галочку),     остальные   настройки   на       ваш    выбор.          В других          программах настройки аналогичны...

Так           как     каждый        компилятор            и          среда            разработки "по свойму         нарушает     стандарт      C++" (что-то            убирает        или     добавляет), а          так      же       из        за        развития      C++     с          момента      его      создания, некоторые   программы этой   книги нужно           писать           по       другому.      По этой   причине       я          привожу список   сайтов           на       которых       вы       сможете найти             ответы          на       интересующие      вас      вопросы: http://codenet.ru/ http://hashcode.ru http://rsdn.ru/

http://ci-plus-plus.blogspot.com/ http://programmersclub.ru/ http://cyberforum.ru/

Глава     1:     Из   истории создания      C++

Язык       C++     —        единственный       (из      самых           значительных)       язык программирования,       который может    освоить        любой           программист.        Это может           показаться  очень            серьезным  заявлением,           но оно          —        не преувеличение.    C++     —        это      центр             притяжения,           вокруг           которого "вращается"           всё современное  программирование.       Его      синтаксис    и принципы   разработки определяют           суть объектно-ориентированного программирования.       Более            того,  C++     проложил   "лыжню"     для разработки языков          будущего.   Например,  как     Java,   так      и          C#       —        прямые потомки       языка            C++. C++       также            можно          назвать         универсальным языком         программирования,       поскольку   он позволяет         программистам обмениваться        идеями.       Сегодня        быть  профессиональным программистом высокого     класса           означает      быть  компетентным      в          C++.   C++     —        это ключ  к современному   программированию.

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

Истоки           C++

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

Создание        языка           С

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

практикам.

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

Язык       С          изобрел       Дэнис            Ритчи (Dennis          Ritchie)          для компьютера           PDP-11          (разработка компании  DEC    —        Digital Equipment   Corporation),          который       работал        под    управлением операционной       системы       (ОС)   UNIX. Язык  С          —        это      результат     процесса разработки,            который сначала был    связан           с          другим         языком         — BCPL, созданным Мартином   Ричардсом  (Martin Richards). Язык  BCPL   индуцировал появление   языка,           получившего         название      В          (его    автор — Кен Томпсон       (Ken   Thompson)),            который       в          свою  очередь       привел          к разработке языка            С.        Это случилось       в          начале          70-х    годов.

На            протяжении            многих          лет      стандартом             для     языка            С де-факто      служил         язык, поддерживаемый           ОС      UNIX  и          описанный  в книге Брайана       Кернигана   (Brian Kernighan)   и Дэниса      Ритчи The     С Programming          Language      (Prentice-Hall,         1978).           Однако         формальное отсутствие   стандарта    стало причиной    расхождений         между          различными реализациями       языка С.       Чтобы           изменить     ситуацию,    в          начале лета   1983  г.         был    учрежден    комитет        по       созданию ANSI-стандарта, призванного          —        раз     и          навсегда      —        определить            язык  С. Конечная     версия этого          стандарта    была  принята        в          декабре       1989, а его      первая          копия            стала доступной   для желающих      в          начале 1900. Эта      версия          языка            С          получила     название      С89,   и          именно она     явилась фундаментом, на       котором       был    построен      язык  C++.

На            заметку:      Стандарт  С          был    обновлен     в          1999  году,  и          эта версия          языка            С          получила название         С99.   Новый           стандарт содержит  ряд     новых            средств,      причем         некоторые из        них позаимствованы            из        C++,   тем   не       менее            они     полностью совместимы         с          оригинальным стандартом С89.   Насколько  мне известно,   на       данный        момент       ни       один  из        широко доступных компиляторов     не       поддерживает    версию         С99,   и          по-прежнему версия          С89 определяет  то,     что    обычно         подразумевается           под языком         С.        Более            того, именно         стандарт С89     послужил    основой для     создания     языка            C++.   Вполне         возможно,  что    будущий стандарт языка C++     будет           включать   средства,   добавленные         версией С99,   но       пока  они     не       являются частью          C++.

Язык       С          часто называют    компьютерным    языком         "среднего    уровня". Применительно    к          С это  определение         не       имеет            негативного оттенка,        поскольку   оно     отнюдь         не       означает,     что      язык С           менее мощный       и          развитый     (по      сравнению  с          языками      "высокого   уровня") или     его      сложно использовать    (подобно     ассемблеру).          (Для   языка ассемблера            характерно символическое представление         реального машинного кода, который       может           выполнять  компьютер.)           С называют языком         среднего      уровня,         поскольку   он       сочетает       элементы    языков высокого     уровня (например           Pascal,           Modula-2     или     Visual Basic) с функциональностью       ассемблера.

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

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

языка     С.

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

Необходимо    упомянуть   еще    об       одном           аспекте         языка            С, который       очень            важен            для     C++:   С —    структурированный    язык. Самой           характерной           особенностью       структурированного       языка является использование      блоков.        Блок  —        это      набор            инструкций,            которые логически    связаны между    собой.           Например,  представьте            себе инструкцию            IF,       которая        при     успешной    проверке своего  выражения должна        выполнить  пять   отдельных  инструкций.            А         если   эти инструкции (специальным      образом)     сгруппировать       и          обращаться            к ним    как     к          единому      целому,        то        такая группа          образует      блок.

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

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

Предпосылки           возникновения     языка           C++

Приведенная  выше характеристика     языка            С          может           вызвать справедливое        недоумение: зачем        же       тогда,            мол,   был    изобретен язык  C++?  Если   С          —        такой хороший      и          полезный    язык, то        почему возникла     необходимость     в          чем-то           еще?  Оказывается,         все      дело  в сложности. На протяжении     всей   истории        программирования        усложнение программ    заставляло программистов     искать           пути,  которые       бы позволили  справиться  со        сложностью.          C++     можно считать      одним           из способов     ее        преодоления.        Попробуем лучше           раскрыть     эту взаимосвязь.

Отношение      к          программированию       резко изменилось            с          момента     изобретения           компьютера.

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

Первым широко        распространенным         языком         программирования был,   конечно       же, FORTRAN.         Несмотря     на       то        что      это      был    очень значительный        шаг     на       пути   прогресса    в          области программирования, FORTRAN      все      же       трудно          назвать         языком,       который       способствовал написанию  ясных            и          простых        для     понимания программ.  Шестидесятые годы  двадцатого столетия      считаются    периодом   появления   структурированного программирования.       Именно такой       метод            программирования        и          был реализован             в          языке            С.        С          помощью    структурированных языков программирования        можно          было писать           программы средней сложности, причем         без особых героических           усилий          со        стороны программиста.      Но       если   программный       проект достигал   определенного размера,      то        даже  с          использованием  упомянутых            структурированных методов       его      сложность   резко возрастала  и          оказывалась непреодолимой   для     возможностей программиста.           Когда (к        концу            70-х)         к          "критической"       точке подошло     довольно    много проектов,   стали рождаться   новые           технологии программирования.       Одна из        них     получила название      объектно-ориентированного          программирования        (ООП). Вооружившись      методами ООП,   программист         мог     справляться            с программами        гораздо        большего    размера,      чем    прежде. Но язык  С          не поддерживал        методов       ООП. Стремление            получить      объектно-ориентированную версию       языка            С          в          конце            концов          и привело       к          созданию    C++.

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

Рождение      C++

Итак,       C++     появился     как     ответ  на       необходимость     преодолеть            еще большую     сложность программ.    Он      был    создан          Бьерном      Страуструпом (Bjarne          Stroustrup)  в          1979  году   в          компании Bell        Laboratories            (г. Муррей-Хилл,       шт.      Нью-Джерси).        Сначала        новый           язык  получил       имя "С        с классами"            (С        with    Classes),        но       в          1983  году   он       стал называться C++.

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

Большинство  новшеств,    которыми    Страуструп  обогатил      язык  С,        было     предназначено     для поддержки    объектно-ориентированного     программирования.       По       сути,  C++     стал    объектноориентированной     версией        языка            С.        Взяв   язык  С          за        основу,         Страуструп     подготовил плавный переход            к          ООП. Теперь,         вместо          того,     чтобы            изучать         совершенно           новый           язык, Спрограммисту     достаточно было освоить        только          ряд     новых            средств,       и          он     мог     пожинать     плоды использования   объектно-ориентированной     технологии программирования.

Однако  в          основу          C++     лег      не       только          язык  С.        Страуструп утверждает,            что      некоторые объектно-ориентированные   средства      были инспирированы    другим         объектноориентированным   языком,       а          именно Simula67.     Таким            образом,     C++     представляет         собой симбиоз     двух мощных       методологий         программирования.

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

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

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

Эволюция      C++

С   момента      изобретения           C++     претерпел   три     крупных       переработки, причем         каждый        раз язык      как     дополнялся            новыми        средствами, так      и          в          чем-то           изменялся. Первой         ревизии        он       был подвергнут в          1985  году,  а          второй          —        в          1990. Третья           ревизия        имела место            в          процессе стандартизации,       который       активизировался  в начале          1990-х.         Специально            для     этого  был сформирован объединенный      ANSI/ISO-комитет             (я        был    его      членом),      который       25 января          1994 года    принял         первый         проект          предложенного    на рассмотрение        стандарта.   В          этот    проект          были включены   все      средства, впервые       определенные      Страуструпом,       и          добавлены новые.          Но       в целом           он       отражал       состояние    C++     на       тот      момент         времени.

Вскоре   после завершения            работы         над     первым        проектом     стандарта C++     произошло событие,      которое        заставило    значительно           расширить существующий      стандарт.     Речь   идет   о создании Александром         Степановым стандартной           библиотеки            шаблонов    (Standard     Template Library   — STL).   Как     вы       узнаете         позже,          STL      —        это      набор            обобщенных функций,     которые можно    использовать         для     обработки   данных.        Он довольно    большой      по       размеру.      Комитет ANSI/ISO            проголосовал        за включение  STL      в          спецификацию      C++.   Добавление            STL расширило сферу            рассмотрения        средств         C++     далеко          за        пределы исходного   определения языка.

Однако  включение  STL,    помимо        прочего,       замедлило  процесс стандартизации    C++, причем           довольно    существенно.         Помимо       STL,    в сам     язык  было добавлено  несколько   новых средств       и          внесено множество  мелких         изменений.             Поэтому       версия          C++     после рассмотрения комитетом        по       стандартизации    стала намного       больше         и сложнее       по       сравнению  с          исходным вариантом    Страуструпа.          Конечный результат     работы         комитета      датируется  14       ноября          1997 года,   а реально       ANSI/ISO-стандарт           языка            C++     увидел          свет    в          1998 году.  Именно        эта спецификация            C++     обычно         называется Standard       C++. И         именно         она     описана        в          этой   книге. Эта    версия          C++ поддерживается  всеми            основными С++-компиляторами,     включая       Visual C++ (Microsoft)   и          C++     Builder           (Borland).     Поэтому       код     программ, приведенных         в          этой   книге, полностью            применим   ко       всем современным        С++-средам.

Что      такое           объектно-ориентированное           программирование

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

Все           языки            объектно-ориентированного  программирования характеризуются  тремя общими      признаками:          инкапсуляцией, полиморфизмом и          наследованием.   Рассмотрим            кратко каждый     из        них (подробно  они     будут описаны       ниже  в          этой   книге).

Инкапсуляция

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

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

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

Полиморфизм

Полиморфизм            (от      греческого  слова polymorphism,       означающего "много          форм")         —        это свойство,         позволяющее        использовать один  интерфейс   для     целого          класса           действий. Конкретное   действие определяется        характерными       признаками            ситуации.     В          качестве простого      примера      полиморфизма     можно          привести      руль   автомобиля. Для    руля   (т.е. интерфейса)  безразлично,         какой тип     рулевого      механизма используется         в          автомобиле. Другим      словами,      руль   работает одинаково, независимо            от        того,  оснащен       ли       автомобиль рулевым управлением         прямого       действия      (без    усилителя), рулевым      управлением с усилителем         или     механизмом          реечной       передачи.    Если   вы       знаете, как     обращаться            с          рулем,          вы сможете            вести автомобиль любого         типа.  Тот      же       принцип       можно          применить  к программированию.      Рассмотрим,          например,   стек, или     список, добавление            и          удаление элементов      к          которому     осуществляется     по принципу    "последним           прибыл        —        первым обслужен".        У          вас может           быть  программа,            в          которой        используются        три различных  типа   стека. Один            стек    предназначен        для     целочисленных значений,    второй          —        для     значений     с          плавающей точкой         и третий           —        для     символов.   Алгоритм     реализации            всех    стеков           — один  и          тот      же, несмотря         на       то,       что      в          них     хранятся       данные различных  типов.           В          необъектноориентированном           языке программисту       пришлось    бы      создать         три     различных  набора подпрограмм обслуживания       стека,            причем         подпрограммы     должны       были бы иметь различные имена,          а          каждый        набор            —        собственный интерфейс.  Но       благодаря   полиморфизму в C++     можно          создать         один общий           набор            подпрограмм        (один интерфейс),            который       подходит для     всех    трех    конкретных            ситуаций.     Таким            образом,     зная,  как использовать         один  стек,   вы можете  использовать         все      остальные.

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

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

Наследование

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

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

C++        и          реализация            ООП

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

Связь    C++     с          языками     Java   и          C#

Вероятно,         многие         читатели      знают            о          существовании      таких языков          программирования,       как Java        и          С#.      Язык  Java    разработан в компании    Sun     Microsystems,         a          C#       —        в          компании    Microsoft. Поскольку   иногда          возникает    путаница      относительно         того,  какое отношение  эти      два     языка имеют          к          C++,   попробуем внести           ясность в          этот    вопрос.

C++          является       родительским       языком         для     Java    и          С#.      И         хотя разработчики        Java    и          C# добавили          к          первоисточнику,  удалили       из него   или     модифицировали            различные  средства,     в целом        синтаксис    этих трех    языков          практически           идентичен.  Более            того,  объектная   модель, используемая        C++,   подобна       объектным моделям     языков          Java    и          С#. Наконец,      очень            сходно общее       впечатление           и          ощущение   от использования      всех    этих    языков.        Это     значит,          что,    зная C++,     вы можете         легко изучить         Java    или     С#.      Схожесть      синтаксисов            и объектных   моделей      — одна         из        причин         быстрого     освоения     (и одобрения)            этих    двух   языков          многими      опытными   С++программистами. Обратная     ситуация      также            имеет            место:           если   вы       знаете           Java или     С#, изучение          C++     не       доставит      вам    хлопот.

Основное          различие     между          C++,   Java    и          C#       заключается           в типе   вычислительной   среды,          для которой           разрабатывался    каждый        из этих    языков.        C++     создавался с          целью           написания высокоэффективных программ,  предназначенных            для     выполнения           под    управлением определенной       операционной       системы       и          в          расчете         на       ЦП конкретного           типа.  Например,  если вы        хотите           написать высокоэффективную      программу для     выполнения           на       процессоре             Intel Pentium        под    управлением         операционной       системы       Windows,     лучше всего использовать         для этого     язык  C++.

Языки     Java    и          C#       разработаны          в          ответ  на       уникальные потребности           сильно распределенной           сетевой         среды,          которая может           служить        типичным    примером   современных вычислительных сред. Java    позволяет    создавать    межплатформенный      (совместимый       с несколькими          операционными   средами)     переносимый        программный       код для     Internet. Используя         Java,   можно          написать      программу,            которая будет выполняться          в          различных вычислительных   средах,         т.е.      в широком     диапазоне   операционных      систем          и          типов ЦП.     Таким образом, Java-пpoгpaммa    может           свободно     "бороздить просторами"          Internet.       C# разработан для    среды           .NET   Framework  (Microsoft), которая        поддерживает многоязычное программирование (mixed-language    programming)        и компонентно-ориентированный      код, выполняемый         в          сетевой         среде.

Несмотря          на       то        что      Java    и          C#       позволяют  создавать переносимый        программный       код, который         работает      в          сильно распределенной  среде,           цена   этой   переносимости     — эффективность. Java-пpoгpaммы   выполняются         медленнее,            чем    С++-программы.  То       же справедливо          и          для     С#.      Поэтому,      если   вы       хотите           создавать высокоэффективные      приложения, используйте       C++.   Если   же       вам    нужны переносимые        программы,           используйте           Java    или     С#.

И  последнее. Языки           C++,   Java    и          C#       предназначены    для     решения различных  классов задач.      Поэтому       вопрос          "Какой          язык  лучше?" поставлен    некорректно.         Уместнее     задать вопрос       по-другому:           "Какой язык  наиболее     подходит     для     решения      данной         задачи?".

Глава     2:     Обзор    элементов   языка    C++

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

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

Первая            С++-программа

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

/* Программа №1     -           Первая          С++-программа. Введите          эту     программу,            затем скомпилируйте     ее        и          выполните.

*/

#include <iostream> using       namespace     std;

//  main()            -           начало          выполнения     программы. int     main()

{

 cout    <<       "Это   моя    первая          С++-программа.";

 return            0;

}

Итак,      вы       должны       выполнить  следующие действия.

1.  Ввести           текст  программы.

2.  Скомпилировать  ее.

3.  Выполнить.

Исходный        код     —        это    текстовая форма          программы. Объектный код     —        это    форма программы,       которую     может выполнить            компьютер.

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

Ввод     текста        программы

Программы,    представленные   в          этой   книге,            можно          загрузить     с Web-сайта   компании Osborne          с          адресом:     www.osborne.com.          При желании      вы       можете         ввести           текст  программ вручную.        В          этом случае           необходимо           использовать         какой-нибудь        текстовый    редактор (например   WordPad),   а          не       текстовой    процессор   (word processor).   Дело  в том,   что      при     вводе текста          программ    должны       быть  созданы исключительно     текстовые    файлы,         а          не       файлы,         в которых    вместе с          текстом        сохраняется            информация          о          его      форматировании. Помните,     что информация   о          форматировании помешает    работе          С++-компилятора.

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

Компилирование    программы

Способ   компиляции           программы MyProg.срр зависит         от        используемого компилятора         и выбранных         опций.          Более            того,  многие компиляторы,       например    Visual C++     (Microsoft)   и          C++ Builder  (Borland), предоставляют     два     различных  способа        компиляции           программ:  с помощью компилятора            командной строки           и          интегрированной среды           разработки (Integrated Development           Environment           —        IDE). Поэтому       для     компилирования С++-программ       невозможно дать универсальные     инструкции,            которые       подойдут     для     всех    компиляторов. Это     значит,          что вы           должны       следовать    инструкциям,         приведенным в          сопроводительной          документации, прилагаемой к          вашему компилятору.

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

Чтобы     скомпилировать   программу МуРrog.срр,           используя   Visual C++, введите следующую      командную строку:

C:\...>cl  -GX     MyProg.cpp

Опция    -GX     предназначена     для     повышения             качества       компиляции. Чтобы           использовать компилятор       командной строки           Visual C++, необходимо           выполнить  пакетный     файл VCVARS32.bat,       который       входит в          состав           Visual C++.

Чтобы     скомпилировать   программу MyProg.срр,           используя   C++     Builder, введите        такую командную           строку:

С: \...>bcc32     MyProg.срр

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

На            заметку.      Если   при     попытке     скомпилировать первую программу вы       получили сообщение     об       ошибке,       но       уверены,      что ввели ее        текст          корректно,            то,     возможно,  вы используете   старую версию         С++-компилятора,         который     был    создан          до       принятия С++стандарта    ANSI/ISO.     В          этом случае          обратитесь          к приложению         Б          за        инструкциями     по использованию          старых компиляторов.

Выполнение  программы

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

С:\...>MyProg.срр

Результаты      выполнения           этой   программы таковы:

Это          моя    первая          С++-программа.

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

Необходимо    отметить,     что      все      эти      программы представляют        собой консольные приложения,        а          не       приложения,          основанные            на применении           окон, т.е.      они     выполняются         в сеансе       приглашения         на ввод  команды.    При    этом   вам,   должно        быть, известно,     что      язык  С++ не просто          подходит     для     Windows-программирования,            C++     —        основной язык, применяемый       в разработке          Windows-приложений.  Однако         ни одна  из        программ,  представленных   в          этой книге, не       использует графического         интерфейса             пользователя         (GUI   —        graphics        use interface). Дело     в          том,   что      Windows      —        довольно    сложная       среда для     написания   программ    включающая множество          второстепенных    тем,    не связанных   напрямую   с          языком         C++     В          то        же       время консольные приложения           гораздо        короче          графических           и          лучше           подходят для     обучения программированию.         Освоив         C++,   вы       сможете       без проблем      применить  свои   знания          в          сфере создания    Windows-приложений.

Построчный            "разбор        полетов"

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

Итак,  наша  программа начинается  с          таких строк.

/* Программа №1     -           Первая          С++-программа. Введите          эту     программу,            затем скомпилируйте     ее        и          выполните.

*/

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

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

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

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

#include <iostream>

В   языке            C++     определен  ряд     заголовков (header),      которые       обычно содержат     информацию, необходимую  для     программы.           В          нашу программу включен       заголовок    <iostream>  (он используется  для     поддержки в С++-системы          ввода-вывода),     который       представляет         собой внешний исходный    файл, помещаемый         компилятором      в          начало          программы с помощью директивы     #include.       Ниже в          этой   книге мы      ближе познакомимся      с          заголовками          и          узнаем, почему    они     так      важны.

Рассмотрим    следующую            строку           программы:

using       namespace   std;

Эта           строка           означает,     что      компилятор            должен        использовать пространство         имен  std. Пространства             имен  —        относительно         недавнее дополнение           к          языку            C++.   Подробнее о          них     мы поговорим позже,          а          пока   ограничимся          их        кратким       определением. Пространство    имен (namespace)            создает         декларативную     область,       в которой        могут размещаться          различные элементы     программы. Пространство         имен  позволяет    хранить        одно  множество  имен отдельно     от другого.       Другими      словами,      имена,          объявленные         в          одном пространстве         имен,            не будут       конфликтовать      с          такими          же именами,    объявленными     в          другом.        Пространства         имен позволяют упростить    организацию         больших      программ.  Ключевое    слово using информирует компилятор      об       использовании     заявленного           пространства имен  (в        данном        случае           std). Именно          в          пространстве         имен  std объявлена   вся      библиотека             стандарта    C++.   Таким            образом, используя пространство         имен  std,     вы       упрощаете  доступ          к          стандартной библиотеке             языка.

Очередная       строка           в          нашей           программе представляет         собой     однострочный     комментарий.

//  main()            -           начало          выполнения           программы.

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

int            main()

Как          сообщается             в          только          что      рассмотренном    комментарии, именно         с          этой   строки           и начинается          выполнение           программы.

С  функции      main()           начинается           выполнение           любой           С++-программы.

Все           С++-программы   состоят         из        одной           или     нескольких функций. (Под  функцией    main понимаем    подпрограмму.)   Каждая         С++-функция имеет            имя,   и          только          одна  из        них     (её      должна включать каждая         С++-программа)   называется main().          Выполнение           C++ программы начинается             и          заканчивается       (в        большинстве          случаев) выполнением        функции       main().          (Точнее, С++-программа          начинается  с вызова          функции       main()           и          обычно         заканчивается       возвратом   из функции       main().)         Открытая     фигурная     скобка          на       следующей (после int       main())          строке указывает на       начало          кода  функции       main(). Ключевое    слово int       (сокращение          от        слова integer), стоящее  перед именем        main(),          означает      тип     данных         для     значения,    возвращаемого функцией main(). Как     вы       скоро узнаете,        C++     поддерживает       несколько встроенных             типов данных,        и          int —  один  из        них.

Рассмотрим    очередную строку           программы:

cout         <<       "Это   моя    первая          С++-программа.";

Это           инструкция вывода         данных         на       консоль.      При    ее выполнении           на       экране          компьютера отобразится         сообщение  Это   моя первая          С++-программа.. В          этой   инструкции используется оператор вывода "<<".  Он      обеспечивает         вывод           выражения,            стоящего      с          правой стороны,      на устройство,       указанное   с          левой.           Слово            cout представляет         собой            встроенный            идентификатор (составленный         из частей           слов   console          output),        который       в          большинстве          случаев означает экран     компьютера.          Итак,  рассматриваемая             инструкции обеспечивает         вывод           заданного сообщения   на       экран.           Обратите внимание    на       то,       что      эта      инструкция завершается           точкой          с запятой. В          действительности            все      выполняемые       С++-инструкции    завершаются точкой          с

запятой.

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

return     0;

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

Закрывающая фигурная     скобка          в          конце            программы формально завершает   ее.      Хотя фигурная       скобка          в          действительности            не является       частью          объектного кода  программы,           её "выполнение" (т.е. обработку   закрывающей        фигурной     скобки          функции       main()) мысленно можно            считать         концом         С++-программы.  И         в          самом деле, если   в          этом   примере      программы инструкция            return отсутствовала        бы,     программа автоматически      завершилась          бы      по достижении            этой   закрывающей        фигурной     скобки.

Обработка   синтаксических   ошибок

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

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

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

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

Вторая           С++-программа

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

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

//  Программа №2     -           Использование     переменной. #include    <iostream> using     namespace   std;

int    main() {

 int       x;         //         Здесь объявляется           переменная.

 x          =          1023; //         Здесь переменной           х          присваивается       число     1023.

 cout    <<       "Эта    программа выводит       значение      переменной           х:     ";  cout      <<       х;         //         Отображение         числа 1023.

 return            0;

}

Что          же       нового          в          этой   программе?           Во-первых,  инструкция:

int            х;         //         Здесь объявляется           переменная.

объявляет        переменную          с          именем        х          целочисленного   типа.  В     C++     все      переменные           должны

быть   объявлены  до       их        использования.    В          объявлении            переменной помимо        ее        имени необходимо        указать,        значения      какого           типа   она может           хранить.       Тем    самым          объявляется           тип переменной.            В данном        случае           переменная            х          может           хранить        целочисленные значения,    т.е. целые    числа,           лежащие      в          диапазоне   -32      768--32         767. В          C++     для     объявления            переменной целочисленного            типа достаточно поставить    перед            ее        именем        ключевое    слово int.      Таким образом,      инструкция int       х;         объявляет   переменную          х          типа   int. Ниже вы       узнаете,        что      C++ поддерживает          широкий      диапазон встроенных             типов переменных.          (Более           того,  C++     позволяет программисту       определять             собственные          типы  данных.)

Во-вторых,       при     выполнении           следующей инструкции переменной присваивается конкретное      значение:

х   =          1023; //         Здесь переменной           х          присваивается       число 1023.

В   C++     оператор   присваивания       представляется     одиночным            знаком равенства    (=).     Его действие          заключается           в          копировании         значения, расположенного  справа          от        оператора,  в переменную,      указанную  слева от него.  После            выполнения           этой   инструкции присваивания переменная     x будет содержать   число 1023.

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

cout         <<       х;         //         Отображение         числа 1023.

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

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

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

//  Эта      программа преобразует           галлоны       в     литры. #include     <iostream> using   namespace   std;

int    main() {

 int       gallons,          liters;

 cout    <<       "Введите      количество галлонов:";

  cin  >>       gallons;          //         Ввод  данных         от        пользователя.

 liters   =          gallons           *          4;        //         Преобразование  в          литры.

 cout    <<       "Литров:      "          <<       liters;

 return            0;

}

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

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

Для         приема         значения,    вводимого  пользователем,    используется     следующая инструкция:

cin            >>       gallons;          //         Ввод  данных         от        пользователя.

Здесь      применяется          еще    один  встроенный            идентификатор     —        cin —        предоставляемый           С ++-компилятором.      Он      составлен    из        частей слов   console          input  и          в          большинстве          случаев означает ввод  данных с          клавиатуры.           В          качестве       оператора   ввода            используется символ         ">>". При     выполнении           этой   инструкции значение,    введенное пользователем     (которое      в          данном случае      должно        быть целочисленным),             помещается            в          переменную,         указанную  с правой стороны   от        оператора   ">>"   (в        данном        случае           это переменная            gallons). В    этой   программе заслуживает           внимания    и          эта инструкция:

cout         <<       "Литров:      "          <<       liters;

Здесь      интересно   то,       что      в          одной           инструкции использовано сразу два     оператора   вывода         " <<". При    ее        выполнении           сначала будет выведена    строка           "Литров:    ",         а          за        ней     —        значение переменной           liters.  В          общем          случае           в          одной           инструкции Можно         соединять   любое           количество операторов            вывода, предварив  каждый        элемент        вывода         "своим"        оператором

Новый  тип   данных

Несмотря          на       то        что      для     приблизительных            подсчетов   вполне сгодится       рассмотренная выше    программа преобразования   галлонов     в литры,          для     получения   более            точных          результатов ее      необходимо переделать.            Как     отмечено     выше,            с          помощью    целочисленных типов данных невозможно      представить           значения      с          дробной      частью. Для    них     нужно           использовать         один  из типов       данных         с плавающей             точкой,         например    double           (двойной     точности).    Данные этого типа   обычно         находятся    в          диапазоне   1,7Е-308--1,7Е+308. Операции,   вы-полняемые      над числами          с          плавающей             точкой, сохраняют   любую          дробную      часть  результата  и, следовательно, обеспечивают        более            точное          преобразование.

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

/* Эта      программа преобразует           галлоны       в          литры            с          помощью            чисел с плавающей         точкой.

*/ #include     <iostream> using     namespace   std;

int    main() {

 double           gallons,          liters;

 cout    <<       "Введите      количество галлонов:    ";

  cin  >>       gallons;          //         Ввод  данных         от        пользователя.

 liters   =          gallons           *          3.7854;         //         Преобразование  в          литры.

 cout    <<       "Литров:      "          <<       liters;

 return            0;

}

Для          получения   этого  варианта      в          предыдущую         программу было внесено        два     изменения. Во-первых,            переменные           gallons          и liters   объявлены  на       этот    раз     с          использованием  типа   double. Во-вторых, коэффициент         преобразования   задан в          виде  числа 3.7854,         что позволяет    получить более    точные          результаты.            Если   С++-компилятор встречает     число,           содержащее           десятичную точку,          он       автоматически воспринимает       его      как     константу    с          плавающей             точкой.         Обратите также внимание    на       то,       что      инструкции cout    и          cin       остались      такими же,     как     в          предыдущем варианте программы,           в          которой использовались    переменные           типа   int.      Это     очень            важный момент:  С++-система        ввода-вывода       автоматически      настраивается        на       тип     данных, указанный в           программе.

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

литра.

Повторим     пройденное

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

1.   Каждая         С++-программа    должна        иметь            функцию      main(),            которая        означает      начало выполнения        программы.

2.   Все     переменные           должны       быть  объявлены  до       их        использования.

3.   C++     поддерживает       различные  типы  данных,        включая       целочисленные            и          с          плавающейточкой.

4.   Оператор     вывода         данных         обозначается         символом   "<<",  а          при            использовании     в инструкции         cout    он       обеспечивает         отображение            информации          на       экране          компьютера.

5.   Оператор     ввода            данных         обозначается         символом   ">>",  а          при            использовании     в инструкции         cin       он       считывает    информацию         с            клавиатуры.

6.   Выполнение           программы завершается           с          окончанием           функции            main().

Функции

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

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

Функции          —        это    "строительные  блоки"          С++-программы.

В   C++     ни       одна  функция       не       может           быть  встроена      в          другую. В          отличие        от        таких языков программирования,   как     Pascal,           Modula-2 и          некоторых   других,         которые       позволяют использование      вложенных функций,     в          C++     все      функции       рассматриваются как     отдельные компоненты.          (Безусловно,          одна  функция       может           вызывать     другую.)

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

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

Поскольку       функции       образуют     фундамент  C++,   займемся    ими     вплотную.

Программа   с          двумя           функциями

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

/* Эта      программа содержит     две     функции:     main()            и          myfunc().

*/ #include     <iostream> using     namespace   std;

void         myfunc();     //         прототип      функции     myfunc() int main()

{

 cout    <<       "В        функции       main().";

 myfunc();      //         Вызываем   функцию      myfunc().

 cout    <<       "Снова          в          функции       main().";

 return            0;

} void     myfunc()     {

 cout    <<       "          В          функции       myfunc().     ";

}

Программа      работает      следующим            образом.     Вызывается            функция main()           и          выполняется          ее первая    cout-инструкция.  Затем            из функции       main()           вызывается             функция       myfunc().     Обратите внимание на       то,       как     этот    вызов            реализуется            в          программе: указывается           имя    функции       myfunc, за   которым      следуют       пара   круглых скобок          и          точка с          запятой.       Вызов            любой           функции представляет         собой            С++-инструкцию   и          поэтому       должен завершаться           точкой          с          запятой. Затем      функция       myfunc() выполняет  свою  единственную       cout-инструкцию  и          передает управление назад функции       main(),          причем         той     строке           кода, которая расположена непосредственно        за        вызовом      функции.     Наконец,      функция main()           выполняет  свою  вторую cout-инструкцию,         которая        завершает   всю программу.            Итак,  на       экране          мы      должны       увидеть такие результаты.

В   функции       main().

В   функции       myfunc().

Снова     в          функции       main().

В  этой   программе необходимо           рассмотреть           следующую     инструкцию.

void         myfunc();     //         прототип      функции       myfunc()

Прототип      объявляет функцию     до       ее        первого        использования.

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

Как          видите,         функция       myfunc()       не       содержит     инструкцию return.           Ключевое    слово void, которое         предваряет как     прототип,    так      и определение         функции       myfunc(),     формально заявляет о   том,   что      функция myfunc()       не       возвращает             никакого      значения.    В          C++     функции,     не возвращающие     значений,    объявляются          с          использованием  ключевого слова void.

Аргументы  функций

Функции            можно          передать      одно  или     несколько   значений.    Значение, передаваемое функции,          называется аргументом.        Несмотря     на       то        что в          программах,          которые       мы рассматривали          до       сих      пор,   ни одна  из        функций       (ни      main(),          ни       myfunc())     не       принимала никаких значений,    функции       в          C++     могут принимать  один  или     несколько аргументов. Верхний     предел         числа принимаемых       аргументов определяется конкретным           компилятором.

Согласно      стандарту    C++     он       равен            256.

Аргумент         —        это    значение,    передаваемое       функции      при     вызове.

Рассмотрим     короткую     программу,            которая        для     отображения абсолютного          значения числа    использует  стандартную          библиотечную       (т.е. встроенную)           функцию      abs(). Эта      функция принимает       один  аргумент, преобразует           его      в          абсолютное            значение      и          возвращает результат.

//  Использование     функции       abs().

#include <iostream> #include <cstdlib> using     namespace   std;

int    main() {

 cout    <<       abs(-10);

 return            0;

}

Здесь      функции       abs()  в          качестве       аргумента   передается число -10. Функция       abs() принимает   этот    аргумент      при     вызове          и          возвращает его      абсолютное            значение,    которое        в          свою очередь        передаётся инструкции cout    для     отображения         на       экране          абсолютного          значения числа -10.    Дело  в          том,   что      если   функция       является       частью выражения,            она     автоматически вызывается     для     получения   возвращаемого ею      значения.    В          данном        случае           значение, возвращаемое         функцией abs(), оказывается           справа          от        оператора   "<<"   и          поэтому       законно отображается         на       экране.

Обратите          также            внимание    на       то,       что      рассматриваемая     программа включает     заголовок

<cstdlib>.      Этот    заголовок    необходим для     обеспечения          возможности вызова          функции       abs(). Каждый        раз,    когда вы       используете библиотечную       функцию,    в          программу необходимо включать соответствующий             заголовок.  Заголовок,  помимо        прочей          информации, содержит прототип        библиотечной       функции.

Параметр         —        это    определяемая       функцией    переменная,          которая принимает передаваемый    функции      аргумент.

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

void         mul     (int      х,     int       у) {

 cout    <<       х          *          у          <<       "          ";

}

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

Рассмотрим     следующую            короткую     программу,            которая демонстрирует     использование функции          mul().

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

#include <iostream> using     namespace   std; void     mul(int          x,         int     у);       //         Прототип     функции       mul().

int    main() {

 mul     (10,    20);

 mul     (5,       6);

 mul     (8,       9);

 return            0;

} void      mul(int          x,     int       y) {

 cout    <<       x          *          у          <<       "          ";

}

Эта           программа выведет       на       экран числа 200,   30       и          72.      При вызове          функции       mul()  С++компилятор    копирует      значение      каждого аргумента   в          соответствующий             параметр.    В          данном случае      при первом         вызове          функции       mul()  число 10       копируется в          переменную х,         а          число 20       — в    переменную          у.         При    втором         вызове          5 копируется в          х,         а          6          —        в          у.         При    третьем        вызове          8 копируется  в          х,         а          9          —        в          у.

Если        вы       никогда        не       работали     с          языком         программирования, в          котором       разрешены параметризованные      функции,     описанный  процесс может           показаться  несколько   странным. Однако          волноваться           не стонт:            по       мере  рассмотрения        других          С++-программ       принцип использования      функций,     их        аргументов и          параметров            станет более            понятным.

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

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

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

В   C++     многие         библиотечные       функции       возвращают           значения. Например,  уже    знакомая вам        функция       abs()  возвращает             абсолютное значение      своего           аргумента.  Функции, написанные   программистом,  также могут возвращать             значения.    В          C++     для     возврата значения используется         инструкция return.           Общий          формат         этой   инструкции таков:

return     значение;

Нетрудно          догадаться, что      здесь элемент        значение     представляет собой            значение, возвращаемое         функцией.

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

//  Демонстрация       возврата      функциями значений. #include <iostream> using   namespace   std;

int            mul     (int      x,         int       у);       //         Прототип     функции       mul().

int    main() {

 int       answer;

 answer          =          mul     (10,    11);    //         Присваивание       значения,            возвращаемого функцией.

 cout    <<       "Ответ           равен"          <<       answer;

 return            0;

}

//  Эта      функция       возвращает     значение. int          mul     (int      х,         int     у) {

 return            х          *          у;         //         Функция       возвращает             произведение     х          и          у.

}

В  этом   примере      функция       mul()  возвращает             результат     вычисления     выражения х*у     с помощью инструкции return.           Затем            значение     этого  результата  присваивается       переменной answer.      Таким            образом,     значение,    возвращаемое      инструкцией           return,           становится     значением функции       mul()  в          вызывающей         программе.

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

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

//  Устаревший            способ          записи          функции       mul().

mul          (int      X,        int       у)        /*        По       умолчанию в          качестве       типа            значения, возвращаемого       функцией,   используется         тип     int.*/

{

 return            х          *          у;         //         Функция       возвращает             произведение     х          и          у.

}

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

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

Функция         main()

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

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

Общий формат      С++-функций

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

тип_возвращаемого_значения   имя    (список_параметров)     { . .//            тело   метода .      

}

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

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

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

В   фигурные    скобки          заключено  тело   функции.     Тело   функции составляют С++инструкции,    которые       определяют           действия      функции. Функция       завершается           (и        управление передается            вызывающей процедуре) при     достижении            закрывающей        фигурной     скобки          или инструкции return.

Некоторые   возможности      вывода         данных

До            сих      пор     у          нас     не       было потребности           при     выводе данных         обеспечивать         переход       на следующую      строку.          Однако такая необходимость     может           потребоваться       очень            скоро.           В          C++ последовательность       символов    "возврат    каретки/перевод            строки" генерируется         с          помощью символа         новой            строки.       Для    вывода этого  символа       используется         такой код:   \n        (символ обратной           косой черты            и          строчная      буква n).       Продемонстрируем        использование последовательности       символов    "возврат    каретки/перевод            строки"       на примере      следующей программы.

/* Эта      программа демонстрирует     \n-последовательность,            которая обеспечивает         переход       на       новую           строку.

*/ #include     <iostream> using     namespace   std;

int    main() {

 cout    <<       "один\n";

 cout    <<       "два\n";

 cout    <<       "три";

 cout    <<       "четыре";

 return            0;

}

При         выполнении           программа генерирует  такие результаты:

один два

тричетыре

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

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

Для          рассмотрения        более            реальных     примеров    программ    нам необходимо           познакомиться      с двумя        С++-инструкциями:         if          и          for. (Более           подробное  их        описание     приведено  ниже  в          этой книге.)

Инструкция if

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

Инструкция      if          в          C++     действует    подобно      инструкции IF, определенной       в          любом          другом языке        программирования.       Её простейший            формат         таков:

if(условие)       инструкция;

Здесь      элемент        условие        —        это      выражение,            которое        при вычислении            может           оказаться равным           значению    ИСТИНА        или ЛОЖЬ.          В          C++     ИСТИНА        представляется     ненулевым значением,             а ЛОЖЬ            —        нулем.          Если   условие,       или     условное     выражение, истинно,       элемент инструкция       выполнится,           в          противном  случае           — нет.    При    выполнении           следующего фрагмента            кода  на       экране отобразится            фраза            10       меньше        11.

if(10        <          11)      cout    <<       "10     меньше        11";

Такие      операторы  сравнения,  как     "<"      (меньше)     и          ">="   (больше       или равно),         используются во  многих          других          языках          программирования. Но       следует        помнить,      что      в          C++     в          качестве оператора        равенства применяется          двойной       символ         "равно"        (==).   В          следующем примере cout-инструкция        не       выполнится,           поскольку   условное выражение дает   значение      ЛОЖЬ. Другими   словами,      поскольку   10       не равно            11,      cout-инструкция   не       отобразит    на       экране приветствие.

if(10==11)         cout    <<       "Привет";

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

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

//  Эта      программа демонстрирует     использование      if-инструкции. #include <iostream> using   namespace   std;

int    main() {

 int       a,b;

 cout    <<       "Введите      первое          число:           ";

  cin  >>       a;

 cout    <<       "Введите      второе          число:           ";

  cin  >>       b;

 if(a      <          b)        cout    <<       "Первое       число меньше        второго.";

 return            0;

}

Цикл     for

for           —        одна  из        циклических           инструкций,          определенных       в     C++.

Цикл       for      повторяет    указанную  инструкцию            заданное     число раз. Инструкция             for      в          C++ действует        практически           так      же,     как инструкция FOR,   определенная       в          таких языках программирования,    как Java,   С#,      Pascal            и          BASIC.            Ее        простейший            формат         таков:

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

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

Например,       следующая программа с          помощью    цикла            for      выводит на       экран числа от        1          до 100.

//  Программа демонстрирует     использование      for-цикла. #include <iostream> using   namespace   std;

int    main() {

 int       count;

 for(count=1;            count<=100;           count=count+1)

  cout           <<       count <<       "          ";

 return            0;

}

На            рис.    2.1      схематично             показано     выполнение           цикла            for     в          этом   примере.     Как     видите, сначала   переменная            count     инициализируется           числом         1.        При    каждом        повторении     цикла проверяется          условие        count<=100.           Если   результат     проверки     оказывается           истинным,   coutинструкция     выводит       значение     переменной           count,            после чего   ее        содержимое          увеличивается на            единицу.      Когда значение      переменной           count превысит     100,     проверяемое         условие        даст   в результате           ЛОЖЬ,           и     выполнение           цикла            прекратится.

В   профессионально            написанном           С++-коде     редко            можно встретить     инструкцию count=count+1,   поскольку   для     инструкций такого рода  в          C++     предусмотрена     специальная сокращенная      форма:         count++. Оператор     "++"   называется оператором         инкремента.         Он увеличивает операнд       на       единицу.      Оператор     "++"   дополняется          оператором            "--" (оператором декремента),  который       уменьшает  операнд       на       единицу.      С помощью    оператора   инкремента используемую    в          предыдущей программе инструкцию            for      можно          переписать следующим образом.

for(count=1;     count<=100;           count++)       cout    <<       count <<       "          ";

Блоки   кода

Поскольку        C++     —        структурированный        (а        также            объектно-ориентированный)   язык, он поддерживает            создание     блоков          кода.     Блок  —        это      логически    связанная    группа          программных инструкций,     которые       обрабатываются   как     единое         целое.           В          C++     программный       блок  создается путем   размещения           последовательности     инструкций между          фигурными (открывающей      и закрывающей)     скобками.   В          следующем            примере if(х<10)  {

 cout    <<       "Слишком   мало, попытайтесь           еще    раз.";

  cin  >>       х;

}

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

Блок       —        это    набор            логически   связанных   инструкций.

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

//  Программа демонстрирует     использование      блока     кода. #include        <iostream> using   namespace   std;

int    main() {

 int       a,         b;

 cout    <<       "Введите      первое          число:           ";         cin     >>       a;  cout      <<       "Введите      второе          число:     ";         cin       >>       b;

 if(a      <          b)        {

  cout           <<       "Первое       число меньше        второго.\n";

  cout           <<       "Их     разность      равна:           "          <<       b-a;

 }

 return            0;

}

Эта           программа предлагает пользователю       ввести           два     числа с клавиатуры.           Если   первое число         меньше        второго,       будут выполнены обе cout-инструкцин.  В          противном  случае           обе     они будут    опущены.    Ни       при каких условиях      не       выполнится            только          одна  из        них.

Точки    с          запятой      и          расположение      инструкции

В   C++     точка с          запятой        означает      конец            инструкции.            Другими словами,      каждая         отдельная инструкция   должна        завершаться           точкой с          запятой.

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

Язык       C++     не       воспринимает       конец            строки           в          качестве признака     конца            инструкции.            Поэтому       для     компилятора         не имеет            значения,    в          каком            месте строки           располагается инструкция. Например,            с          точки зрения          С++-компилятора, следующий            фрагмент     кода х           =          у; у     =          у+1;

mul(x,     у); аналогичен     такой строке:

x   =          у;         у          =          у+1;    mul(x,            у);

Практика      отступов

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

Ключевые      слова            C++

В   стандарте    C++     определено            63       ключевых    слова.           Они    показаны в          табл.  2.1.     Эти ключевые       слова (в        сочетании    с          синтаксисом операторов             и          разделителей)       образуют определение             языка            C++. В          ранних          версиях        C++     определено            ключевое    слово overload,      но теперь           оно     устарело.

Следует иметь            в          виду, что      в          C++     различается            строчное      и прописное  написание   букв. Ключевые    слова не       являются     исключением,       т.е. все      они     должны       быть  написаны    строчными буквами.      Например,  слово RETURN         не       будет распознано             в          качестве       ключевого  слова return.

Идентификаторы            в          C++

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

В   C++     нельзя          использовать         в          качестве       идентификаторов ключевые    слова.           Нельзя          же использовать  в          качестве идентификаторов            имена           стандартных           функций       (например  abs). Помните,     что      идентификатор     не       должен        начинаться  с          цифры.         Так, 12х— недопустимый идентификатор.        Конечно,      вы       вольны         называть переменные           и          другие          программные        элементы по          своему усмотрению,          но       обычно         идентификатор     отражает      н          чение            или смысловую характеристику    элемента,    которому     он       принадлежит.

Стандартная         библиотека          C++

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

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

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

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

Помимо            библиотеки            функций,     каждый        С++-компилятор   также содержит     библиотеку классов,    которая        является       объектно-ориентированной            библиотекой.         Наконец,      в          C++ определена стандартная           библиотека             шаблонов    (Standard     Template      Library— STL).   Она предоставляет         процедуры "многократного    использования",  которые можно          настраивать            в соответствии       с          конкретными         требованиями. Но       прежде         чем    применять  библиотеку             классов или            STL,    нам необходимо           познакомиться      с          классами,    объектами  и          понять,         в чем    состоит суть           шаблона.

Глава     3:     Основные    типы      данных

Как          вы       узнали          из        главы 2,        все      переменные           в          C++ должны       быть  объявлены  до       их использования.           Это     необходимо           для компилятора,        которому     нужно           иметь            информацию         о типе данных,        содержащихся      в          переменных.          Только          в          этом   случае компилятор            сможет надлежащим     образом       скомпилировать   инструкции, в          которых       используются        переменные.         В C++ определено            семь основных     типов данных:        символьный,         символьный           двубайтовый, целочисленный,   с          плавающей            точкой,       с          плавающей            точкой двойной       точности, логический (или   булев)           и          "не     имеющий значения".  Для    объявления            переменных           этих    типов используются ключевые    слова charwchar_t,       int,      floatdouble,          bool    и          void соответственно. Типичные      размеры      значений     в          битах и          диапазоны представления      для     каждого       из        этих    семи типов приведены в          табл.  3.1. Помните,     что      размеры      и          диапазоны,            используемые       вашим компилятором,     могут отличаться  от        приведенных         здесь.            Самое большое      различие существует      между          16-      и          32-разрядными    средами: для     представления      целочисленного значения       в          16-разрядной        среде используется,        как     правило,      16       бит,    а          в          32-разрядной        —        32.

Переменные   типа   char   используются        для     хранения     8-разрядных ASCII-символов (например      букв   А,        Б          или     В)        либо  любых          других 8-разрядных          значений.    Чтобы           задать           символ, необходимо заключить   его      в          одинарные кавычки.      Тип     wchar_t        предназначен        для хранения символов,       входящих    в          состав           больших      символьных наборов.      Вероятно,    вам    известно,     что      в некоторых           естественных языках          (например  китайском) определено            очень            большое количество символов,   для     которых       8-разрядное           представление      (обеспечиваемое типом            char) весьма           недостаточно.       Для    решения      проблем      такого рода  в          язык  C++     и          был    добавлен     тип wchar_t,           который       вам пригодится,            если   вы       планируете выходить     со        своими         программами на международный        рынок.

Переменные   типа   int       позволяют  хранить        целочисленные    значения      (не содержащие дробных   компонентов).       Переменные          этого  типа   часто используются        для     управления циклами     и          в          условных     инструкциях. К          переменным          типа   float   и          double           обращаются           либо  для обработки   чисел с          дробной      частью,         либо  при     необходимости выполнения           операций     над очень    большими   или     очень            малыми числами.      Типы  float   и          double           различаются          значением наибольшего (и        наименьшего)       числа,           которые       можно          хранить        с          помощью переменных           этих типов.  Как     показано     в          табл.  3.1,     тип     double           в C++     позволяет    хранить        число,           приблизительно в           десять           раз превышающее      значение      типа   float.

Тип          bool    предназначен        для     хранения     булевых       (т.е.    ИСТИНА/ЛОЖЬ) значений.    В          C++ определены  две     булевы         константы:  true    и          false, являющиеся           единственными    значениями, которые    могут иметь переменные           типа   bool.

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

Объявление  переменных

Общий   формат         инструкции объявления            переменных           выглядит     так:

тип          список_переменных;

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

int            i,          j,     k; char     ch,      chr; float        f,     balance;

double    d;

В  C++     имя    переменной           никак            не       связано        с          ее        типом.

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

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

Локальные    переменные

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

#include <iostream> using     namespace   std;

void     func(); int     main() {

 int       x;         //         Локальная  переменная            для     функции       main().

 х          =          10;

 func();

 cout    <<       "\n";

 cout    <<       x;         //         Выводится  число 10.

 return            0;

} void     func() {

 int       x;         //         Локальная  переменная            для     функции       func().

 x          =          -199;

 cout    <<       x;         //         Выводится  число -199.

}

Локальная       переменная           известна    только        функции,     в          которой     она     определена.

В  этой   программе целочисленная     переменная            с          именем        х     объявлена   дважды:      сначала        в функции   main(),          а          затем в     функции       func().           Но       переменная            х          из        функции       main()     не       имеет никакого    отношения  к          переменной           х          из        функции     func().           Другими      словами,      изменения, которым     подвергается     переменная            х          из        функции       func(),           никак            не     отражаются            на       переменной х       из        функции       main().          Поэтому     приведенная          выше программа выведет       на       экран числа -199   и 10.

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

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

Формальные            параметры

Формальный  параметр    —        это    локальная  переменная,          которая получает    значение аргумента,   переданного          функции.

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

int            funс1 (int      first,   int       last,    char    ch)

{

 .

 .

 .

}

Здесь      функция       funс1()          имеет            три     параметра   с          именами first,   last     и          ch.      С          помощью    такого объявления          мы      сообщаем компилятору         тип     каждой         из        переменных,          которые       будут принимать  значения,    передаваемые      функции.     Несмотря     на       то        что формальные          параметры выполняют специальную         задачу          получения значений     аргументов,            передаваемых      функции,     их можно    также использовать         в          теле   функции       как     обычные      локальные  переменные. Например, мы      можем          присвоить   им      любые          значения      или использовать         в          ка-ких-нибудь       (допустимых для C++)   выражениях.          Но, подобно      любым         другим         локальным переменным,        их        значения теряются      по       завершении            функции.

Глобальные  переменные

Глобальные    переменные           известны    всей   программе.

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

В   следующей программе переменная            count объявляется           вне     всех функций.     Ее        объявление предшествует      функции       main().          Но       ее        с таким            же       успехом       можно          разместить  в          другом         месте, главное, чтобы            она     не       принадлежала      какой-нибудь        функции.     Помните: поскольку переменную            необходимо           объявить      до       ее        использования, глобальные           переменные           лучше           всего объявлять   в          начале программы. #include      <iostream> using   namespace   std;

void         funс1(); void           func2(); int   count;     //         Это     глобальная переменная.

int    main() {

 int       i;          //         Это     локальная   переменная.

 for(i=0;          i<10;  i++){

  count        =          i           *          2;

  funс1();

 }

 return            0;

}

void     func1() {

 cout    <<       "count:          "          <<       count;            //         Обращение             к            глобальной переменной.

 cout    <<       '\n';     //         Вывод           символа       новой            строки.

 func2();

}

void     func2() {

 int       count;            //         Это     локальная   переменная.

 for(count=0;            count<3;       count++)       cout    <<'.';

}

Несмотря          на       то        что      переменная            count не       объявляется           ни в          функции       main(),          ни       в          функции func1(),  обе     они     могут ее использовать.        Но       в          функции       func2()          объявляется           локальная переменная            count.            Здесь при     обращении к          переменной           count выполняется          доступ          к локальной,          а          не       к          глобальной переменной.          Важно           помнить,      что,    если   глобальная и локальная переменные           имеют           одинаковые           имена,          все      ссылки          на "спорное"   имя    переменной внутри       функции,     в          которой        определена локальная   переменная,          относятся     к          локальной, а          не       к глобальной переменной.

Модификаторы     типов

В   C++     перед            такими          типами         данных,        как     charint       и double,          разрешается           использовать модификаторы.         Модификатор служит          для     изменения  значения      базового      типа,  чтобы            он более точно соответствовал      конкретной             ситуации.     Перечислим           возможные модификаторы типов.

signed unsigned long

short

Модификаторы          signed,          unsigned,     long    и          short  можно применять  к          целочисленным базовым        типам.           Кроме           того, модификаторы     signed            и          unsigned       можно          использовать         с типом char, а          модификатор        long—           с          типом            double.          Все допустимые           комбинации           базовых       типов и модификаторов            для     16- и          32-разрядных        сред   приведены в          табл.  3.2      и          3.3.     В          этих таблицах также    указаны       типичные     размеры      значений     в          битах и диапазоны  представления      для     каждого типа.       Безусловно,            реальные диапазоны,            поддерживаемые            вашим          компилятором,     следует уточнить       в          соответствующей             документации.

Изучая   эти      таблицы,      обратите      внимание    на       количество битов, выделяемых          для     хранения коротких,        длинных      и          обычных целочисленных     значений.    Заметьте:     в          большинстве          16разрядных сред   размер         (в        битах)            обычного    целочисленного   значения совпадает    с          размером короткого      целого.         Также            отметьте,     что      в большинстве          32-разрядных        сред   размер         (в        битах) обычного целочисленного   значения      совпадает    с          размером    длинного     целого. "Собака        зарыта" в     С++-определении            базовых       типов.           Согласно стандарту    C++     размер         длинного     целого          должен быть         не       меньше размера       обычного    целочисленного   значения,    а          размер         обычного целочисленного   значения      должен        быть  не       меньше        размера       короткого целого.         Размер обычного            целочисленного   значения      должен        зависеть от        среды           выполнения.          Это     значит,          что в  16-разрядных        средах для     хранения     значений     типа   int       используется         16       бит,    а          в          32-разрядных —         32.      При    этом   наименьший          допустимый           размер         для целочисленных     значений     в          любой среде          должен        составлять   16       бит. Поскольку   стандарт      C++     определяет             только          относительные требования к          размеру       целочисленных     типов,           нет      гарантии,     что      один  тип будет больше         (по количеству      битов),          чем    другой.         Тем    не       менее размеры,     указанные   в          обеих            таблицах, справедливы            для     многих компиляторов.

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

Различие           между          целочисленными             значениями            со        знаком и          без     него   заключается           в интерпретации  старшего      разряда.      Если задано          целочисленное     значение      со        знаком,        С++компилятор сгенерирует            код     с          учетом          того,  что      старший       разряд          значения используется         в качестве   флага           знака.           Если   флаг   знака равен            0, число считается     положительным, а          если   он равен      1,        — отрицательным.   Отрицательные     числа почти всегда           представляются    в дополнительном            коде. Для    получения   дополнительного            кода  все разряды       числа берутся         в обратном коде, а          затем полученный           результат увеличивается       на       единицу.

Целочисленные         значения      со        знаком         используются        во       многих алгоритмах,            но максимальное            число,           которое        можно представить           со        знаком,        составляет   только          половину     от максимального     числа,           которое        можно          представить           без     знака. Рассмотрим,          например, максимально          возможное 16-разрядное        целое число (32      767):

0  1111111       11111111

Если        бы      старший       разряд          этого  значения      со        знаком         был установлен равным        1,        то        оно     бы интерпретировалось           как     -1        (в дополнительном коде).            Но       если   объявить      его      как     unsigned intзначение,           то        после установки    его      старшего      разряда        в          1          мы получили     бы      число 65       535.

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

#include <iostream> using     namespace   std;

/* Эта      программа демонстрирует     различие     между          signed-          и unsigned-значениями     целочисленного   типа.

*/ int main() {

 short  int       i;          //         короткое      int-значение           со        знаком

 short  unsigned       int       j;          //         короткое      int-значение           без     знака

 j           =          60000;

 i           =          j;

 cout    <<       i           <<       "          "          <<       j;

 return            0;

}

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

-5536      60000

Дело       в          том,   что      битовая        комбинация,          которая        представляет число 60000            как     короткое целочисленное         значение      без     знака, интерпретируется            в          качестве       короткого    int-значения           со знаком    как число -5536.

В   C++     предусмотрен       сокращенный        способ          объявления            unsigned-, short- и          long-значений целочисленного         типа.  Это     значит,          что      при объявлении            int-значений           достаточно использовать        слова unsigned, short  и          long,  не       указывая     тип     int,      т.е.      тип     int       подразумевается. Например,  следующие две     инструкции объявляют  целочисленные    переменные без     знака.

unsigned            х;

unsigned            int       у;

Переменные   типа   char   можно          использовать         не       только          для хранения     ASCII-символов,    но и   для     хранения     числовых     значений. Переменные          типа   char   могут содержать   "небольшие" целые       числа в диапазоне   -128--127     и          поэтому       их        можно          использовать         вместо intпеременных,     если   вас      устраивает  такой диапазон     представления      чисел. Например,  в следующей         программе char-переменная используется         для управления            циклом,       который выводит            на       экран алфавит английского           языка.

//  Эта      программа выводит       алфавит        в          обратном     порядке. #include            <iostream> using   namespace   std;

int    main() {

 char    letter;

 for(letter='Z';           letter  >=       'A';      letter--)         cout    <<       letter;

 return            0;

}

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

Литералы

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

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

ch =          'Z';

Чтобы     использовать         двубайтовый         символьный           литерал        (т.е. константу    типа   wchar_t), предварите     нужный        символ         буквой          L. Например,  так.

wchar_t wc;

wc            =          L'A';

Здесь      переменной           wc       присваивается       двубайтовая           символьная константа,   эквивалентная букве     А.

Целочисленные         константы   задаются     как     числа без     дробной      части. Например,  10       и          -100 —          целочисленные    литералы.   Вещественные литералы     должны       содержать   десятичную            точку, за      которой        следует дробная       часть  числа,           например    11.123.         Для    вещественных       констант можно          также            использовать         экспоненциальное          представление чисел.

Существует       два     основных     вещественных       типа:  float   и          double. Кроме           того,  существует несколько   модификаций        базовых       типов, которые       образуются с          помощью    модификаторов типов. Интересно,  а          как же       компилятор            определяет             тип     литерала?   Например,  число 123.23 имеет            тип     float   или     double?         Ответ на       этот    вопрос          состоит         из двух   частей.          Во-первых,  С++компилятор    автоматически      делает определенные      предположения   насчет           литералов.  Вовторых,    при     желании программист         может           явно   указать         тип     литерала.

По            умолчанию компилятор            связывает    целочисленный    литерал        с совместимым        и одновременно  наименьшим         по       занимаемой           памяти тип     данных,        начиная        с          типа   int. Следовательно,         для     16-разрядных сред   число 10       будет связано        с          типом            int,      а          103     000— с типом long    int.

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

Чтобы     задать           точный         тип     числовой     константы,  используйте соответствующий             суффикс. Для         вещественных       типов действуют следующие суффиксы:   если   вещественное        число завершить  буквой          F,         оно будет обрабатываться    с          использованием  типа   float,  а          если   буквой          L, подразумевается тип     long    double.          Для    целочисленных     типов суффикс       U означает использование          модификатора      типа   unsigned,     а          суффикс       Llong.  (Для   задания модификатора            unsigned       long    необходимо           указать оба     суффикса     U         и          L.)       Ниже приведены некоторые  примеры.

Шестнадцатеричные     и          восьмеричные      литералы

Иногда   удобно         вместо          десятичной системы       счисления   использовать восьмеричную      или шестнадцатеричную.         В          восьмеричной       системе основанием            служит          число 8,        а          для     выражения всех   чисел используются        цифры          от        0          до       7.        В          восьмеричной       системе число 10       имеет            то        же значение,          что      число 8          в          десятичной. Система        счисления   по       основанию  16       называется шестнадцатеричной   и использует  цифры          от        0          до       9          плюс  буквы            от        А         до       F, означающие шестнадцатеричные    "цифры"      10,      11,      12,      13,      14       и          15. Например,  шестнадцатеричное        число 10 равно      числу 16       в          десятичной системе.       Поскольку   эти      две     системы       счисления

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

int            hex     =          OxFF; //         255     в     десятичной системе int oct      =          011;   //         9     в          десятичной системе Строковые     литералы

Язык       C++     поддерживает       еще    один  встроенный            тип     литерала, именуемый            строковым. Строка— это      набор            символов,   заключенных в          двойные      кавычки,      например    "это  тест".         Вы уже         видели примеры     строк в          некоторых   cout-инструкциях,            с          помощью    которых мы      выводили текст    на       экран.           При    этом   обратите      внимание    вот      на что.    Хотя   C++     позволяет    определять строковые  литералы,   он       не       имеет встроенного           строкового  типа   данных.        Строки          в          C++,   как будет показано     ниже  в          этой   книге,            поддерживаются в          виде  символьных массивов.    (Кроме того,          стандарт      C++     поддерживает       строковый   тип     с помощью    библиотечного      класса           string, который     также            описан ниже  в          этой   книге.)

Осторожно!     Не       следует       путать       строки         с          символами. Символьный          литерал      заключается в     одинарные кавычки,      например    'а'. Однако         "а"      —        это    уже    строка,       содержащая          только одну букву.

Управляющие          символьные          последовательности

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

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

#include <iostream> using     namespace   std;

int    main() {

 cout<<"\n\\\b";

 return            0;

}

Инициализация      переменных

          При     объявлении    переменной    ей     можно     присвоить     некоторое значение,        т.е.

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

тип          имя_переменной             =          значение;

Вот          несколько   примеров.

char         ch        =     'а'; int     first     =          0;

float        balance         =          123.23F;

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

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

Рассмотрим     простой        пример         инициализации     переменных.          В следующей программе используется         функция       total(),           которая предназначена     для     вычисления            суммы          всех последовательных чисел,           начиная        с          единицы      и          заканчивая числом,        переданным ей       в качестве   аргумента.  Например,  сумма           ряда  чисел,           ограниченного числом         3,        равна            1          +          2          +          3 =      6.        В          процессе вычисления            итоговой      суммы          функция       total()            отображает промежуточные результаты. Обратите     внимание    на       использование переменной           sum    в          функции       total().

//  Пример        использования      инициализации     переменных. #include    <iostream> using   namespace   std;

void         total(int     x); int main() {

 cout    <<       "Вычисление          суммы          чисел от        1          до       5.\n";

 total(5);

 cout    <<       "\n      Вычисление            суммы          чисел от        1          до       6.\n";

 total(6);

 return            0;

}

void         total(int     x) {

 int       sum=0;          //         Инициализируем переменную          sum.

 int       i,          count;

 for(i=1;          i<=x;   i++)     {

  sum           =          sum    +          i;

  for(count=0;       count<10;    count++)       cout    <<       '.';

  cout           <<       "Промежуточная  сумма           равна            "          <<       sum    <<     '\n';

 }

}

Результаты      выполнения           этой   программы таковы.

Вычисление     суммы          чисел от        1          до       5.

..........Промежуточная        сумма           равна            1

..........Промежуточная        сумма           равна            3

..........Промежуточная        сумма           равна            6

..........Промежуточная        сумма           равна            10

..........Промежуточная        сумма           равна            15 Вычисление     суммы          чисел от        1          до       6.

..........Промежуточная        сумма           равна            1

..........Промежуточная        сумма           равна            3

..........Промежуточная        сумма           равна            6

..........Промежуточная        сумма           равна            10

..........Промежуточная        сумма           равна            15

..........Промежуточная        сумма           равна            21

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

Операторы

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

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

В   табл.  3.5      перечислены         арифметические   операторы, разрешенные        для применения           в C++.            Действие      операторов             +,        -,         *          и          / совпадает    с          действием   аналогичных          операторов             в любом      другом языке            программирования        (да      и          в          алгебре,       если   уж       на       то пошло).        Их       можно применять           к          данным        любого         встроенного числового   типа.  После            применения           оператора деления        (/)       к целому         числу остаток         будет отброшен.   Например,  результат целочисленного деления        10/3   будет равен            3.

Остаток  от        деления       можно          получить      с          помощью    оператора деления       по       модулю        (%).    Этот оператор       работает      практически           так же,     как     в          других          языках          программирования:       возвращает остаток от        деления       нацело.        Например,  10%3 равно            1.        Это     означает,     что в          C++     оператор     "%" нельзя  применять  к          типам            с          плавающей точкой          (float  или     double).        Деление       по       модулю применимо       только к          целочисленным   типам.           Использование     этого  оператора демонстрируется в          следующей программе.

#include <iostream> using     namespace   std;

int    main() {

 int       x,         y;

x        =         10;

y       =         3;

 cout    <<       х/у;     //         Будет отображено            число 3.

 cout    <<       "\n";

 cout    <<       х%у;   /*        Будет отображено            число 1,        т.е.      остаток         от деления       нацело.        */

 cout    <<       "\n";

 х          =          1;

 y          =          2;

 cout    <<       х/у      <<       "          "          <<       х%у;   //         Будут выведены   числа 0     и          1.

 return            0;

}

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

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

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

В   C++     есть    два     оператора,  которых       нет      в          некоторых   других языках          программирования. Это           операторы  инкремента            (++)    и декремента            (--).     Они    упоминались         в          главе 2,        когда речь шла     о цикле            for.     Оператор     инкремента            выполняет  сложение    операнда     с числом         1,        а          оператор декремента    вычитает      1          из        своего операнда.   Это     значит,          что      инструкция

х   =          х          +          1; аналогична      такой     инструкции:

++х;

А  инструкция

х   =          х          -           1; аналогична      такой     инструкции:

--x;

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

х   =          х          +          1;

можно   переписать в          виде  префиксной            формы

++х;         //         Префиксная            форма           оператора     инкремента. или в          виде  постфиксной          формы:

х++;         //         Постфиксная          форма           оператора   инкремента.

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

х   =          10;

у   =          ++x;

В   этом   случае           переменная            у          будет установлена           равной          11. Но       если   в          этом   коде префиксную            форму           записи          заменить постфиксной,         переменная            у          будет установлена           равной 10:

х   =          10;

у   =          x++;

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

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

Арифметические       операторы  подчиняются         следующему          порядку     выполнения           действий.

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

История         происхождения   имени           C++

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

Бьерн     Страуструп  сначала        назвал          свой   язык  "С        с          классами"   (С with    Classes),        но,      по предложению Рика   Маскитти     (Rick   Mascitti),      он позже            изменил       это      название      на       C++.   И         хотя успех   нового языка            еще    только          предполагался,    принятие     нового          названия (C++)  практически гарантировало   ему    видное         место            в          истории, поскольку   это      имя    было узнаваемым           для каждого          С-программиста.

Операторы  отношений           и          логические операторы

Операторы       отношений  и          логические (булевы)      операторы, которые часто идут   "рука об руку",     используются        для     получения   результатов            в виде  значений     ИСТИНА/ЛОЖЬ. Операторы    отношений  оценивают  по "двубалльной       системе"      отношения  между          двумя значениями,         а логические определяют           различные  способы       сочетания    истинных     и ложных значений.           Поскольку   операторы  отношений  генерируют ИСТИНА/ЛОЖЬ-результаты,    то они           часто выполняются         с          логическими операторами.        Поэтому       мы      и          рассматриваем     их        в одном       разделе.

Операторы       отношений  и          логические (булевы)      операторы  перечислены в          табл.  3.6. Обратите         внимание    на       то,       что      в          языке            C++     в качестве       оператора   отношения  "не     равно" используется      символ         "!=",   а для     оператора   "равно"        —        двойной       символ         равенства    (==). Согласно стандарту    C++     результат     выполнения           операторов             отношений  и логических операторов             имеет            тип     bool,  т.е.      при     выполнении операций     отношений  и          логических операций     получаются            значения      true или     false.  При    использовании     более            старых компиляторов    результаты выполнения           этих    операций     имели           тип     int       (нуль или     ненулевое целое,           например    1).       Это     различие     в          интерпретации      значений имеет            в          основном теоретическую         основу,         поскольку   C++ автоматически      преобразует           значение      true    в          1,        а значение  false   — в          0,        и          наоборот.

Операнды,       участвующие         в          операциях   "выяснения"          отношений, могут иметь            практически любой        тип,    главное,       чтобы            их        можно было сравнивать.            Что     касается       логических  операторов, то     их        операнды должны       иметь            тип     bool,  и          результат     логической операции     всегда будет иметь           тип     bool.  Поскольку   в          C++     любое           ненулевое   число оценивается           как     истинное      (true),            а нуль           эквивалентен         ложному значению    (false),           то        логические операторы  можно          использовать в любом          выражении,            которое        дает   нулевой       или     ненулевой результат.

Помните,        что    в          C++     любое           ненулевое   число оценивается         как     true,   а          нуль—          как     false.

Логические      операторы  используются        для     поддержки базовых логических  операций     И, ИЛИ          и          НЕ       в          соответствии          со следующей таблицей     истинности.            Здесь 1          используется         как значение ИСТИНА,      а          0          —        как     значение      ЛОЖЬ.

Несмотря          на       то        что      C++     не       содержит     встроенный логический оператор     "исключающее ИЛИ"     (XOR),            его      нетрудно     "создать" на       основе          встроенных.           Посмотрите,           как     следующая функция использует  операторы  И,        ИЛИ   и          НЕ       для     выполнения           операции "исключающее ИЛИ". bool      хоr(bool        a,         bool    b) {

 return            (а        ||        b)        &&      !(а       &&      b);

}

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

(Помните,    что      здесь единица       будет обработана             как     значение      true,   а нуль   —        как     false.)

//  Эта      программа демонстрирует     использование      функции       хоr(). #include     <iostream> using   namespace   std;

bool         хоr(bool        a,     bool    b); int main() {

 bool    p,        q;

 cout    <<       "Введите      P          (0        или     1):       ";

  cin  >>       p;

 cout    <<       "Введите      Q         (0        или     1):       ";

  cin  >>       q;

 cout    <<       "P        И         Q:        "          <<       (p        &&      q)        <<       '           \n';

 cout    <<       "P        ИЛИ   Q:        "          <<       (p        ||        q)     <<       '           \n';  cout   <<       "P        XOR    Q:        "          <<     xor(p, q)        <<       '\n';

 return     0; }

bool         хоr(bool        a,     bool    b) {

 return            (a        ||        b)        &&      !(a       &&      b);

}

Вот          как     выглядит     возможный            результат     выполнения           этой     программы.

Введите             Р          (0        или     1):       1

Введите             Q         (0        или     1):       1

Р  И         Q:        1

Р  ИЛИ   Q:        1

Р  XOR    Q:        0

В   этой   программе обратите      внимание    вот      на       что.    Хотя   параметры функции       xor()   указаны с    типом            bool,  пользователем     вводятся целочисленные    значения      (0        или     1).       В          этом   ничего          нет странного, поскольку   C++     автоматически      преобразует           число 1          в          true,   а          0 в          false.  И         наоборот, при       выводе         на       экран bool-значения, возвращаемого    функцией    xor(),  оно     автоматически преобразуется           в число 0          или     1          (в        зависимости           от        того,  какое значение "вернулось":          false   или true).     Интересно   отметить,     что,    если   типы параметров            функции       хоr()   и          тип     возвращаемого ею         значения заменить     типом            int,      эта      функция       будет работать      абсолютно  так      же. Причина проста:  все      дело  в          автоматических    преобразованиях, выполняемых        С++-компилятором между      целочисленными             и булевыми   значениями.

Как          операторы  отношений,            так      и          логические операторы  имеют более            низкий          приоритет по         сравнению  с          арифметическими операторами.        Это     означает,     что      такое выражение,            как     10 >    1+12 будет вычислено  так,     как     если   бы      оно     было записано      в          таком виде: 10       >(1      +          12)

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

cout         <<       "Р        И         Q:        "          <<       (р        &&      q)        <<       '\n';

cout         <<       "Р        ИЛИ   Q:        "          <<       (р        |          |          q)        <<       '\n';

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

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

var>15   ||        !(10<count) &&      3<=item

Приоритет        операторов             отношений  и          логических  операторов показан        в          следующей таблице.

Выражения

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

Преобразование     типов           в          выражениях

Если        в          выражении смешаны     различные  типы  литералов   и переменных,          компилятор преобразует         их        к          одному        типу.  Во-первых,        все      char-  и          short  int-значения           автоматически преобразуются (с         расширением        "типоразмера")    к          типу   int.      Этот    процесс называется целочисленным   расширением        (integral        promotion). Во-вторых,  все операнды    преобразуются (также  с          расширением        "типоразмера")    к типу   самого          большого    операнда.   Этот    процесс называется        расширением типа (type  promotion), причем         он       выполняется          по       операционно. Например,  если   один  операнд       имеет            тип     int,      а          другой          —        long int,      то        тип     int       расширяется          в тип  long    int.      Или,   если   хотя    бы один  из        операндов  имеет            тип     double,          любой           другой          операнд приводится к          типу   double.          Это     означает,     что      такие преобразования,  как из        типа   char   в          тип double,  вполне          допустимы.            После преобразования   оба     операнда     будут иметь            один  и          тот      же тип,         а результат     операции     —        тип,    совпадающий        с          типом            операндов.

Рассмотрим,    например,   преобразование   типов,           схематически представленное   на       рис.    3.1. Сначала           символ         ch        подвергается процессу      "расширения"       типа   и          преобразуется       в          значение типа       int. Затем            результат     операции     ch/i     приводится             к          типу   double, поскольку   результат произведения          f*d      имеет            тип     double.          Результат всего выражения получит        тип     double, поскольку           к          моменту      его вычисления            оба     операнда     будут иметь            тип     double.

Преобразования,    связанные  с          типом         bool

Как          упоминалось         выше,            значения      типа   bool    автоматически преобразуются     в          целые           числа 0         или     1          при     использовании     в выражении целочисленного   типа.  При    преобразовании целочисленного результата  в          тип     bool    нуль   преобразуется       в          false,  а          ненулевое значение      —        в true.           И         хотя    тип     bool    относительно         недавно       был добавлен     в          язык  C++,   выполнение автоматических  преобразований, связанных   с          типом            bool,  означает,     что      его      введение     в          C++ не имеет            негативных последствий           для     кода, написанного          для     более ранних          версий          C++. Более  того,  автоматические    преобразования позволяют  C++     поддерживать       исходное определение             значений     ЛОЖЬ и          ИСТИНА        в          виде  нуля   и          ненулевого значения.    Таким образом,   тип bool    очень            удобен         для     программиста.

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

В   C++     предусмотрена     возможность         установить  для     выражения заданный тип.    Для    этого используется          операция     приведения            типов           (cast). C++     определяет             пять   видов            таких операций. В           этом   разделе        мы рассмотрим            только          один  из        них,    а          остальные   четыре          описаны ниже  в этой            книге (после           темы  создания     объектов).   Итак,  общий формат         операции     приведения типов           таков:

(тип)       выражение

Здесь      элемент        тип   означает      тип,    к          которому     необходимо привести      выражение. Например,            если   вы       хотите,          чтобы выражение х/2      имело           тип     float,  необходимо           написать

следующее:

(float)     х          /          2

Приведение     типов рассматривается  как     унарный      оператор,    и          поэтому он       имеет            такой же приоритет,       как     и          другие          унарные операторы.

Иногда   операция     приведения            типов оказывается           очень полезной.    Например,  в следующей         программе для     управления            циклом используется         некоторая   целочисленная переменная,  входящая    в          состав выражения,            результат     вычисления            которого      необходимо получить   с дробной      частью. #include   <iostream> using   namespace   std;

int            main()            /*        Выводим     i           и          значение      i/2       с     дробной      частью.*/

{

 int       i;

 for(i=1;          i<=100;         ++i      )

  cout           <<       i           <<       "/        2          равно:           "          <<       (float)            i     /          2          <<       '\n';

 return            0;

}

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

Использование        пробелов     и          круглых       скобок

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

х=10/у*(127/х);

х   =          10       /          у          *          (127/х);

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

х   =          у/3-34*temp+127;

X   =          (у/3)   -           (34*temp)    +          127;

Глава     4:     Инструкции управления

В   этой   главе вы       узнаете,        как     управлять    ходом           выполнения           С++-программы.           Существует три     категории    управляющих        инструкций: инструкции           выбора         (if,       switch),         итерационные инструкции (состоящие из        for-,    while-            и          do-while-циклов)  и          инструкции перехода     (break, continue,   return            и          goto).

За исключением        return,           все      остальные   перечисленные     выше инструкции описаны       в          этой главе.

Инструкция if

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

Инструкция      if          была  представлена        в          главе 2,        но       здесь мы рассмотрим            ее        более            детально. Полный          формат         ее        записи таков.

if(выражение)             инструкция;

else          инструкция;

Здесь      под    элементом  инструкция           понимается            одна  инструкция языка            C++.   Часть else необязательна.        Вместо          элемента     инструкция может           быть  использован          блок  инструкций.            В этом           случае формат         записи          if-инструкции         принимает  такой вид.

if(выражение) {

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

} else {

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

}

Если        элемент        выражение,           который       представляет         собой условное     выражение,            при вычислении   даст   значение      ИСТИНА,      будет выполнена  if-инструкция;        в          противном  случае else-инструкция  (если таковая существует).           Обе    инструкции никогда        не       выполняются. Условное выражение,            управляющее        выполнением        if-инструкции,        может иметь            любой           тип, действительный      для     С++-выражений,   но       главное; чтобы            результат     его      вычисления            можно было          интерпретировать как     значение      ИСТИНА        или     ЛОЖЬ.

Использование          if-инструкции         рассмотрим            на       примере программы,           которая        представляет собой       версию         игры  "Угадай магическое число".         Программа генерирует  случайное   число и предлагает          вам его      угадать.        Если   вы       угадываете число,           программа выводит       на экран сообщение             одобрения  **       Правильно  **.      В          этой   программе представлена        еще    одна библиотечная         функция       rand(),           которая возвращает             случайным образом       выбранное  целое число.          Для использования      этой   функции       необходимо           включить     в          программу заголовок <cstdlib>.

//  Программа "Угадай        магическое число".

#include <iostream> #include <cstdlib> using     namespace   std;

int    main() {

 int       magic;            //         магическое число

 int       guess;            //         вариант        пользователя

 magic =          rand();           //         Получаем    случайное   число.

 cout    <<       "Введите      свой   вариант        магического           числа:           ";

  cin  >>       guess;

 if(guess          ==       magic)           cout    <<       "**     Правильно  **";

 return            0;

}

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

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

//  Программа "Угадай        магическое число":

//  1-е      усовершенствование.

#include <iostream>

#include <cstdlib> using     namespace   std;

int    main() {

 int       magic;            //         магическое число

 int       guess;            //         вариант        пользователя

 magic =          rand();           //         Получаем    случайное   число.

 cout    <<       "Введите      свой   вариант        магического           числа:           ";

  cin  >>       guess;

 if(guess          ==       magic)           cout    <<       "**     Правильно  **";

 else     cout    <<       "...Очень      жаль, но       вы       ошиблись.";

 return            0;

}

Условное        выражение

Иногда   новичков     в          C++     сбивает         с          толку тот      факт,  что      для управления            if-инструкцией можно   использовать         любое действительное    С++-выражение.   Другими      словами,      тип выражения необязательно      ограничивать         операторами         отношений  и          логическими операторами          или     операндами           типа   bool.  Главное,       чтобы            результат вычисления            условного выражения   можно          было интерпретировать            как значение      ИСТИНА        или     ЛОЖЬ.          Как     вы помните            из        предыдущей главы,           нуль   автоматически      преобразуется       в          false,  а          все ненулевые значения—  в          true.   Это     означает,     что      любое           выражение, которое        дает   в          результате  нулевое или           ненулевое   значение,    можно использовать         для     управления            if-инструкцией.     Например, следующая программа считывает    с          клавиатуры            два     целых            числа и отображает             частное         от деления  первого        на       второе.         Чтобы           не допустить    деления       на       нуль, в          программе используется if-инструкция.

//  Деление       первого        числа на       второе.

#include <iostream>

using       namespace   std;

int    main() {

 int       a,         b;

 cout    <<       "Введите      два     числа:           ";

 cin       >>       a          >>       b;

 if(b)    cout    <<       a/b      <<       '\n';

 else     cout    <<       "На     нуль   делить          нельзя.\n";

 return            0;

}

Обратите           внимание    на       то,       что      значение      переменной           b (делимое)   сравнивается         с          нулем           с помощью инструкции if(b),   а          не инструкции if(b!=0).        Дело  в          том,   что,    если   значение      b          равно нулю, условное     выражение,            управляющее        инструкцией           if,        оценивается как     ЛОЖЬ,           что приводит         к          выполнению          else-ветви.   В противном  случае           (если b          содержит     ненулевое значение)     условие оценивается           как     ИСТИНА,      и          деление       благополучно        выполняется. Нет никакой           необходимости     использовать         следующую            if-инструкцию, которая        к          тому  же       не свидетельствует         о          хорошем      стиле программирования        на       C++.

if(b           !=        0)        cout    <<       а/b      <<       '\n';

Эта          форма           if-инструкции         считается     устаревшей             и     потенциально        неэффективной.

Вложенные   if-инструкции

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

if(i)           {

 if(j)      statement1;

 if(k)     statement2;             //         Эта      if-инструкция

 else     statement3;             //         связана         с          этой   else-инструкцией.

}

else          statement4;             //         Эта      else-инструкция    связана         с          if(i).

Как          утверждается         в          комментариях,      последняя   else-инструкция    не связана         с          инструкцией if(j), поскольку   они     не       находятся    в          одном блоке            (несмотря    на       то,       что      эта      if-инструкция         — ближайшая, которая        не       имеет            при     себе   "else-пары").          Внутренняя else-инструкция связана         с инструкцией       if(k),   поскольку   она     —        ближайшая             и находится    внутри          того    же       блока.

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

Язык       C++     позволяет    256     уровней       вложения,   но       на       практике редко            приходится вкладывать           if-инструкции         на       "такую          глубину". продемонстрируем        использование вложенных     инструкций с          помощью очередного            усовершенствования      программы "Угадай магическое       число" (здесь           игрок получает      реакцию      программы на       неправильный      ответ).

//  Программа "Угадай        магическое число":

//  2-е      усовершенствование.

#include <iostream> #include <cstdlib> using     namespace   std;

int    main() {

 int       magic;            //         магическое число

 int       guess;            //         вариант        пользователя

 magic =          rand();           //         Получаем    случайное   число.

 cout    <<       "Введите      свой   вариант        магического           числа:           ";

  cin  >>       guess;

 if(guess          ==       magic)           {

  cout           <<       "          **       Правильно  **\n";

  cout           <<       magic <<       "          и          есть    то        самое            магическое     число.\n";

 }

 else     {

  cout           <<       "...Очень      жаль, но       вы       ошиблись.";

  if(guess     >          magic)           cout    <<"Ваш         вариант        превышает            магическое число.\n";

  else            cout    <<       "          Ваш    вариант        меньше        магического     числа.\n";

 }

 return            0;

}

Конструкция           if-else-if

Очень     распространенной           в          программировании        конструкцией,       в основе          которой        лежит вложенная            if-инструкция,        является "лестница"  if-else-if.        Ее        можно          представить           в          следующем виде.

if(условие)  инструкция; else     if(условие)  инструкция; else     if(условие)

 инструкция;

.

. . else

 инструкция;

Здесь      под    элементом  условие        понимается            условное     выражение. Условные    выражения вычисляются         сверху           вниз.  Как     только          в какой-нибудь        ветви обнаружится          истинный результат,      будет выполнена инструкция,            связанная    с          этой   ветвью,         а          вся      остальная "лестница" опускается. Если   окажется,    что      ни       одно  из        условий        не является       истинным,   будет выполнена последняя   else-инструкция    (можно считать,        что      она     выполняет  роль  условия,       которое действует           по умолчанию).          Если   последняя   else-инструкция    не       задана,         а          все остальные оказались     ложными,   то        вообще         никакое        действие      не будет выполнено.

"Лестница"     if-else-if—   это    последовательность   вложенных if-else-инструкций. Работа           if-else-if         -           "лестницы" демонстрируется в     следующей программе.

//  Демонстрация       использования      "лестницы" if-else-if. #include <iostream> using   namespace   std;

int    main() {

 int       x;

 for(x=0;         x<6;    x++)    {

  if(x==1)    cout    <<       "x        равен            единице.\n";

  else            if(x==2)         cout    <<       "x        равен            двум.\n";

  else            if(x==3)         cout<<           "x        равен            трем.\n";

  else            if(x==4)         cout    <<       "x        равен            четырем.\n";

  else            cout    <<       "x        не       попадает     в          диапазон     от        1          до     4.\n";

 }

 return            0;

}

Результаты      выполнения           этой     программы таковы. х     не       попадает     в          диапазон     от        1          до       4.

х   равен            единице, х  равен     двум, х         равен            трем, х     равен            четырем. х  не       попадает     в          диапазон     от        1          до       4.

Как          видите,         последняя   else-инструкция    выполняется          только          в том     случае,         если   все предыдущие  if-условия    дали  ложный        результат.

Цикл     for

Цикл      for      —        самый           универсальный    цикл  C++.

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

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

for(инициализация; выражение;            инкремент) инструкция;

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

fоr           (инициализация;  выражение;     инкремент) {

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

}

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

В   следующей программе цикл  for      используется         для     вывода         значений квадратного           корня, извлеченных       из        чисел от        1          до       99.      Обратите внимание    на       то,       что      в          этом   примере управляющая переменная цикла            называется num.

#include <iostream> #include <cmath> using       namespace     std;

int            main()

{

 int       num;

 double           sq_root;

 for(num=1;  num<100;    num++)         {

  sq_root    =          sqrt((double)           num);

  cout           <<       num   <<       "          "          <<       sq_root         <<       '\n';

 }

 return            0;

}

Вот          как     выглядят     первые         строки           результатов,           выводимых     этой   программой.

1            1

2            1.41421

3            1.73205

4            2

5            2.23607

6            2.44949

7            2.64575

8            2.82843

9            3

10       3.16228

11       3.31662

В   этой   программе использована        еще    одна  стандартная           функция       C++: sqrt(). Эта      функция возвращает      значение      квадратного           корня            из своего           аргумента.  Аргумент     должен        иметь            тип double,  и          именно поэтому       при     вызове          функции       sqrt()  параметр     num   приводится             к типу double.           Сама  функция       также            возвращает             значение      типа double.          Обратите     внимание    на       то, что           в          программу включен заголовок    <cmath>,      поскольку   этот    заголовочный       файл обеспечивает поддержку функции       sqrt().

Важно!  Помимо       функции      sqrt(), C++     поддерживает    широкий      набор других математических        функций,     например    sin(),   cos(),  tan(), log(),  ceil()   и floor().           Помните,   что    все математические   функции      требуют включения  в          программу заголовка   <cmath>.

Управляющая переменная            цикла            for      может           изменяться как     с положительным, так      и          с отрицательным приращением,      причем         величина этого  приращения           также            может           быть любой.          Например, следующая программа выводит       числа в          диапазоне   от        100     до       -100   с декрементом,        равным        5. #include   <iostream> using   namespace   std;

int    main() {

 int       i;

 for(i=100;     i>=-100;        i=i-5)  cout    <<       i           <<       '           ';

 return            0;

}

Важно    понимать,   что      условное     выражение всегда           тестируется             в начале          выполнения цикла         for.     Это     значит,          что      если   первая          же проверка     условия        даст   значение      ЛОЖЬ,          код     тела цикла  не выполнится            ни       разу.  Вот     пример:

for(count=10;  count<5;       count++)

cout         <<       count;            //         Эта      инструкция не       выполнится.

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

Вариации       на       тему цикла            for

Цикл       for      —        одна  из        наиболее     гибких           инструкций в          С++, поскольку   она     позволяет получить        широкий      диапазон     вариантов   ее использования.    Например,  для     управления циклом       for      можно использовать         несколько   переменных.          Рассмотрим            следующий фрагмент кода.

for(x=0,  у=10; х<=10;           ++х,    --у)

 cout    <<       х          <<       '           '           <<       у          <<       '\n';

Здесь      запятыми    отделяются две     инструкции инициализации     и          два инкрементных выражения.    Это     делается      для     того,  чтобы            компилятор "понимал", что      существует  две инструкции    инициализации     и          две инструкции инкремента            (декремента).        В          C++     запятая представляет собой            оператор,    который,      по       сути,  означает      "сделай        это    и то".   Другие применения        оператора   "запятая"     мы      рассмотрим            ниже  в этой   книге,            но       чаще  всего он используется   в          цикле            for.     При входе            в          данный        цикл  инициализируются          обе     переменные           — х и      у.         После            выполнения           каждой         итерации     цикла переменная            х          инкрементируется,          а переменная        у декрементируется.          Использование     нескольких управляющих        переменных в цикле         иногда          позволяет    упростить    алгоритмы. В          разделах инициализации     и          инкремента цикла          for      можно          использовать любое           количество инструкций,            но       обычно         их        число не превышает двух.

Условным         выражением,         которое        управляет    циклом         for,     может быть  любое           допустимое            С ++-выражение.  При    этом   оно     необязательно должно        включать     управляющую       переменную цикла.       В          следующем примере      цикл  будет выполняться          до       тех      пор,   пока   пользователь         не нажмет         клавишу       на       клавиатуре.            В          этой   программе представлена еще    одна  (очень           важная) библиотечная  функция:      kbhit().          Она возвращает             значение      ЛОЖЬ,          если   ни       одна  клавиша       не была нажата          на       клавиатуре,            и          значение      ИСТИНА        в          противном случае.         Функция       не       ожидает нажатия            клавиши,     позволяя     тем самым          циклу            выполняться          до       тех      пор,   пока   оно     не произойдет. Функция       kbhit()           не       определяется        стандартом             C++,   но       включена в          расширение языка         C++,   которое        поддерживается  большинством компиляторов.      Для    ее        использования      в программу          необходимо включить     заголовок    <conio.h>.    (Этот  заголовок    необходимо указывать с расширением        .h,       поскольку   он       не       определен  стандартом             C++.)

#include <iostream> #include <conio.h> using     namespace   std;

int    main() {

 int       i;

 //         Вывод           чисел на       экран до       нажатия       любой           клавиши.

 for(i=0;          !kbhit();         i++)

  cout           <<       i           <<       '           ';

 return            0;

}

На            каждой         итерации     цикла            вызывается             функция       kbhit(). Если   после запуска         программы нажать         какую-нибудь       клавишу,     эта функция       возвратит    значение      ИСТИНА,      в          результате  чего выражение !kbhit()          даст   значение      ЛОЖЬ,          и          цикл  остановится.           Но       если   не нажимать    клавишу, функция           возвратит    значение      ЛОЖЬ,          а выражение !kbhit()          даст   значение      ИСТИНА,      что позволит          циклу продолжать           "крутиться".

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

Отсутствие            элементов             в          определении         цикла

В   C++     разрешается           опустить       любой           элемент        заголовка    цикла (инициализацияусловное выражение,   инкремент)           или     даже  все      сразу. Например,  мы      хотим            написать      цикл, который должен  выполняться          до тех      пор,   пока   с          клавиатуры            не       будет введено       число 123.   Вот     как выглядит     такая программа. #include       <iostream> using   namespace   std;

int    main() {

 int       x;

 for(x=0;         x!=123;         )           {

  cout           <<       "Введите      число:           ";

  cin  >>       x;

 }

 return            0;

}

Здесь      в          заголовке    цикла            for      отсутствует  выражение инкремента. Это     означает,     что      при каждом           повторении            цикла            выполняется только          одно  действие:     значение      переменной           х сравнивается      с числом         123.   Но       если   ввести           с          клавиатуры            число 123,   условное выражение, проверяемое       в          цикле,           станет            ложным,      и          цикл завершится.            Поскольку   выражение инкремента в        заголовке    цикла            for отсутствует,            управляющая        переменная            цикла            не модифицируется.

Приведем         еще    один  вариант        цикла            for,     в          заголовке    которого, как     показано     в          следующем фрагменте кода, отсутствует  раздел инициализации.

cout         <<       "Введите      номер           позиции:      ";

 cin       >>       х;

for(          ;           х<limit;          х++)    cout    <<       '           ';

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

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

Бесконечный            цикл

Бесконечный цикл  —        это    цикл, который     никогда       не     заканчивается.

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

for(;;) {

 //...

}

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

Циклы временной задержки

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

for(x=0;  х<1000;        х++);

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

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

Инструкция switch

Инструкция     switch—       это    инструкция           многонаправленного ветвления,            которая позволяет      выбрать     одну  из        множества альтернатив.

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

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

 case    константа1:

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

  break;

 case    константа2:

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

  break;

 case    константа3:

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

  break;

 .

 .

 .

 default:

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

}

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

Инструкция   break завершает            выполнение           кода, определенного     инструкцией         switch.

Последовательность            инструкций default-ветви          выполняется          в          том случае,         если   ни       одна  из заданных           case-констант         не       совпадет      с результатом           вычисления            switch-выражения.          Ветвь default необязательна.     Если   она     отсутствует,            то        при     несовпадении результата  выражения ни с    одной           из        case-констант         никакое        действие выполнено не       будет.           Если   такое совпадение            всетаки         обнаружится, будут выполняться          инструкции,            соответствующие             данной         case-ветви,            до тех            пор,   пока   не       встретится   инструкция break или     не будет достигнут    конец            switchинструкции             (либо в          default-,        либо  в последней  case-ветви).

Инструкции     default-ветви         выполняются       в          том   случае,         если   ни одна  из        case    констант не        совпадет    с          результатом       вычисления switch-выражения.

Итак,      для     применения           switch-инструкции           необходимо           знать     следующее.

■  Инструкция             switch            отличается  от        инструкции if          тем,    что switch-выражение           можно тестировать         только          с          использованием условия        равенства    (т.е.    на       совпадение            switchвыражения             с заданными case-константами),          в          то        время            как     условное     if-выражение может быть            любого         типа.

■  Никакие       две     case-константы     в          одной           switch-инструкции           не могут иметь            идентичных значений.

■  Инструкция             switch            обычно         более            эффективна,           чем     вложенные if-инструкции.

■  Последовательность      инструкций,            связанная    с          каждой         case-ветвью,         не       является       блоком. Однако   полная          switch-инструкция определяет             блок. Значимость             этого  факта станет очевидной            после того,  как     вы       больше         узнаете         о          C++.

Согласно           стандарту    C++     switch-конструкция         может           иметь            не более            16       384     caseинструкций.   Но       на       практике      (исходя         из соображений         эффективности)    обычно ограничиваются           гораздо        меньшим их        количеством.

Использование          switch-инструкции           демонстрируется в          следующей программе.            Она создает           простую       "справочную"        систему,       которая описывает   назначение             for-,    if-        и          switchинструкций.           После отображения         списка           предлагаемых       тем,    по       которым      возможно предоставление    справки,       программа переходит   в          режим          ожидания    до тех      пор,   пока пользователь          не       сделает        свой   выбор.          Введенное пользователем     значение      используется         в инструкции         switch            для отображения         информация          по       указанной   теме. (Вы     могли            бы      в качестве       упражнения           дополнить  информацию         по       имеющимся темам,          а          также            ввести           в          эту

"справочную"        систему        новые           темы.)

//  Демонстрация       switch-инструкции           на       примере      простой "справочной"         системы. #include            <iostream> using   namespace   std;

int    main() {

 int       choice;

 cout    <<       "Справка      по       темам:\n\n";

 cout    <<       "1.      for\n";

 cout    <<       "2.      if\n";

 cout    <<       "3.      switch\n\n";

 cout    <<       "Введите      номер           темы  (1-3): ";

  cin  >>       choice;

 cout    <<       "\n";  switch(choice)  {

  case           1:

   cout      <<       "for    -           это      самый           универсальный     цикл  в     С++.\n";

   break;

  case           2:

   cout      <<       "if        -           это      инструкция условного   ветвления.\n";

   break;

  case           3:

   cout      <<"switch     -           это      инструкция многонаправленного ветвления.\n";

   break;

  default:

   cout      <<       "Вы    должны       ввести           число от        1          до       З.\n";

  }

 return            0;

}

Вот          один  из        вариантов   выполнения           этой   программы.

Справка по       темам:

1.          for

2.          if

3.          switch

Введите номер           темы  (1-3)   :           2

if  -           это    инструкция           условного   ветвления.

Формально      инструкция break необязательна,     хотя    в          большинстве случаев        использования switch-конструкций             она     присутствует. Инструкция             break,            стоящая        в          последовательности инструкций любой           case-ветви,  приводит     к          выходу         из        всей   switch-конструкции и          передает управление    инструкции,            расположенной    сразу после нее.    Но если   инструкция break в          caseветви     отсутствует,            будут выполнены все инструкции,            связанные   с          данной         case-ветвью,           а также         все последующие        инструкции,            расположенные   под    ней,    до       тех      пор, пока   все-таки       не встретится         инструкция break,            относящаяся          к одной           из        последующих        case-ветвей,            или     не будет       достигнут конец            switch-конструкции.

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

#include <iostream> using     namespace   std;

int    main() {

 int       i;

 for(i=0;          i<5;     i++)     {

  switch(i)   {

   case      0:        cout    <<       "меньше      1\n";

   case      1:        cout    <<       "меньше      2\n";

   case      2:        cout    <<       "меньше      3\n";

   case      3:        cout    <<       "меньше     4\n";    case  4:        cout    <<     "меньше      5\n";

  }

 cout    <<       '           \n';

 }

 return            0;

}

Вот          как     выглядят     результаты выполнения           этой   программы.

меньше 1 меньше 2 меньше 3 меньше 4

меньше 5

меньше 2 меньше 3 меньше 4 меньше 5

меньше 3 меньше 4 меньше 5

меньше 4

меньше 5 меньше 5

Как          видно            по       результатам,          если   инструкция break в          одной case-ветви   отсутствует, выполняются       инструкции,            относящиеся          к следующей case-ветви.

Как          показано     в          следующем            примере,     в          switch-конструкцию можно          включать     "пустые" case-ветви.

switch(i) {  case         1:

 case    2:

 case    3:        do_something();

  break;

 case    4:        do_something_else();

  break;

}

Если        переменная            i           в          этом   фрагменте   кода  получает      значение 1,        2          или     3,        вызывается функция      do_something().    Если   же       значение переменной           i           равно            4,        делается      обращение к функции do_something_else().      Использование     "пачки"        нескольких пустых          case-ветвей характерно           для     случаев,       когда они     используют            один  и          тот же       код.

Вложенные   инструкции           switch

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

switch(ch1)       {

 case    'А':      cout    <<"Эта           константа    А         -           часть  внешней            инструкции switch";

  switch(ch2)         {

   case      'A':      cout    <<"Эта           константа    A         -           часть  внутренней инструкции switch";

    break;

   case      'В':      //         ...

  }

  break;

 case    'В':      //         ...

Цикл     while

Инструкция     while —        еще    один  способ          организации     циклов          в          C++. Общая форма           цикла            while     имеет            такой вид:

while(выражение)     инструкция;

Здесь      под    элементом  инструкция           понимается            либо  одиночная инструкция,            либо  блок инструкций.             Работой        цикла            управляет элемент        выражение,           который       представляет         собой любое допустимое            С++-выражение.   Элемент       инструкция           выполняется          до тех      пор,   пока условное       выражение возвращает             значение      ИСТИНА.      Как только          это      выражение становится ложным,      управление            передается инструкции,            которая        следует        за        этим   циклом.

Использование          цикла            while  демонстрируется на       примере следующей небольшой программы.          Практически           все      компиляторы поддерживают     расширенный        набор            символов, который        не ограничивается     символами ASCII. В          расширенный        набор            часто включаются специальные       символы      и          некоторые  буквы            из алфавитов   иностранных          языков.        ASCII-символы используют     значения,    не превышающие      число 127,   а          расширенный        набор            символов— значения из        диапазона   128-255.      При    выполнении           этой   программы выводятся   все символы, значения         которых       лежат            в          диапазоне   32-255          (32      — это      код     пробела).    Выполнив    эту программу,     вы       должны       увидеть        ряд очень            интересных             символов.

/* Эта      программа выводит       все      печатаемые            символы,     включая расширенный        набор            символов,   если   таковой        существует.

*/ #include     <iostream> using     namespace   std;

int            main(){

 unsigned       char    ch;

 ch        =          32;

 while(ch)       {

  cout           <<       ch;

  ch++;

 }

 return            0;

}

Рассмотрим     while-выражение из        предыдущей          программы. Возможно,  вас      удивило,      что оно         состоит         всего лишь из        одной переменной           ch.      Но       "ларчик"      здесь открывается           просто. Поскольку переменная            ch        имеет            здесь тип     unsigned       char,  она     может содержать   значения      от        0 до   255.   Если   ее        значение      равно            255,   то после инкрементирования       оно     "сбрасывается"     в          нуль. Следовательно, факт   равенства    значения      переменной           ch        нулю служит          удобным способом завершить      while-цикл.

Подобно           циклу            for,     условное     выражение проверяется           при входе            в          цикл  while, а          это значит,  что      тело   цикла            (при   ложном результате  вычисления            условного   выражения)            может           не выполниться ни       разу.  Это     свойство      цикла            устраняет     необходимость     отдельного тестирования         до       начала          цикла.           Следующая            программа выводит строку,          состоящую  из        точек. Количество           отображаемых      точек равно значению,   которое        вводит          пользователь.       Программа не       позволяет "рисовать"  строки,         если   их        длина            превышает  80       символов.   Проверка на допустимость  числа выводимых            точек выполняется          внутри          условного выражения цикла,           а          не снаружи.

#include <iostream> using     namespace   std;

int    main() {

 int       len;

 cout    <<       "Введите      длину            строки           (от      1          до       79):    ";

  cin  >>       len;

 while(len>0  &&      len<80)         {

  cout           <<       '.';

  len--;

 }

 return            0;

}

Тело       while-цикла            может           вообще         не       содержать   ни       одной     инструкции.            Вот     пример:

while(rand()     !=        100);

Этот         цикл  выполняется          до       тех      пор,   пока   случайное   число, генерируемое       функцией rand(), не       окажется      равным        числу 100.

Цикл     do-while

Цикл       do-while       —        это    единственный     цикл, который     всегда делает        итерацию  хотя  бы один       раз.

В   отличие        от        циклов          for      и          while, в          которых       условие проверяется           при     входе,           цикл  do-while проверяет         условие        при выходе         из        цикла.           Это     значит,          что      цикл  do-while       всегда выполняется хотя            бы      один  раз.    Его      общий           формат         имеет такой вид.

do            {

 инструкции;

}while(выражение);

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

В   следующей программе цикл  do-while       выполняется          до       тех      пор, пока   пользователь         не введет     число 100.

#include <iostream> using     namespace   std;

int    main() {

 int       num;

 do       {

  cout           <<       "Введите      число (100   -           для     выхода):      ";

  cin  >>       num;

 }while(num  !=        100);

 return            0;

}

Используя        цикл  do-while,      мы      можем          еще    более усовершенствовать         программу "Угадай магическое        число".         На       этот    раз программа "не     выпустит"    вас      из        цикла            угадывания,           пока   вы не угадаете       это      число.

//  Программа "Угадай        магическое число":

//  3-е      усовершенствование.

#include <iostream> #include <cstdlib> using     namespace   std;

int    main() {

 int       magic;            //         магическое число

 int       guess;            //         вариант        пользователя

 magic =          rand();           //         Получаем    случайное   число.

 do       {

  cout           <<       "Введите      свой   вариант        магического           числа:           ";

   cin         >>       guess;

  if(guess     ==       magic)           {

   cout      <<       "**     Правильно  **       ";

   cout      <<       magic <<"     и          есть    то        самое            магическое число.\n";

  }

  else            {

   cout      <<       "...Очень      жаль, но       вы       ошиблись.";

   if(guess            >          magic)

    cout <<"     Ваш    вариант        превышает  магическое число.\n";

   else       cout    <<"     Ваш    вариант        меньше        магического           числа.\n";

  }

 }while(guess             !=        magic);

 return            0;

}

Использование        инструкции           continue

Инструкция    continue       позволяет  немедленно           перейти      к выполнению          следующей итерации цикла.

В   C++     существует  средство      "досрочного"        выхода         из        текущей итерации     цикла.           Этим средством   является       инструкция continue.      Она принудительно     выполняет  переход       к следующей         итерации,    опуская выполнение           оставшегося           кода  в          текущей.      Например,  в следующей программе инструкция continue       используется         для     "ускоренного"       поиска чётных чисел         в          диапазоне   от        0          до       100.

#include <iostream> using     namespace   std;

int    main() {

 int       x;

 for(x=0;         x<=100;        x++)    {

  if(x%2)      continue;   cout         <<       x          <<       '     ';

 }

 return            0;

}

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

В   циклах          while  и          do-while       инструкция continue       передает управление            непосредственно инструкции,          проверяющей        условное выражение,            после чего   циклический          процесс продолжает      "идти своим чередом".   А         в          цикле            for      после выполнения           инструкции continue сначала        вычисляется           инкрементное       выражение,            а          затем— условное.    И         только          после этого циклический          процесс        будет продолжен.

Использование        инструкции           break для     выхода         из        цикла

Инструкция   break позволяет  немедленно           выйти          из        цикла.

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

#include <iostream> using     namespace   std;

int    main() {

 int       t;

 //         Цикл  работает      для     значений     t           от        0          до       9,        а          не     до       100!

 for(t=0;         t<100;           t++)    {

  if(t==10)  break;   cout         <<       t           <<'     ';

 }

 return            0;

}

Эта           программа выведет       на       экран числа от        0          до       9,        а          не до       100,   поскольку   инструкция break при     значении     t,         равном         10, обеспечивает         немедленный        выход           из        цикла.

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

for(i=0;   i<1000;         i++)     {

 //         Какие-то       действия.

 if(kbhit())      break;

}

Инструкция     break приводит     к          выходу         из        самого          внутреннего     цикла.           Рассмотрим            пример.

#include <iostream> using     namespace   std;

int    main() {

 int       t,         count;

 for(t=0;         t<100;           t++)    {

  count        =          1;

  for(;;)        {

   cout      <<       count <<       '           ';

   count++;

   if(count==10) break;

  }

  cout           <<       '\n';

 }

 return            0;

}

Эта           программа 100     раз     выводит       на       экран числа от        0          до       9. При    каждом        выполнении инструкции          break управление            передается назад во       внешний      цикл  for.

На            заметку.      Инструкция           break,            которая      завершает выполнение           инструкции           switch, влияет      только        на       инструкцию switch,           а          не       на       содержащий         ее        цикл.

На            примере      предыдущей          программы вы       убедились, что      в          C++ с          помощью    инструкция for можно   создать         бесконечный         цикл. (Бесконечные        циклы           можно          также            создавать,   используя инструкции while  или     do-while,      но       цикл  for      —        это      традиционное решение.)    Для    выхода         из бесконечного   цикла            необходимо использовать         инструкцию            break.            (Безусловно,          инструкцию break можно          использовать         и          для     завершения            небесконечного    цикла.)

Вложенные   циклы

Как          было продемонстрировано    на       примере      предыдущей программы,           один  цикл  можно вложить    в          другой.         В          C++ разрешено  использовать         до       256     уровней       вложения.   Вложенные циклы используются        для     решения      задач самого          разного        профиля. Например,  в          следующей программе            вложенный             цикл  for позволяет    найти             простые       числа в          диапазоне   от        2          до       1000.

/* Эта      программа выводит       простые       числа,           найденные в            диапазоне   от 2    до       1000.

*/ #include     <iostream> using     namespace   std;

int    main() {

 int       i,          j;

 for(i=2;          i<1000;         i++)     {

  for(j=2;     j<=(i/j);          j++)

   if(!(i%j))           break;            //         Если   число имеет            множитель,            значит, оно не       простое.

  if(j  >          (i/j))    cout    <<       i           <<       "          -           простое        число\n";

 }

 return            0;

}

Эта           программа определяет,           является       ли       простым      число, которое        содержится            в переменной        i,          путем            последовательного его      деления       на       значения,    расположенные   между числом      2          и результатом           вычисления            выражения i/j.       (Остановить            перебор множителей           можно на    значении     выражения i/j,       поскольку   число, которое        превышает  i/j,       уже    не       может           быть множителем           значения i.)        Если   остаток         от        деления       i/j        равен            нулю, значит,          число i не       является простым.          Но       если   внутренний             цикл  завершится полностью  (без    досрочного            окончания   по инструкции       break),          это означает,     что      текущее        значение      переменной           i           действительно является       простым      числом.

Инструкция goto

Инструкция     goto   —        это    С++-инструкция  безусловного        перехода.

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

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

Метка    —        это    идентификатор,            за        которым    стоит     двоеточие.

Например,       с          помощью    инструкции goto   и          метки          можно организовать         следующий            цикл на        100     итераций.

х   =          1;

 loop1:

  х++;

  if(х <          100)   goto   loop1;

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

for(...)     {

 for(...)            {

  while(...)  {

   if(...)     goto   stop;

  }

 }

} stop:

 cout    <<       "Ошибка      в          программе.\n";

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

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

Итак,   подведем    итоги...

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

//  Программа "Угадай        магическое число":         последняя   версия.

#include <iostream> #include <cstdlib> using     namespace   std;

void         play(int     m); int     main()

{

 int       option;

 int       magic;

 magic =          rand();

 do       {

  cout           <<       "1.      Получить     новое            магическое число\n";

  cout           <<       "2.      Сыграть\n";

  cout           <<       "3.      Выйти            из        программы\n";

  do  {

   cout      <<       "Введите      свой   вариант:       ";

    cin    >>       option;

  }while(option<1 ||        option>3);

  switch(option){    case         1:

    magic          =          rand();

    break;

   case      2:

    play(magic);

    break;

   case      3:

    cout <<       "До     свидания     !\n";

    break;

  }

 }while(option           !=        3);

 return            0;

}

//  Сыграем      в     игру. void     play(int          m) {

 int       t,         x;

 for(t=0;         t<100;           t++)    {

  cout           <<       "Угадайте    магическое число:           ";

   cin         >>       x;

  if(x==m)   {

   cout      <<       "**     Правильно  **\n";

   return;

  }

  else

   if(x<m) cout    <<       "Маловато.\n";

   else       cout    <<       "Многовато.\n";

 }

            cout        <<       "Вы         использовали          все       шансы         угадать          число.              "             <<

"Попытайтесь        снова.\n";

}

Глава     5:     Массивы      и      строки

В   этой   главе мы      рассматриваем     массивы.     Массив         (array)           —        это коллекция   переменных одинакового       типа,  обращение к          которым происходит             с          применением        общего         для     всех    имени. В      C++ массивы       могут быть  одно- или     многомерными,   хотя    в          основном используются одномерные    массивы.     Массивы      представляют        собой удобное       средство      группирования связанных       переменных.

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

Одномерные            массивы

Одномерный  массив         —        это    список          связанных   переменных.          Для объявления            одномерного массива  используется       следующая форма записи.

тип          имя_массива         [размер];

Здесь      с          помощью    элемента     записи          тип   объявляется           базовый тип     массива.      Базовый       тип определяет   тип     данных         каждого       элемента, составляющего     массив.         Количество элементов, которые       будут храниться    в массиве,      определяется        элементом  размер.        Например,  при выполнении приведенной         ниже  инструкции объявляется           int-массив   (состоящий из        10 элементов)  с          именем        sample.

int            sample[10];

Индекс  идентифицирует           конкретный          элемент      массива.

Доступ   к          отдельному            элементу     массива        осуществляется     с помощью    индекса.      Индекс описывает           позицию      элемента     внутри массива.      В          C++     первый         элемент        массива        имеет нулевой      индекс. Поскольку   массив          sample          содержит     10       элементов,  его      индексы изменяются от      0          до       9.        Чтобы           получить      доступ          к          элементу массива        по       индексу,      достаточно указать нужный    номер           элемента     в квадратных            скобках.       Так,    первым        элементом  массива        sample является       sample[0],    а          последним —        sample[9].    Например,  следующая программа помещает    в массив      sample          числа от        0          до       9.

#include <iostream> using     namespace   std;

int    main() {

 int       sample[10]; //         Эта      инструкция резервирует           область        памяти для          10       элементов   типа   int.

 int       t;

 //         Помещаем  в          массив          значения.  for(t=0;  t<10;  ++t)    sample[t]=t;

  //    Отображаем          массив.

  for(t=0;    t<10;  ++t)

   cout      <<       sample[t]      <<       '           ';

 return            0;

}

В   C++     все      массивы       занимают    смежные     ячейки          памяти.        (Другими словами,      элементы массива          в          памяти          расположены последовательно             друг   за        другом.)      Ячейка          с          наименьшим адресом       относится    к          первому       элементу     массива,      а          с наибольшим          —        к          последнему. Например,           после выполнения этого  фрагмента   кода

int            i     [7]; int     j;

for(j=0;  j<7;     j++)i[j]=j; массив   i     будет выглядеть   следующим     образом.

Для         одномерных          массивов     общий           размер         массива        в     байтах           вычисляется           так:

всего      байтов        =          размер         типа в          байтах        х          количество     элементов.

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

#include <iostream>

#include <cstdlib> using     namespace   std;

int    main() {

 int       i,          min_value,   max_value;

 int       list       [10];

 for(i=0;          i<10;  i++)     list[i]   =          rand();

 //         Находим      минимальное        значение.

 min_value    =          list[0];

 for(i=1;          i<10;  i++)

  if(min_value       >          list[i]) min_value    =          list[i];

 cout    <<       "Минимальное     значение:    "          <<       min_value    <<       '           \n';

 //         Находим      максимальное      значение.

 max_value    =          list[0];

 for(i=1;          i<10;  i++)

  if(max_value       <          list[i]) max_value   =          list[i];

 cout    <<       "Максимальное    значение:    "          <<       max_value   <<       '\n';

 return            0;

}

В   C++     нельзя          присвоить   один  массив          другому.      В          следующем фрагменте   кода, например, присваивание         а          =          b;        недопустимо.

int            а[10], b[10];

//  ...

а   =          b;        //         Ошибка!!!

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

На          границах     массивов     погранзаставы   нет

В   C++     не       выполняется          никакой       проверки     "нарушения            границ" массивов,    т.е.      ничего          не может     помешать    программисту       обратиться  к массиву        за        его      пределами.            Если   это происходит     при     выполнении инструкции присваивания,       могут быть  изменены    значения      в ячейках     памяти, выделенных           некоторым другим         переменным          или     даже  вашей программе. Другими     словами,      обращение к          массиву        (размером  N элементов) за        границей     N-гo   элемента может   привести      к          разрушению программы при     отсутствии   каких-либо замечаний  со стороны компилятора         и без     выдачи         сообщений об       ошибках       во       время            работы программы.           Это означает,         что      вся      ответственность    за        соблюдение границ          массивов     лежит            только          на программистах,          которые должны       гарантировать       корректную            работу          с          массивами. Другими словами,           программист         обязан          использовать         массивы достаточно большого    размера,      чтобы            в них  можно          было без осложнений           помещать    данные,       но       лучше           всего в          программе предусмотреть      проверку     пересечения           границ          массивов.

Например,       С++-компилятор   "молча"       скомпилирует       и          позволит запустить     следующую программу            на       выполнение,          несмотря     на       то, что      в          ней     происходит             выход           за        границы       массива crash.

Осторожно!     Не       выполняйте          следующий пример         программы. Это   может         разрушить вашу систему.

//  Некорректная        программа.            Не       выполняйте            ее!

int    main() {

 int       crash[10],     i;

 for(i=0;          i<100;            i++)     crash[i]=i;

 return            1;

}

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

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

Сортировка  массива

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

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

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

#include <iostream> #include <cstdlib> using     namespace   std;

int    main() {

 int       nums[10];

 int       a,         b,        t;

 int       size;

 size     =          10;      //         Количество элементов,  подлежащих          сортировке.

 //         Помещаем  в          массив          случайные  числа.  for(t=0;  t<size;            t++)    nums[t]         =          rand();

 //         Отображаем          исходный    массив.

 cout    <<       "Исходный  массив:         ";

 for(t=0;         t<size;            t++)    cout    <<       nums[t]         <<       '           ';

 cout    <<       '\n';

 //         Реализация             метода         пузырьковой         сортировки.

 for(a=1;         a<size;           а++)

  for(b=size-1;       b>=a; b--)     {

   if(nums[b-1]   >          nums[b])       {                                                           //         Элементы     неупорядочены.

    //      Меняем        элементы    местами.

    t        =          nums[b-1];

    nums[b-1]  =          nums[b];

    nums[b]      =          t;

   }

  }

 }

 //         Конец            пузырьковой         сортировки.

 //         Отображаем          отсортированный            массив.

 cout    <<       "Отсортированный          массив:         ";

 for(t=0;         t<size;            t++)

  cout           <<       nums[t]         <<       '           ';

 return            0;

}

Хотя        алгоритм     пузырьковой         сортировки пригоден     для     небольших массивов,    для     массивов большого        размера       он       становится неэффективным.  Более            универсальным    считается     алгоритм Quicksort.        В стандартную          библиотеку             C++     включена    функция       qsort(),          которая реализует одну    из        версий          этого  алгоритма.  Но,     прежде         чем использовать         ее,      вам    необходимо           изучить больше    средств         C++. (Подробно  функция       qsort()           рассмотрена          в          главе 20.)

Строки

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

Строка   —        это    символьный           массив,        который     завершается     нулевым      символом.

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

char         str       [11];

Заданный         здесь размер         (11)    позволяет    зарезервировать  место            для нулевого      символа       в конце         строки.

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

"Привет"

"Мне      нравится      C++"

"#$%@@#$"

""

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

Вам         не       нужно           вручную       добавлять   в          конец            строковых констант       нулевые       символы.     С++компилятор    делает          это      автоматически. Следовательно,    строка           "ПРИВЕТ"     в          памяти размещается      так,     как показано     на       этом   рисунке:

Считывание строк           с          клавиатуры

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

// Использование     cin-инструкции      для     считывания             строки           с клавиатуры.

#include <iostream> using     namespace   std;

int    main() {

 char    str[80];

 cout    <<       "Введите      строку:          ";

  cin  >>       str;      //         Считываем  строку           с          клавиатуры.

 cout    <<       "Вот   ваша  строка:          ";

 cout    <<       str;

 return            0;

}

Несмотря          на       то        что      эта      программа формально корректна,  она     не лишена         недостатков. Рассмотрим        следующий            результат     ее выполнения.

Введите строку:          Это     проверка

Вот          ваша  строка:          Это

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

Для          решения      этой   проблемы   можно          использовать         еще    одну библиотечную       функцию gets().    Общий          формат         ее        вызова          таков.

gets(имя_массива);

Если        в          программе необходимо           считать         строку           с клавиатуры,           вызовите     функцию      gets(),            а          в качестве   аргумента передайте   имя    массива,      не       указывая     индекса.      После            выполнения этой функции        заданный    массив          будет содержать   текст, введенный  с клавиатуры.           Функция       gets() считывает    вводимые   пользователем     символы до       тех      пор,   пока   он       не       нажмет         клавишу <Enter>. Для    вызова функции       gets() в          программу необходимо           включить     заголовок    <cstdio>.

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

           //           Использование           функции           gets()          для          считывания       строки         с

клавиатуры.

#include <iostream> #include <cstdio> using     namespace   std;

int    main() {

 char    str[80];

 cout    <<       "Введите      строку:          ";

 gets(str);       //         Считываем  строку           с          клавиатуры.

 cout    <<       "Вот   ваша  строка:          ";

 cout    <<       str;

 return            0;

}

На            этот    раз     после запуска         новой            версии          программы на выполнение           и          ввода            с          клавиатуры текста           "Это простой тест"          строка           считывается            полностью, а          затем так      же полностью  и

отображается.

Введите строку:          Это     простой        тест

Вот          ваша  строка:          Это     простой        тест

В  этой   программе следует        обратить      внимание    на       следующую     инструкцию.

cout         <<       str;

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

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

Некоторые   библиотечные     функции      обработки             строк

Язык       C++     поддерживает       множество  функций       обработки   строк. Самыми распространенными            из        них     являются     следующие.

strcpy()

strcat()

strlen()

strcmp()

Для         вызова          всех    этих    функций       в          программу необходимо     включить     заголовок    <cstring>.

Теперь          познакомимся      с          каждой         функцией    в          отдельности.

Функция     strcpy() Общий      формат         вызова     функции       strcpy()         таков:

strcpy     (to,     from);

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

Использование          функции       strcpy()         демонстрируется в          следующей программе,            которая копирует            строку           "Привет"    в          строку           str.

#include <iostream> #include <cstring> using     namespace   std;

int    main() {

 char    str[80];

 strcpy(str,     "Привет");

 cout    <<       str;

 return            0;

}

Функция         strcat()

Обращение     к          функции       strcat()          имеет            следующий            формат.

strcat(s1,           s2);

Функция            strcat()          присоединяет        строку           s2        к          концу строки           s1,       при     этом   строка           s2        не изменяется.      Обе    строки должны       завершаться           нулевым      символом.  Результат     вызова          этой функции,      т.е.      результирующая  строка           s1        также            будет завершаться нулевым      символом. Использование      функции       strcat()          демонстрируется в следующей программе,            которая        должна вывести   на       экран строку "Привет      всем!".

#include <iostream> #include <cstring> using     namespace   std;

int    main() {

 char    s1[20],           s2[10];

 strcpy(s1,      "Привет");

 strcpy(s2,      "          всем!");

 strcat  (s1,     s2);

 cout    <<       s1;

 return            0;

}

Функция         strcmp()

Обращение     к          функции       strcmp()        имеет            следующий            формат:

strcmp(s1,         s2);

Функция            strcmp()        сравнивает  строку           s2        со        строкой        s1        и возвращает             значение      0,        если   они равны. Если   строка           s1 лексикографически         (т.е.    в          соответствии          с          алфавитным порядком) больше         строки           s2,       возвращается        положительное    число. Если   строка           s1        лексикографически меньше   строки           s2,       возвращается отрицательное      число.

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

#include <iostream>

#include <cstring>

#include <cstdio> using     namespace   std;

bool     password(); int            main() {

 if(password())         cout    <<       "Вход разрешен.\n";

 else     cout    <<       "В        доступе        отказано.\n";

 return            0;

}

// Функция       возвращает             значение      true,   если   пароль          принят,         и значение      false   в          противном  случае.

bool     password() {

 char    s[80];

 cout    <<       "Введите      пароль:        ";

 gets(s);

 if(strcmp(s,   "пароль"))   {           //         Строки          различны.

  cout           <<       "Пароль       недействителен.\n";

  return       false;

 }

 //         Сравниваемые      строки           совпадают.

 return            true;

}

При         использовании     функции       strcmp()        важно           помнить,      что      она возвращает             число 0          (т.е. значение        false), если   сравниваемые       строки равны.          Следовательно,    если   вам    необходимо выполнить           определенные действия      при     условии        совпадения            строк,            вы       должны использовать оператор            НЕ       (!).      Например,  при     выполнении следующей программы запрос          входных       данных продолжается   до       тех      пор, пока   пользователь         не       введет           слово "Выход".

#include <iostream>

#include <cstdio> #include <cstring> using     namespace   std; int     main() {

 char    s          [80];

 for(;;) {

  cout           <<       "Введите      строку:          ";

  gets            (s);

  if(!strcmp("Выход",     s))       break;

 }

 return            0;

}

Функция     strlen() Общий      формат         вызова     функции       strlen()          таков:

strlen(s);

Здесь     s          —        строка.          Функция       strlen()          возвращает             длину     строки,         указанной   аргументом            s.

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

#include <iostream>

#include <cstdio> #include <cstring> using     namespace   std;

int    main() {

 char    str[80];

 cout    <<       "Введите      строку:          ";         gets(str);

 cout    <<       "Длина         строки           равна:           "          <<       strlen(str);

 return            0;

}

Если        пользователь         введет           строку           "Привет      всем!",     программа выведет       на       экране          число

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

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

//  Отображение         строки           в          обратном    порядке.

#include <iostream>

#include <cstdio> #include <cstring> using     namespace   std;

int    main() {

 char    str[80];

 int       i;

 cout    <<       "Введите      строку:          ";

 gets(str);

 for(i=strlen(str)-1;  i>=0;  i--)

  cout           <<       str[i];

 return            0;

}

В   следующем            примере      продемонстрируем        использование      всех    этих четырех        строковых функций.

#include <iostream>

#include <cstdio> #include <cstring> using     namespace   std;

int    main() {

 char    s1[80],           s2        [80];

 cout    <<       "Введите      две     строки:         ";

 gets     (s1);    gets(s2);

 cout    <<       "Их     длины           равны:          "          <<       strlen (s1);

 cout    <<       '           '<<      strlen(s2)      <<       '\n';

 if(!strcmp(s1,           s2))     cout    <<       "Строки        равны           \n";

 else     cout    <<       "Строки        не       равны           \n";

 strcat(s1,       s2);

 cout    <<       s1        <<       '\n';

 strcpy(s1,      s2);

 cout    <<       s1        <<       "          и          "          <<       s2        <<       '           ';

 cout    <<       "теперь         равны\n";

 return            0;

}

Если        запустить     эту      программу на       выполнение           и          по приглашению        ввести           строки "привет" и          "всем",         то        она отобразит    на       экране          следующие результаты:

Их            длины     равны:          6          4 Строки   не       равны привет   всем

всем       и          всем  теперь           равны

Последнее       напоминание:       не       забывайте,  что      функция       strcmp() возвращает             значение      false, если   строки           равны.          Поэтому,      если   вы проверяете равенство    строк,            необходимо           использовать оператор            "!" (НЕ),   чтобы            реверсировать      условие        (т.е.    изменить     его      на обратное),   как     было показано     в          предыдущей          программе.

Использование        признака     завершения           строки

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

//  Преобразование  символов    строки                       в          их        прописные     эквиваленты.

#include <iostream>

#include <cstring> #include <cctype> using     namespace   std;

int    main() {

 char    str[80];

 int       i;

 strcpy(str,     "test");

 for(i=0;          str[i];  i++)     str[i]   =          toupper(str[i]);

  cout           <<       str;

 return            0;

}

Эта          программа при     выполнении           выведет       на       экран слово TEST.     Здесь используется библиотечная   функция       toupper(),    которая     возвращает             прописной  эквивалент  своего символьного       аргумента.  Для     вызова          функции       toupper()      необходимо           включить     в     программу заголовок    <cctype>.

Обратите           внимание    на       то,       что      в          качестве       условия завершения            цикла            for      используется массив     str,      индексируемый управляющей        переменной           i           (str[i]).           Такой            способ управления циклом       вполне          приемлем,  поскольку   за        истинное      значение в          C++     принимается          любое ненулевое            значение.    Вспомните, что      все печатные     символы      представляются    значениями,           не равными            нулю, и только          символ,        завершающий       строку,          равен            нулю. Следовательно, этот цикл     работает      до       тех      пор,   пока   индекс          не       укажет          на нулевой       признак        конца            строки,         т.е.      пока значение       str[i]   не станет            нулевым.     Поскольку   нулевой       символ         отмечает      конец строки,         цикл останавливается    в          точности      там,    где     нужно.          При дальнейшей           работе          с          этой   книгой          вы увидите множество примеров,   в          которых       нулевой       признак        конца            строки используется подобным          образом.

Важно!  Помимо       функции      toupper(),    стандартная       библиотека          C++ содержит  много других         функций      обработки символов.   Например,  функцию toupper()      дополняет функция tolower(),          которая      возвращает строчный   эквивалент           своего           символьного         аргумента.           Часто используются      такие           функции,     как     isalpha(),      isdigit(),         isspace()       и ispunct(),      которые принимают  символьный           аргумент   и          определяют, принадлежит      ли       он       к          соответствующей категории.     Например, функция       isalpha()       возвращает          значение     ИСТИНА,      если   ее аргументом является буква (элемент    алфавита).

Двумерные   массивы

В   C++     можно          использовать         многомерные        массивы.     Простейший многомерный       массив          — двумерный.      Двумерный            массив,         по сути,  представляет         собой            список          одномерных          массивов. Чтобы объявить      двумерный массив          целочисленных     значений     размером    10x20 с именем        twod, достаточно записать       следующее:           

int            twod[10][20];

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

Чтобы     получить      доступ          к          элементу     массива        twod  с координатами       3,5      ,           необходимо использовать      запись           twod[3][5].  В следующем            примере      в          двумерный массив          помещаются последовательные          числа от        1          до       12.

#include <iostream> using     namespace   std;

int    main() {

 int       t,i,       num[3]          [4];

 for(t=0;         t<3;    ++t)    {

  for(i=0;     i<4;     ++i)     {

   num[t][i]         =          (t*4)+i+l;

   cout      <<       num[t][i]       <<       '           ';

  }

  cout           <<       '\n';

 }

 return            0;

}

В   этом   примере      элемент        num[0][0]    получит        значение      1,        элемент num[0][1]    —        значение 2, элемент        num[0][2]    —        значение      3          и          т.д. Значение     элемента     num[2][3]    будет равно            числу 12.     Схематически        этот массив          можно          представить,          как     показано     на       рис.    5.1.

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

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

число     байтов        =          число строк           х          число столбцов    х          размер     типа в          байтах

Следовательно,         двумерный целочисленный    массив          размерностью 10x5   занимает     в памяти      10x5x2,         т.е.      100     байт   (если целочисленный    тип имеет            размер         2          байт).

Многомерные         массивы

В   C++,   помимо        двумерных,            можно          определять             массивы трех    и          более            измерений.             Вот     как объявляется   многомерный массив.

тип          имя[размер1]        [размер2]...            [размерN];

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

int            multidim[4][10][3];

Как          упоминалось         выше,            память,         выделенная           для     хранения всех    элементов   массива, используется  в          течение        всего времени существования      массива.      Массивы      с          числом измерений,        превышающим три,    используются        нечасто,       хотя    бы      потому,        что      для     их        хранения требуется     большой      объем           памяти.        Например,  хранение     элементов четырехмерного символьного          массива        размером    10x6x9x4      займет          2 160     байт.            А         если   каждую        размерность увеличить            в          10       раз, то        занимаемая           массивом    память          возрастет     до       21       600     000 байт.            Как видите,             большие      многомерные        массивы       способны "съесть"       большой      объем           памяти,        а программа,         которая        их использует,            может           очень            быстро          столкнуться            с проблемой нехватки памяти.

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

В   C++     предусмотрена     возможность         инициализации     массивов.    Формат инициализации массивов       подобен       формату       инициализации     других переменных.

тип          имя_массива         [размер]      =          {список_значений};

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

Например,       в          следующем            примере      10-элементный     целочисленный массив инициализируется       числами       от        1          до       10.

int            i           [           10]      =          {1,       2,        3,        4,        5,        6,        7,        8,        9,     10};

После     выполнения           этой   инструкции элемент        i[0]      получит        значение 1,        а          элемент        i[9]      — значение            10.

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

char         имя_массива[размер]   =          "строка";

Например,       следующий            фрагмент     кода  инициализирует   массив          str     фразой         "привет",

     char    str[7]  =          "привет";

Это          равнозначно          поэлементной       инициализации.

     char    str       [7]       =          {'п',     'р',      'и',      'в',       'е',       'т',       '\0'};

Поскольку        в          C++     строки           должны       завершаться           нулевым символом,  убедитесь,  что      при объявлении   массива        его      размер         указан с          учетом          признака     конца.           Именно        поэтому       в предыдущем примере      массив          str       объявлен     как     7-элементный,      несмотря     на       то, что      в          слове "привет"     только          шесть            букв.  При    использовании строкового  литерала      компилятор            добавляет нулевой         признак        конца строки           автоматически.

Многомерные            массивы       инициализируются          по       аналогии     с одномерными.     Например,  в следующем        фрагменте   программы массив          sqrs инициализируется           числами       от        1          до       10       и квадратами        этих чисел.

int            sqrs[10][2]   =          {

 1,         1,

 2,         4,

 3,         9,

 4,         16,

 5,         25,

 6,         36,

 7,         49,

 8,         64,

 9,         81,

 10,      100

};

Теперь   рассмотрим,          как     элементы    массива        sqrs    располагаются      в     памяти          (рис.  5.2).

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

int            sqrs[10][2]   =          {

 {1,       1},

 {2,       4},

 {3,       9},

 {4,       16},

 {5,       25},

 {6,       36},

 {7,       49},

 {8,       64},

 {9,       81},

 {10,     100}

};

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

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

#include <iostream> using     namespace   std;

int            sqrs[10][2]   =          {

 {1,       1},

 {2,       4},

 {3,       9},

 {4,       16},

 {5,       25},

 {6,       36},

 {7,       49},

 {8,       64},

 {9,       81},

 {10,     100}

}; int main() {

 int       i,          j;

 cout    <<       "Введите      число от        1          до       10:      ";

  cin  >>       i;

 //         Поиск            значения      i.

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

  if(sqrs[j][0]==i)  break;

 cout    <<       "Квадрат      числа "          <<       i           <<       "          равен            ";

 cout    <<       sqrs[j][1];

 return            0;

}

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

#include <iostream> #include <cstring> using     namespace   std;

void     f1(); int    main() {

 f1();

 f1();

 return            0;

}

void     f1() {

 char    s[80]="Этo   просто          тест\n";

 cout    <<       s;

 strcpy(s,        "ИЗМЕНЕНО\n");  //         Изменяем   значение      строки           s.

 cout    <<       s;

}

При         выполнении           этой   программы получаем    такие результаты.

Это          просто          тест

ИЗМЕНЕНО

Это          просто          тест

ИЗМЕНЕНО

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

Это          просто          тест

"Безразмерная"      инициализация    массивов

Предположим,           что      мы      используем            следующий            вариант инициализации     массивов     для построения    таблицы       сообщений об       ошибках. char    e1[14]           =          "Деление     на       0\n"; char    е2[23]           =          "Конец файла\n";

char         еЗ[21]            =          "В        доступе        отказано\п";

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

char         е1[]    =          "Деление     на     0\n"; char    е2[]    =          "Конец     файла\n";

char         еЗ[]     =          "В        доступе        отказано\n";

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

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

В  следующем            примере      массив          sqrs    объявляется           как     "безразмерный".

int            sqrs[][2]        =          {

 1,         1,

 2,         4,

 3,         9,

 4,         16,

 5,         25,

 6,         36,

 7,         49,

 8,         64,

 9,         81,

 10,      100

};

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

Массивы        строк

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

char         str_array[30][80];

Массив строк —        это    специальная          форма          двумерного            массива     символов.

Получить          доступ          к          отдельной   строке           довольно    просто: достаточно указать         только          левый индекс.       Например,  следующая инструкция вызывает     функцию      gets() для     записи          третьей строки      массива str_array.

gets(str_array[2]);

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

//  Вводим        строки           текста            и          отображаем           их        на       экране.

#include <iostream> #include <cstdio> using     namespace   std; int     main() {

 int       t,         i;

 char    text[100][80];

 for(t=0;         t<100;           t++)    {

  cout           <<       t           <<       ":         ";

  gets(text[t]);

  if(!text[t]  [0])     break;            //         Выход           из        цикла            по       пустой     строке.  }

 //         Отображение         строк на       экране.

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

  cout           <<       text[i] <<       '           \n';

 return            0;

}

Обратите           внимание    на       то,       как     в          программе выполняется проверка     на       ввод  пустой           строки. Функция  gets() возвращает             строку нулевой       длины,         если   единственной        нажатой       клавишей оказалась клавиша       <Enter>.        Это     означает,     что      первым        байтом         в          строке будет нулевой символ.  Нулевое       значение      всегда           интерпретируется            как ложное,       но       взятое           с          отрицанием            (!) дает          значение      ИСТИНА, которое        позволяет    выполнить  немедленный        выход           из        цикла            с помощью    инструкции break.

Пример           использования     массивов     строк

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

char         name[10][80];        //         Массив         имен  служащих. char     phone[10][20];       //         Массив         телефонных            номеров     служащих. float    hours[10];    //         Массив         часов,     отработанных        за        неделю.

float        wage[10];     //         Массив         окладов.

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

//  Функция       ввода            информации          в     базу   данных. void          enter() {

 int       i;

 char    temp[80];

 for(i=0;          i<10;  i++)     {

  cout           <<       "Введите      фамилию:   ";

   cin         >>       name[i];

  cout           <<       "Введите      номер           телефона:    ";

   cin         >>       phone[i];

  cout           <<       "Введите      количество отработанных        часов:            ";

   cin         >>       hours[i];

  cout           <<       "Введите      оклад:           ";

   cin         >>       wage[i];

 }

}

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

//  Отображение         отчета.

void         report()

{

 int       i;

 for(i=0;          i<10;  i++)     {

  cout           <<       name[i]         <<       '           '           <<       phone[i]        <<       '\n';

  cout           <<       "Заработная           плата за        неделю:       "<<     wage[i]          *     hours[i];   cout            <<       '\n';

 }

}

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

//  Простая        программа ведения       базы  данных     служащих. #include         <iostream> using   namespace   std;

char         name[10][80];        //         Массив         имен  служащих.

char         phone[10]    [20];   //         Массив         телефонных            номеров     служащих. float    hours[10];    //         Массив         часов,            отработанных     за        неделю. float         wage[10];     //         Массив         окладов.

int            menu(); void     enter(),          report(); int            main() {

 int       choice;

 do       {

  choice       =          menu();         //         Получаем    команду,     выбранную пользователем.

  switch(choice)    {

   case      0:        break;

   case      1:        enter();

    break;

   case      2:        report();

    break;

   default:            cout    <<       "Попробуйте          еще    раз.\n\n";

  }

 }while(choice           !=        0);

 return            0;

}

//  Функция       возвращает             команду,     выбранную пользователем.

int   menu() {

 int       choice;

 cout    <<       "0.      Выход           из        программы\n";

 cout    <<       "1.      Ввод  информации\n";

 cout    <<       "2.      Генерирование     отчета\n";

 cout    <<       "\n      Выберите     команду:     ";

  cin  >>       choice;

 return            choice;

}

//  Функция       ввода            информации          в     базу   данных. void          enter() {

 int       i;

 char    temp[80];

 for(i=0;          i<10;  i++)     {

  cout           <<       "Введите      фамилию:   ";

   cin         >>       name[i];

  cout           <<       "Введите      номер           телефона:    ";

   cin         >>       phone[i];

  cout           <<       "Введите      количество отработанных        часов:            ";

   cin         >>       hours[i];

  cout           <<       "Введите      оклад:           ";

   cin         >>       wage[i];

 }

}

//  Отображение         отчета.

void     report() {

 int       i;

 for(i=0;          i<10;  i++)     {

  cout           <<       name[i]         <<       '           '           <<       phone[i]        <<       '\n';

  cout           <<       "Заработная           плата за        неделю:       "<<     wage[i]          *     hours[i];

  cout           <<       '\n';

 }

}

Глава     6:     Указатели

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

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

Что      представляют    собой            указатели

Указатели       —        это      переменные,         которые       хранят           адреса памяти.        Чаще всего эти      адреса обозначают         местоположение  в          памяти других          переменных.          Например,  если   х          содержит адрес   переменной у,         то        о          переменной,          х          говорят,       что      она     "указывает"         на у.

Указатель         —        это    переменная,          которая      содержит  адрес другой     переменной.

Переменные-указатели   (или   переменные           типа указатель) должны быть  соответственно объявлены.   Формат         объявления            переменной-указателя     таков:

тип          *имя_переменной;

Здесь      элемент        тип   означает      базовый       тип     указателя,   причем         он должен        быть  допустимым С++-типом.          Элемент       имя_переменной представляет         собой            имя    переменной-указателя. Рассмотрим          пример. Чтобы           объявить      переменную          р          указателем на       int-значение, используйте следующую         инструкцию.

int            *р;

Для         объявления            указателя    на       float-значение       используйте     такую            инструкцию.

float        *р;

В   общем          случае           использование      символа       "звездочка"            (*) перед            именем        переменной           в инструкции         объявления превращает            эту      переменную          в          указатель.

Базовый           тип   указателя  определяет           тип   данных,        на       которые     он       будет           ссылаться.

Тип          данных,        на       которые       будет ссылаться    указатель,   определяется его      базовым      типом. Рассмотрим        еще    один  пример.

int            *ip;     //         указатель    на       целочисленное     значение

double    *dp;   //         указатель    на       значение      типа   double

Как          отмечено     в          комментариях,      переменная            ip         —        это указатель    на       int-значение,          поскольку его       базовым      типом            является тип     int,      а          переменная            dp       —        указатель    на       double-значение, поскольку   его      базовым      типом            является       тип     double,          Следовательно, в          предыдущих          примерах переменную ip         можно          использовать         для указания      на       int-значения,          а          переменную          dp       на double-значения. Однако         помните:      не       существует  реального   средства,     которое        могло бы помешать         указателю   ссылаться    на       "бог-знает-что".           Вот     потому-то указатели    потенциально опасны.

Операторы, используемые      с          указателями

С   указателями           используются        два     оператора:  "*"      и          "&"     Оператор "&"     —        унарный.     Он возвращает      адрес            памяти,        по       которому расположен           его      операнд.      (Вспомните:            унарному оператору      требуется только          один  операнд.)    Например,  при     выполнении           следующего фрагмента   кода

balptr      =          &balance;

в   переменную          balptr помещается            адрес            переменной           balance. Этот    адрес            соответствует области   во       внутренней             памяти компьютера,          которая        принадлежит         переменной           balance. Выполнение этой   инструкции никак            не       повлияло     на       значение      переменной balance. Назначение       оператора   можно          "перевести"          на       русский язык  как     "адрес          переменной",        перед которой      он       стоит. Следовательно,    приведенную        выше инструкцию            присваивания        можно выразить      так:     "переменная         balptr получает    адрес переменной           balance". Чтобы           лучше           понять суть этого  присваивания,       предположим,      что переменная            balance         расположена         в          области памяти    с          адресом 100.   Следовательно,    после выполнения           этой   инструкции переменная balptr получит        значение      100.

Второй   оператор     работы         с          указателями           (*)       служит дополнением        к          первому       (&).     Это также    унарный      оператор,    но       он обращается            к          значению    переменной,          расположенной    по адресу, заданному  его      операндом.            Другими      словами,      он       ссылается    на значение      переменной, адресуемой       заданным   указателем.            Если (продолжая            работу          с          предыдущей          инструкцией присваивания) переменная            balptr содержит     адрес            переменной           balance,        то        при выполнении инструкции

value       =          *balptr;

переменной    value  будет присвоено   значение      переменной           balance,        на которую       указывает переменная  balptr.           Например,  если   переменная balance         содержит     значение      3200, после выполнения          последней инструкции переменная            value  будет содержать   значение      3200, поскольку   это как     раз     то        значение,    которое        хранится      по       адресу          100. Назначение            оператора "*"       можно          выразить     словосочетаинем             "по адресу".       В          данном        случае           предыдущую инструкцию       можно прочитать    так:     "переменная         value  получает    значение     (расположенное) по адресу          balptr".         Действие      приведенных         выше двух   инструкций схематично             показано     на рис.          6.1.

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

#include <iostream> using     namespace   std; int     main() {

 int       balance;

 int       *balptr;

 int       value;

 balance          =          3200;

 balptr =          &balance;

 value  =          *balptr;

 cout    <<       "Баланс        равен:"         <<       value  <<'\n';

 return            0;

}

При         выполнении           этой   программы получаем    такие результаты:

Баланс   равен:           3200

К   сожалению,            знак   умножения (*)       и          оператор     со        значением  "по адресу"        обозначаются одинаковыми  символами "звездочка",          что      иногда сбивает         с          толку новичков     в          языке            C++.   Эти операции        никак не       связаны        одна  с          другой.         Имейте         в          виду, что      операторы  "*" и          "&"     имеют более         высокий       приоритет,  чем    любой           из арифметических   операторов,           за        исключением унарного            минуса, приоритет   которого      такой же,     как     у          операторов,           применяемых        для работы с      указателями.

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

Операция        непрямого доступа        —        это    процесс        использования указателя  для     доступа      к некоторому       объекту.

О            важности  базового     типа указателя

На            примере      предыдущей          программы была  показана      возможность присвоения переменной          value  значения      переменной           balance посредством          операции     непрямого  доступа, т.е.           с          использованием указателя.   Возможно,  при     этом   у          вас      промелькнул         вопрос:         "Как С++компилятор  узнает,        сколько        необходимо           скопировать         байтов в          переменную          value  из области памяти,      адресуемой            указателем balptr?".       Сформулируем     тот      же       вопрос          в          более общем         виде: как С++-компилятор   передает      надлежащее          количество байтов          при выполнении операции  присваивания        с          использованием  указателя?  Ответ звучит           так.     Тип     данных, адресуемый     указателем,            определяется базовым      типом            указателя.   В          данном        случае,         поскольку balptr представляет         собой            указатель    на       целочисленный    тип,    С++-компилятор            скопирует    в переменную       value  из        области        памяти, адресуемой            указателем balptr,           четыре          байт информации            (что справедливо          для     32-разрядной        среды),         но       если   бы      мы      имели дело  с          doubleуказателем,           то        в          аналогичной          ситуации скопировалось      бы      восемь          байт.

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

int     *р; double     f; //     ...

р   =          &f;      //         ОШИБКА!

Некорректность         этого  фрагмента   состоит         в          недопустимости присваивания        doubleуказателя   int-указателю.       Выражение             &f генерирует  указатель    на       double-значение,  а          р          — указатель           на целочисленный    тип     int.      Эти     два     типа   несовместимы,     поэтому компилятор отметит      эту      инструкцию            как     ошибочную            и          не скомпилирует       программу.

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

int     *р; double     f; //     ...

р   =          (int      *)        &f;      //         Теперь          формально все      ОК!

Операция         приведения            к          типу   (int      *)        вызовет преобразование   double-         к          int-указателю.       Все же          использование операции     приведения            в          таких целях несколько   сомнительно, поскольку именно          базовый       тип     указателя    определяет,           как компилятор            будет обращаться            с          данными,    на которые он ссылается.   В          данном        случае,         несмотря     на       то,       что      p          (после выполнения           последней инструкции)            в          действительности указывает    на       значение      с          плавающей             точкой,         компилятор по-прежнему    "считает",    что      он       указывает    на       целочисленное     значение (поскольку  р          по определению  —        int-указатель).

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

//  Эта      программа не       будет выполняться     правильно. #include       <iostream> using   namespace   std;

int    main() {

 double           x,         у;

 int       *p;

 x          =          123.23;

 p          =          (int      *)        &x;      //         Используем            операцию    приведения            типов для присваивания           double-указателя  int-указателю.

 у          =          *р;      //         Что     происходит             при     выполнении           этой     инструкции?

 cout    <<       у;         //         Что     выведет       эта      инструкция?

 return            0;

}

Как          видите,         в          этой   программе переменной           p          (точнее, указателю   на       целочисленное значение)       присваивается       адрес переменной           х          (которая       имеет            тип     double).        Следовательно, когда переменной           y          присваивается       значение,    адресуемое указателем р,        переменная            y получает   только          четыре          байт   данных (а        не       все      восемь,        требуемые  для     double-значения), поскольку р— указатель    на       целочисленный    тип     int.      Таким            образом,     при выполнении           coutинструкции    на       экран будет выведено    не       число 123.23, а,         как     говорят         программисты, "мусор".          (Выполните            программу и убедитесь    в          этом   сами.)

Присваивание          значений     с          помощью   указателей

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

*р =          101;

число      101     присваивается       области        памяти,        адресуемой указателем р.        Таким            образом,     эту инструкцию     можно          прочитать    так: "по     адресу          р          помещаем  значение     101". Чтобы инкрементировать        или декрементировать          значение,    расположенное    в          области        памяти, адресуемой            указателем,            можно          использовать         инструкцию, подобную   следующей.

(*р)++;

Круглые скобки          здесь обязательны,         поскольку   оператор     "*"      имеет более            низкий приоритет,          чем    оператор     "++".

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

#include <iostream> using     namespace   std;

int    main() {

 int       *p,      num;

 p          =          &num;

 *p       =          100;

 cout    <<       num   <<       '           ';

 (*p)++;

 cout    <<       num   <<       '           ';

 (*p)--;

 cout    <<       num   <<       '\n';

 return            0;

}

Вот          такие результаты генерирует  эта      программа.

100          101     100

Использование        указателей           в          выражениях

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

Арифметические  операции    над     указателями

С   указателями           можно          использовать         только          четыре арифметических   оператора:  ++,      --,        + и      -.         Чтобы           лучше           понять, что      происходит             при     выполнении           арифметических   действий      с указателями,          начнем         с          примера.     Пусть p1       —        указатель    на       int-переменную           с          текущим значением       2          ООО   (т.е.    p1       содержит адрес            2          ООО).            После            выполнения           (в        32-разрядной среде) выражения

p1++;

содержимое    переменной-указателя  p1       станет            равным        2          004,   а не       2          001!   Дело  в          том,   что при         каждом        инкрементировании указатель    p1       будет указывать    на       следующее int-значение. Для            операции декрементирования       справедливо          обратное     утверждение,        т.е.      при каждом декрементировании             значение      p1       будет уменьшаться          на       4. Например,  после выполнения инструкции

p1--;

указатель          p1       будет иметь            значение      1          996,   если   до       этого  оно было равно            2          000.   Итак, каждый        раз,    когда указатель инкрементируется,          он       будет указывать    на       область        памяти, содержащую следующий            элемент        базового      типа   этого  указателя.   А         при     каждом декрементировании       он       будет указывать    на       область        памяти, содержащую         предыдущий элемент   базового      типа   этого  указателя.

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

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

p1 =          p1       +          9;

         заставляет                                                              p1        ссылаться                                                               на         девятый                                                          элемент         базового                                                             типа        указателя                                                              p1                                                      относительно

элемента,    на       который       p1       ссылался     до       выполнения           этой инструкций.

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

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

Чтобы     понять,         как     формируется         результат     выполнения арифметических   операций     над указателями, выполним   следующую            короткую программу.            Она    выводит       реальные физические   адреса,         которые содержат     указатель    на       int-значение           (i)        и          указатель    на floatзначение         (f).      Обратите     внимание    на       каждое         изменение  адреса (зависящее от        базового      типа указателя),   которое        происходит             при повторении            цикла.           (Для   большинства          32-разрядных компиляторов значение      i           будет увеличиваться       на       4,        а          значение      f           —        на 8.)       Отметьте      также, что   при     использовании     указателя    в          cout-инструкции его      адрес            автоматически      отображается в    формате адресации, применяемом       для     текущего      процессора             и          среды выполнения.

//  Демонстрация       арифметических   операций     над     указателями. #include <iostream> using   namespace   std;

int    main() {

 int       *i,       j[10];

 double           *f,       g[10];

 int       x;

 i           =          j;

 f           =          g;

 for(x=0;         x<10; x++)

  cout           <<       i+x       <<       '           '           <<       f+x      <<       '\n';

 return            0;

}

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

0012FE5C          0012FE84

0012FE60          0012FE8C

0012FE64          0012FE94

0012FE68          0012FE9C

0012FE6C          0012FEA4

0012FE70          0012FEAC

0012FE74          0012FEB4

0012FE78          0012FEBC

0012FE7C          0012FEC4

0012FE80          0012FECC

Узелок   на       память.        Все     арифметические            операции    над указателями        выполняются относительно          базового     типа указателя.

Сравнение      указателей

Указатели         можно          сравнивать,            используя   операторы  отношения  ==, <          и          >.        Однако         для того,      чтобы            результат     сравнения указателей  поддавался            интерпретации,     сравниваемые указатели         должны быть  каким-то      образом       связаны.      Например,  если   p1       и          р2       — указатели, которые        ссылаются   на       две     отдельные  и          никак            не связанные   переменные,         то        любое сравнение             p1       и          р2       в общем          случае           не       имеет            смысла.        Но       если   p1       и          р2 указывают  на переменные,   между          которыми    существует  некоторая   связь (как,   например,   между элементами         одного          и          того    же       массива),     то результат     сравнения   указателей  p1       и          р2       может иметь          определенный смысл.          Ниже в          этой   главе мы      рассмотрим            пример         программы, в которой    используется         сравнение   указателей.

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

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

char         str[80]; char         *p1;

p1 =          str;

Здесь      str       представляет         собой            имя    массива,      содержащего         80 символов,   a          p1       —        указатель на          тип     char.  Особый        интерес представляет         третья           строка,          при     выполнении           которой переменной p1       присваивается       адрес            первого        элемента     массива        str.      (Другими словами,      после этого присваивания        p1       будет указывать    на       элемент str[0].)           Дело  в          том,   что      в          C++ использование         имени           массива без     индекса        генерирует  указатель    на       первый         элемент        этого массива. Таким            образом,     при     выполнении           присваивания        p1       =          str адрес            stг[0] присваивается указателю        p1.      Это     и          есть    ключевой момент,       который       необходимо           четко понимать: неиндексированное         имя массива,      использованное   в          выражении,            означает      указатель    на начало этого  массива.

Имя        массива       без     индекса        образует    указатель  на       начало     этого           массива.

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

str[4] или

*(p1+4)

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

Необходимость          использования      круглых        скобок,         в          которые заключено  выражение p1+4, обусловлена          тем,    что      оператор     "*"      имеет более            высокий       приоритет,  чем    оператор     "+".    Без этих        круглых скобок          выражение бы      свелось        к          получению значения,    адресуемого указателем p1,     т.е.      значения      первого        элемента     массива,      которое затем было бы      увеличено   на       4.

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

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

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

тест.

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

//  Программа разбиения   строки           на       слова:

//  версия          с          использованием  указателей.

#include <iostream> #include <cstdio> using     namespace   std;

int    main() {

 char    str[80];

 char    token[80];

 char    *p,      *q;

 cout    <<       "Введите      предложение:       ";

 gets(str);

p                      =          str;

 //         Считываем  лексему       из        строки.

 while(*р)      {

q                      =          token;            //         Устанавливаем      q          для     указания      на         начало          массиваtoken.

  /*   Считываем  символы      до       тех      пор,   пока   не       встретится   либо пробел,        либо  нулевой       символ         (признак      завершения            строки).        */

  while(*p   !=        '           '           &&      *р)      {

   *q          =          *р;

   q++;      р++;

  }

  if(*p)         р++;   //         Перемещаемся     за        пробел.

  *q  =          '\0';     //         Завершаем лексему       нулевым      символом.

  cout           <<       token <<       '\n';

 }

 return            0;

}

А   вот      как     выглядит     версия          той     же       программы с использованием  индексирования массивов.

//  Программа разбиения   строки           на       слова:

//  версия          с          использованием  индексирования   массивов.

#include <iostream> #include <cstdio> using     namespace   std; int     main() {

 char    str[80];

 char    token[80];

 int       i,          j;

 cout    <<       "Введите      предложение:       ";

 gets(str);

 //         Считываем  лексему       из        строки.

 for(i=0;          ;           i++)     {

  /*   Считываем  символы      до       тех      пор     пока   не       встретится   либо пробел,        либо  нулевой       символ         (признак      завершения            строки).        */

  for(j=0;     str[i]!='          '           &&      str[i];  j++,     i++)

   token[j]           =          str[i];

  token[j]    =          '\0';     //         Завершаем лексему       нулевым      символом.

  cout           <<       token <<       '\n';

  if(!str[i])   break;

 }

 return            0;

}

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

Индексирование     указателя

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

//  Индексирование  указателя    подобно      массиву.

#include <iostream> #include <cctype> using     namespace   std;

int    main() {

 char    str[20]           =          "I         love    you";

 char    *p;

 int       i;

 p          =          str;

 //         Индексируем         указатель.

 for(i=0;          p[i];    i++)     p[i]      =          toupper(p[i]);

 cout    <<       p;        //         Отображаем          строку.

 return            0;

}

При         выполнении           программа отобразит    на       экране          следующее.

I    LOVE  YOU

Вот           как     работает      эта      программа.            Сначала        в          массив          str вводится      строка           "I         love    you".  Затем адрес           начала          этой   строки присваивается       указателю   р.        После            этого  каждый        символ         строки str с    помощью    функции       toupper()      преобразуется       в          его      прописной эквивалент  посредством индексирования           указателя    р.        Помните,     что выражение р[i]      по       своему          действию     идентично выражению             *(p+i).

О            взаимозаменяемости указателей           и          массивов

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

int            num[10]; int     i; for(i=0;      i<10;     i++)     {

 *num =          i;          //         Здесь все      в          порядке.

 num++;         //         ОШИБКА      —        переменную          num   модифицировать     нельзя.

}

Здесь      используется         массив          целочисленных     значений     с          именем num.  Как     отмечено     в комментарии,    несмотря     на       то,       что      совершенно приемлемо             применить  к          имени           num   оператор "*"         (который обычно         применяется          к          указателям),          абсолютно  недопустимо модифицировать значение     num.  Дело  в          том,   что      num   —        это константа,   которая        указывает    на       начало          массива. И  ее,      следовательно, инкрементировать          никак            нельзя.         Другими      словами,      несмотря     на то,       что имя        массива        (без    индекса)      действительно      генерирует  указатель на       начало          массива,      его значение          изменению             не       подлежит.

Хотя        имя    массива        генерирует  константу-указатель,      на       него,  тем     не менее,          (подобно указателям)   можно          включать     в          выражения, если,  конечно,      оно     при     этом   не       модифицируется. Например  следующая инструкция,            при     выполнении           которой        элементу     num[3] присваивается значение          100,   вполне          допустима.

*(num+3)          =          100;   //         Здесь все      в          порядке,      поскольку   num   не изменяется.

Указатели     и          строковые             литералы

Возможно,       вас      удивит          способ          обработки   С++-компиляторами строковых   литералов, подобных    следующему.

cout         <<       strlen("С++-компилятор");

Если        С++-компилятор   обнаруживает       строковый   литерал,       он       сохраняет его      в          таблице строк       программы и          генерирует  указатель    на       нужную строку.          Поэтому       следующая программа совершенно           корректна   и          при выполнении           выводит       на       экран фразу:           Работа        с          указателями сплошное    удовольствие!. #include          <iostream> using   namespace   std;

int    main() {

 char    *s;

 s           =          "Работа        с          указателями           -           сплошное     удовольствие!\n";

 cout    <<       s;

 return            0;

}

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

Таблица            строк —        это    таблица,    сгенерированная компилятором    для хранения     строк, используемых    в          программе.

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

Все        познается  в          сравнении

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

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

//  Пример        сравнения   указателей.

#include <iostream> using     namespace   std;

int    main() {

 int       num[10];

 int       *start,            *end;

 start    =          num;

 end     =          &num[9];

 while(start    <=       end)   {

  cout           <<       "Введите      число:           ";

   cin         >>       *start;

  start++;

 }

 start    <<       num;  /*        Восстановление    исходного   значения      указателя

*/

 while(start    <=       end)   {

  cout           <<       *start <<       '           ';

  start++;

 }

 return            0;

}

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

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

Указатели,       подобно      данным        других          типов,           могут храниться    в     массивах.    Вот,    например,

как      выглядит     объявление            10-элементного    массива        указателей  на       int-значения.

int            *ipa[10];

Здесь     каждый        элемент        массива        ipa      содержит     указатель    на     целочисленное     значение.

Чтобы     присвоить   адрес            int-переменной     с          именем        var      третьему элементу     этого  массива указателей,       запишите     следующее.

ipa[2]      =          &var;

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

x   =          *ipa[2];

Поскольку        адрес            переменной           var      хранится      в          элементе ipa[2],           применение           оператора   "*" к   этой   индексированной переменной           позволит      получить      значение      переменной           var.

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

char         *fortunes[]  =          {

 "Вскоре        деньги          потекут         к          Вам    рекой.\n",

 "Вашу            жизнь            озарит           новая            любовь.\n",

 "Вы     будете          жить  долго и          счастливо.\n",

 "Деньги,       вложенные сейчас           в          дело, принесут      доход.\n",

 "Близкий      друг   будет искать           Вашего         расположения.\n"

};

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

cout         <<       fortunes[1];

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

#include <iostream>

#include <cstdlib> #include <conio.h> using     namespace   std;

char         *fortunes[]  =          {

 "Вскоре        деньги          потекут         к          Вам    рекой.\n",

 "Вашу            жизнь            озарит           новая            любовь.\n",

 "Вы     будете          жить  долго и          счастливо.\n",

 "Деньги,       вложенные сейчас           в          дело, принесут      доход.\n",

 "Близкий      друг   будет искать           Вашего         расположения.\n"

};

int    main() {

 int       chance;

 cout    <<"Чтобы    узнать           свою  судьбу,         нажмите      любую          клавишу:     ";

 //         Рандомизируем   генератор    случайных   чисел.

 while(!kbhit())         rand();

 cout    <<       '\n';

 chance           =          rand();

 chance           =          chance           %         5;

 cout    <<       fortunes[chance];

 return            0;

}

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

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

//  Простая        памятка        по       ключевым   словам         C++.

#include <iostream> #include <cstring> using     namespace   std;

char         *keyword[][2]        =          {

 "for", "for(инициализация;      условие;       инкремент)",

 "if",     "if(условие)            ...        else     ...        ",

 "switch",       "switch(значение)            {case-список}",

 "while",         "while(условие)     ...",

 //         Сюда нужно           добавить     остальные   ключевые    слова C++.

 "",       ""        //         Список          должен        завершаться           нулевыми   строками.

};

int    main() {

 char    str[80];

 int       i;

 cout    <<       "Введите      ключевое    слово:           ";

  cin  >>       str;

 //         Отображаем          синтаксис.

 for(i=0;          *keyword[i][0];      i++)

  if(!strcmp(keyword[i][0],       str))

   cout      <<       keyword[i][1];

 return            0;

}

Вот          пример         выполнения           этой     программы. Введите      ключевое    слово:     for

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

В   этой   программе обратите      внимание    на       выражение,            управляющее циклом         for.     Оно приводит       к          завершению           цикла,           когда элемент keyword[i][0]          содержит     указатель    на       нуль, который       интерпретируется как     значение      ЛОЖЬ.          Следовательно,    цикл  останавливается, когда встречается             нулевая        строка,          которая        завершает   массив указателей.

Соглашение  о          нулевых       указателях          

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

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

float        *р       =          0;        //         р          —        теперь           нулевой       указатель.

Для          тестирования         указателя    используется         инструкция if          (любой из        следующих ее вариантов).

if(р)         //         Выполняем что-то,           если   р          —        не       нулевой     указатель.

if(!p)        //         Выполняем что-то,           если   р          —        нулевой       указатель.

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

Указатели     и          16-разрядные       среды

Несмотря          на       то        что      в          настоящее   время            большинство вычислительных  сред   32-разрядные, все          же       немало         пользователей      до сих      пор     работают     в          16-разрядных        (в        основном,   это      DOS    и Windows      3.1)    и,        естественно,           с          16-разрядным      кодом.          Эти операционные      системы       были разработаны          для     процессоров семейства    Intel    8086, которые       включают    такие модификации, как          80286, 80386,          80486            и          Pentium        (при   работе          в          режиме        эмуляции процессора             8086).           И         хотя при       написании   нового          кода программисты,     как     правило,      ориентируются     на       использование 32-разрядной   среды           выполнения,          все      же       некоторые  программы по-прежнему    создаются   и поддерживаются         в          более            компактных            16-разрядных  средах.         Поскольку   некоторые  темы актуальны   только          для     16-разрядных  сред, программистам,   которые       работают     в          них,    будет полезно получить      информацию         о          том,   как     адаптировать         "старый"      код     к новой            среде,           т.е. переориентировать             16-разрядный       код     на       32-разрядный.

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

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

И  последнее. При    написании   программ    для     современной         32-разрядной среды необходимо         знать,            что      в          ней     используется         единственная модель         организации          памяти, которая   называется одноуровневой, несегментированной   или     линейной    (flat    model).        

Многоуровневая    непрямая   адресация

Можно  создать         указатель,   который       будет ссылаться    на       другой     указатель,   а          тот      —        на конечное           значение.    Эту      ситуацию     называют    многоуровневой  непрямой    адресацией            (multiple indirection)     или     использованием  указателя    на       указатель.   Идея  многоуровневой     непрямой адресации     схематично             проиллюстрирована       на       рис.    6.2.     Как     видите,         значение      обычного указателя       (при   одноуровневой     непрямой    адресации) представляет         собой            адрес            переменной, которая содержит     некоторое   значение.    В          случае           применения     указателя    на       указатель    первый содержит            адрес            второго,       а     тот      ссылается    на       переменную,         содержащую         определенное значение.

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

     Переменную,         которая        является       указателем на       указатель,   нужно объявить соответствующим    образом.     Для    этого  достаточно перед            ее именем        поставить дополнительный    символ         "звездочка"(*).     Например, следующее объявление            сообщает компилятору            о          том,   что      balance —        это      указатель    на       указатель    на       значение      типа   int.

int            **balance;

Необходимо    помнить,      что      переменная            balance         здесь —        не указатель    на       целочисленное значение,       а          указатель    на       указатель    на int-значение.

Чтобы     получить      доступ          к          значению,   адресуемому         указателем на указатель,   необходимо дважды     применить  оператор     "*"      как     показано     в следующем            коротком     примере.

//  Использование     многоуровневой  непрямой    адресации.

#include <iostream> using     namespace   std;

int    main() {

 int       x,         *p,      **q;

 x          =          10;

p       =         &x;

q       =         &p;

 cout    <<       **q;   //         Выводим     значение      переменной           x.

 return            0;

}

Здесь      переменная            p          объявлена   как     указатель    на       int-значеине, а          переменная            q          —        как указатель        на       указатель    на       int-значеине.     При    выполнении           этой   программы мы      получим значение переменной           х,         т.е.      число 10.

Проблемы,    связанные  с          использованием  указателей

Для          программиста       нет      ничего          более            страшного,  чем "взбесившиеся"    указатели! Указатели     можно          сравнить      с          энергией атома:           они     одновременно      и          чрезвычайно          полезны       и чрезвычайно опасны.        Если   проблема    связана         с          получением           указателем неверного   значения, то          такую            ошибку         отыскать      труднее        всего.

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

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

Неинициализированные             указатели

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

//  Эта      программа     некорректна. int   main() {

 int       х,         *р;

 х          =          10;

 *р       =          х;         //         На       что      указывает    переменная            р?

 return            0;

}

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

Некорректное         сравнение   указателей

Сравнение        указателей,             которые       не       ссылаются   на       элементы одного          и          того    же       массива,      в общем       случае           некорректно          и часто приводит     к          возникновению    ошибок.       Никогда       не       стоит полагаться  на       то,       что      различные  объекты       будут размещены            в памяти          каким-то определенным         образом       (где-то          рядом)         или     на то,       что      все      компиляторы        и          операционные среды   будут обрабатывать ваши  данные         одинаково. Поэтому       любое           сравнение   указателей, которые ссылаются   на       различные  объекты,      может           привести      к          неожиданным последствиям. Рассмотрим    пример. char         s[80]; char   у[80]; char   *p1,   *р2;

p1 =     s; р2     =     у;

if(p1        <          р2)      .           .           .

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

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

int            first[101; int     second[10]; int     *p,      t; p      =     first; for(t=0;     t<20;  ++t)    {

 *p       =          t;

 p++;

}

Цель       этой   программы —        инициализировать          элементы    массивов     first и          second           числами       от 0    до       19.      Однако         этот    код     не позволяет    надеяться    на       достижение            желаемого  результата, несмотря     на то,       что      в          некоторых   условиях      и          при     использовании     определенных компиляторов эта           программа будет работать      так,     как     задумано    автором. Не       стоит  полагаться  на       то,       что массивы           first     и          second           будут расположены        в          памяти          компьютера           последовательно,            причем первым        обязательно           будет массив          first.   Язык  C++     не       гарантирует определенного расположения          объектов      в          памяти,        и          потому         эту программу нельзя          считать         корректной.

Не          забывайте            об       установке  указателей

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

//  Эта      программа некорректна.

#include <iostream>

#include <cstdio> #include <cstring> using     namespace   std;

int    main() {

 char    s          [80];

 char    *p1;

 p1       =          s;

 do       {

  cout           <<       "Введите      строку:          ";

  gets(p1);  //         Считываем  строку.

  //    Выводим     ASCII-значения      каждого       символа.

  while(*p1)           cout    <<       (int)    *p1++            <<       '           ';

  cout           <<       '           \n';

 }while(strcmp          (s,        "конец"));

 return            0;

}

Сможете           ли       вы       сами  найти             здесь ошибку?

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

Вот          как     выглядит     корректный            вариант        той     же       программы.

//  Эта      программа корректна.

#include <iostream>

#include <cstdio> #include <cstring> using     namespace   std;

int    main() {

 char    s[80];

 char    *p1;

 do       {

  p1  =          s;         //         Устанавливаем      p1       при     каждой         итерации     цикла.

  cout           <<       "Введите      строку:          ";

  gets(p1);  //         Считываем  строку.

  //    Выводим     ASCII-значения      каждого       символа.

  while(*p1)           cout    <<       (int)    *p1++            <<'      ';

  cout           <<       '\n';

 }while(strcmp(s,     "конец"));

 return            0;

}

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

Узелок   на       память.        Чтобы         использование     указателей           было     безопасным,          нужно           в          любой момент    знать,          на       что    они     ссылаются.

Глава     7:Функции,  часть      первая: ОСНОВЫ

В   этой   главе мы      приступаем            к          углубленному       рассмотрению функций.     Функции      —        это строительные            блоки            C++,   а          потому без     полного       их        понимания невозможно           стать  успешным С++-программистом.   Мы     уже    коснулись   темы  функций       в          главе 2          и использовали        их        в каждом    примере      программы.           В          этой   главе мы познакомимся      с          ними более            детально.    Данная тема          включает рассмотрение        правил          действия      областей      видимости  функций, рекурсивных функций, некоторых   специальных          свойств         функции       main(), инструкции return            и          прототипов функций.

Правила          действия    областей    видимости            функций

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

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

Локальные    переменные

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

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

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

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

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

#include <iostream> using     namespace   std;

void     f1(); int    main() {

 char    str[]    =          "Это   -           массив          str       в          функции       main().";

 cout    <<       str       <<       '\n';

 f1();

 cout    <<       str       <<       '\n';

 return            0;

}

void     f1() {

 char    str[80];

 cout    <<       "Введите      какую-нибудь       строку:          ";

  cin  >>       str;

 cout    <<       str       <<       '\n';

}

Символьный   массив          str       объявляется           здесь дважды:      первый         раз в          функции       main()           и          еще раз        —        в          функции       f1().    При этом   массив          str,      объявленный         в          функции       main(),          не       имеет никакого      отношения  к          одноименному     массиву        из        функции       f1().    Как разъяснялось         выше, каждый      массив          (в        данном        случае           str) известен       только          блоку            кода, в          котором       он       объявлен. Чтобы убедиться    в          этом, достаточно выполнить  приведенную        выше программу. Как     видите, несмотря            на       то,       что      массив          str       получает      строку, вводимую   пользователем     при     выполнении функции    f1(),    содержимое массива        str       в          функции       main()           остается       неизменным.

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

auto        char    ch;

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

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

/* Эта      программа демонстрирует     локальность           переменных           по отношению             к          блоку.

*/

#include <iostream> #include <cstring> using     namespace   std;

int            main()

{

int       choice;

 cout    <<       "(1)     сложить       числа или     ";

 cout    <<       "(2)     конкатенировать  строки?:       ";

  cin  >>       choice;

 if(choice        ==       1)        {

  int  a,         b;        /*        Активизируются   две     int-переменные.   */

  cout           <<       "Введите      два     числа:           ";

   cin         >>       а          >>       b;

  cout           <<       "Сумма         равна            "          <<       a+b     <<       '\n';

 }

 else     {

  char           s1        [80],   s2[80];           /*        Активизируются   две     строки.         */

  cout           <<       "Введите      две     строки:         ";

   cin         >>       s1;

   cin         >>       s2;

  strcat(s1,  s2);

  cout           <<       "Конкатенация      равна            "          <<       s1        <<       '\n';

 }

 return            0;

}

Эта           программа в          зависимости           от        выбора         пользователя обеспечивает         ввод  либо  двух чисел, либо  двух   строк.            Обратите     внимание на       объявление            переменных           а          и          b          в          if-блоке        и переменных           s1        и          s2        в          else-блоке.  Существование     этих переменных           начнется      с          момента      входа в         соответствующий блок  и          прекратится            сразу после выхода         из        него.  Если   пользователь выберет        сложение    чисел,           будут созданы       переменные           а          и          b, а          если   он       захочет конкатенировать         строки—      переменные           s1        и s2.       Наконец,      ни       к          одной           из        этих    переменных нельзя обратиться  извне их        блока,           даже  из        части кода, принадлежащей   той     же функции. Например,      если   вы       попытаетесь           скомпилировать   следующую (некорректную)    версию программы,       то        получите      сообщение  об       ошибке.

/* Эта      программа некорректна.         */

#include <iostream> #include <cstring> using     namespace   std;

int    main() {

 int       choice;

 cout    <<       "(1)     сложить       числа или     ";

 cout    <<       "(2)     конкатенировать  строки?:       ";

  cin  >>       choice;

 if(choice        ==       1)        {

  int  a,         b;        /*        Активизируются   две     int-переменные.   */

  cout           <<       "Введите      два     числа:           ";

   cin         >>       а          >>       b;

  cout           <<       "Сумма         равна            "          <<       a+b     <<       '\n';

 }

 else     {

  char           s1        [80],   s2        [80];   /*        Активизируются   две     строки.         */

  cout           <<       "Введите      две     строки:         ";

   cin         >>       s1;

   cin         >>       s2;

  strcat         (s1,     s2);

  cout           <<       "Конкатенация      равна            "          <<       s1        <<       '\n';

}

 a          =          10;      //         ***     Ошибка        ***

 //         Переменная           а          здесь неизвестна!

 return            0;

}

Поскольку        в          данном        случае           переменная            а          неизвестна  вне своего           if-блока,       компилятор выдаст        ошибку         при     попытке       ее использовать.

Если        имя    переменной,          объявленной         во       внутреннем            блоке, совпадает    с          именем переменной,    объявленной         во       внешнем      блоке, то        "внутренняя"         переменная            переопределяет "внешнюю" в          пределах области        видимости  внутреннего           блока.           Рассмотрим            пример.

#include <iostream> using     namespace   std;

int    main() {

 int       i,          j;

i          =         10;

j         =         100;

 if(j       >          0)        {

  int  i;          //         Эта      переменная            i           отделена     от        внешней     переменной           i.

  i      =          j           /2;

  cout           <<       "Внутренняя           переменная            i:          "          <<       i           <<     '\n';

 }

 cout    <<       "Внешняя    переменная            i:          "          <<       i           <<       '\n';

 return            0;

}

Вот          как     выглядят     результаты выполнения           этой   программы.

Внутренняя      переменная            i:          50

Внешняя           переменная            i:          10

Здесь      переменная            i,          объявленная          внутри          if-блока, переопределяет,  или     скрывает, внешнюю       переменную          i.          Изменения, которым      подверглась           внутренняя переменная            i,          не оказывают никакого      влияния       на       внешнюю    i.          Более            того,  вне     if-блока внутренняя переменная i         больше         не       существует,             и          поэтому внешняя       переменная            i           снова становится  видимой.

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

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

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

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

#include <iostream> using     namespace   std;

int    main() {

 cout    <<       "Введите      число:           ";

 int       a;         //         Объявляем одну  переменную.

 cin       >>       a;

 cout    <<       "Введите      второе          число:           ";

 int       b;        //         Объявляем еще    одну  переменную.

cin       >>       b;

 cout    <<       "Произведение     равно:           "          <<       а*Ь     <<       '\n';

 return            0;

}

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

Объявление  переменных          в          итерационных    инструкциях        и   инструкциях        выбора

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

#include <iostream> using     namespace   std;

int    main() {

 //         Переменная           i           локальная   для     цикла            for.

 for(int            i=0;     i<10;  i++)     {

  cout           <<       i           <<       "          ";

  cout           <<       "в        квадрате      равно            "          <<       i           *          i           <<     "\n";

 }

 //         i           =          10;      //         ***     Ошибка        ***     --         i           здесь     неизвестна!

 return            0;

}

Здесь     переменная            i           объявляется           в          разделе        инициализации     цикла            for      и          используется         для управления    этим   циклом.       А     за        пределами цикла            переменная            i           неизвестна.

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

Важно!  Утверждение       о          том, что    переменная,          объявленная          в разделе        инициализации цикла   for,     является     локальной  по       отношению к          этому          циклу или     не       является     таковой, изменилось   со        временем (имеется    в          виду   время,          в          течение      которого    развивался язык C++). Первоначально     такая           переменная           была доступна    после выхода из        цикла            for.     Однако стандарт          C++     ограничивает      область видимости этой переменной           рамками     цикла            for.     Но следует иметь          в          виду, что    различные  компиляторы      и          теперь         по-разному       "смотрят"            на       эту ситуацию.

Если        ваш    компилятор            полностью  соблюдает  стандарт      C++,   то        вы можете         также            объявить переменную  в          условном    выражении инструкций if,        switch            или     while. Например,  в          следующем фрагменте кода if(int    х          =          20)      {

 cout    <<       "Это   значение      переменной           х:         ";

 cout    <<       х;

}

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

Формальные            параметры

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

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

Глобальные  переменные

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

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

Использование          глобальных            переменных           демонстрируется в следующей программе.            Как видите,             переменные           count и          num_right объявлены  вне     всех    функций,     следовательно,     они— глобальные.         Из обычных      практических         соображений         лучше           объявлять   глобальные переменные           поближе      к          началу          программы.           Но       формально они просто          должны       быть объявлены   до       их        первого        использования. Предлагаемая       для     рассмотрения        программа—         всего лишь простой тренажер     по       выполнению          арифметического             сложения.   Сначала пользователю предлагается   указать         количество упражнений.          Для выполнения           каждого       упражнения вызывается          функция       drill(), которая генерирует  два     случайных   числа в          диапазоне   от        0          до       99. Пользователю       предлагается         сложить       эти      числа,           а          затем проверяется           ответ.            На       каждое упражнение       дается           три     попытки. В          конце            программа отображает             количество правильных ответов. Обратите     особое          внимание    на       глобальные            переменные, используемые       в          этой программе.

//  Простая        программа-тренажер     по       выполнению          сложения.

#include <iostream> #include <cstdlib> using     namespace   std;

void         drill(); int      count;            //         Переменные          count и     num_right    —        глобальные.

int     num_right; int     main()

{

 cout    <<       "Сколько     практических         упражнений:          ";

  cin  >>       count;

 num_right    =          0;

 do       {

  drill();        count--;

 }while(count);

 cout    <<       "Вы    дали  "          <<       num_right<<           "          правильных     ответов.\n";

 return            0;

}

void     drill() {

 int       count;            /*        Эта      переменная            count —        локальная   и            никак            не связана   с          одноименной        глобальной.*/

 int       а,         b,        ans;

 //         Генерируем            два     числа между          0          и          99.

 а          =          rand()            %         100;

 b          =          rand()            %         100;

 //         Пользователь        получает      три     попытки       дать   правильный     ответ.

 for(count=0;            count<3;       count++)       {

  cout           <<       "Сколько     будет "          <<       а          <<       "          +          "          <<     b          <<       "?        ";

   cin         >>       ans;

  if(ans==a+b)       {

   cout      <<       "Правильно\n";

   num_right++;

   return;

  }

 }

 cout    <<       "Вы    использовали        все      свои   попытки.\n";

 cout    <<       "Ответ           равен            "          <<       a+b     <<       '\n';

}

При         внимательном      изучении     этой   программы вам    должно        быть ясно,  что      как     функция main(),   так      и          функция       drill()  получают     доступ к          глобальной            переменной           num_right.  Но       с переменной        count дело  обстоит         несколько   сложнее.      В          функции       main()           используется глобальная переменная            count.            Однако         в          функции       drill() объявляется           локальная   переменная count.         Поэтому       здесь при использовании     имени           count подразумевается именно         локальная,  а не глобальная переменная            count.            Помните,     что,    если   в          функции глобальная и          локальная переменные            имеют           одинаковые           имена, то        при     обращении к          этому            имени           подразумевается локальная, а не       глобальная переменная.

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

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

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

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

Передача        указателей           и          массивов     в          качестве   аргументов

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

Вызов  функций      с          указателями

В   C++     разрешается           передавать функции       указатели.   Для    этого достаточно объявить параметр         типа   указатель.   Рассмотрим            пример.

//  Передача     функции     указателя. #include     <iostream> using   namespace     std;

void         f     (int      *j); int     main() {

 int       i;

 int       *p;

 p          =          &i;       //         Указатель    p          теперь           содержит     адрес     переменной           i.

 f(p);

 cout    <<       i;          //         Переменная           i           теперь           содержит     число 100.

 return            0;

}

void         f     (int      *j) {

 *j        =          100;   //         Переменной,         адресуемой            указателем j, присваивается       число 100.

}

Как          видите,         в          этой   программе функция       f()        принимает  один параметр:    указатель    на целочисленное           значение.    В          функции       main() указателю   р          присваивается       адрес            переменной           i. Затем         из функции       main()           вызывается             функция       f(),      а          указатель    р передается ей       в          качестве аргумента.       После            того    как     параметр-указатель     j           получит        значение      аргумента   р,        он       (так    же, как         и р)        будет указывать    на       переменную          i,          определенную      в          функции main().          Таким            образом, при        выполнении           операции     присваивания

*j =          100;

переменная    i           получает      значение      100.   Поэтому       программа     отобразит    на       экране          число 100.

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

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

//  Передача     указателя    функции       --         исправленная     версия. #include   <iostream> using   namespace   std;

void         f     (int      *j); int     main() {

 int       i;

 f(&i);

 cout    <<       i;

 return            0;

}

void         f     (int      *          j) {

 *j        =          100;   //         Переменной,         адресуемой            указателем j, присваивается       число 100.

}

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

Вызов  функций      с          массивами

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

#include <iostream> using     namespace   std;

void         display(int     num[10]); int          main() {

 int       t[10], i;

 for(i=0;          i<10;  ++i)     t[i]=i;

 display(t);     //         Передаем    функции       массив          t.

 return            0;

}

//  Функция       выводит       все      элементы     массива. void         display(int    num[10]) {

 int       i;

 for(i=0;          i<10;  i++)     cout    <<       num[i]           <<'      ';

}

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

Второй   способ          объявления            параметра-массива         состоит         в          его представлении      в          виде безразмерного      массива,      как     показано     ниже.

void         display(int     num[]) {

 int       i;

 for(i=0;          i<10;  i++)     cout    <<       num[i]           <<       '           ';

}

Здесь      параметр     num   объявляется           как     целочисленный    массив неизвестного         размера. Поскольку       C++     не       обеспечивает         проверку нарушения  границ          массива,      то        реальный    размер массива   — нерелевантный     фактор          для     подобного  параметра   (но,    безусловно,            не для программы    в          целом).        Целочисленный    массив          при     таком способе        объявления            также автоматически     преобразуется       С++-компилятором      в          указатель    на       целочисленное     значение.

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

void         display(int     *num) {

 int       i;

 for(i=0;          i<10;  i++)     cout    <<       num[i]           <<       '           ';

}

Возможность  такого           объявления            параметра   (в        данном        случае num)  объясняется           тем,    что любой   указатель    (подобно     массиву)      можно индексировать      с          помощью    символов    квадратных скобок         ([]).     Таким образом,     все      три     способа        объявления            параметра-массива приводятся к одинаковому     результату, который       можно          выразить     одним словом:        указатель.

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

#include <iostream> using     namespace   std; void     display(int    num); int     main() {

 int       t[10],i;

 for(i=0;          i<10;  ++i)     t[i]=i;

 for(i=0;          i<10;  i++)     display(t[i]);

 return            0;

}

//  Функция       выводит       одно  число.

void         display(int     num) {

 cout    <<       num   <<       '           ';

}

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

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

#include <iostream> using     namespace   std;

void         cube(int        *n,      int       num);

int    main() {

 int       i,          nums[10];

 for(i=0;          i<10;  i++)     nums[i]          =          i+1;

 cout    <<       "Исходное   содержимое          массива:      ";

 for(i=0;          i<10;  i++)     cout    <<       nums[i]          <<       '           ';

 cout    <<       '\n';

 cube(nums,  10);    //         Вычисляем кубы  значений.

 cout    <<       "Измененное         содержимое:         ";

 for(i=0;          i<10;  i++)     cout    <<       nums[i]          <<       '           ';

 return     0; }

void         cube(int        *n,      int     num) {

 while(num)  {

  *n  =          *n       *          *n       *          *n;

  num--;

  n++;

 }

}

Результаты      выполнения           этой   программы таковы.

Исходное          содержимое          массива:      12345678910

Измененное    содержимое:         1          8          27       64       125     216     343     512     729     1000

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

Передача        функциям  строк

Как          вы       уже    знаете,          строки           в          C++     —        это      обычные символьные           массивы,     которые завершаются   нулевым      символом.  Таким образом,     при     передаче     функции       строки           реально передается       только указатель    (типа  char*)            на       начало          этой   строки.         Рассмотрим, например, следующую программу.            В          ней     определяется        функция stringupper(),         которая        преобразует строку        символов    в          ее прописной  эквивалент.

//  Передача     функции       строки.

#include <iostream>

#include <cstring> #include <cctype> using     namespace   std;

void         stringupper(char    *str); int            main() {

 char    str[80];

 strcpy(str,     "Мне нравится      C++");

 stringupper(str);

 cout    <<       str;      //         Отображаем          строку           с          использованием            прописного написания  символов.

 return            0;

}

void         stringupper(char    *str) {

 while(*str)    {

  *str            =          toupper(*str);         //         Получаем    прописной  эквивалент одного          символа.

  str++;        //         Переходим к          следующему          символу.

 }

}

Результаты      выполнения           этой   программы таковы.

МНЕ        НРАВИТСЯ   C++

Обратите           внимание    на       то,       что      параметр     str       функции stringupper()           объявляется           с использованием           типа   char*.            Это позволяет    получить      указатель    на       символьный           массив, который  содержит строку.          Рассмотрим            еще    один  пример         передачи     строки           функции. Как     вы узнали   в          главе 5,        стандартная           библиотечная        функция strlen()          возвращает             длину            строки.         В следующей         программе показан        один  из        возможных вариантов   реализации            этой   функции.

//  Одна из        версий          функции     strlen(). #include   <iostream> using     namespace   std;

int            mystrlen(char     *str); int        main() {

 cout    <<       "Длина         строки           ПРИВЕТ         ВСЕМ равна:           ";

 cout    <<       mystrlen("ПРИВЕТ            ВСЕМ");

 return            0;

} //           Нестандартная      реализация             функции     strlen(). int   mystrlen(char         *str) {

 int       i;

 for(i=0;          str[i];  i++);   //         Находим      конец            строки.

 return            i;

}

Вот          как     выглядят     результаты выполнения           этой   программы.

Длина    строки           ПРИВЕТ         ВСЕМ равна:           11

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

Аргументы  функции      main():          argc   и          argv  

Аргумент        командной строки         представляет     собой            информацию, задаваемую           в          командной строке         после имени           программы.

Иногда   возникает    необходимость     передать      информацию         программе при ее        запуске.       Как правило,          это      реализуется            путем            передачи аргументов командной строки           функции       main(). Аргумент  командной строки представляет         собой            информацию,        указываемую        в          команде (командной            строке),        предназначенной            для     выполнения операционной       системой,    после имени          программы.           (В        Windows команда      "Run" (Выполнить)           также            использует  командную строку.) Например,  С++-программы   можно          компилировать     путем            выполнения следующей команды,

cl  prog_name

Здесь      элемент        prog_name —        имя    программы,           которую       мы хотим            скомпилировать. Имя программы   передается С++-компилятору в качестве       аргумента   командной строки.

В   C++     для     функции       main()           определено            два     встроенных,           но необязательных   параметра, argc   и          argv,  которые       получают     свои   значения от        аргументов командной строки.         В конкретной         операционной       среде могут поддерживаться  и          другие          аргументы  (такую информацию необходимо           уточнить      по       документации,      прилагаемой         к          вашему компилятору). Рассмотрим     параметры  argc   и          argv   более            подробно.

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

Параметр          argc   имеет            целочисленный    тип     и          предназначен        для хранения     количества аргументов командной строки.         Его      значение      всегда не       меньше        единицы,     поскольку   имя программы   также            является одним           из        учитываемых         аргументов.            Параметр    argv   представляет собой            указатель    на       массив          символьных           указателей.             Каждый указатель    в          массиве        argv ссылается      на       строку,          содержащую аргумент      командной строки.         Элемент       argv[0]          указывает на         имя программы;           элемент        argv[1]          —        на       первый         аргумент,    элемент argv[2]          —        на       второй          и т.д. Все     аргументы  командной строки передаются            программе как     строки,         поэтому       числовые аргументы необходимо           преобразовать      в          программе в          соответствующий внутренний             формат. Важно     правильно  объявить      параметр     argv.  Обычно это      делается      так.

char         *argv[];

Доступ   к          отдельным аргументам            командной строки           можно получить      путем            индексации массива      argv.  Как     это      сделать,       показано в          следующей программе.            При    ее        выполнении           на экран выводится   приветствие            ("Привет"    ),         а          за        ним    —        ваше  имя, которое        должно        быть первым         аргументом            командной строки. #include <iostream> using   namespace   std; int           main(int        argc,   char    *argv[]) {

 if(argc!=2)    {

  cout           <<       "Вы    забыли         ввести           свое   имя.\n";

  return       1;

 }

 cout    <<       "Привет,       "          <<       argv[1]          <<       '\n';

 return            0;

}

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

A>name Том

Привет,  Том

А>

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

один,      два     и          три

состоит  из        четырех        строковых   аргументов,            в          то        время            как     строка

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

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

"это         лишь один  аргумент"

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

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

/* Эта      программа посимвольно         выводит       все      аргументы  командной строки,         с          которыми    она     была  вызвана.

*/ #include     <iostream> using     namespace   std;

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

 int       t,         i;

 for(t=0;         t<argc;           ++t)    {

  i      =          0;

  while(argv[t][i]) {

   cout      <<       argv[t][i];

   ++i;

  }

  cout           <<       '           ';

 }

 return            0;

}

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

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

Передача        числовых    аргументов          командной строки

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

/* Эта      программа отображает             сумму           двух   числовых     аргументов командной строки.

*/

#include <iostream> #include <cstdlib> using     namespace   std;

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

 double           a,         b;

 if(argc!=3)    {

  cout           <<       "Использование:  add     число число\n";

  return       1;

 }

a       =         atof(argv[1]);

b       =         atof(argv[2]);

 cout    <<       a          +          b;

 return            0;

}

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

C>add     100.2 231

Преобразование     числовых    строк           в          числа

Стандартная    библиотека             C++     включает     несколько   функций,     которые позволяют преобразовать       строковое    представление      числа в          его внутренний             формат.        Для    этого используются        такие функции,     как atoi(), atol()  и          atof().            Они    преобразуют          строку           в целочисленное значение      (типа  int),    длинное       целое            (типа  long)  и          значение      с плавающей точкой         (типа  double)         соответственно.    Использование     этих функций       (для   их        вызова необходимо       включить     в          программу заголовочный       файл  <cstdlib>)     демонстрируется в следующей         программе.

//  Демонстрация       использования      функций       atoi(), atol()  и          atof().

#include <iostream> #include <cstdlib> using     namespace   std;

int    main() {

 int       i;

 long    j;

 double           k;

i          =         atoi     ("100");

j         =         atol("100000");

k       =         atof("-0.123");

 cout    <<       i           <<       '           '           <<       j           <<       '           '           <<       k;

 cout    <<       '           \n';

 return            0;

}

Результаты      выполнения           этой   программы таковы.

100          100000         -0.123

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

Инструкция return

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

Завершение  функции

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

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

void         power(int     base,  int       exp)

{

 int       i;

 if(exp<0)       return;           /*        Чтобы           не       допустить    возведения числа в отрицательную  степень,       здесь выполняется          возврат         в вызывающую функцию            и          игнорируется         остальная    часть  функции.     */

 i           =          1;

 for(     ;           exp;    exp--) i           =          base   *          i;

 cout    <<       "Результат   равен:           "          <<       i;

}

Функция            может           содержать   несколько   инструкций return.           Функция будет завершена  при выполнении  хотя    бы      одного          из        них.    Например, следующий            фрагмент     кода  совершенно правомерен. void          f() {

 //         ...

 switch(с)       {   case         'a':       return;   case         'b':      //     ...

  case           'c':       return;

 }

 if(count<100)          return;

 //         ...

}

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

Возврат         значений

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

х   =          power(у); if(max(х,           у))       >     100)   cout    <<       "больше";

switch(abs         (х))      {

Несмотря          на       то        что      все      не       void-функции         возвращают значения,    они     необязательно должны           быть  использованы       в программе.            Самый          распространенный          вопрос          относительно значений,    возвращаемых      функциями,            звучит           так:     "Поскольку функция возвращает             некоторое значение,      то        разве я          не       должен        (должна) присвоить   это      значение      какой-нибудь переменной?".            Ответ:            нет,    это необязательно.     Если   значение,    возвращаемое      функцией,   не участвует           в присваивании,      оно     попросту      отбрасывается       (теряется).

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

#include <iostream> #include <cstdlib> using     namespace   std;

int    main() {

 int       i;

 i           =          abs(-10);       //         строка           1

 cout    <<       abs(-23);       //         строка           2

 abs(100);      //         строка           3

 return            0;

}

Функция            abs()  возвращает             абсолютное            значение      своего целочисленного   аргумента.  Она использует    заголовок    <cstdlib>.      В          строке 1          значение,    возвращаемое      функцией    abs(), присваивается       переменной i.          В          строке           2          значение,    возвращаемое      функцией    abs(), ничему не присваивается,            но       используется         инструкцией           cout.  Наконец,      в строке           3          значение, возвращаемое         функцией    abs(), теряется, поскольку   не       присваивается       никакой       другой переменной        и          не используется         как     часть  выражения.

Если        функция,      тип     которой        отличен        от        типа   void,   завершается в          результате  обнаружения закрывающейся          фигурной     скобки,         то значение,    которое        она     возвращает,           не       определено            (т.е. неизвестно).           Из-за особенностей        формального         синтаксиса  C++     не void-функция         не       обязана выполнять         инструкцию            return.           Это может           произойти   в          том     случае,         если   конец            функции будет достигнут    до       обнаружения         инструкции return.           Но,     поскольку   функция объявлена   как возвращающая         значение,    значение      будет таки   возвращено, даже  если   это      просто          "мусор".      В общем      случае           любая создаваемая          вами  не       void-функция         должна        возвращать             значение посредством          явно   выполняемой        инструкции return.

Выше      упоминалось,        что      void-функция         может           иметь несколько   инструкций return.           То       же самое     относится    и          к функциям,  которые       возвращают           значения.    Например,  представленная    в следующей программе функция       find_substr()           использует  две     инструкции return,           которые позволяют        упростить    алгоритм     ее        работы.        Эта функция       выполняет  поиск заданной подстроки      в          заданной     строке.          Она возвращает             индекс          первого        обнаруженного    вхождения заданной подстроки   или     значение      -1,       если   заданная     подстрока   не       была  найдена. Например, если   в          строке           "Я       люблю          C++     "          необходимо отыскать      подстроку   "люблю",    то        функция find_substr()     возвратит    число 2 (которое      представляет         собой            индекс          символа       "л"      в          строке "Я люблю    C++     ").

#include <iostream> using     namespace   std;

int            find_substr(char    *sub,  char    *str); int     main() {

 int       index;

 index  =          find_substr("три", "один            два     три     четыре");

 cout    <<       "Индекс       равен            "          <<       index; //         Индекс         равен     9.

 return            0;

}

// Функция       возвращает             индекс          искомой       подстроки   или     -1,            если   она не           была  найдена.

int            find_substr(char    *sub,  char    *str) {

 int       t;

 char    *p,      *p2;

 for(t=0;         str[t]; t++)    {

  p     =          &str[t];          //         установка    указателей

  p2  =          sub;

  while(*p2            &&      *p2==*p)     {           //         проверка     совпадения

   p++;      p2++;

  }

  /*   Если   достигнут    конец            р2-строки    (т.е.    подстроки),            то подстрока   была  найдена.      */

  if(!*p2)     return            t;         //         Возвращаем           индекс          подстроки.

 }

 return            -1;       //         Подстрока   не       была  обнаружена.

}

Результаты      выполнения           этой   программы таковы.

Индекс  равен            9

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

Функцию           можно          объявить      так,     чтобы            она     возвращала значение      любого         типа   данных, действительного        для     C++     (за исключением        массива:      функция       не       может           возвратить  массив). Способ          объявления            типа   значения,    возвращаемого    функцией, аналогичен тому, который используется   для     объявления            переменных: имени           функции       должен        предшествовать спецификатор         типа. Спецификатор       типа   сообщает     компилятору,        значение      какого           типа данных         должна        возвратить  функция.      Указываемый        в          объявлении функции       тип     должен        быть совместимым         с          типом            данных, используемым      в          инструкции return.           В          противном  случае компилятор отреагирует            сообщением          об       ошибке.

Функции,        которые     не       возвращают        значений     (void-функции)

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

#include <iostream> using     namespace   std;

void         print_vertical(char            *str); int            main(int        argc,   char     *argv[]) {

 if(argc==2)   print_vertical(argv[1]);

 return            0;

}

void         print_vertical(char            *str) {

 while(*str)

 cout    <<       *str++            <<       '\n';

}

Поскольку        print_vertical()       объявлена   как     void-функция,        ее        нельзя использовать         в выражении.        Например,  следующая инструкция неверна        и поэтому       не       скомпилируется.

х   =          print_vertical("Привет!");          //         ошибка

Важно!  В          первых         версиях        языка            С          не       был    предусмотрен тип   void.   Таким           образом,     в старых     С-программах       функции,     не возвращающие    значений,    по       умолчанию имели           тип   int. Если       вам придется    встретиться       с          такими       функциями при     переводе     старых С-программ           "на рельсы"           C++,   просто        объявите    их        с использованием  ключевого  слова void,   сделав          их        voidфункциями.

Функции,        которые     возвращают        указатели

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

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

int            *f();

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

В   следующей программе демонстрируется использование      указателя    в качестве       типа возвращаемого      значения.    Это     —        новая            версия приведенной         выше функции       find_substr(), только       теперь           она возвращает             не       индекс          найденной  подстроки,  а          указатель    на       нее. Если заданная       подстрока   не       найдена,      возвращается        нулевой указатель.

//  Новая            версия          функции       find_substr().

//  которая        возвращает             указатель    на     подстроку. #include        <iostream> using   namespace     std;

char         *find_substr(char  *sub,  char    *str); int     main() {

 char    *substr;

 substr =          find_substr("три", "один            два     три     четыре");

 cout    <<       "Найденная            подстрока:  "          <<       substr;

 return            0;

}

// Функция       возвращает             указатель    на       искомую      подстроку   или            нуль, если   таковая         не       будет найдена.

char         *find_substr(char  *sub,  char    *str) {

 int       t;

 char    *p,      *p2,   *start;

 for(t=0;         str[t]; t++)    {

  p     =          &str[t];          //         установка    указателей

  start           =          p;

  р2  =          sub;

  while(*р2            &&      *p2==*p)     {           //         проверка     совпадения

   р++;      р2++;

  }

  /*   Если   достигнут    конец            р2-подстроки,       то        эта      подстрока            была найдена.      */

  if(!*р2)     return            start;  //         Возвращаем           указатель    на       начало найденной  подстроки.

 }

 return            0;        //         подстрока   не       найдена

}

При         выполнении           этой   версии          программы получен       следующий     результат.

Найденная       подстрока:  три     четыре

В   данном        случае,         когда подстрока   "три"           была  найдена       в строке           "один            два     три   четыре", функция          find_substr() возвратила указатель    на       начало          искомой       подстроки   "три",          который в функции   main()            был    присвоен     переменной           substr.           Таким образом,     при     выводе         значения substr    на       экране          отобразился остаток         строки,         т.е.      "три четыре".

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

Прототипы функций

Прототип      объявляет функцию     до       ее        первого        использования.

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

■  тип     возвращаемого    ею     значения; ■            тип     ее     параметров;

■  количество параметров.

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

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

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

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

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

type         func_name(type    parm_name1,         type     parm_name2, ..., type     parm_nameN);

Использование          имен  параметров            в          прототипе   необязательно,     но позволяет    компилятору идентифицировать      любое           несовпадение        типов при возникновении     ошибки,       поэтому       лучше имена         параметров            все      же включать     в          прототип      функции.

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

/* В          этой   программе используется         прототип      функции,     который позволяет    осуществить           строгий         контроль     типов.

*/ void    sqr_it(int       *i);      //         прототип     функции int            main() {

 int       х;

 х          =          10;

 sqr_it(x);       //         ***     Ошибка        ***     —        несоответствие      типов!

 return            0;

}

void         sqr_it(int     *i) {

 *i=*i   *          *i;

}

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

Подробнее     о          заголовках

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

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

Сравнение      старого      и          нового          стилей         объявления   параметров         функций

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

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

float        f(int    a,         int       b,        char    ch)

{   ... будет        выглядеть   с          использованием  старого     стиля несколько   по-другому.

float        f(a,      b,     ch) int            а,     b; char     ch;

{    ...

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

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

Рекурсия

Рекурсивная   функция      —        это    функция,     которая      вызывает   сама     себя.

Рекурсия           —        это      последняя   тема, которую       мы      рассмотрим            в этой   главе.            Рекурсия, которую          иногда          называют    циклическим определением,     представляет         собой            процесс определения    чего-либо    на собственной           основе.         В          области        программирования        под рекурсией понимается  процесс        вызова          функцией    самой            себя.  Функцию, которая        вызывает     саму  себя, называют     рекурсивной.

Классическим примером   рекурсии     является       вычисление            факториала числа с          помощью функции         factr().           Факториал  числа N         представляет собой            произведение       всех    целых            чисел от        1 до   N.        Например, факториал   числа 3          равен            1x2x3,           или     6.        Рекурсивный         способ вычисления факториала          числа демонстрируется в          следующей программе. Для    сравнения   сюда  же включен            и          его      нерекурсивный     (итеративный) эквивалент.

#include <iostream> using     namespace   std;

int            factr(int     n); int fact(int     n); int main() {

 //         Использование     рекурсивной          версии.

 cout    <<       "Факториал            числа 4          равен            "          <<       factr(4);

 cout    <<       '\n';

 //         Использование     итеративной           версии.

 cout    <<       "Факториал            числа 4          равен            "          <<       fact(4);

 cout    <<       '\n';

 return            0;

}

//  Рекурсивная           версия.

int            factr(int     n) {

 int       answer;

 if(n==1)         return(1);

 answer          =          factr(n-1)*n;

 return(answer);

}

//  Итеративная           версия.

int     fact(int     n) {

 int       t,         answer;

 answer          =1;

 for(t=1;         t<=n;  t++)    answer          =          answer*        (t);

 return            (answer);

}

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

Рекурсивная    функция       factr()            несколько   сложнее.      Если   она вызывается             с          аргументом, равным     1,        то        сразу возвращает значение      1.        В          противном  случае           она     возвращает             произведение factr(n-1)      *          n.        Для    вычисления            этого  выражения вызывается метод            factr()            с          аргументом            n-1. Этот       процесс        повторяется до       тех      пор,   пока   аргумент      не       станет            равным        1,        после чего вызванные  ранее            методы        начнут           возвращать             значения. Например,  при     вычислении факториала          числа 2          первое          обращение к методу         factr()            приведет     ко       второму       обращению            к тому           же методу,        но       с          аргументом,           равным        1.        Второй          вызов метода         factr()            возвратит    значение 1, которое        будет умножено   на       2 (исходное    значение      параметра   n).       Возможно,  вам    будет интересно   вставить в          функцию      factr()            инструкции cout,  чтобы            показать      уровень каждого вызова   и          промежуточные   результаты.

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

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

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

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

Рассмотрим     еще    один  пример         рекурсивной          функции.     Функция reverse()       использует рекурсию    для     отображения         своего           строкового аргумента   в          обратном    порядке.

//  Отображение         строки           в          обратном    порядке       с          помощью     рекурсии. #include          <iostream> using   namespace   std;

void         reverse(char            *s);

int    main() {

 char    str[]    =          "Это   тест";

 reverse(str);

 return            0;

}

//  Вывод           строки           в          обратном     порядке. void        reverse(char            *s) {

 if(*s)reverse(s+1);

 else     return;

 cout    <<       *s;

}

Функция            reverse()       проверяет,  не       передан       ли       ей       в          качестве параметра   указатель    на       нуль, которым      завершается           строка.          Если   нет, то        функция       reverse()       вызывает     саму  себя   с указателем          на следующий            символ         в          строке.          Этот    "закручивающийся"       процесс повторяется            до тех            пор,   пока   той     же       функции       не       будет передан указатель    на       нуль. Когда,           наконец, обнаружится  символ         конца строки,         пойдет          процесс        "раскручивания", т.е.      вызванные  ранее функции начнут           возвращать             значения,    и          каждый        возврат         будет сопровождаться "довыполнением"            метода,        т.е.      отображением      символа s.         В          результате  исходная     строка посимвольно      отобразится            в обратном    порядке.

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

Глава     8:     Функции,     часть      вторая:  ссылки, перегрузка и использование       аргументов по   умолчанию

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

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

Два       способа        передачи     аргументов

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

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

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

Второй   способ          передачи     аргумента   подпрограмме      называется вызовом по       ссылке          (callby-reference). В          этом   случае           в          параметр копируется адрес аргумента   (а        не       его      значение).   В пределах вызываемой подпрограммы     этот    адрес            используется         для     доступа        к реальному аргументу,  заданному  при     ее        вызове.        Это     значит,          что изменения,             внесенные  в          параметр, окажут            воздействие           на аргумент,    используемый      при     вызове          подпрограммы.

Как        в          C++     реализована          передача     аргументов

По            умолчанию для     передачи     аргументов в          C++     используется метод            вызова          по       значению. Это       означает,     что      в          общем случае           код     функции       не       может           изменить     аргументы, используемые при     вызове          функции.     Во       всех    программах           этой   книги, представленных   до       сих пор,        использовался      метод            вызова          по значению. Рассмотрим следующую            функцию. #include           <iostream> using namespace   std;

int            sqr_it(int     x); int main() {

 int       t=10;

 cout    <<       sqr_it(t)         <<       '           '           <<       t;

 return            0;

}

int            sqr_it(int     x) {

 x          =          x*x;

 return            x;

}

В   этом   примере      значение      аргумента,  передаваемого     функции       sqr_it(), 10,      копируется в параметр х.         При    выполнении           присваивания        х          = х*х      изменяется лишь локальная   переменная х.      Переменная           t, используемая        при     вызове          функции       sqr_it(),         по-прежнему         будет иметь значение    10,      и          на       нее     никак            не       повлияют    операции, выполняемые       в          этой   функции. Следовательно,        после запуска рассматриваемой            программы на       экране          будет выведен       такой результат:    100     10.

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

Использование        указателя  для     обеспечения          вызова         по   ссылке

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

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

void         swap(int        *х,      int     *у) {

 int       temp;

 temp  =          *x;      //         Временно    сохраняем   значение,    расположенное    по адресу          х.

 *х        =          *у;      //         Помещаем  значение,    хранимое    по       адресу          у,            в          адрес х.

 *у       =          temp; //         Помещаем  значение,    которое        раньше            хранилось   по адресу    х,         в          адрес            у.

}

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

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

#include <iostream> using     namespace   std;

//  Объявляем функцию      swap(),          которая        использует  указатели. void         swap(int        *х,      int       *у); int           main() {

 int       i,          j;

i          =         10;

j         =         20;

 cout    <<       "Исходные  значения      переменных           i           и          j:          ";

 cout    <<       i           <<       '           '           <<       j           <<       '\n';

 swap(&j,       &i);     //         Вызываем   swap()           с          адресами    переменных     i           и          j.

 cout    <<       "Значения   переменных           i           и          j           после обмена:        ";

 cout    <<       i           <<       '           '           <<       j           <<       '\n';

 return            0;

}

//  Обмен          аргументами.

void         swap(int        *x,      int     *y) {

 int       temp;

 temp  =          *x;      //         Временно    сохраняем   значение,    расположенное    по адресу          х.

 *х        =          *у;      //         Помещаем  значение,    хранимое    по       адресу          у,            в          адрес х.

 *у       =          temp; //         Помещаем  значение,    которое        раньше            хранилось   по адресу    х,         в          адрес            у.

}

Результаты      выполнения           этой   программы таковы.

Исходные         значения      переменных           i           и          j:          10       20

Значения          переменных           i           и          j           после обмена:        20       10

В   этом   примере      переменной           i           было присвоено   начальное   значение 10,      а          переменной           j           — 20. Затем            была  вызвана       функция swap()           с          адресами    переменных           i           и          j.          Для    получения адресов здесь       используется         унарный      оператор     Следовательно,    функции swap()           при     вызове          были переданы    адреса          переменных           i           и j,          а          не       их        значения.    После            выполнения           функции       swap() переменные           i           и          j           обменялись            своими         значениями.

Ссылочные    параметры

Ссылочный      параметр  автоматически  получает    адрес соответствующего     аргумента.

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

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

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

//  Использование     ссылочного параметра. #include <iostream> using   namespace   std;

void         f(int     &i); int     main() {

 int       val       =          1;

 cout    <<       "Старое        значение      переменной           val:     "          <<       val       <<     '\n';

 f(val); //         Передаем    адрес            переменной           val       функции       f().

 cout    <<       "Новое          значение      переменной           val:     "          <<       val       <<     '\n';

 return            0;

}

void         f(int     &i) {

 i           =          10;      //         Модификация       аргумента,  заданного   при     вызове.

}

При         выполнении           этой   программы получаем    такой результат.

Старое   значение      переменной           val:     1

Новое     значение      переменной           val:     10

Обратите          особое          внимание    на       определение         функции       f().

void         f     (int      &i) {

 i           =          10;      //         Модификация       аргумента,  заданного   при     вызове.

}

Итак,       рассмотрим            объявление            параметра   i.          Его      имени предшествует        символ         который "превращает" переменную          i           в ссылочный параметр.    (Это    объявление            также            используется         в прототипе    функции.)    Инструкция

i    =          10;

(в  данном        случае           она     одна  составляет   тело   функции)     не присваивает           переменной           i значение   10.      В          действительности значение      10       присваивается       переменной,          на       которую ссылается переменная            i           (в        нашей           программе ею      является       переменная val).    Обратите внимание        на       то,       что      в          этой   инструкции не используется         оператор     который       необходим при работе  с          указателями. Применяя    ссылочный параметр,    вы       тем     самым          уведомляете С++компилятор    о          передаче     адреса          (т.е.    указателя),  и          компилятор автоматически разыменовывает      его      за        вас.    Более            того,  если   бы      вы попытались            "помочь"     компилятору, использовав     оператор     то        сразу же получили     бы      сообщение  об       ошибке         (и        вправду       "ни     одно доброе дело  не       остается       безнаказанным").

Поскольку        переменная            i           была  объявлена   как     ссылочный параметр, компилятор автоматически    передает      функции       f()        адрес            любого аргумента,  с          которым      вызывается             эта функция.          Таким            образом, в          функции       main()           инструкция

f(val);      //         Передаем    адрес            переменной           val       функции       f().

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

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

#include <iostream> using     namespace   std;

// Объявляем функцию      swap()           с          использованием ссылочных            параметров.

void         swap(int        &х,      int     &у); int          main() {

 int       i,          j;

 i           =          10;      j           =          20;

 cout    <<       "          Исходные    значения      переменных           i           и          j:          ";

 cout    <<       i           <<       '           '           <<       j           <<       '\n';

 swap  (j,        i);

 cout    <<       "          Значения     переменных           i           и          j           после обмена:     ";

 cout    <<       i           <<       '           '           <<       j           <<       '\n';

 return            0;

}

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

*/ void    swap(int        &х,     int       &у) {

 int       temp;

 temp  =          x;         //         Сохраняем  значение,    расположенное    по       адресу     х.

 х          =          у;         //         Помещаем  значение,    хранимое    по       адресу          у,     в          адрес            х.

 у          =          temp; //         Помещаем  значение,    которое        раньше            хранилось   по адресу    х,         в          адрес            у.

}

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

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

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

Объявление  ссылочных параметров

В   изданной     в          1986  г.         книге Язык  программирования        C++     (в которой        был    впервые       описан синтаксис             C++)   Бьерн            Страуструп представил стиль объявления            ссылочных  параметров, одобренный       другими программистами.            В          соответствии          с          этим   стилем          оператор     "&" связывается            с          именем        типа,  а          не       с          именем        переменной. Например,  вот      как     выглядит     еще один    способ          записи          прототипа функции       swap().

void         swap(int&    х,         int&    у);

Нетрудно          заметить,     что      в          этом   объявлении            символ         "&" прилегает    вплотную    к          имени типа int,      а          не       к          имени переменной           х.

Некоторые       программисты      определяют           в          таком            стиле и     указатели,   связывая      символ         "*"      с типом,       а          не       с     переменной,          как     в          этом   примере.

float*      р;

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

int*          а,         b;

Здесь      b          объявляется           как     целочисленная     переменная            (а        не как     указатель    на целочисленную          переменную),        поскольку,  как определено            синтаксисом           C++,   используемый      в объявлении        символ "*"      или     "&"     связывается            с          конкретной             переменной,          которой он предшествует,             а          не       с          типом,          за        которым      он       следует.

Важно    понимать,   что      для     С++-компилятора            абсолютно  безразлично, как     именно         вы напишете          объявление:           int       *р       или     int*     р. Таким            образом,     если   вы       предпочитаете      связывать символ           "*"      или "&"     с          типом,          а          не       переменной,          поступайте,             как     вам удобно.        Но,     чтобы избежать    в          дальнейшем          каких-либо недоразумений,   в          этой   книге мы      будем           связывать символ           "*"      или "&"     с          именем        переменной,          а          не       с          именем        типа.

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

Возврат         ссылок

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

Если        функция       возвращает             ссылку,         это      означает,     что      она возвращает             неявный       указатель    на значение,          передаваемое       ею      в инструкции return.           Этот    факт   открывает   поразительные возможности: функцию,    оказывается,          можно          использовать         в          левой            части инструкции присваивания!     Например,  рассмотрим            следующую            простую программу.

//  Возврат        ссылки. #include <iostream> using     namespace   std;

double    &f(); double    val       =     100.0; int     main() {

 double           newval;

 cout    <<       f()        <<       '\n';     //         Отображаем          значение      val.

 newval           =          f();      //         Присваиваем         значение      val       переменной     newval.

 cout    <<       newval          <<       '\n';     //         Отображаем          значение      newval.

 f()        =          99.1;  //         Изменяем   значение      val.

 cout    <<       f()        <<       '\n';     //         Отображаем          новое            значение      val.

 return            0;

}

double     &f() {

 return            val;     //         Возвращаем           ссылку          на       val.

}

Вот          как     выглядят     результаты выполнения           этой   программы.

100

100

99.1

Рассмотрим     эту      программу подробнее.             Судя  по       прототипу   функции f(),      она     должна возвращать        ссылку          на       double-значение.  За объявлением         функции       f()        следует        объявление глобальной переменной           val,     которая        инициализируется           значением  100.   При выполнении следующей          инструкции выводится   исходное     значение переменной           val.

cout         <<       f()        <<       '\n';     //         Отображаем          значение      val.

После     вызова          функция       f()        возвращает             ссылку          на переменную          val.     Поскольку   функция       f() объявлена         с "обязательством"            вернуть        ссылку,         при     выполнении           строки

return     val;     //         Возвращаем           ссылку          на       val.

                                                                                 автоматически                                                              возвращается                                                                     ссылка                                                                         на                                                               глобальную                                                               переменную                                                                        val.                                                                        Эта                                                                     ссылка                                                                                   затем

используется          инструкцией           cout    для     отображения         значения      val. При        выполнении     строки

newval   =          f();      //Присваиваем      значение      val       переменной           newval.

          ссылка  на  переменную  val,  возвращенная  функцией  f(),   используется  для   присвоения

значения      val      переменной           newval.

А  вот      самая            интересная строка           в          программе.

f() =          99.1;  //         Изменяем   значение      val.

При         выполнении           этой   инструкции присваивания        значение переменной           val      становится равным         числу 99,1.  И         вот      почему: поскольку   функция       f()        возвращает             ссылку          на       переменную val,  эта ссылка          и          является       приемником          инструкции присваивания.       Таким образом,     значение 99,1       присваивается       переменной           val      косвенно, через ссылку          на       нее,    которую       возвращает функция     f().

Наконец,          при     выполнении           строки

cout         <<       f()        <<       '\n';     //         Отображаем          новое            значение      val.

отображается новое            значение      переменной           val      (после           того,  как     ссылка          на       переменную          val

будет возвращена            в          результате  вызова          функции       f()        в инструкции cout).

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

#include <iostream> using     namespace   std;

double    &change_it(int        i); //    Функция     возвращает             ссылку. double      vals[]  =     {1.1,   2.2,     3.3,     4.4,     5.5}; int         main() {

 int       i;

 cout    <<       "Вот   исходные    значения:    ";

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

  cout           <<       vals[i] <<       '           ';

 cout    <<       '\n';

 change_it(1)            =          5298.23;       //         Изменяем   2-й      элемент.

 change_it(3)            =          -98.8; //         Изменяем   4-й      элемент.

 cout    <<       "Вот   измененные           значения:    ";

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

  cout           <<       vals[i] <<       '           ';

 cout    <<       '\n';

 return            0;

}

double    &change_it(int        i) {

 return            vals[i];            //         Возвращаем           ссылку          на       i-й       элемент.

}

Эта           программа изменяет     значения      второго        и          четвертого элементов   массива        vals. Результаты   ее        выполнения           таковы.

Вот          исходные    значения:    1.1      2.2      3.3      4.4      5.5

Вот          измененные           значения:    1.1      5298.23        3.3      -98.8  5.5

Давайте разберемся,           как     они     были получены.   Функция       change_it() объявлена   как возвращающая         ссылку          на       значение      типа   double. Говоря          более            конкретно,  она     возвращает ссылку        на       элемент массива        vals,   который       задан ей       в          качестве       параметра   i.          Таким образом, при        выполнении           следующей инструкции функции       main()

change_it(1)     =          5298.23;       //         Изменяем   2-й      элемент.

                                                                      функция                                                  change_it()                                                  возвращает                                                      ссылку                                                           на                                                     элемент                                                      vals[1].                                                        Через                                                          эту                                                      ссылку                                                                                          элементу

vals[1]           теперь           присваивается       значение      5298,23.       Аналогичные события       происходят при выполнении  и          этой   инструкции.

change_it(3)     =          -98.8; //         Изменяем   4-й      элемент.

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

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

//  Здесь ошибка:       нельзя          возвращать     ссылку //     на       локальную  переменную. int     &f() {

 int       i=10;

 return            i;

}

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

Создание        ограниченного     массива

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

Один      из        способов     создания     ограниченного      массива иллюстрируется   в          следующей программе.

//  Простой       способ          организации          безопасного           массива.

#include <iostream> using     namespace   std;

int            &put(int        i);        //         Помещаем  значение      в     массив. int  get(int            i);        //         Считываем  значение     из        массива.

int     vals[10]; int     error  =          -1;

int    main() {

 put(0) =          10;      //         Помещаем  значения      в          массив.

 put(1) =          20;

 put(9) =          30;

 cout    <<       get(0) <<       '           ';

 cout    <<       get(1) <<       '           ';

 cout    <<       get(9) <<       '           ';

 //         А         теперь           специально            генерируем     ошибку.  put(12)           =1;      //         Индекс         за     пределами границ          массива. return     0; } // Функция     занесения    значения      в          массив.

int     &put(int     i) {

 if(i>=0            &&      i<10)

   return  vals[i];            //         Возвращаем           ссылку          на       i-й       элемент.

  else            {

   cout      <<       "Ошибка      нарушения  границ!\n";

   return  error; //         Возвращаем           ссылку          на       error.

  }

}

//  Функция       считывания             значения      из        массива.

int     get(int     i) {

 if(i>=0            &&      i<10)

   return  vals[i];            //         Возвращаем           значение      i-го     элемента.

  else            {

   cout      <<       "Ошибка      нарушения  границ!\n";

   return  error; //         Возвращаем           значение      переменной           error.

  }

}

Результат,        полученный           при     выполнении           этой   программы,     выглядит     так.

10 20       30       Ошибка        нарушения  границ!

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

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

Независимые           ссылки

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

Независимая  ссылка          —        это    просто        еще    одно  название     для переменных           некоторого иного          типа.

Независимая   ссылка          должна        указывать    на       некоторый  объект. Следовательно, независимая            ссылка          должна        быть  инициализирована при     ее        объявлении.           В          общем          случае           это означает,         что      ей будет присвоен     адрес            некоторой   ранее            объявленной         переменной. После этого            имя    такой ссылочной  переменной           можно          применять везде,           где     может           быть использована         переменная,          на       которую она     ссылается.   И         в          самом           деле, между          ссылкой       и переменной, на       которую       она     ссылается,   практически           нет      никакой       разницы. Рассмотрим, например,           следующую            программу. #include       <iostream> using  namespace   std;

int    main() {

 int       j,          k;

 int       &i        =          j;          //         независимая          ссылка

j         =         10;

 cout    <<       j           <<       "          "          <<       i;          //         Выводится: 10       10

k       =         121;

 i           =          k;         //         Копирует     в          переменную          j           значение            переменной           k,         а          не адрес      переменной           k.

 cout    <<       "\n"    <<       j;          //         Выводится: 121

 return            0;

}

При         выполнении           эта      программа выводит       следующие результаты.

10 10

121

Адрес,    который       содержит     ссылочная   переменная,          фиксирован            и его      нельзя          изменить. Следовательно,      при     выполнении           инструкции i =          k          в          переменную          j           (адресуемую         ссылкой       i) копируется значение      переменной           k,         а          не       ее        адрес.           В          качестве       еще одного          примера      отметим, что         после выполнения           инструкции i++ ссылочная   переменная            i           не       станет            содержать   новый адрес,         как можно          было бы      предположить.     В          данном        случае           на       1 увеличится  содержимое переменной        j.

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

Ограничения            при     использовании     ссылок

На            применение           ссылочных  переменных           накладывается      ряд     следующих ограничений.

■  Нельзя          ссылаться    на       ссылочную переменную.

■  Нельзя          создавать    массивы       ссылок.

■  Нельзя          создавать    указатель    на       ссылку,         т.е.      нельзя          к     ссылке          применять  оператор     "&"

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

Перегрузка    функций

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

В   этом   разделе        мы      узнаем          об       одной           из        самых удивительных       возможностей       языка            C++     — перегрузке        функций.     В C++     несколько   функций       могут иметь            одинаковые           имена,          но       при условии,       что      их        параметры  будут различными.         Такую            ситуацию называют    перегрузкой функций    (function       overloading),           а          функции, которые       в          ней     задействованы,    — перегруженными      (overloaded). Перегрузка функций       —        один  из        способов     реализации полиморфизма   в C++.

Рассмотрим    простой        пример         перегрузки функций.

//  "Трехкратная"       перегрузка  функции       f().

#include <iostream>

using       namespace   std;

void         f(int    i);        //         один  целочисленный    параметр void     f(int    i,          int       j);        //         два     целочисленных     параметра void     f(double        k);       //         один  параметр     типа     double

int    main() {

 f           (10);   //         вызов            функции       f(int)

 f(10,   20);    //         вызов            функции       f           (int,    int)

 f(12.23);       //         вызов            функции       f(double)

 return     0; }

void         f(int     i) {

 cout    <<       "В        функции       f(int), i           равно            "          <<       i           <<       '\n';

}

void         f(int    i,     int       j) {

 cout    <<       "В        функции       f(int,   int),    i           равно            "          <<       i;

 cout    <<       ",         j           равно            "          <<       j           <<       '\n';

}

void         f(double     k) {

 cout    <<       "В        функции       f(double),     k          равно            "          <<       k          <<     '           \n';

}

При         выполнении           эта      программа генерирует  следующие результаты.

В   функции       f(int), i           равно            10 В    функции     f(int,   int),    i           равно            10,      j           равно     20

В   функции       f(double),     к          равно            12.23

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

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

Чтобы     лучше           понять          выигрыш     от        перегрузки функций, рассмотрим            три     функции       из стандартной     библиотеки:           abs(), labs() и fabs().            Они    были впервые       определены           в          языке            С,        а затем ради  совместимости      включены   в          C++.   Функция       abs()  возвращает абсолютное            значение (модуль)         целого          числа,           функция       labs() возвращает             модуль         длинного     целочисленного значения       (типа  long), a fabs() —        модуль         значения      с          плавающей             точкой          (типа  double). Поскольку   язык  С          не       поддерживает       перегрузку  функций,     каждая функция       должна        иметь собственное          имя,   несмотря     на       то,       что      все три     функции       выполняют,            по       сути,  одно  и          то        же действие.          Это делает          ситуацию     сложнее,      чем    она     есть    на       самом           деле. Другими словами,      при одних   и          тех      же       действиях    программисту       необходимо помнить       имена           всех    трех    (в        данном случае)    функций       вместо одного.        Но       в          C++,   как     показано     в          следующем            примере, можно использовать     только          одно  имя    для     всех    трех    функций.

// Создание     функций       myabs()         —        перегруженной     версии          функции abs().

#include <iostream> using     namespace   std; //     Функция       myabs()     перегружается      тремя     способами. int     myabs(int     i); double     myabs(double         d); long         myabs(long  l);

int    main() {

 cout    <<       myabs(-10)  <<       "\n";

 cout    <<       myabs(-11.0)          <<       "\n";

 cout    <<       myabs(-9L)   <<       "\n";

 return     0; }

int     myabs(int     i) {

 cout    <<       "Использование   int-функции            myabs():       ";

 if(i<0) return            -i;

 else     return            i;

}

double    myabs(double         d)

{

 cout    <<       "Использование   double-функции    myabs():       ";

 if(d<0.0)        return            -d;

 else     return            d;

}

long     myabs(long  l) {

 cout    <<       "Использование   long-функции         myabs():       ";

 if(1<0)           return            -1;

 else     return            1;

}

Результаты      выполнения           этой   программы таковы.

Использование          int-функции            myabs():       10

Использование          double-функции    myabs():       11

Использование          long-функции         myabs():       9

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

Каждая  версия          перегруженной     функции       может           выполнять  любые действия.     Другими словами,           не       существует  правила,      которое        бы обязывало  программиста       связывать перегруженные      функции       общими действиями.           Однако         с          точки зрения          стилистики перегрузка  функций все-таки       подразумевает     определенное       "родство"    его      версий.         Таким образом,      несмотря     на       то,       что      одно  и          то        же       имя    можно использовать         для     перегрузки не связанных         общими       действиями функций,     этого  делать          не       стоит.            Например,  в          принципе можно использовать         имя    sqr      для     создания     функции,     которая        возвращает квадрат        целого числа,        и          функции,     которая        возвращает             значение квадратного           корня            из        вещественного      числа (типа double).        Но, поскольку   эти      операции     фундаментально  различны,   применение механизма перегрузки методов       в          этом   случае           сводит          на       нет      его первоначальную  цель. (Такой           стиль программирования,       наверное,    подошел бы      лишь для     того,  чтобы            ввести           в заблуждение      конкурента.)          На практике      перегружать           имеет            смысл           только          тесно связанные операции.

Анахронизм  в          виде   ключевого  слова            overload

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

Общая   форма           использования      ключевого  слова overload       такова.

overload            func_name;

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

overload            Counter;

Если        вы       встретите     overload-объявления     при     работе          со        старыми программами,      их        можно просто       удалить:       они     больше         не       нужны. Поскольку   ключевое    слово overload       —        анахронизм, его  не       следует использовать         в          новых            С++-программах. На       самом           деле большинство компиляторов  его      попросту      не       воспримет.

Аргументы, передаваемые     функции      по       умолчанию           

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

Задание             аргументов,            передаваемых      функции       по       умолчанию, синтаксически       аналогично инициализации    переменных.          Рассмотрим следующий            пример,       в          котором       объявляется функция    myfunc(), принимающая       один  аргумент      типа   double           с          действующим        по умолчанию значением  0.0      и          один  символьный           аргумент      с действующим        по       умолчанию значением  'Х'.

void         myfunc(double       num   =          0.0,     char    ch     =          'Х') {

 .

 .

 .

}

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

myfunc(198.234,        'A');     //         Передаем    явно   заданные    значения.

myfunc(10.1);  //         Передаем    для     параметра   num   значение      10.1,  а          для параметра   ch        позволяем  применить              аргумент,    задаваемый           по умолчанию ('Х').

myfunc();          //         Для    обоих            параметров            num   и          ch     позволяем  применить

аргументы, задаваемые           по       умолчанию.

При         первом         вызове          параметру   num   передается значение      198.234, а          параметру   ch        — символ    'А'.      Во       время            второго        вызова параметру   num   передается значение      10.1,  а          параметр     ch по  умолчанию устанавливается   равным        символу       'Х'.      Наконец,      в          результате  третьего вызова как  параметр     num,  так      и          параметр     ch        по       умолчанию устанавливаются  равными      значениям, заданным   в          объявлении            функции.

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

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

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

значение.

#include <iostream> using     namespace   std;

void         clrscr(int       size=25);

int    main() {

 int       i;

 for(i=0;          i<30;  i++      )           cout    <<       i           <<       '\n';  clrscr();   //         Очищаем     25       строк.

 for(i=0;          i<30;  i++      )           cout    <<       i           <<       '\n';  clrscr(10);          //         Очищаем     10       строк.

 return            0;

}

void         clrscr(int     size) {

 for(;    size;    size--) cout    <<       '\n';

}

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

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

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

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

void         f(int    а          =          1,        int       b);

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

int            myfunc(float           f,         char    *str,   int       i=10,  int       j);

Поскольку        для     параметра   i           определено            значение      по умолчанию,           для     параметра   j           также нужно          задать           значение      по умолчанию.

Сравнение      возможности      передачи     аргументов          по   умолчанию            с          перегрузкой функций

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

Допустим,        что      вы       назвали        свои   функции       именем        mystrcat()    и предложили          такой вариант их  прототипов.

void         mystrcat(char         *s1,    char    *s2,    int       len);

void         mystrcat(char         *s1,    char    *s2);

Первая   версия          должна        скопировать           len      символов    из        строки s2        в          конец            строки           s1.       Вторая версия       копирует      всю    строку, адресуемую           указателем s2,       в          конец            строки,         адресуемой указателем s1,       т.е.      действует    подобно      стандартной           функции       strcat().

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

//  Применение           пользовательской           версии          функции       strcat().

#include <iostream> #include <cstring> using     namespace   std; void     mystrcat(char         *s1,     char    *s2,    int       len     =          -1); int     main() {

 char    str1[80]         =          "Это   тест.";

 char    str2[80]         =          "0123456789";

 mystrcat(str1,         str2,   5);       //         Присоединяем      5          символов.

 cout    <<       str1     <<       '\n';

 strcpy(str1,  "Это   тест.");          //         Восстанавливаем             str1.

 mystrcat(str1,         str2);  //         Присоединяем      всю    строку.

 cout    <<       str1     <<       '\n';

 return            0;

}

//  Пользовательская           версия          функции     strcat(). void           mystrcat(char         *s1,    char    *s2,     int       len) {

 //         Находим      конец            строки           s1.

 while(*s1)    s1++;

 if(len   ==       -1)       len      =          strlen(s2);

 while(*s2      &&      len)     {

  *s1 =          *s2;    //         Копируем    символы.

  s1++;         s2++;  len--;

 }

 *s1      =          '\0';     //         Завершаем строку           s1        нулевым      символом.

}

Здесь      функция       mystrcat()    присоединяет        len      символов    строки, адресуемой            параметром           s2, к   концу            строки,         адресуемой параметром           s1.       Но       если   значение      len      равно            -1,       как     и          в случае разрешения         передачи     этого  аргумента   по       умолчанию,           функция mystrcat()    присоединит          к строке        s1        всю    строку,          адресуемую параметром           s2.       (Другими     словами,      если   значение      len равно     -1, функция       mystrcat()    действует    подобно      стандартной           функции       strcat().) Используя для      параметра   len      возможность         передачи     аргумента   по умолчанию,           обе     операции     можно объединить         в          одной           функции.

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

Об          использовании     аргументов,         передаваемых     по   умолчанию

Несмотря          на       то        что      аргументы, передаваемые      функции       по умолчанию,           —        очень            мощное средство            программирования (при   их        корректном            использовании),   с          ними могут иногда возникать проблемы.  Их       назначение             —        позволить   функции       эффективно выполнять  свою работу,          обеспечивая           при     всей   простоте      этого механизма  значительную        гибкость.     В          этом смысле          все      передаваемые по       умолчанию аргументы  должны       отражать      способ          наиболее общего использования      функции       или     альтернативного  ее        применения.          Если   не существует некоторого единого       значения,    которое        обычно         присваивается тому  или     иному           параметру, то и    нет      смысла         объявлять соответствующий             аргумент      по       умолчанию.           На       самом           деле объявление            аргументов,            передаваемых      функции       по       умолчанию, при     недостаточном     для этого     основании   деструктуризирует          код, поскольку   такие аргументы  способны    сбить с          толку любого,       кому  придется разбираться           в          такой программе.            Наконец,      основным   принципом использования      аргументов по       умолчанию должен        быть, как     у          врачей, принцип       "не навредить".  Другими      словами,      случайное   использование аргумента   по       умолчанию не должно  привести      к          необратимым отрицательным    последствиям.      Ведь  такой аргумент можно  просто          забыть указать         при     вызове          некоторой   функции,     и,        если   это      случится, подобный   промах         не       должен        вызвать,       например,   потерю         важных данных!

Перегрузка      функций       и          неоднозначность

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

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

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

int            myfunc(double     d); .

. .

cout         <<       myfunc('c'); //         Ошибки        нет,    выполняется     преобразование

типов.

Как          отмечено     в          комментарии,       ошибки        здесь нет,    поскольку   C++ автоматически преобразует   символ         'c'        в          его      double-эквивалент. Вообще        говоря,         в          C++     запрещено  довольно мало     видов преобразований   типов.           Несмотря     на       то        что      автоматическое преобразование типов —        это      очень            удобно,        оно,   тем     не       менее, является       главной        причиной    неоднозначности. Рассмотрим         следующую программу.

//  Неоднозначность             вследствие  перегрузки функций. #include <iostream> using   namespace   std;

float        myfunc(float           i); double    myfunc(double       i);

int    main() {

 //         Неоднозначности            нет,    вызывается             функция       myfunc(double).

 cout    <<       myfunc          (10.1) <<       "          ";

 //         Неоднозначность.

 cout    <<       myfunc(10);

 return            0;

}

float        myfunc(float     i) {

 return            i;

}

double    myfunc(double     i) {

 return            -i;

}

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

cout         <<       myfunc          (10.1) <<       "          ";

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

функции       myfunc(double),    поскольку,  если   не       задано          явным           образом иное, все      литералы     с плавающей         точкой          в          C++     автоматически получают     тип     double.          Но       при     вызове          функции myfunc()            с аргументом,           равным        целому         числу 10,      в          программу вносится неоднозначность, поскольку компилятору         неизвестно,            в          какой тип     ему следует        преобразовать      этот    аргумент: float      или     double.          Оба преобразования   допустимы.            В          такой неоднозначной     ситуации      будет выдано         сообщение  об       ошибке,       и          программа не       скомпилируется.

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

А   вот      еще    один  пример         неоднозначности,            вызванной автоматическим   преобразованием типов          в          C++.

//  Еще    одна  ошибка,       вызванная   неоднозначностью. #include <iostream> using   namespace   std;

char         myfunc(unsigned   char    ch); char     myfunc(char            ch);

int    main() {

 cout    <<       myfunc('c'); //         Здесь вызывается             myfunc(char).

 cout    <<       myfunc(88)  <<       "          ";         //         Вносится      неоднозначность.

 return            0;

}

char         myfunc(unsigned   char    ch) {

 return            ch-1;

}

char         myfunc(char     ch) {

 return            ch+1;

}

В   C++     типы  unsigned       char   и          char   не       являются     существенно неоднозначными.           (Это    — различные         типы.)           Но       при     вызове функции       myfunc()       с          целочисленным   аргументом            88 компилятор      "не знает",        какую            функцию      ему    выполнить, т.е.      в          значение      какого типа   ему следует           преобразовать      число 88:      типа   char   или     типа   unsigned char? Оба    преобразования здесь  вполне          допустимы.

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

//  Еще    один  пример         неоднозначности.

#include <iostream> using     namespace   std;

int            myfunc(int   i); int     myfunc(int   i,          int       j=1);

int    main() {

 cout    <<       myfunc(4,    5)        <<       "          ";         //         неоднозначности             нет

 cout    <<       myfunc(10); //         неоднозначность

 return            0;

}

int     myfunc(int   i) {

 return            i;

}

int            myfunc(int   i,     int       j) {

 return            i*j;

}

Здесь      в          первом         обращении к          функции       myfunc()       задается       два аргумента,  поэтому       у компилятора      нет      никаких        сомнений    в          выборе нужной         функции,     а          именно         myfunc(int   i,          int       j), т.е. никакой неоднозначности             в          этом   случае           не       привносится.          Но       при втором         обращении к функции   myfunc()       мы      получаем    неоднозначность, поскольку   компилятор            "не     знает",        то        ли ему          вызвать        версию функции       myfunc(),     которая        принимает  один  аргумент,    то        ли использовать         возможность         передачи     аргумента   по       умолчанию к версии,         которая        принимает два     аргумента.

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

Глава     9:     Еще о      типах     данных и      операторах

Прежде чем    переходить к          более            сложным     средствам   C++,   имеет смысл           подробнее познакомиться      с          некоторыми           типами         данных и          операторами.        Кроме           уже    рассмотренных нами    типов данных,        в C++     определены           и          другие.         Одни из        них     состоят         из модификаторов, добавляемых         к          уже    известным  вам    типам.           Другие включают    перечисления,       а          третьи используют          ключевое    слово typedef. C++     также            поддерживает       ряд     операторов,           которые значительно расширяют область        действия      языка            и          позволяют  решать          задачи программирования         в          весьма          широком     диапазоне. Речь   идет   о поразрядных         операторах, операторах           сдвига,         а          также операторах "?"      и          sizeof.            Кроме           того,  в          этой   главе рассматриваются такие специальные         операторы, как     new    и          delete.           Они предназначены    для поддержки    С++-системы          динамического     распределения памяти.

Спецификаторы    типа const и          volatile

Спецификаторы      типа const  и          volatile          управляют            доступом   к     переменной.

В   C++     определено            два     спецификатора     типа,  которые       оказывают влияние       на       то,       каким образом     можно          получить      доступ          к переменным          или     модифицировать их.      Это спецификаторы        const  и volatile.         Официально          они     именуются  cv-спецификаторами   и          должны предшествовать    базовому     типу   при     объявлении            переменной.

Спецификатор       типа const

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

const       double           version          =          3.2;

         создается double-переменная  version,   которая  содержит  значение  3.2,   и   это        значение

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

Спецификатор          const  предотвращает модификацию      переменной           при выполнении программы.

Спецификатор            const  имеет            ряд     важных         применений. Возможно,  чаще  всего его используют     для     создания     const-параметров типа   указатель.   Такой            параметр-указатель защищает          объект,         на который       он       ссылается,   от        модификации        со        стороны       функции. Другими словами,           если   параметр-указатель        предваряется         ключевым словом         const, никакая инструкция       этой   функции       не       может модифицировать переменную,         адресуемую           этим параметром. Например,  функция       code()            в          следующей короткой     программе сдвигает каждую букву       в          сообщении на       одну  алфавитную           позицию      (т.е. вместо          буквы            'А'       ставится       буква 'Б'        и т.д.),           отображая  таким образом       сообщение  в          закодированном  виде. Использование спецификатора const  в          объявлении            параметра   не       позволяет    коду  функции модифицировать объект,        на       который       указывает    этот    параметр.

#include <iostream> using     namespace   std;

void         code(const   char    *str);

int    main() {

 code("Это     тест.");

 return            0;

}

/* Использование     спецификатора     const  гарантирует,           что      str       не может            изменить     аргумент,    на       который       он       указывает.

*/ void    code(const   char     *str) {

 while(*str)    {

  cout           <<       (char) (*str+1);

  str++;

 }

}

Поскольку        параметр     str       объявляется           как     const-указатель,   у функции       code()            нет      никакой возможности   внести           изменения  в строку,          адресуемую           параметром           str.      Но       если   вы попытаетесь написать      функцию      code()            так,     как     показано     в          следующем примере, то           обязательно           получите      сообщение  об       ошибке,       и программа не       скомпилируется.

//  Этот    код     неверен.

void         code(const   char    *str)

{

 while(*str)    {

  *str            =          *str     +          1;        //         Ошибка,       аргумент     модифицировать нельзя.

  cout           <<       (char) *str;

  str++;

 }

}

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

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

//  Нельзя          модифицировать const-ссылки. #include <iostream> using   namespace   std;

void         f(const           int       &i);

int    main() {

 int       к          =          10;

 f(к);

 return            0;

} //           Использование     ссылочного const-параметра. void        f           (const            int       &i) {

 i           =          100;   //         Ошибка,       нельзя          модифицировать const-ссылку.

 cout    <<       i;

}

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

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

#include <iostream> using     namespace   std;

const       int       size     =          10;

int    main() {

 int       A1[size],        A2[size],        A3[size];

 //         .           .           .

}

Если        в          этом   примере      понадобится          использовать         новый размер         для     массивов,    вам потребуется   изменить     только          объявление переменной           size     и          перекомпилировать       программу.

В          результате  все      три     массива        автоматически      получат        новый размер.

Спецификатор       типа volatile

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

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

Например,       в          следующем            фрагменте   программы предположим,      что переменная            clock обновляется            каждую        миллисекунду       часовым механизмом          компьютера.          Но,     поскольку переменная clock  не объявлена   с          использованием  спецификатора     volatile,         этот    фрагмент кода может           иногда          работать      недолжным           образом.     (Обратите    особое внимание    на       строки, обозначенные   буквами       "А"     и          "Б".)

int            clock, timer;

 //         ...

timer       =          clock; //         строка     A //     ...        Какие-нибудь     действия.

cout         <<       "Истекшее   время            "          <<       clock-timer; //         строка           Б

В   этом   фрагменте   переменная            clock  получает      свое   значение,    когда она присваивается переменной    timer  в          строке           А.        Но,     поскольку переменная            clock  не       объявлена   с использованием           спецификатора volatile,         компилятор            волен            оптимизировать   этот    код, причем таким            способом,   при     котором       значение      переменной           clock, возможно,  не       будет опрошено   в          инструкции cout    (строка         Б),       если между          строками     А         и          Б          не       будет ни       одного промежуточного присваивания        значения      переменной           clock. (Другими     словами,      в строке           Б компилятор        может           просто          еще    раз     использовать значение,    которое        получила     переменная clock            в          строке           А.)       Но если   между          моментами             выполнения           строк А         и          Б          поступят очередные импульсы   сигнала        времени,     то        значение      переменной clock  обязательно           изменится,  а          строка Б       в          этом   случае           не отразит         корректный            результат.

Для          решения      этой   проблемы   необходимо           объявить      переменную clock  с          ключевым   словом volatile.

volatile   int       clock;

Теперь   значение      переменной           clock  будет опрашиваться        при     каждом     ее        использовании.

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

const       volatile          unsigned       char    *port =          (const            volatile          char    *) 0x2112;

В   этом   примере      для     преобразования   целочисленного   литерала      0x2112 в          const-указатель на          volatile-символ      необходимо           применить операцию    приведения            типов.

Спецификаторы             классов     памяти C++          поддерживает       пять     спецификаторов   классов        памяти:

auto extern register static

mutable

Спецификаторы      классов        памяти       определяют,        как     должна     храниться  переменная.

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

Спецификатор            mutable        применяется          только          к          объектам     классов,       о          которых       речь   впереди.

Остальные  спецификаторы    мы      рассмотрим            в          этом   разделе.

Спецификатор       класса          памяти       auto

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

Спецификатор            auto   объявляет   локальную  переменную.         Но       он используется         довольно редко   (возможно, вам    никогда        и          не доведется   применить  его),   поскольку   локальные переменные           являются "автоматическими"         по       умолчанию.           Вряд  ли       вам    попадется   это ключевое     слово и          в          чужих            программах.

Спецификатор       класса          памяти       extern

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

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

Спецификатор   extern            объявляет переменную,         но       не       выделяет   для          нее     области      памяти.

В   файле            F1       объявляются          и          определяются       переменные           х, у          и          ch.      В          файле            F2       используется скопированный            из файла            F1       список          глобальных            переменных,          к          объявлению которых добавлено        ключевое    слово extern.           Спецификатор       extern делает          переменную          известной    для модуля,           но       в действительности            не       создает         ее.      Другими      словами,      ключевое слово extern предоставляет     компилятору         информацию         о          типе   и имени           глобальных            переменных,          повторно не           выделяя       для     них памяти.        Во       время            компоновки           этих    двух   модулей      все      ссылки на       эти внешние           переменные           будут определены.

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

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

Рассмотрим     следующий            пример.       Обратите     внимание    на       то,       что глобальные            переменные first и          last     объявляются          не       перед,           а после функции       main().

#include <iostream> using     namespace   std;

int    main() {

 extern            int       first,   last;    //         Использование глобальных   переменных.

 cout    <<       first     <<       "          "          <<       last      <<       "\n";

 return            0;

}

//  Глобальное            определение         переменных           first     и          last.

int            first     =          10,      last      =          20;

При         выполнении           этой   программы на       экран будут выведены   числа 10 и          20,      поскольку глобальные  переменные           first     и          last,    используемые в          инструкции cout,  инициализируются этими       значениями.           Поскольку extern-объявление          в          функции       main()           сообщает     компилятору         о том,    что      переменные           first     и          last     объявляются          где-то            в другом         месте (в        данном        случае           ниже, но      в          том     же       файле), программу можно          скомпилировать   без     ошибок,       несмотря     на       то,       что переменные           first     и          last     используются        до       их        определения.

Важно    понимать,   что      extern-объявления          переменных,          показанные в          предыдущей программе,        необходимы          здесь только          по       той причине,      что      переменные           first     и          last     не       были определены           до их        использования      в          функции       main().          Если   бы      их        определения компилятор обнаружил           раньше         определения         функции       main(), необходимости     в          extern-инструкции           не было       бы.     Помните,     если компилятор            обнаруживает       переменную,         которая        не       была объявлена в           текущем       блоке,           он       проверяет,  не       совпадает    ли       она с          какой-нибудь        из        переменных, объявленных     внутри          других включающих          блоков.        Если   нет,    компилятор            просматривает ранее объявленные         глобальные            переменные.         Если   обнаруживается совпадение            их        имен, компилятор          предполагает,       что      ссылка была  именно         на       эту      глобальную            переменную. Спецификатор  extern необходим только          в          том     случае,         если   вы       хотите           использовать переменную,         которая        объявляется           либо  ниже  в          том     же       файле, либо  в          другом.

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

Статические           переменные

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

Локальные    static-переменные

Локальная static-переменная            поддерживает    свое   значение     между          вызовами    функции.

Если        к          локальной  переменной           применен    модификатор        static, то для     нее     выделяется постоянная            область        памяти          практически           так же,     как     и          для     глобальной            переменной.          Это позволяет статической            переменной           поддерживать       ее        значение      между вызовами    функций. (Другими         словами,      в          отличие        от        обычной локальной  переменной,          значение      staticпеременной             не       теряется       при выходе         из        функции.)    Ключевое    различие     между          статической локальной   и          глобальной            переменными       состоит         в          том,   что статическая             локальная переменная известна       только          блоку,           в котором       она     объявлена. Таким            образом,     статическую локальную переменную          в          некоторой   степени        можно          назвать         глобальной переменной, которая    имеет            ограниченную       область        видимости.

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

static       int       count;

Статической     переменной           можно          присвоить   некоторое   начальное значение.    Например,  в этой            инструкции переменной           count присваивается начальное   значение      200:

static       int       count =          200;

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

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

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

/* Вычисляем текущее        среднее        значение      от        чисел,           вводимых пользователем.

*/ #include     <iostream> using     namespace   std;

int            r_avg(int       i);

int    main() {

 int       num;

 do       {

  cout           <<       "Введите      числа (-1       означает      выход):         ";

   cin         >>       num;

  if(num       !=        -1)

   cout      <<       "Текущее     среднее        равно:           "          <<       r_avg(num);

  cout           <<       '\n';

 }while(num  >          -1);

 return            0;

}

//  Вычисляем текущее     среднее. int            r_avg(int       i) {

 static  int       sum=0,          count=0;

 sum    =          sum    +          i;

 count++;

 return            sum    /          count;

}

Здесь      обе     локальные  переменные           sum    и          count объявлены статическими         и инициализированы     значением  0.        Помните,     что      для статических             переменных           инициализация выполняется             только один  раз     (при   первом         выполнении           функции),    а          не       при     каждом входе            в функцию. В          этой   программе функция       r_avg()          используется для     вычисления            текущего среднего          значения      от        чисел, вводимых   пользователем.    Поскольку   обе     переменные           sum    и count являются     статическими,        они     поддерживают     свои   значения      между вызовами    функции r_avg(), что      позволяет    нам    получить      правильный результат     вычислений.          Чтобы           убедиться    в необходимости модификатора static, попробуйте            удалить        его      из        программы.           После            этого программа  не       будет работать      корректно,  поскольку   промежуточная    сумма будет теряться при          каждом        выходе         из        функции       r_avg().

Глобальные  static-переменные

Глобальная     static-переменная            известна    только        для     файла,          в     котором     она     объявлена.

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

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

//---------------------Первый  файл--------------------#include    <iostream> using       namespace   std;

int            r_avg(int       i); void         reset();

int    main() {

 int       num;

 do       {

  cout           <<"Введите числа (-1       для     выхода,        -2        для     сброса):        ";

   cin         >>       num;

  if(num==-2)        {

   reset();

   continue;

  }

  if(num       !=        -1)

   cout      <<       "Среднее     значение      равно:           "          <<       r_avg(num);

  cout           <<       '\n';

 }while(num  !=        -1);

 return            0;

}

//---------------------Второй    файл--------------------#include    <iostream> static       int       sum=0,          count=0;

int     r_avg(int       i) {

 sum    =          sum    +          i;

 count++;

 return            sum    /          count;

}

void     reset() {

 sum    =          0;

 count =          0;

}

В   этой   версии          программы переменные           sum    и          count являются глобально   статическими, т.е.           их        глобальность         ограничена вторым файлом.       Итак,  они     используются        функциями r_avg()         и          reset(), причем         обе     они     расположены        во       втором         файле.          Этот    вариант программы позволяет   сбрасывать накопленную         сумму           (путем           установки в          исходное     положение переменных          sum    и          count),          чтобы можно          было усреднить   другой          набор            чисел.           Но       ни       одна  из функций,      расположенных    вне     второго        файла,          не       может           получить доступ          к          этим переменным.         Работая        с          данной         программой, можно          обнулить     предыдущие          накопления, введя         число -2.       В этом   случае           будет вызвана       функция       reset().          Проверьте   это.     Кроме того, попытайтесь            получить      из        первого        файла            доступ          к любой           из        переменных           sum    или     count.            (Вы получите сообщение  об       ошибке.)

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

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

Регистровые           переменные

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

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

Спецификатор          register         в          объявлении            переменной           означает требование           оптимизировать код  для     получения   максимально возможной скорости    доступа      к          ней.

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

Поскольку        в          действительности            только          для     ограниченного количества  переменных           можно обеспечить          быстрый      доступ,         важно тщательно   выбрать,      к          каким            из        них     применить модификатор register.        (Только        правильный           выбор           может           повысить быстродействие программы.)           Как     правило,      чем    чаще  к          переменной требуется     доступ,         тем     большая      выгода будет         получена     в результате  оптимизации         кода  с          помощью    спецификатора     register. Поэтому объявлять         регистровыми       имеет            смысл           управляющие переменные           цикла            или     переменные,         к которым   выполняется доступ          в          теле   цикла.           На       примере      следующей функции       показано, как register-переменная           типа   int       используется         для     управления циклом.       Эта      функция       вычисляет результат      выражения mе       для целочисленных     значений     с          сохранением          знака исходного   числа (т.е.   при m        =          -2        и          е          =          2          результат     будет равен            -4).

int            signed_pwr(register          int       m,       register          int     e) {

 register          int       temp;

 int       sign;

 if(m     <          0)        sign     =          -1;

 else     sign     =          1;

 temp  =          1;

 for(     ;           e;         e--)     temp  =          temp  *          m;

 return            temp  *          sign;

}

В   этом   примере      переменные           m,       е          и          temp  объявлены  как регистровые,         поскольку   все      они используются           в          теле   цикла,           и потому         к          ним    часто выполняется          доступ.         Однако         переменная sign     объявлена   без     спецификатора     register,        поскольку   она     не       является частью          цикла            и используется      реже.

Происхождение      модификатора   register

Модификатор register         был    впервые       определен  в          языке            С. Первоначально     он       применялся только        к          переменным          типа   int       и char    или     к          указателям и          заставлял    хранить        переменные этого типа   в          регистре       ЦП,     а          не       в          ОЗУ,   где     хранятся       обычные переменные.         Это     означало,    что операции         с          регистровыми переменными       могли            выполняться          намного       быстрее,      чем    операции с          остальными           (хранимыми           в          памяти),       поскольку   для     опроса или     модификации        их        значений не           требовался доступ          к          памяти.

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

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

Чтобы     показать      влияние,      оказываемое         register-переменными   на быстродействие программы, в          следующем            примере      измеряется время выполнения           двух   циклов          for,     которые отличаются      друг   от        друга только          типом            управляющих        переменных.          В          программе используется          стандартная           библиотечная        С++-функция          clock(), которая        возвращает количество            импульсов  сигнала        времени системных   часов,            подсчитанных       с          начала выполнения        этой программы.           Программа должна        включать     заголовок    <ctime>.

/* Эта      программа демонстрирует     влияние,      которое        может           оказать использование      register-переменной       на       скорость      выполнения программы.

*/

#include <iostream>

#include <ctime> using     namespace   std;

unsigned            int       i;          //не    register-переменная unsigned            int       delay;

int    main() {

 register          unsigned       int       j;

 long    start,  end;

 start    =          clock();

 for(delay=0; delay<50;     delay++)

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

 end     =          clock();

 cout    <<       "Количество           тиков для     не       register-цикла:       ";

 cout    <<       end-start       <<       '           \n';

 start    =          clock();

 for(delay=0; delay<50;     delay++)

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

 end     =          clock();

 cout    <<       "Количество           тиков для     register-цикла:       ";

 cout    <<       end-start       <<       '\n';

 return            0;

}

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

На            заметку.      При    написании  этой книги была использована        среда Visual C++,   которая игнорирует   ключевое    слово register.        Visual C++ применяет            оптимизацию      "как   считает нужным".       Поэтому     вы можете      не       заметить  влияния       спецификатора   register         на выполнение предыдущей          программы.           Однако         ключевое    слово register         все      еще принимается компилятором          без     сообщения об       ошибке.       Оно    просто не       оказывает никакого     воздействия.

Перечисления         

В   C++     можно          определить            список          именованных        целочисленных констант.     Такой            список называется          перечислением     (enumeration).       Эти константы   можно          затем использовать         везде, где   допустимы целочисленные значения      (например, в          целочисленных     выражениях). Перечисления определяются       с          помощью    ключевого  слова enum,            а          формат         их определения имеет        такой вид:

enum      type_name  {           список_перечисления    }           список_переменных;

Под         элементом  список_перечисления    понимается            список разделенных         запятыми    имен, которые      представляют        значения перечисления.       Элемент       список_переменных       необязателен, поскольку переменные           можно          объявить      позже,          используя   имя    типа перечисления.       В следующем        примере      определяется        перечисление apple и          две     переменные           типа   apple с именами  red      и          yellow.

enum      apple  {Jonathan,    Golden_Del,            Red_Del,       Winesap,      Cortland, McIntosh}     red,    yellow;

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

apple       fruit;

Эту          инструкцию            можно          записать       и          так.

enum      apple  fruit;

Ключевое         слово enum объявляет перечисление.

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

С  учетом          предыдущих          объявлений            следующие типы  инструкций     совершенно           допустимы.

fruit         =          Winesap; if(fruit==Red_Del)      cout    <<     "Red   Delicious\n"; Важно         понимать,   что     каждый        символ         списка           перечисления     означает      целое            число, причем       каждое     следующее число (представленное     идентификатором)          на       единицу       больше предыдущего.           По       умолчанию значение     первого        символа       перечисления        равно     нулю, следовательно,    значение      второго        —     единице       и          т.д.     Поэтому       при     выполнении           этой инструкции

cout         <<       Jonathan       <<       '           '     <<       Cortland; на            экран будут     выведены   числа 0          4.

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

fruit         =1;      //         ошибка

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

              fruit        =         (apple)          1;        //        Теперь        все       в        порядке,        но     стиль            не

совершенен.

Теперь   переменная            fruit    будет содержать   значение      Golden_Del, поскольку   эта      appleконстанта      связывается            со        значением  1.        Как отмечено     в          комментарии,       несмотря     на       то,       что эта          инструкция стала корректной,           ее        стиль оставляет     желать          лучшего,      что простительно лишь        в          особых         обстоятельствах.

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

enum      apple  {Jonathan,    Golden_Del,            Red_Del,       Winesap=10,           Cortland, McIntosh};

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

//  Слово            "McIntosh"  на       экран таким            образом       не       попадет.

fruit         =          McIntosh;

cout         <<       fruit;

He            забывайте,  что      символ         McIntosh      —        это      просто          имя    для некоторого целочисленного значения,      а          не       строка.          Следовательно,    при выполнении           предыдущего        кода  на       экране отобразится        числовое значение      константы   McIntosh,     а          не       строка           "McIntosh". Конечно, можно          создать         код     ввода            и          вывода         символов    перечисления в          виде  строк,            но       он       выходит несколько         громоздким.          Вот, например,   как     можно          отобразить  на       экране          названия      сортов яблок, связанных   с          переменной           fruit.

switch(fruit)      {

 case    Jonathan:     cout    <<       "Jonathan";

  break;

 case    Golden_Del:            cout    <<       "Golden        Delicious";

  break;

 case    Red_Del:       cout    <<       "Red   Delicious";

  break;

 case    Winesap:      cout    <<       "Winesap";

  break;

 case    Cortland:      cout    <<       "Cortland";

  break;

 case    McIntosh:     cout    <<       "McIntosh";

  break;

}

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

#include <iostream> using     namespace   std;

enum      apple  {Jonathan,    Golden_Del,            Red_Del,       Winesap,      Cortland, McIntosh};

//  Массив         строк,            связанных   с          перечислением     apple.

char         name[][20]  =          {  "Jonathan",

 "Golden         Delicious",

 "Red   Delicious",

 "Winesap",

 "Cortland",

 "McIntosh",

};

int    main() {

 apple  fruit;

 fruit    =          Jonathan;

 cout    <<       name[fruit]  <<       '\n';

 fruit    =          Winesap;

 cout    <<       name[fruit]  <<       '\n';

 fruit    =          McIntosh;

 cout    <<       name[fruit]  <<       '\n';

 return            0;

}

Результаты      выполнения           этой   программы таковы.

Jonathan

Winesap

McIntosh

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

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

Ключевое       слово            typedef

Ключевое         слово typedef         позволяет  создать       новое            имя    для     существующего  типа данных.

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

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

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

Например,       с          помощью    следующей инструкции можно          создать     новое            имя    для     типа   float,

typedef  float   balance;

Эта           инструкция является       предписанием      компилятору         распознавать идентификатор balance            как     еще    одно  имя    для     типа   float.  После этой   инструкции можно          создавать    floatпеременные  с          использованием имени           balance.

balance  over_due;

Здесь      объявлена   переменная            с          плавающей             точкой          over_due типа   balance,        который представляет  собой            стандартный          тип     float,  но имеющий    другое          название.

Еще       об       операторах         

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

Поразрядные           операторы           

Поразрядные операторы            обрабатывают  отдельные биты.

Поскольку        C++     нацелен       на       то,       чтобы            позволить   полный доступ          к          аппаратным           средствам компьютера,           важно,          чтобы он       имел  возможность         непосредственно             воздействовать     на отдельные биты  в          рамках          байта или     машинного слова.           Именно        поэтому       C++ и          содержит поразрядные            операторы. Поразрядные        операторы предназначены    для     тестирования, установки          или     сдвига           реальных битов в          байтах           или     словах,         которые       соответствуют символьным   или целочисленным   С++-типам.  Поразрядные        операторы  не       используются        для операндов  типа   boolfloatdouble,          long    double,          void    или     других          еще более            сложных      типов данных.       Поразрядные        операторы  (они перечислены         в          табл.  9.1)    очень            часто используются для           решения широкого    круга задач программирования        системного уровня,         например,   при опросе          информации          о          состоянии    устройства  или     ее        формировании. Теперь          рассмотрим каждый      оператор     этой   группы         в          отдельности.

Поразрядные           операторы            И,        ИЛИ,  исключающее       ИЛИ   и   НЕ

Поразрядные  операторы  И,        ИЛИисключающее       ИЛИ   и          НЕ (обозначаемые     символами &, |,   ^          и          ~          соответственно)    выполняют те же       операции,   что      и          их        логические эквиваленты          (т.е. они действуют   согласно      той     же       таблице        истинности).           Различие     состоит лишь в          том,   что поразрядные операции     работают     на       побитовой   основе. В          следующей таблице        показан результат           выполнения           каждой поразрядной         операции     для     всех    возможных сочетаний операндов    (нулей и          единиц).

Как          видно            из        таблицы,      результат     применения           оператора   XOR (исключающее      ИЛИ)  будет равен            значению    ИСТИНА        (1)       только          в том     случае,         если   истинен        (равен           значению    1)        лишь один  из операндов; в          противном  случае           результат     принимает  значение      ЛОЖЬ (0).

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

 1101  0011

1010  1010

 1000  0010

Следующая      программа считывает    символы      с          клавиатуры            и преобразует           любой           строчный символ            в          его      прописной эквивалент  путем            установки    шестого        бита   равным        значению    0. Набор символов    ASCII  определен  так,     что      строчные     буквы            имеют           почти такой же       код,   что      и прописные,         за        исключением        того,  что      код первых         отличается  от        кода  вторых          ровно            на 32[только          для латинского  алфавита].   Следовательно,    как     показано     в          этой   программе, чтобы из      строчной      буквы            сделать        прописную,            достаточно обнулить ее        шестой          бит.

//  Получение  прописных  букв. #include <iostream> using     namespace   std;

int    main() {

 char    ch;

 do       {

  cin  >>       ch;

  //    Эта      инструкция обнуляет      6-й      бит.

  ch   =          ch        &         223;   //         В          переменной           ch        теперь     прописная   буква.

  cout           <<       ch;

 }while(ch!     =          'Q');

 return            0;

}

Значение           223,   используемое       в          инструкции поразрядного        И, является       десятичным представлением            двоичного   числа 1101  1111. Следовательно,    эта      операция     И         оставляет     все биты      в          переменной ch        нетронутыми,        за        исключением        шестого        (он      сбрасывается         в нуль).

Оператор          И         также            полезно       использовать,        если   нужно определить,           установлен ли интересующий            вас      бит     (т.е.    равен            ли он       значению    1)        или     нет.    Например,  при     выполнении следующей инструкции вы       узнаете,        установлен ли       4-й      бит     в          переменной status,

if(status  &         8)        cout    <<       "Бит   4          установлен";

Чтобы     понять,         почему         для     тестирования         четвертого  бита используется         число 8, вспомните,         что      в          двоичной    системе счисления   число 8          представляется     как     0000  1000, т.е.      в числе         8 установлен только          четвертый   разряд.         Поэтому       условное     выражение инструкции if даст            значение      ИСТИНА        только          в          том     случае, если   четвертый   бит     переменной           status также установлен            (равен           1). Интересное             использование      этого  метода         показано     на       примере функции       disp_binary().         Она    отображает             в          двоичном    формате конфигурацию      битов своего аргумента.           Мы     будем           использовать функцию      disp_binary()          ниже  в          этой   главе для исследования возможностей       других          поразрядных         операций. //          Отображение конфигурации       битов в          байте.

void         disp_binary(unsigned     u) {

 register          int       t;

 for(t=128;    t>0;    t=t/2)

  if(u &         t)         cout    <<       "1";

  else            cout    <<       "0        ";

 cout    <<       "\n";

}

Функция            disp_binary(),         используя   поразрядный         оператор     И, последовательно             тестирует каждый           бит     младшего   байта переменной u,        чтобы            определить,           установлен он       или     сброшен. Если      он установлен,            отображается        цифра           1,        в          противном  случае           — цифра           0.        Интереса      ради попробуйте расширить  эту      функцию      так, чтобы            она     отображала            все      биты  переменной           u,        а          не только ее        младший     байт.

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

 1101  0011

|   1010  1010

 1111  1011

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

//  Получение  строчных     букв. #include     <iostream> using     namespace   std;

int    main() {

 char    ch;

 do       {

  cin  >>       ch;

  /*   Эта      инструкция делает          букву строчной,    устанавливая         ее        6-й бит.*/

  ch   =          ch        |          32;

  cout           <<       ch;

 }while(ch       !=        'q');

 return            0;

}

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

 0111  1111

1011  1001

 1100  0110

Унарный           оператор     НЕ       (или   оператор     дополнения           до       1) инвертирует           состояние    всех битов   своего           операнда.   Например,  если целочисленное     значение      (хранимое   в          переменной А),    представляет собой            двоичный    код     1001  0110, то        в          результате  операции     получим двоичный         код     0110  1001.

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

#include <iostream> using     namespace   std;

void         disp_binary(unsigned     u); int main() {

 unsigned       u;

 cout    <<       "Введите      число между          0          и          255:   ";

  cin  >>       u;

 cout    <<       "Исходное   число в          двоичном    коде: ";

 disp_binary(u);

 cout    <<       "Его    дополнение           до       единицы:     ";

 disp_binary(~u);

 return            0;

}

//  Отображение         битов,           составляющих     байт. void    disp_binary(unsigned       u) {

 register          int       t;

 for(t=128;    t>0;    t=t/2)

  if(u &         t)         cout    <<       "1";

  else            cout    <<       "0";

 cout    <<       "\n";

}

Вот          как     выглядят     результаты выполнения           этой   программы.

Введите число между          0          и          255:   99

Исходное          число в          двоичном    коде: 01100011

Его           дополнение           до       единицы:     10011100

И  еще.   Не       путайте         логические и          поразрядные         операторы. Они выполняют различные действия.     Операторы &,        |          и          ~          применяются непосредственно             к          каждому      биту   значения      в отдельности. Эквивалентные     логические операторы  обрабатывают       в          качестве операндов значения      ИСТИНА/ЛОЖЬ     (не      нуль/нуль). Поэтому       поразрядные операторы  нельзя использовать      вместо          их        логических  эквивалентов         в условных     выражениях.          Например,  если значение       х          равно            7,        то выражение х          &&      8          имеет            значение      ИСТИНА,      в          то        время как выражение     х          &         8          дает   значение      ЛОЖЬ.

Узелок   на       память.        Оператор   отношения            или     логический оператор   всегда           генерирует результат,         который     имеет          значение ИСТИНА        или     ЛОЖЬ,           в          то      время            как     аналогичный поразрядный оператор   генерирует           значение,    получаемое           согласно      таблице истинности конкретной       операции.

Операторы  сдвига

Операторы      сдвига,         ">>"   и          "<<"   сдвигают     все      биты  в     значении     вправо          или     влево. Общий        формат         использования     оператора   сдвига           вправо          выглядит     так.

значение           >>       число_битов

А  оператор     сдвига           влево            используется         так.

значение           <<       число_битов

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

Здесь      элемент        число_битов        указывает,  на       сколько        позиций должно        быть  сдвинуто значение.        При    каждом        сдвиге           влево            все биты, составляющее       значение,    сдвигаются влево            на одну        позицию,     а в          младший     разряд          записывается         нуль. При    каждом        сдвиге вправо          все      биты сдвигаются, соответственно,    вправо.         Если   сдвигу вправо          подвергается         значение      без     знака,            в старший    разряд записывается         нуль. Если   же       сдвигу           вправо          подвергается         значение со        знаком, значение            знакового    разряда        сохраняется.           Как     вы помните,      отрицательные     целые           числа представляются   установкой старшего разряда        числа равным        единице.      Таким            образом,     если сдвигаемое значение      отрицательно,       при     каждом        сдвиге           вправо          в          старший разряд записывается     единица,      а          если   положительно       —        нуль. Не забывайте,  сдвиг,            выполняемый операторами   сдвига,         не       является циклическим,        т.е.      при     сдвиге           как     вправо,         так      и          влево крайние биты  теряются,     и          содержимое          потерянного           бита   узнать невозможно.

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

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

Следующая      программа наглядно     иллюстрирует        результат     использования операторов сдвига.

//  Демонстрация       выполнения           поразрядного     сдвига. #include    <iostream> using   namespace   std;

void         disp_binary(unsigned       u);

int    main() {

 int       i=1,     t;

 for(t=0;         t<8;    t++)    {

  disp_binary(i);

  i      =          i           <<       1;

 }

 cout    <<       "\n";

 for(t=0;         t<8;    t++)    {

  i      =          i           >>       1;

  disp_binary(i);

 }

 return            0;

}

//  Отображение         битов,           составляющих       байт.

void         disp_binary(unsigned     u) {

 register          int       t;

 for(t=128;    t>0;    t=t/2)

  if(u &         t)         cout    <<       "1";

  else            cout    <<       "0        ";

 cout    <<       "\n";

}

Результаты      выполнения           этой   программы таковы.

0   0          0          0          0          0          0          1

0   0          0          0          0          0          1          0

0   0          0          0          0          1          0          0

0   0          0          0          1          0          0          0

0   0          0          1          0          0          0          0

0   0          1          0          0          0          0          0

0       1         0          0          0          0          0          0

1       0         0          0          0          0          0          0

1   0          0          0          0          0          0          0

0   1          0          0          0          0          0          0

0   0          1          0          0          0          0          0

0   0          0          1          0          0          0          0

0   0          0          0          1          0          0          0

0   0          0          0          0          1          0          0

0   0          0          0          0          0          1          0

0   0          0          0          0          0          0          1

Оператор  "знак вопроса"

Одним   из        самых           замечательных     операторов             C++     является оператор     "?".     Оператор     "?" можно   использовать         в          качестве       замены if-else-инструкций,           употребляемых    в          следующем общем        формате. if(условие)

 переменная            =     выражение 1; else

 переменная            =          выражение 2;

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

Оператор          "?"      называется тернарным,            поскольку   он       работает      с тремя            операндами.          Вот     его общий   формат         записи:

Выражение1? Выражение2          :           Выражение3;

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

Значение           ?-выражения         определяется        следующим            образом. Вычисляется           Выражение1. Если          оно     оказывается           истинным, вычисляется           Выражение2,        и          результат     его      вычисления становится значением  всего ?-выражения.        Если   результат     вычисления            элемента Выражение1          оказывается           ложным,      значением  всего ?-выражения становится  результат вычисления   элемента     Выражение3.        Рассмотрим следующий            пример.

while(something)        {  х     =          count >          0     ?          0          :           1;

 //         ...

}

Здесь      переменной           x          будет присваиваться       значение      0          до       тех пор,   пока   значение переменной   count не       станет            меньше        или     равно нулю. Аналогичный         код     (но      с использованием           if-else-инструкции) выглядел     бы      так.

while(something)        {

 if(count         >0)      x          =          0;

 else     x          =          1;

 //         ...

}

А   вот      еще    один  пример         практического       применения           оператора   ?. Следующая            программа делит            два     числа,           но       не       допускает деления       на       нуль.

/* Эта      программа использует  оператор     ?          для     предотвращения деления            на       нуль.

*/ #include     <iostream> using     namespace   std;

int            div_zero();

int    main() {

 int       i,          j,          result;

 cout    <<       "Введите      делимое      и          делитель:    ";

  cin  >>       i           >>       j;

 //         Эта      инструкция не       допустит      возникновения     ошибки        деления            на нуль.

 result  =          j           ?          i/j        :           div_zero();

 cout    <<       "Результат: "          <<       result;

 return            0;

}

int     div_zero() {

 cout    <<       "Нельзя        делить          на       нуль. \n";

 return            0;

}

Здесь,     если   значение      переменной           j           не       равно            нулю, выполняется          деление       значения переменной   i           на       значение переменной           j,          а          результат     присваивается       переменной           result. В противном          случае           вызывается             обработчик             ошибки        деления на       нуль   div_zeго(),   и          переменной result          присваивается       нулевое значение.

Составные    операторы            присваивания

В   C++     предусмотрены    специальные         составные   операторы  присваивания, в          которых объединено     присваивание        с          еще    одной           операцией. Начнем         с          примера      и          рассмотрим следующую         инструкцию.

х   =          x          +          10;

Используя        составной    оператор     присваивания,       ее        можно     переписать в          таком            виде.

х   +=       10;

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

Составные        версии          операторов             присваивания        существуют             для всех    бинарных    операторов (т.е.   для     всех    операторов,           которые       работают с          двумя           операндами).        Таким            образом,     при таком   общем формате       бинарных    операторов             присваивания

переменная    =          переменная            ор     выражение; общая         форма           записи     их        составных    версий          выглядит     так:

переменная     ор       =          выражение;

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

х   =          х          -     100; аналогична     такой:

x   -=        100;

Обе         эти      инструкции присваивают          переменной           х          ее        прежнее значение,    уменьшенное        на 100.

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

"ты"   .

Оператор      "запятая"

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

var           =          (count=19,   incr=10,        count+1);

переменной    count сначала        присваивается       число 19,      переменной           incr —        число 10,      а          затем к         значению    переменной           count прибавляется единица,      после чего   переменной           var присваивается           значение      крайнего справа          выражения,            т.е.      count+1,       которое        равно            20. Круглые скобки          здесь обязательны,         поскольку   оператор     "запятая"  имеет более            низкий приоритет,          чем    оператор     присваивания.

Чтобы     понять          назначение             оператора   "запятая", попробуем выполнить  следующую программу.

#include <iostream> using     namespace   std;

int    main() {

 int       i,          j;

 j           =          10;

 i           =          (j++,   j+100,            999+j);

 cout    <<       i;

 return            0;

}

Эта           программа выводит       на       экран число 1010. И         вот      почему: сначала        переменной           j присваивается     число 10,      затем переменная            j инкрементируется           до       11.      После            этого вычисляется           выражение j+100,            которое        нигде не       применяется.         Наконец,      выполняется сложение     значения      переменной           j           (оно   по-прежнему         равно            11) с          числом         999,   что      в          результате дает    число 1010.

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

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

Несколько      присваиваний       "в        одном"

Язык       C++     позволяет    применить  очень            удобный      метод одновременного  присваивания многим  переменным          одного          и          того    же значения.    Речь   идет   об       объединении         сразу нескольких присваиваний        в одной           инструкции.            Например,  при     выполнении           этой инструкции переменным          count,            incr     и          index  будет присвоено   число 10.

count      =          incr     =          index  =          10;

Этот         формат         присвоения            нескольким            переменным          общего значения      можно          часто встретить     в          профессионально            написанных программах.

Использование        ключевого  слова            sizeof

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

sizeof      (type)

sizeof      value

Оператор       sizeof во       время            компиляции           программы            получает     размер         типа или     значения.

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

Чтобы     понять,         как     работает      оператор     sizeof,            испытайте следующую            короткую     программу. Для   многих          32-разрядных        сред   она должна        отобразить  значения      1,        4,        4          и          8.

//  Демонстрация       использования      оператора   sizeof. #include <iostream> using   namespace   std;

int    main() {

 char    ch;

 int       i;

 cout    <<       sizeof ch        <<       '           ';          //         размер         типа   char

 cout    <<       sizeof i           <<       '           ';          //         размер         типа   int

 cout    <<       sizeof (float)            <<       '           ';          //         размер         типа   float

 cout    <<       sizeof (double)        <<       '           ';          //         размер         типа   double

 return            0;

}

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

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

int            nums[4];

cout         <<       sizeof nums;            //         Будет выведено    число 16.

Для          4-байтных   значений     типа   int       при     выполнении           этого фрагмента   кода  на       экране отобразится        число 16       (которое      получается  в результате  умножения 4          байт   на       4          элемента массива).

Оператор          sizeof главным      образом       используется         при     написании кода, который       зависит         от размера  С++-типов    данных.        Помните: поскольку   размеры      типов данных         в          C++     определяются конкретной реализацией,         не       стоит  полагаться  на       размеры      типов,           определенные в реализации,        в          которой        вы       работаете    в          данный        момент.

Динамическое         распределение     памяти       с          использованием   операторов          new    и          delete

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

9.2.)

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

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

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

Оператор       new    позволяет  динамически         выделить   область      памяти.

Язык       C++     содержит     два     оператора,  new    и          delete,           которые выполняют функции       по выделению      и          освобождению     памяти. Приводим   их        общий           формат. переменная-указатель         =          new тип_переменной;

delete     переменная-указатель;

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

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

Поскольку        объем           "кучи"           конечен,      она     может           когда-нибудь исчерпаться.          Если   для удовлетворения       очередного            запроса        на выделение  памяти          не       существует  достаточно свободной  памяти,        оператор new    потерпит      фиаско,        и          будет сгенерировано      исключение. Исключение— это      ошибка         специального        типа,  которая        возникает    во       время выполнения программы          (в        C++     предусмотрена     целая            подсистема, предназначенная             для     обработки   таких ошибок).     (Исключения          описаны в          главе 17.)    В          общем          случае           ваша  программа должна обработать подобное    исключение            и          по       возможности         выполнить  действие, соответствующее конкретной            ситуации.     Если   это      исключение            не будет обработано            вашей           программой,         ее выполнение     будет прекращено.

Такое      поведение  оператора   new    в          случае           невозможности удовлетворить      запрос          на выделение        памяти          определено стандартом             C++.   На       такую            реализацию           настроены   также            все современные         компиляторы,       включая       последние  версии          Visual C++     и C++     Builder.         Однако дело         в          том,   что      некоторые  более            ранние компиляторы        обрабатывают       new-инструкции   подругому. Сразу после изобретения           языка            C++     оператор     new    при     неудачном  выполнении возвращал  нулевой       указатель.   Позже           его      реализация             была  изменена так,     чтобы            в          случае неудачи     генерировалось    исключение,          как было описано       выше.            Поскольку   в          этой   книге мы придерживаемся стандарта    C++,   то        во       всех    представленных   здесь примерах    предполагается именно         генерирование      исключения.          Если   же       вы       используете более            старый          компилятор, обратитесь           к          прилагаемой         к нему  документации       и          уточните,     как     реализован             оператор     new (при необходимости     внесите         в          примеры     соответствующие             изменения).

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

Рассмотрим     пример         программы,           которая        иллюстрирует использование      операторов             new    и delete.

#include <iostream> using     namespace   std; int     main() {

 int       *p;

 p          =          new    int;      //         Выделяем   память          для     int-значения.

 *p       =          20;      //         Помещаем  в          эту      область        памяти          значение     20.

 cout    <<       *р;      //         Убеждаемся           (путем           вывода         на       экран) в            работоспособности         этого  кода.

 delete             р;        //         Освобождаем       память.

 return            0;

}

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

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

Инициализация      динамически         выделенной           памяти

Используя        оператор     new,   динамически         выделяемую          память можно          инициализировать. Для           этого  после имени           типа   задайте начальное   значение,    заключив     его      в          круглые       скобки. Например,          в следующей программе область        памяти,        адресуемая            указателем p, инициализируется           значением  99. #include <iostream> using   namespace   std;

int    main() {

 int       *p;

 p          =          new    int       (99);   //         Инициализируем память          числом         99.

 cout    <<       *p;      //         На       экран выводится   число 99.

 delete             p;

 return            0;

}

Выделение    памяти       для     массивов

С   помощью    оператора   new    можно          выделять     память          и          для массивов.    Вот     как     выглядит общий  формат         операции     выделения  памяти для     одномерного         массива:

переменная-указатель       =          new    тип     [размер];

Здесь     элемент        размер         задает           количество элементов   в          массиве.

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

delete     []         переменная-указатель;

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

Важно!  Более            старые        С++-компиляторы         могут          требовать задания       размера удаляемого     массива,      поскольку   в          ранних          версиях C++     для     освобождения      памяти, занимаемой   удаляемым            массивом, необходимо           было применять такой           формат      оператора delete:

delete     [размер]      переменная-указатель;

Здесь      элемент      размер         задает         количество           элементов в массиве.      Стандарт  C++     больше не  требует     указывать размер         при     его удалении.

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

#include <iostream> using     namespace   std;

int    main() {

 double           *p;

 int       i;

 p          =          new    double           [10];   //         Выделяем   память          для     10-элементного массива.  

 //Заполняем           массив          значениями            от        100     до       109.

 for(i=0;          i<10;  i++)     р[i]      =          100.00          +          i;

 //         Отображаем          содержимое          массива.  for(i=0;   i<10;  i++)     cout    <<       p[i]      <<       "          ";

 delete             []         p;        //         Удаляем      весь   массив.

 return            0;

}

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

Динамическое         распределение     памяти       в          языке            С:   функции      malloc()        и          free()

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

Ядро       С-системы   распределения     памяти          составляют функции       malloc() и          free(). Функция malloc() предназначена     для     выделения  памяти,        а функция       frее() —        для     ее        освобождения. Другими          словами,      каждый раз,    когда с          помощью    функции       malloc()        делается      запрос,         часть свободной  памяти          выделяется             в          соответствии          с          этим запросом.   При    каждом        вызове          функции frее()      соответствующая область памяти          возвращается        системе.       Любая           программа,            которая использует  эти      функции,     должна        включать     заголовок    <cstdlib>. Функция malloc()        имеет            такой прототип,

void         *malloc(size_t         num_bytes);

Здесь      num_bytes   означает      количество байтов          запрашиваемой    памяти. (Тип   size_t представляет         собой            разновидность      целочисленного   типа   без знака.)          Функция       malloc() возвращает        указатель    типа   void,   который играет           роль  обобщенного         указателя.   Чтобы           из этого        обобщенного указателя    получить      указатель    на       нужный        вам    тип,    необходимо использовать         операцию    приведения            типов.           В          результате успешного  вызова          функция       malloc() возвратит           указатель    на       первый байт   области        памяти,        выделенной           из        "кучи".          Если   для удовлетворения   запроса        свободной  памяти          в          системе        недостаточно, функция       malloc() возвращает        нулевой       указатель.

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

void         free(void       *ptr);

Здесь      параметр     ptr      представляет         собой            указатель    на       память, ранее            выделенную          с помощью функции       malloc().       Никогда       не следует        вызывать     функцию      free() с недействительным      аргументом;           это может           привести      к          разрушению          списка           областей      памяти, подлежащих          освобождению.

Использование          функций       malloc()        и          free() иллюстрируется   в     следующей программе.

//  Демонстрация       использования      функций       malloc()         и          free().

#include <iostream> #include <cstdlib> using     namespace   std;

int    main() {

 int       *i;

 double           *j;

i          =         (int      *)        malloc(sizeof(int));

 if(!i)    {

  cout           <<       "Выделить  память          не       удалось.\n";

  return       1;

 }

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

 if(!       j           )           {

  cout           <<       "Выделить  память          не       удалось.\n";

  return       1;

 }

 *i         =          10;

 *j         =          100.123;

 cout    <<       *i         <<       '           '           <<       *j;

 //         Освобождение      памяти.

 free     (i);

 free     (j);

 return            0;

}

Несмотря          на       то        что      функции       malloc()        и          fгее() — полностью  пригодны    для     динамического распределения        памяти,        есть    ряд причин,        по       которым      в          C++     определены           собственные средства динамического     распределения     памяти.        Во-первых,  оператор     new автоматически вычисляет       размер         выделяемой          области        памяти          для заданного   типа,  т.е.      вам    не       нужно использовать       оператор     sizeof,            а значит,          налицо         экономия    в          коде  и          трудовых     затратах программиста.      Но       важнее         то,       что      автоматическое    вычисление            не допускает    выделения неправильного     объема         памяти.        Во-вторых,  С++-оператор      new    автоматически      возвращает корректный          тип     указателя,   что освобождает          программиста       от        необходимости     использовать операцию приведения            типов.           В-третьих,    используя   оператор     new,   можно инициализировать объект,     для     которого      выделяется             память.         Наконец, как     будет показано     ниже  в          этой   книге, программист        может           создать собственные          версии          операторов             new    и          delete.

И  последнее. Из-за возможной несовместимости             не       следует смешивать  функции       malloc() и     free() с          операторами         new    и          delete в          одной           программе.

Сводная          таблица     приоритетов       С++-операторов

В   табл.  9.2      показан        приоритет   выполнения           всех    С++-операторов    (от высшего       до       самого низкого).  Большинство         операторов             ассоциированы слева направо.      Но       унарные      операторы, операторы         присваивания       и оператор     "?"      ассоциированы     справа          налево.         Обратите     внимание на то,       что      эта      таблица        включает     несколько   операторов,           которые       мы пока   не       использовали        в наших        примерах,   поскольку   они     относятся     к объектно-ориентированному программированию     (и        описаны       ниже).

Глава     10:  Структуры   и      объединения

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

В   C++     структуры    и          объединения         имеют           как     объектно-ориентированные,        так      и          не объектно-ориентированные     атрибуты.    В этой   главе рассматриваются только          последние. Об объектно-ориентированных и          их        свойствах     речь   пойдет          в          следующей главе после введения понятия        о          классах         и          объектах.

Структуры

Структура         —        это    группа          связанных   переменных.

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

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

Член       структуры   —        это    переменная,          которая      является     частью     структуры.

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

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

struct      inv_type       {

 char    item[40];      //         наименование       товара

 double           cost;   //         стоимость

 double           retail; //         розничная   цена

 int       on_hand;      //         имеющееся            в          наличии       количество

 int       lead_time;    //         число дней  до       пополнения            запасов

};

Имя        структуры   —        это    ее        спецификатор     типа.

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

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

inv_type inv_var;

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

При         объявлении            структурной           переменной           C++     автоматически выделит       объем           памяти, достаточный     для     хранения     всех    членов структуры.  На       рис.    10.1   показано,    как     переменная inv_var       будет размещена в          памяти          компьютера           (в        предположении,  что      double-значение занимает         8          байт,  а          int-значение           —        4).

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

struct      inv_type       {

 char    item[40];      //         наименование       товара

 double           cost;   //         стоимость

 double           retail; //         розничная   цена

 int       on_hand;      //         имеющееся            в          наличии       количество

 int       lead_time;    //         число дней  до       пополнения            запасов

}    inv_varA,      inv_varB,      inv_varC;

Этот         фрагмент     кода  определяет             структурный           тип     inv_type       и объявляет   переменные inv_varAinv_varB       и          inv_varC       этого  типа.  Важно понимать,   что      каждая         структурная переменная          содержит     собственные копии            членов          структуры.  Например,  поле  cost    структуры inv_varA изолировано          от        поля  cost    структуры    inv_varB.      Следовательно, изменения, вносимые   в          одно  поле, никак            не       влияют         на содержимое          другого        поля.

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

struct      {

 char    item[40];      //         наименование       товара

 double           cost;   //         стоимость

 double           retail; //         розничная   цена

 int       on_hand;      //         имеющееся            в          наличии       количество

 int       lead_time;    //         число дней  до       пополнения            запасов

}    temp;

Этот         фрагмент     кода  объявляет   одну  переменную          temp  в соответствии          с          предваряющим    ее определением            структуры.

Ключевое         слово struct означает    начало         объявления            структуры.

Общий   формат         объявления            структуры    выглядит     так.

struct      имя_типа_структуры      {

 тип      имя_элемента1;

 тип     имя_элемента2;  тип     имя_элемента3;

 .

 .

 .

 тип      имя_элементаN;

}    структурные_переменные;

Доступ           к          членам         структуры

К   отдельным членам         структуры    доступ          осуществляется     с          помощью оператора   "точка". Например,       при     выполнении           следующего           кода значение      10.39 будет присвоено   полю cost структурной  переменной           inv_var, которая        была  объявлена   выше.

inv_var.cost      =          10.39;

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

имя_структурной_переменной.имя_члена

Оператор       "точка"       (.)        позволяет  получить    доступ         к          любому     члену любой           структуры.

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

cout         <<       inv_var.cost;

Аналогичным способом    можно          использовать         символьный           массив inv_var.item            в          вызове функции   gets().

gets(inv_var.item);

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

Если        вам    нужно           получить      доступ          к          отдельным элементам массива        inv_var.item, используйте        индексацию.          Например,  с          помощью этого  кода  можно          посимвольно         вывести        на экран       содержимое массива        inv_var.item.

int            t; for(t=0;     inv_var.itern[t];     t++)  cout  <<       inv_var.item[t];

Массивы        структур

Структуры        могут быть  элементами            массивов.    И         в          самом деле, массивы       структур используются  довольно    часто.            Чтобы           объявить массив          структур,      необходимо           сначала определить       структуру,   а затем объявить      массив          элементов   этого  структурного          типа. Например, чтобы            объявить      100-элементный  массив          структур       типа   inv_type (который определен      выше),          достаточно записать       следующее.

inv_type invtry[100];

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

cout         <<       invtry[2].on_hand;

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

Простой         пример        инвентаризации             склада

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

Инвентарная   ведомость   будет храниться    в          структурах   типа   inv_type, организованных   в массиве    invtry.

const       int       SIZE    =          100;

struct      inv_type       {

 char    item[40];      //         наименование       товара

 double           cost;   //         стоимость

 double           retail; //         розничная   цена

 int       on_hand;      //         имеющееся            в          наличии       количество

 int       lead_time;    //         число дней  до       пополнения            запасов

}    invtry[SIZE];

Размер   массива        выбран         произвольно.        При    желании      его      можно легко изменить.    Обратите внимание        на       то,       что      размерность          массива задана          с          использованием  const-переменной.          А поскольку           размер массива        во       всей   программе используется         несколько   раз,    применение const-переменной            для     этой   цели  весьма          оправданно.          Чтобы изменить     размер         массива, достаточно      изменить     значение      константной переменной           SIZE,   а          затем перекомпилировать программу.     Использование const-переменной           для     определения         "магического        числа",         которое часто  употребляется       в          программе,            —        обычная       практика      в профессиональном         С++-коде.

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

■  ввод  информации          о          товарах,     хранимых    на       складе; ■     отображение         инвентарной     ведомости; ■        модификация     заданного   элемента.

Прежде всего напишем     функцию      main(),          которая        должна        иметь     примерно    такой вид.

int    main() {

 char    choice;

 init_list();

 for(;;) {

  choice       =          menu();

  switch(choice)    {

   case      'e':       enter();

    break;

   case      'd':      display();

    break;

   case      'u':      update();

    break;

   case      'q':      return            0;

  }

 }

}

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

//  Инициализация    массива        структур.

void     init_list() {

 int       t;

 //         Имя    нулевой       длины           означает      пустое           имя.

 for(t=0;         t<SIZE;           t++)    *invtry[t].item        =          '\0';

}

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

Функция            menu_select()         отображает             команды     меню и принимает  вариант,       выбранный пользователем.

//  Получение  команды     меню,           выбранной     пользователем. int         menu() {

 char    ch;

 cout    <<       '\n';

 do       {

  cout           <<       "(E)nter\n"; //         Ввести           новый           элемент.

  cout           <<       "(D)isplay\n";          //         Отобразить всю    ведомость.

  cout           <<       "(U)pdate\n";          //         Изменить     элемент.

  cout           <<       "(Q)uit\n\n";           //         Выйти            из        программы.

  cout           <<       "Выберите   команду:     ";

   cin         >>       ch;

 }while(!strchr("eduq",      tolower(ch)));

 return            tolower(ch);

}

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

Например,  чтобы            отобразить  всю    инвентарную         ведомость, нажмите букву "D".

Функция            menu()          вызывает     библиотечную       функцию      C++     strchr(), которая        имеет            такой прототип.

char         *strchr(const           char    *str,   int       ch);

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

Функция            enter()           предваряет вызов            функции       input(),          которая "подсказывает" пользователю        порядок       ввода            данных         и принимает  их.      Рассмотрим            код     обеих            функций.

//  Ввод  элементов   в          инвентарную         ведомость.

void     enter() {

 int       i;

 //         Находим      первую         свободную структуру.

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

  if(   !*invtry[i].item)      break;

 //         Если   массив          полон,          значение      i           будет равно            SIZE.

 if(i==SIZE)     {   cout   <<     "Список        полон.\n";

  return;

 }

 input  (i);

} //           Ввод     информации. void         input(int     i) {

 cout    <<       "Товар:         ";

  cin  >>       invtry[i].item;

 cout    <<       "Стоимость:            ";

  cin  >>       invtry[i].cost;

 cout    <<       "Розничная цена: ";

  cin  >>       invtry[i].retail;

 cout    <<       "В        наличии:      ";

  cin  >>       invtry[i].on_hand;

 cout    <<       "Время         до       пополнения            запасов        (в        днях):            ";

  cin  >>       invtry[i].lead_time;

}

Функция            enter()           сначала        находит        пустую          структуру.   Для этого  проверяется           поле  item каждого         (по      очереди)      элемента     массива invtry,            начиная        с          первого.       Если   поле  item оказывается пустым,        то предполагается,   что      структура,   к          которой        оно     относится,   еще    ничем не занята.          Если   не       отыщется     ни       одной           свободной  структуры    при проверке     всего массива структур,            управляющая        переменная            цикла i           станет            равной          его      размеру.      Это     говорит        о          том, что массив          полон,          и          в          него   уже    нельзя          ничего          добавить. Если   же       в          массиве        найдется свободный     элемент,      будет вызвана функция       input()           для     получения   информации          о          товаре, вводимой пользователем.    Если   вас      интересует,             почему         код     ввода            данных о          новом           товаре           не является             частью          функции       enter(),          то ответ  таков:            функция       input()           используется         также            и функцией update(),      о          которой        речь   впереди.

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

//  Модификация       существующего    элемента.

void     update() {

 int       i;

 char    name[80];

 cout    <<       "Введите      наименование       товара:         ";

  cin  >>       name;

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

  if(!strcmp(name,           invtry[i].item))        break;

 if(i==SIZE)     {

  cout           <<       "Товар          не       найден.\n";

  return;

 }

 cout    <<       "Введите      новую           информацию.\n";

 input(i);

}

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

Нам         осталось      рассмотреть           функцию      display().      Она    выводит       на экран инвентарную ведомость          в          полном        объеме.        Код    функции display()        выглядит     так.

//  Отображение         на       экране          инвентарной          ведомости.

void         display()

{

 int       t;

 for(t=0;         t<SIZE;           t++      )           {

  if(*invtry[t].item)          {

   cout      <<       invtry[t].item           <<       '\n';

   cout      <<       "Стоимость:            $"        <<       invtry[t].cost;

   cout      <<       "\nB   розницу:      $";

   cout      <<       invtry[t].retail         <<       '\n';

   cout      <<       "В        наличии:      "          <<       invtry[t].on_hand;

   cout      <<       "\nДо            пополнения            осталось:     ";

   cout      <<       invtry[t].lead_time            <<       "          дней\n\n";

  }

 }

}

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

/* Простая        программа ведения       инвентарной          ведомости, в          которой используется         массив          структур.

*/

#include <iostream>

#include <cctype>

#include <cstring> #include <cstdlib> using     namespace   std; const     int       SIZE    =          100;

struct      inv_type       {

 char    item[40];      //         наименование       товара

 double           cost;   //         стоимость

 double           retail; //         розничная   цена

 int       on_hand;      //         имеющееся            в          наличии       количество

 int       lead_time;    //         число дней  до       пополнения            запасов

}    invtry[SIZE]; void   enter(),          init_list(),     display(); void         update(),      input(int     i);

int            menu();

int    main() {

 char    choice;

 init_list();

 fоr       (;;)       {

  choice       =          menu();

  switch(choice)    {

   case      'e'        :           enter();

    break;

   case      'd'        :           display();

    break;

   case      'u':      update();

    break;

   case      'q':      return            0;

  }

 }

}

//  Инициализация    массива     структур. void        init_list() {

 int       t;

 //         Имя    нулевой       длины           означает      пустое           имя.

 for(t=0;         t<SIZE;           t++      )           *invtry[t].item        =          '\0';

}

//Получение    команды     меню,           выбранной     пользователем. int         menu() {

 char    ch;

 cout    <<       '\n';

 do       {

  cout           <<       "(E)nter\n"; //         Ввести           новый           элемент.

  cout           <<       "(D)isplay\n";          //         Отобразить всю    ведомость.   cout     <<       "          (U)      pdate\n";      //         Изменить     элемент.

  cout           <<       "          (Q)      uit\n\n";       //         Выйти            из        программы.

  cout           <<       "Выберите   команду:     ";

   cin         >>       ch;

 }while(!strchr("eduq",      tolower(ch)));

 return            tolower(ch);

}

//Ввод    элементов   в          инвентарную         ведомость.

void     enter() {

 int       i;

 //         Находим      первую         свободную структуру.

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

  if(!*invtry[i].item)         break;

 //         Если   массив          полон,          значение      i           будет равно            SIZE.

 if(i==SIZE)     {   cout   <<     "Список        полон.\n";

  return;

 }

 input(i);

}

//  Ввод     информации. void         input(int     i) {

 cout    <<       "Товар:         ";

  cin  >>       invtry[i].item;

 cout    <<       "Стоимость:            ";

  cin  >>       invtry[i].cost;

 cout    <<       "Розничная цена: ";

  cin  >>       invtry[i].retail;

 cout    <<       "В        наличии:      ";

  cin  >>       invtry[i].on_hand;

 cout    <<       "Время         до       пополнения            запасов        (в        днях):            ";

  cin  >>       invtry[i].lead_time;

}

//  Модификация       существующего     элемента. void      update() {

 int       i;

 char    name[80];

 cout    <<       "Введите      наименование       товара:         ";

  cin  >>       name;

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

  if(!strcmp(name,           invtry[i].item))        break;

 if(i==SIZE)     {

  cout           <<       "Товар          не       найден.\n";

  return;

 }

 cout    <<       "Введите      новую           информацию.\n";

 input  (i);

}

//  Отображение         на       экране          инвентарной     ведомости. void   display() {

 int       t;

 for(t=0;         t<SIZE;           t++)    {

  if(*invtry[t].item)          {

   cout      <<       invtry[t].item           <<       '\n';

   cout      <<       "Стоимость:            $"        <<       invtry[t].cost;

   cout      <<       "\nB   розницу:      $";

   cout      <<       invtry[t].retail         <<       '\n';

   cout      <<       "В        наличии:      "          <<       invtry[t].on_hand;

   cout      <<       "\nДо            пополнения            осталось:     ";

   cout      <<       invtry[t].lead_time            <<       "          дней\n\n";

  }

 }

}

Передача        структур   функциям

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

Используя        структуру     в          качестве       параметра, помните,      что      тип аргумента   должен соответствовать           типу   параметра. Например,  в следующей программе сначала        объявляется структура  sample,         а          затем функция       f1()     принимает  параметр     типа   sample.

//  Передача     функции       структуры    в          качестве     аргумента. #include        <iostream> using   namespace   std;

//  Определяем          тип     структуры.

struct      sample           {

 int       a;

 char    ch; }; void     f1(sample     parm);

int            main()

{

 struct sample           arg;     //         Объявляем переменную          arg      типа   sample.

 arg.a   =          1000;

 arg.ch =          'x';

 f1(arg);

 return            0;

}

void         f1(sample     parm) {

 cout    <<       parm.a          <<       "          "          <<       parm.ch        <<       "\n";

}

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

Присваивание          структур

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

//  Демонстрация       присваивания        значений     структур. #include <iostream> using   namespace   std;

struct      stype  {

 int       a,         b;

};

int    main() {

 stype  svar1, svar2;

 svar1.a          =          svar1.b          =          10;

 svar2.a          =          svar2.b          =          20;

 cout    <<       "Структуры до       присваивания.\n";

 cout    <<       "svar1:          "          <<       svar1.a          <<       '           '           <<       svar1.b;

 cout    <<'\n';

 cout    <<       "svar2:          "          <<       svar2.a          <<       '           '           <<       svar2.b;

 cout    <<"\n\n";

 svar2  =          svar1; //         присваивание        структур

 cout    <<       "Структуры после присваивания.\n";

 cout    <<       "svar1:          "          <<       svar1.a          <<       '           '           <<       svar1.b;

 cout    <<       '\n';

 cout    <<       "svar2:          "          <<       svar2.a          <<       '           '           <<       svar2.b;

 return            0;

}

Эта          программа генерирует  следующие результаты.

Структуры        до       присваивания.

svar1:     10       10 svar2:     20       20

Структуры        после присваивания,

svar1:     10       10

svar2:     10       10

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

struct      stype1           {

 int       a,         b;

};

struct      stype2           {

 int       a,         b;

};

stype1    svar1; stype2    svar2;

svar2       =          svar1; //         Ошибка        из-за  несоответствия      типов.

Несмотря          на       то        что      структуры    stype1           и          stype2 физически   одинаковы,            с          точки зрения компилятора      они     являются отдельными           типами.

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

Использование        указателей           на       структуры           и   оператора             "стрелка"

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

Указатель         на       структуру     объявляется           так      же,     как     указатель    на любую          другую         переменную, т.е. с          помощью    символа       "*", поставленного      перед            именем        структурной           переменной. Например, используя   определенную      выше структуру     inv_type,      можно          записать следующую инструкцию,         которая        объявляет   переменную          inv_pointer указателем на       данные         типа inv_type:

inv_type *inv_pointer;

Чтобы     найти             адрес            структурной           переменной,          необходимо перед            ее        именем        разместить оператор     "&".    Например,  предположим, с          помощью    следующего           кода  мы      определяем структуру, объявляем структурную           переменную          и          указатель    на       структуру     определенного нами  типа.

struct      bal      {

 float    balance;  char         name[80];

} person;

bal           *p;      //         Объявляем указатель    на       структуру. Тогда      при     выполнении           инструкции

р  =          &person; в   указатель    р          будет помещен     адрес            структурной           переменной           person.

К   членам         структуры    можно          получить      доступ          с          помощью указателя    на       эту      структуру.   Но       в этом           случае           используется         не оператор     "точка",      а          оператор     "->".   Например,  при     выполнении этой инструкции мы      получаем    доступ          к          полю balance         через указатель    р:

p->balance

Оператор          "->"    называется оператором            "стрелка". Он      образуется  с использованием знаков           "минус"        и          "больше".

Оператор        "стрелка"  (->)     позволяет  получить    доступ         к          членам структуры            с          помощью указателя.

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

Узелок   на       память.        Чтобы         получить    доступ         к          членам структуры,          используйте оператор           "точка".      Чтобы         получить доступ         к          членам         структуры            с          помощью    указателя, используйте         оператор   "стрелка".

Пример           использования     указателей           на       структуры

В   качестве       интересного           примера      использования      указателей  на структуры    можно рассмотреть        С++-функции          времени       и          даты.           Эти функции       считывают   значения      текущего системного     времени       и          даты. Для    их        использования      в          программу необходимо           включить заголовок <ctime>.       Этот    заголовок    поддерживает       два     типа   даты, требуемые упомянутыми функциями.      Один из        этих    типов,           time_t,          предназначен для     представления      системного времени      и          даты  в          виде  длинного целочисленного   значения,    которое        используется         в          качестве календарного       времени.     Второй          тип     представляет         собой            структуру tm,      которая        содержит отдельные     элементы    даты  и          времени.     Такое представление      времени       называют поэлементным.     Структура    tm       имеет следующий            формат.

struct      tm       {

 int       tm_sec;         /*        секунды,      0-59   */

 int       tm_min;        /*        минуты,       0-59   */

 int       tm_hour;      /*        часы, 0-23   */

 int       tm_mday;    /*        день  месяца,        1-31   */

 int       tm_mon;      /*        месяц,           начиная        с          января,         0-11   */

 int       tm_year;       /*        год     после 1900  */

 int       tm_wday;     /*        день, начиная        с          воскресенья,          0-6      */

 int       tm_yday;      /*        день, начиная        с          1-го    января,         0-365 */

 int       tm_isdst        /*        индикатор   летнего         времени       */

}

Значение           tm_isdst        положительно,     если   действует    режим          летнего времени       (Daylight       Saving Time),          равно            нулю, если   не       действует,   и отрицательно,       если   информация          об       этом недоступна.

Основным        средством   определения         времени       и          даты  в          C++ является       функция       time(), которая      имеет            такой прототип:

time_t     time(time_t  *curtime);

Функция            time() возвращает             текущее        календарное          время системы.      Если   в          системе        отсчет времени     не       производится, возвращается        значение      -1.       Функцию     time() можно          вызывать     либо с нулевым      указателем,            либо  с          указателем на       переменную          curtime типа   time_t.          В          последнем случае           этой   переменной           будет присвоено   значение      текущего      календарного        времени.

Чтобы     преобразовать      календарное          время            в          поэлементное, используйте           функцию localtime(),       которая        имеет            такой прототип:

struct      tm       *localtime(const     time_t            *curtime);

Функция            localtime()    возвращает             указатель    на       поэлементную форму           параметра   curtime, представленного        в          виде  структуры    tm. Значение     curtime         представляет         локальное   время.          Его обычно получают с          помощью    функции       time().

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

Следующая      программа демонстрирует     использование      функций       time() и localtime(), отображая   на       экране          текущее        системное   время.

//  Эта      программа отображает             текущее        системное   время.

#include <iostream> #include <ctime> using     namespace   std;

int    main() {

 struct tm       *ptr;

 time_t            lt;

 lt          =          time('\0');

 ptr       =          localtime(&lt);

 cout    <<       ptr->tm_hour         <<       ':'         <<       ptr->tm_min;

 cout    <<       ':'         <<       ptr->tm_sec;

 return            0;

}

Вот          один  из        возможных результатов            выполнения           этой     программы:

14:52:30

Несмотря          на       то        что      ваши  программы могут использовать поэлементную      форму представления   времени       и          даты  (как    показано     в предыдущем         примере),    проще           всего сгенерировать       строку           времени и          даты  с          помощью    функции       asctime(),     прототип      который выглядит так:

char         *asctime(const       struct tm       *ptr);

Функция            asctime()      возвращает             указатель    на       строку,          которая содержит     результат преобразования      информации,         хранимой    в адресуемой            параметром           ptr      структуре,   и          имеет следующую форму.

день       месяц            число часы:минуты:секунды   год\n\0

Указатель         на       структуру,   передаваемый      функции       asctime(),     часто получают     с          помощью функции         localtime().

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

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

//  Эта      программа отображает             текущее        системное   время.

#include <iostream> #include <ctime> using     namespace   std;

int    main() {

 struct tm       *ptr;

 time_t            lt;

 lt          =          time('\0');

 ptr       =          localtime(&lt);  cout         <<       asctime(ptr);

 return            0;

}

Вот          один  из        возможных результатов            выполнения           этой     программы.

Wed        Jul       28       15:05:51       2004

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

Ссылки            на       структуры

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

(Оператор   "стрелка"  зарезервирован    для     доступа        к          членам         структуры с          помощью    указателя.)

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

//  Демонстрируем    использование      ссылки          на     структуру. #include          <iostream> using   namespace   std;

struct      mystruct       {

 int       a;         int       b;

}; mystruct        &f(mystruct     &var);

int    main() {

 mystruct       x,         y;

 x.a       =          10;      x.b      =          20;

 cout    <<       "Исходные  значения      полей            x.a       and     x.b:     ";

 cout    <<       x.a       <<       '           '           <<       x.b      <<       '\n';

 y          =          f           (x);

 cout    <<       "Модифицированные    значения      полей            x.a       и          x.b:     ";

 cout    <<       x.a       <<       '           '           <<       x.b      <<       '\n';

 cout    <<       "Модифицированные    значения      полей            y.a      и          y.b:     ";

 cout    <<       y.a      <<       '           '           <<       y.b      <<       '\n';

 return            0;

}

//  Функция,     которая        получает      и          возвращает             ссылку          на     структуру. mystruct         &f(mystruct &var) {

 var.a   =          var.a  *          var.a;  var.b       =          var.b  /          var.b;

 return            var;

}

Вот          результаты выполнения           этой   программы.

Исходные         значения      полей            x.a       and     x.b:     10       20

Модифицированные           значения      полей            х.а       и          x.b:     100     1

Модифицированные           значения      полей            у.а      и          y.b:     100     1

Ввиду     существенных        затрат            системных   ресурсов      на       передачу структуры    функции       (или при      возвращении         ее        функцией)   многие         С++-программисты      для     выполнения           таких задач используют            ссылки          на структуры.

Использование        в          качестве     членов          структур   массивов     и   структур

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

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

struct      stype  {

 int       nums[10][10];        //         Целочисленный    массив          размерностью       10            х 10.

 float    b;

}    var;

Чтобы     обратиться  к          элементу     массива        nums  с          "координатами"   3,7 в          структуре     var      типа stype,  следует        записать       такой код:

var.nums[3][7]

Как          показано     в          этом   примере,     если   массив          является       членом структуры,  то        для     доступа        к элементам           этого  массива        индексируется имя    массива,      а          не       имя    структуры.

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

struct      addr   {

 char    name[40];

 char    street[40];

 char    city[40];

 char    zip[10];

} struct     emp    {

 addr   address;

 float    wage;

}    worker;

Здесь      структура     emp    имеет            два     члена.           Первым        членом является       структура     типа   addr, которая         будет содержать   адрес служащего. Вторым        членом         является       переменная            wage, которая хранит           его      оклад.           При    выполнении           следующего           фрагмента кода  полю zip       структуры address,          которая        является       членом         структуры worker,         будет присвоен     почтовый     индекс          98765:

worker.address.zip    =          98765;

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

Структура         также            может           содержать   в          качестве       своего члена            указатель    на       эту      же структуру.         И         в          самом           деле  для структуры    вполне          допустимо  содержать   член, который является указателем на       нее     саму. Например,  в          следующей структуре     переменная sptr объявляется  как     указатель    на       структуру     типа   mystruct,      т.е.      на объявляемую        здесь структуру.

struct      mystruct     {  int            а;

 char    str[80];

 mystruct       *sptr; //         указатель    на       объекты       типа   mystruct

};

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

Сравнение      С-        и          С++-структур

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

struct      C_struct        {

 int       а;         int       b;

} //           объявление            переменной     C_struct

struct      C_struct        svar;

Обратите           внимание    на       то,       что      приведенное         выше определение структуры    в          точности такое      же,     как     в          языке            C++.   Теперь внимательно          рассмотрите           объявление            структурной переменной svar.   Оно    также            начинается  с          ключевого  слова struct.            В          языке С          после определения        структуры    для     полного       задания        типа   данных все      равно            нужно           использовать ключевое           слово struct совместно   с тегом этой   структуры    (в        данном        случае           с идентификатором        C_struct).

Если        вам    придется      преобразовывать             старые          С-программы        в код     C++,   не       беспокойтесь         о различиях            между          С-        и          С++-структурами,          поскольку   C++     по-прежнему         принимает  Сориентированные объявления.           Например,  предыдущий         фрагмент     С-кода          корректно скомпилируется   как     часть  любой           С++-программы.  С          точки зрения компилятора         C++     в объявлении        переменной           svar    всего лишь избыточно  использовано        ключевое    слово struct,            без которого          в          C++ можно          обойтись.

Битовые        поля  структур

Битовое            поле  —        это    бит-ориентированный           член   структуры.

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

Метод,   который       использован          в          языке            C++     для     доступа        к битам,           основан        на       применении структур.  Битовое        поле  —        это      в действительности            специальный         тип     члена            структуры, который определяет             свой   размер         в          битах.            Общий          формат определения         битовых       полей            таков.

struct      имя_типа_структуры      {

 тип      имя1  :     длина;  тип     имя2  :           длина;

 .

 .

 .

 тип      имяN :           длина;

};

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

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

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

struct      status_type  {

 unsigned       delta_cts:      1;

 unsigned       delta_dsr:     1;

 unsigned       tr_edge:        1;

 unsigned       delta_rec:     1;

 unsigned       cts:      1;

 unsigned       dsr:     1;

 unsigned       ring:   1;

 unsigned       rec_line:        1;

}    status;

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

status      =          get_port_status(); if(status.cts)            cout    <<     "Установка  в          исходное     состояние";

if(status.dsr)     cout    <<       "Данные      готовы";

Чтобы     присвоить   битовому     полю значение,    достаточно использовать такую            же       форму, которая    обычно         применяется          для     элемента структуры    любого         другого        типа.  Например, следующая инструкция очищает битовое        поле  ring:

status.ring         =          0;

Как          видно            из        этих    примеров,   доступ          к          каждому      битовому полю можно          получить      с помощью оператора   "точка".      Но       если   общий доступ          к          структуре     осуществляется     через указатель,  необходимо использовать         оператор     "->".

Следует иметь            в          виду, что      совсем          необязательно      присваивать имя    каждому      битовому полю.   Это     позволяет    обращаться            только          к нужным       битам,           "обходя"      остальные.  Например, если   вас      интересуют только          биты  cts       и          dsr,     вы       могли            бы      объявить      структуру status_type следующим            образом.

struct      status_type  {  unsigned            :           4;

 unsigned       cts:      1;

 unsigned       dsr:     1;

}    status;

Обратите           здесь внимание    на       то,       что      биты  после последнего именованного       dsr      нет необходимости         вообще         упоминать.

В  структуре     можно          смешивать  "обычные"  члены           с          битовыми     полями.       Вот     пример.

struct      emp    {

 struct addr   address;

 float    pay;

 unsigned       lay_off:         1;        //         работает      или     нет

 unsigned       hourly:          1:        //         почасовая   оплата          или     оклад

 unsigned       deductions:  3:        //         удержание  налога

};

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

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

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

В  следующем            разделе        представлена        программа,            в          которой     используются        битовые       поля

для     отображения         символьных           ASCII-кодов в          двоичной    системе счисления.

Объединения

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

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

union      utype {

 short  int       i;

 char    ch;

};

Объявление    объединения         начинается           с          ключевого  слова union.

Здесь      объявляется           объединение,        в          котором       значение      типа short  int       и          значение      типа   char разделяют     одну  и          ту        же       область памяти.        Необходимо          сразу же       прояснить   один  момент: невозможно сделать        так,     чтобы            это      объединение         хранило       и          целочисленное значение,    и символ     одновременно,     поскольку   переменные           i           и          ch накладываются    (в        памяти)        друг   на друга.     Но       программа в          любой момент         может           обрабатывать        информацию,        содержащуюся     в этом объединении,        как     целочисленное     значение      или     как     символ. Следовательно, объединение           обеспечивает         два     (или   больше)       способа представления      одной           и          той     же       порции данных.   Как     видно            из этого  примера,     объединение         объявляется           с          помощью    ключевого слова union.

Как          и          при     использовании     структур,      при     объявлении объединения         не       определяется        ни одна        переменная.          Переменную можно          объявить,    разместив   ее        имя    в          конце            объявления либо воспользовавшись           отдельной   инструкцией           объявления.           Чтобы объявить      переменную объединения      именем        u_var типа   utype, достаточно записать       следующее:

utype      u_var;

В   переменной           объединения         u_var как     переменная            i           типа short  int,      так      и          символьная переменная          ch        разделяют  одну  и          ту же       область        памяти.        (Безусловно,          переменная            i           занимает два байта,            а          символьная            переменная            ch        использует  только один.)           Как     переменные           i           и          ch разделяют         одну  область памяти,        показано     на       рис.    10.2.

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

Чтобы     получить      доступ          к          элементу     объединения,        используйте тот      же       синтаксис,   который применяется   и          для     структур:      операторы "точка"       и          "стрелка". При    непосредственном обращении         к объединению        (или   посредством          ссылки)        используется         оператор "точка".      Если же        доступ          к          переменной           объединения осуществляется     через указатель,   используется оператор "стрелка". Например, чтобы            присвоить   букву 'А'       элементу     ch        объединения         u_var, достаточно  использовать         такую            запись.

u_var.ch =          'А';

В   следующем            примере      функции       передается указатель    на объединение         u_var.           В          теле   этой функции        с          помощью    указателя переменной           i           присваивается       значение      10.

//  ...

 func1(&u_var);      //         Передаем    функции       func1()          указатель                на объединение         u_var.

//  ...

}

void         fund   (utype     *un) {

 un->i  =          10;      /*        Присваиваем         число 10       члену объединения            u_var с помощью указателя.   */

}

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

// Использование     объединения         для     перестановки         двух   байтов          в рамках          короткого    целочисленного   значения.

#include <iostream> using     namespace   std;

void         disp_binary(unsigned       u); union     swap_bytes {

 short  int       num;

 char    ch[2];

};

int    main() {

 swap_bytes  sb;

 char    temp;

 sb.num          =          15;      //         двоичный    код:   0000  0000  0000  1111

 cout    <<       "Исходные  байты:           ";

 disp_binary(sb.ch[1]);

 cout    <<       "          ";

 disp_binary(sb.ch[0]);

 cout    <<       "\n\n";

 //         Обмен          байтов.

 temp  =          sb.ch[0];

 sb.ch[0]         =          sb.ch[1];

 sb.ch[1]         =          temp;

 cout    <<       "Байты          после перестановки:       ";

 disp_binary(sb.ch[1]);

 cout    <<       "          ";

 disp_binary(sb.ch[0]);

 cout    <<       "\n\n";

 return            0;

}

//  Отображение         битов,           составляющих       байт.

void         disp_binary(unsigned       u)

{

 register          int       t;

 for(t=128;    t>0;    t=t/2)

  if(u &         t)         cout    <<       "1        ";

  else            cout    <<       "0        ";

}

При         выполнении           программа генерирует  такие результаты.

Исходные         байты:           0000  0000  0000  1111

Байты     после перестановки:       0000  1111  0000  0000

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

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

//  Отображение         ASCII-кода   символов    в          двоичной    системе     счисления.

#include <iostream> #include <conio.h> using     namespace   std;

//  Битовые       поля, которые       будут расшифрованы.

struct      byte    {

 unsigned       a          :           1;

 unsigned       b          :           1;

 unsigned       с          :           1;

 unsigned       d          :           1;

 unsigned       e          :           1;

 unsigned       f           :           1;

 unsigned       g          :     1;  unsigned         h          :     1;

};

union      bits     {

 char    ch;

 struct byte    bit;

}ascii;

void         disp_bits(bits           b);

int    main() {

 do       {

  cin  >>       ascii.ch;

  cout           <<       ":";

  disp_bits(ascii);

 }while(ascii.ch!='q');          //         Выход           при     вводе            буквы            "q".

 return            0;

} //           Отображение         конфигурации       битов для     каждого     символа. void        disp_bits(bits           b) {

 if(b.bit.h)      cout    <<       "1";

  else            cout    <<       "0";

 if(b.bit.g)       cout    <<       "1";

  else            cout    <<       "0";

 if(b.bit.f)       cout    <<       "1";

  else            cout    <<       "0        ";

 if(b.bit.e)       cout    <<       "1";

  else            cout    <<       "0";

 if(b.bit.d)      cout    <<       "1";

  else            cout    <<       "0";

 if(b.bit.c)       cout    <<       "1";

  else            cout    <<       "0";

 if(b.bit.b)      cout    <<       "1";

  else            cout    <<       "0";

 if(b.bit.a)       cout    <<       "1";

  else            cout    <<       "0";

 cout    <<       "\n";

}

Вот          как     выглядит     один  из        возможных вариантов   выполнения     этой   программы.

а:  0          1          1     0          0          0     0          1 b:     0     1          1          0     0          0          1     0 с:     0          1     1          0          0     0          1          1 d:     0          1          1     0          0          1     0          0 е:     0     1          1          0     0          1          0     1 f:      0          1     1          0          0     1          1          0 g:     0          1          1     0          0          1     1          1 h:     0     1          1          0     1          0          0     0 i:      0          1     1          0          1     0          0          1 j:     0          1          1     0          1          0     1          0 k:     0     1          1          0     1          0          1     1 1:     0          1     1          0          1     1          0          0 m:     0          1          1     0          1          1     0          1 n:     0     1          1          0     1          1          1     0 o:     0          1     1          0          1     1          1          1 p:     0          1          1     1          0          0     0          0

q: 0          1          1          1          0          0          0          1

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

Анонимные   объединения

Анонимные     объединения         позволяют объявлять переменные,         которые разделяют одну  и ту   же      область      памяти.

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

//Демонстрация        использования      анонимного           объединения.

#include <iostream> using     namespace   std;

int    main() {

 //         Это     анонимное объединение.

 union {

  short         int       count;

  char           ch[2];

 };

 //         Вот     как     происходит             непосредственное           обращение к            членам анонимного       объединения.

 ch[0]  =          'X';

 ch[1]  =          'Y';

 cout    <<       "Объединение      в          виде  символов:   "          <<       ch[0]  <<ch[1]            << '\n';

 cout    <<       "Объединение      в          виде  целого          значения:    "          <<count     <<

'\n';

 return            0;

}

Эта          программа отображает             следующий            результат.

Объединение в          виде  символов:   XY

Объединение в          виде  целого          значения:    22872

Число     22872            получено     в          результате  помещения             символов    X и          Y          в          младший     и          старший байты     переменной           count соответственно.    Как     видите,         к          обеим           переменным,        входящим   в состав            объединения,        как     count,            так      и          ch,      можно          получить доступ          так      же,     как     к          обычным переменным,            а          не       как     к составляющим      объединения.        Несмотря     на       то        что      они     объявлены  как часть  анонимного           объединения,        их        имена           находятся    на       том     же уровне          области        видимости, что     и          другие          локальные  переменные, объявленные         на       уровне          объединения.        Таким            образом, член анонимного           объединения         не       может           иметь            имя,   совпадающее с          именем        любой           другой переменной,       объявленной         в          той     же области        видимости.

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

Использование        оператора             sizeof для     гарантии   переносимости   программного кода

Как          было показано,    структуры    и          объединения         создают       объекты различных  размеров, которые         зависят         от        размеров     и          количества  их членов.         Более            того,  размеры      таких встроенных типов,          как     int, могут изменяться при     переходе     от        одного          компьютера           к          другому. Иногда компилятор        заполняет    структуру     или     объединение         так,     чтобы выровнять   их        по       границе четного   слова или     абзаца.         (Абзац           содержит 16       байт.)            Поэтому,      если   в          программе нужно определить          размер (в        байтах)         структуры    или     объединения,        используйте           оператор sizeof.            Не пытайтесь         вручную       выполнять  сложение    отдельных  членов. Из-за заполнения            или     иных аппаратно-зависимых    факторов     размер структуры    или     объединения         может           оказаться     больше суммы     размеров отдельных  их        членов.

И  еще.   Объединение        всегда           будет занимать     область        памяти, достаточную          для     хранения его         самого          большого    члена. Рассмотрим            пример.

union      х          {

 char    ch;

 int       i;

 double           f;

}    u_var;

Здесь      при     выполнении           оператора   sizeof u_var получим      результат     8 (при   условии,      что double-значение       занимает     8          байт).          Во       время выполнения           программы не       имеет            значения,    что реально            будет храниться    в          переменной           u_var;           здесь важен            размер         самой большой переменной,  входящей    в          состав           объединения,        поскольку объединение         должно        иметь            размер самого      большого    его      элемента.

Переходим    к          объектно-ориентированному        программированию

Эта           глава заключает   описание     не       объектно-ориентированных атрибутов    C++.   Начиная со следующей главы,           мы      будем           рассматривать средства,     которые       поддерживают     объектноориентированное программирование        (Object          Oriented       Programming—      OOP), или     ООП. Чтобы понять        объектно-ориентированные   средства      C++     и          научиться    их эффективно            применять, необходимо          глубокое      понимание материала этой   и          предыдущих          девяти          глав.  Поэтому, возможно,      вам    стоит повторить    пройденный          материал.    Особое         внимание    при     повторении уделите        указателям,            структурам,            функциям    и          перегрузке  функций.

Глава     11:  Введение     в      классы

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

Основы           понятия     класса

Объектно-ориентированное    программирование        построено на       понятии     класса.

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

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

Объявление    класса          начинается           с          ключевого  слова class.

Класс      создается    с          помощью    ключевого  слова class.  Объявление класса           синтаксически подобно           объявлению           структуры.  Рассмотрим пример.       Следующий            класс определяет             тип queue,   который предназначен        для     реализации            очереди.      (Под  очередью    понимается список          с дисциплиной      обслуживания       в          порядке       поступления,         т.е. "первым      прибыл        —        первым обслужен".)

//  Создание     класса           queue.

class        queue            {

  int  q[100];   int            sloc,    rloc;  public:

  void           init();

  void           qput(int         i);

  int  qget();

};

Рассмотрим    подробно    объявление            этого  класса.

По           умолчанию члены           класса          являются   закрытыми           (private-членами).

Все           члены           класса           queue            объявлены  в          теле   инструкции class.  Членами      данных         класса queue          являются     переменные           q,        sloc и          rloc.    Кроме           того,  здесь определено            три     функции-члена: init(), qput()            и          qget().

Любой   класс может           содержать   как     закрытые,   так      и          открытые члены.          По       умолчанию все элементы,       определенные      в          классе, являются     закрытыми.            Например,  переменные           q,        sloc     и rloc являются закрытыми.            Это     означает,     что      к          ним    могут получить      доступ только          другие члены        класса           queue;           никакие        другие          части программы этого  сделать        не       могут.           В          этом состоит          одно  из проявлений            инкапсуляции:      программист         в          полной         мере  может управлять доступом       к          определенным     элементам  данных.        Закрытыми можно          объявить      и          функции       (в этом          примере      таких нет),   и тогда их        смогут           вызывать     только          другие          члены           этого  класса.

Ключевое         слово public используется       для     объявления            открытых     членов          класса.

Чтобы     сделать        части класса           открытыми (т.е.    доступными           для других          частей           программы), необходимо       объявить      их        после ключевого  слова public.           Все     переменные           или     функции, определенные после спецификатора     public,           доступны     для     всех    других          функций программы. Итак,           в          классе           queue            функции       init()qput()            и qget() являются     открытыми.            Обычно        в программе          организуется доступ          к          закрытым    членам         класса           через его      открытые     функции. Обратите      внимание    на       то,       что      после ключевого  слова public стоит двоеточие.

Следует иметь            в          виду, что      объект          образует      своего           рода связку           между          кодом           и          данными. Так,       любая           функция-член имеет            доступ          к          закрытым    элементам  класса.          Это     означает,     что функции       init()qput()            и          qget() имеют           доступ          к          переменным q,        sloc     и          rloc.    Чтобы           добавить функцию-член          в          класс, определите            ее        прототип      в          объявлении            этого  класса.

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

queue     Q1,     Q2;

После     создания     объект          класса           будет иметь            собственную копию           членов          данных,        которые составляют       класс.            Это     означает, что      каждый        из        объектов      Q1      и          Q2      будет иметь            собственные отдельные  копии            переменных           q,        sloc     и          rloc.    Следовательно, данные,       связанные   с          объектом Q1,        отделены    (изолированы)      от данных,        связанных   с          объектом     Q2.

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

cout         <<       Q1.sloc;

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

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

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

void         queue::qput(int     i) {

 if(sloc==100)           {

  cout           <<       "Очередь     заполнена.\n";

  return;

 }

 sloc++;

 q[sloc]            =          i;

}

Оператор        разрешения           области      видимости квалифицирует   имя члена            вместе        с          именем его            класса.

Оператор          "::"      называется оператором         разрешения           области видимости.           По       сути,  он сообщает           компилятору,        что      данная версия          функции       qput()            принадлежит         классу           queue. Другими словами,      оператор     "::"      заявляет       о          том,   что      функция       qput() находится    в          области видимости         класса           queue.           Различные  классы могут использовать         одинаковые           имена           функций. Компилятор   же определит, к          какому         классу           принадлежит         функция,      с          помощью оператора разрешения области        видимости  и          имени           класса.

Функции-члены         можно          вызывать     только          относительно заданного   объекта.       Чтобы вызвать      функцию-член       из        части программы, которая        находится    вне     класса,          необходимо использовать      имя    объекта и          оператор     "точка".      Например,  при     выполнении           этого  кода  будет вызвана        функция       init()   для     объекта        ob1.

queue     ob1,   ob2;

ob1.init();

При         вызове          функции       ob1.init()      действия,     определенные      в функции       init(),  будут направлены           на       копии            данных,        относящиеся к          объекту        ob1.   Следует        иметь            в          виду, что      ob1     и ob2 —        это два     отдельных  объекта.       Это     означает,     что,    например,   инициализация объекта        ob1 отнюдь не       приводит     к          инициализации     объекта        ob2. Единственное,       что      связывает    объекты ob1          и          ob2,   состоит         в          том, что      они     имеют           один  и          тот      же       тип.

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

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

#include <iostream> using     namespace   std;

//  Создание     класса           queue.

class        queue            {

  int  q[100];   int            sloc,    rloc;  public:

  void           init();

  void           qput(int         i);

  int  qget();

};

//  Инициализация    класса           queue.

void     queue::init() {

 rloc     =          sloc     =          0;

}

//  Занесение   в          очередь       целочисленного     значения. void      queue::qput(int      i) {

 if(sloc==100)           {

  cout           <<       "Очередь     заполнена.\n";

  return;

 }

 sloc++;

 q[sloc]            =          i;

}

//  Извлечение            из        очереди       целочисленного   значения.

int     queue::qget() {

 if(rloc ==       sloc)   {

  cout           <<       "Очередь     пуста.\n";

  return       0;

 }

 rloc++;

 return            q[rloc];

}

int            main()

{

 queue            a,         b;        //         Создание     двух   объектов      класса           queue.

 а.init();

 b.init();

 a.qput(10);

 b.qput(19);

 a.qput(20);

 b.qput(1);

 cout    <<       "Содержимое        очереди       a:         ";

 cout    <<       a.qget()         <<       "          ";

 cout    <<       a.qget()         <<       "\n";

 cout    <<       "Содержимое        очереди       b:        ";

 cout    <<       b.qget()         <<       "          ";

 cout    <<       b.qget()         <<       "\n";

 return            0;

}

При         выполнении           эта      программа генерирует  такие результаты.

Содержимое   очереди       а:         10       20

Содержимое   очереди       b:        19       1

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

а.rloc      =          0; нельзя      включить     в     функцию      main()            нашей     программы.

Общий формат      объявления           класса

Все           классы          объявляются          подобно      приведенному      выше классу queue.           Общий          формат объявления       класса           имеет            следующий вид.

class        имя_класса            {

  закрытые            данные         и     функции  public:

  открытые            данные         и          функции

}    список_объектов;

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

Доступ           к          членам         класса

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

//  Демонстрация       доступа        к          членам     класса. #include    <iostream> using   namespace     std;

class        myclass         {

  int  a;         //         закрытые     данные  public:

  int  b;        //         открытые     данные

  void           setab(int       i);        //         открытые     функции

  int  geta();

  void           reset();

};

void         myclass::setab(int  i) {

a       =         i;          //         прямое         обращение к          переменной           а

b       =         i*i;      //         прямое         обращение к          переменной           b

}

int     myclass::geta() {

 return            a;         //         прямое         обращение к          переменной           а

}

void         myclass::reset() { //          Прямой     вызов            функции       setab() setab(0);    //     для     уже    известного  объекта. }

int    main() {

 myclass          ob;

 ob.setab(5); //         Устанавливаем      члены           данных         ob.a    и          ob.b.

 cout    <<       "Объект        ob       после вызова          функции       setab(5):       ";

 cout    <<       ob.geta()       <<       '           ';

 cout    <<       ob.b;  //         К          члену b          можно          получить      прямой доступ,            поскольку   он       является       public-членом.

 cout    <<       '\n';

 ob.b   =          20;      //         Член  b          можно          установить  напрямую,            поскольку   он является            public-членом.

 cout    <<       "Объект        ob       после установки    члена            ob.b=20:       ";

 cout    <<       ob.geta()       <<'      ';

 cout    <<       ob.b;

 cout    <<       '\n';

 ob.reset();

 cout    <<       "Объект        ob       после вызова          функции       ob.reset():    ";

 cout    <<       ob.geta()       <<       '           ';

 cout    <<       ob.b;

 cout    <<       '\n';

 return            0;

}

При         выполнении           этой   программы получаем    следующие результаты.

Объект   ob       после вызова          функции       setab(5):       5          25

Объект   ob       после установки    члена            ob.b=20:       5          20

Объект   ob       после вызова          функции       ob.reset():    0          0

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

а   =          i;          //         прямое         обращение к          переменной           а

b   =          i*i;      //         прямое         обращение к          переменной           b

Поскольку        функция       setab()           является       членом         класса,          она может           обращаться            к          членам данных    а          и          b          того    же класса           непосредственно,            без     явного          указания      имени           объекта (и        не используя         оператор     "точка").    Как     упоминалось         выше, функция-член        всегда           вызывается             для определенного        объекта        (а коль   вызов            состоялся,   объект,         стало быть, известен).    Таким образом,   в теле   функции-члена     нет      необходимости     указывать    объект          вторично. Следовательно,    ссылки          на       переменные           а          и          b          будут применяться          к          копиям         этих    переменных, относящимся     к вызывающему      объекту.

Теперь   обратите      внимание    на       то,       что      переменная            b          — открытый    (public)          член   класса myclass.     Это     означает,     что      к          b можно          получить      доступ          из        кода, определенного     вне     тела класса myclass.        Следующая            строка           кода  из        функции       main(),          при выполнении           которой переменной     b          присваивается       число 20, демонстрирует     реализацию           такого           прямого       доступа.

ob.b        =          20;      //         К          члену b          можно          получить      прямой     доступ.

//  поскольку   он       является       public-членом.

Поскольку        эта      инструкция не       принадлежит         телу   класса           myclass, то        к          переменной           b возможен            доступ          только          с использованием  конкретного           объекта        (в        данном        случае           объекта ob)      и          оператора   "точка".

Теперь   обратите      внимание    на       то,       как     вызывается             функция-член reset()            из        функции main().

ob.reset();

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

Наконец,           рассмотрим            код     функции       reset().          Тот      факт,  что      она является       функцией-членом, позволяет            ей       непосредственно обращаться            к          другим         членам         того    же       класса,          не используя оператор       "точка"       или     конкретный            объект.         В          данном случае           она     вызывает     функцию-член setab().  И         снова-таки, поскольку объект          уже    известен       (он      используется         для     вызова          функции reset()),         нет      никакой       необходимости     указывать    его      еще    раз.

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

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

Конструкторы       и          деструкторы

Конструктор    —        это    функция,     которая      вызывается          при     создании     объекта.

Как          правило,      некоторую  часть  объекта,       прежде         чем    его      можно     будет использовать, необходимо     инициализировать.         Например,     рассмотрим            класс queue            (он      представлен           выше в этой     главе).          Прежде        чем    класс queue            можно          будет использовать,     переменным          rloc     и          sloc нужно   присвоить   нулевые       значения.    В     данном        конкретном            случае           это      требование выполнялось        с     помощью    функции       init().  Но,     поскольку   требование             инициализации     членов класса       весьма          распространено,  в          C++     предусмотрена     реализация             этой   возможности         при создании        объектов      класса.     Такая автоматическая    инициализация     выполняется          благодаря использованию         конструктора.

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

//  Определение         класса           queue.

class        queue            {

  int  q[100];   int            sloc,    rloc;

 public:

  queue();   //         конструктор

  void           qput(int         i);

  int  qget();

};

Обратите           внимание    на       то,       что      в          объявлении            конструктора queue()         отсутствует  тип возвращаемого        значения.    В          C++     конструкторы не       возвращают           значений     и,        следовательно, нет        смысла         в указании      их        типа.  (При   этом   нельзя          указывать    даже  тип     void.) Теперь приведем    код     функции       queue(). //   Определение         конструктора. queue::queue() {

 sloc     =          rloc     =          0;

 cout    <<       "Очередь     инициализирована.\n";

}

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

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

Деструктор      —        это    функция,     которая      вызывается          при     разрушении           объекта.

Дополнением к          конструктору     служит          деструктор.         Во       многих случаях         при     разрушении объекту      необходимо           выполнить  некоторое действие      или     даже  некоторую последовательность       действий.    Локальные объекты       создаются   при     входе            в          блок, в          котором они определены,          и          разрушаются         при     выходе         из        него.  Глобальные объекты       разрушаются         при завершении   программы.           Существует множество  факторов,    обуславливающих           необходимость деструктора. Например,  объект          должен        освободить             ранее            выделенную          для него   память.         В C++ именно         деструктору           поручается  обработка   процесса дезактивизации    объекта.       Имя деструктора  совпадает    с          именем конструктора,        но       предваряется         символом   "~"      Подобно конструкторам деструкторы          не       возвращают           значений,    а          следовательно,     в          их объявлениях отсутствует          тип     возвращаемого    значения.

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

//  Определение         класса           queue.

class        queue            {

  int  q[100];   int            sloc,    rloc;

 public:

  queue();   //         конструктор

  ~queue(); //         деструктор

  void           qput(int         i);

  int  qget();

};

//  Определение         конструктора.

queue::queue()

{

 sloc     =          rloc     =          0;

 cout    <<       "Очередь     инициализирована.\n";

}

//  Определение         деструктора.

queue::~queue() {

 cout    <<       "Очередь     разрушена.\n";

}

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

//  Демонстрация       использования      конструктора         и     деструктора. #include    <iostream> using   namespace   std;

//  Определение         класса           queue.

class        queue            {

  int  q[100];   int            sloc,    rloc;

 public:

  queue();   //         конструктор

  ~queue(); //         деструктор

  void           qput(int         i);

  int  qget();

};

//  Определение         конструктора.

queue::queue() {

 sloc     =          rloc     =          0;

 cout    <<       "Очередь     инициализирована.\n";

}

//  Определение         деструктора.

queue::~queue() {

 cout    <<       "Очередь     разрушена.\n";

}

//  Занесение   в          очередь       целочисленного     значения. void      queue::qput(int      i) {

 if(sloc==100)           {

  cout           <<       "Очередь     заполнена.\n";

  return;

 }

 sloc++;

 q[sloc]            =          i;

}

//  Извлечение            из        очереди       целочисленного     значения. int          queue::qget() {

 if(rloc ==       sloc)   {

  cout           <<       "Очередь     пуста.\n";

  return       0;

 }

 rloc++;

 return            q[rloc];

}

int    main() {

 queue            a,         b;        //         Создание     двух   объектов      класса           queue.

 a.qput(10);

 b.qput(19);

 a.qput(20);

 b.qput(1);

 cout    <<       a.qget()         <<       "          ";

 cout    <<       a.qget()         <<     "\n";  cout            <<       b.qget()     <<       "          ";

 cout    <<       b.qget()         <<       "\n";

 return            0;

}

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

Очередь            инициализирована.

Очередь            инициализирована.

10 20

19 1 Очередь     разрушена.

Очередь            разрушена.

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

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

//  Определение         класса           queue.

class        queue            {

  int  q[100];

  int  sloc,    rloc;

  int  who;  //         содержит     идентификационный      номер           очереди

 public:

  queue(int id);      //         параметризованный       конструктор

  ~queue(); //         деструктор

  void           qput(int         i);

  int  qget();

};

Переменная    who    используется         для     хранения     идентификационного     номера         (ID) создаваемой программой           очереди.      Ее        реальное     значение      определяется        значением, передаваемым    конструктору         в     качестве       параметра   id,       при     создании     переменной           типа   queue. Конструктор    queue()         выглядит     теперь           следующим            образом.

//  Определение         конструктора.

queue::queue(int     id) {

 sloc     =          rloc     =          0;

 who    =          id;

 cout    <<       "Очередь     "          <<       who    <<       "          инициализирована.\n";

}

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

queue     а          =          queue            (101);

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

queue     а          (101);

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

тип_класса       имя_переменной(список_аргументов);

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

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

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

//  Использование     параметризованного      конструктора. #include <iostream> using   namespace   std;

//  Определение         класса           queue.

class        queue            {

  int  q[100];

  int  sloc,    rloc;

  int  who;  //         содержит     идентификационный      номер           очереди

 public:

  queue(int id);      //         параметризованный       конструктор

  ~queue(); //         деструктор

  void           qput(int         i);

  int  qget();

};

//  Определение         конструктора.

queue::queue(int     id) {

 sloc     =          rloc     =          0;

 who    =          id;

 cout    <<       "Очередь     "          <<       who    <<       "          инициализирована.\n";

}

//  Определение         деструктора.

queue::~queue()

{

 cout    <<       "Очередь     "          <<       who    <<       "          разрушена.\n";

}

//  Занесение   в          очередь       целочисленного     значения. void      queue::qput(int      i) {

 if(sloc==100)           {

  cout           <<       "Очередь     заполнена.\n";

  return;

 }

 sloc++;

 q[sloc]            =          i;

}

//  Извлечение            из        очереди       целочисленного     значения. int          queue::qget() {

 if(rloc ==       sloc)   {

  cout           <<       "Очередь     пуста.\n";

  return       0;

 }

 rloc++;

 return            q[rloc];

}

int    main() {

 queue            a(1),   b(2);   //         Создание     двух   объектов      класса           queue.

 a.qput(10);

 b.qput(19);

 a.qput(20);

 return            0;

}

При         выполнении           эта      версия          программы генерирует  такие     результаты:

Очередь            1          инициализирована.

Очередь            2          инициализирована.

10 20       19       1 Очередь            2     разрушена.

Очередь            1          разрушена.

Как          видно            из        кода  функции       main(),          очереди,      связанной   с именем        а,        присваивается идентификационный          номер           1,        а очереди,      связанной   с          именем        b,        —        идентификационный номер  2.

Несмотря          на       то        что      в          примере      с          использованием  класса queue            при     создании     объекта передается        только          один  аргумент,    в общем          случае           возможна    передача     двух   аргументов и более.       В следующем            примере      объектам     типа   widget           передается два     значения.

#include <iostream> using     namespace   std;

class        widget           {

  int  i;

  int  j;  public:   widget(int     a,         int     b);

  void           put_widget();

};

//  Передаем    2          аргумента   конструктору     widget(). widget::widget(int      a,         int       b) {

 i           =          a;         j           =          b;

}

void         widget::put_widget() {

 cout    <<       i           <<       "          "          <<       j           <<       "\n";

}

int    main() {

 widget           x(10,  20),    y(0,     0);

 x.put_widget();

 у.put_widget();

 return            0;

}

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

При         выполнении           эта      программа отображает             следующие     результаты.

10 20

0   0

Альтернативный             вариант      инициализации    объекта

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

#include <iostream> using     namespace   std;

class        myclass         {

  int  a;

 public:

  myclass(int          x);

  int  get_a();

};

myclass::myclass(int  x) {

 a     =     x; }

int     myclass::get_a() {

 return            a;

}

int    main() {

 myclass          ob       =          4;        //         вызов            функции       myclass(4)

 cout    <<       ob.get_a();

 return            0;

}

Здесь      конструктор           для     объектов      класса           myclass         принимает только          один  параметр. Обратите       внимание    на       то,       как     в          функции main()            объявляется           объект          ob.      Для    этого используется          такой формат         объявления:

myclass  ob       =          4;

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

myclass  ob       =          myclass(4);

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

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

Классы и          структуры           —        родственные        типы

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

Рассмотрим    пример         структуры    со        свойствами,            подобными     свойствам   класса.

//  Использование     структуры    для     создания     класса.

#include <iostream>

using       namespace   std;

struct      cl         {

  int  get_i();          //         Эти     члены           открыты       (public)

  void           put_i(int        j);        //         по       умолчанию.

 private:

  int     i; };

int     cl::get_i() {

 return     i; }

void         cl::put_i(int  j) {

 i     =     j; }

int    main() {

 cl         s;

 s.put_i           (10);

 cout    <<       s.get_i();

 return            0;

}

В   этой   программе определяется        тип     структуры    с          именем        cl,        в которой        функции-члены get_i()  и          put_i()           являются     открытыми (public), а          член   данных         i           —        закрытым    (private). Обратите          внимание    на то,       что      в          структурах   для     объявления            закрытых     членов используется ключевое            слово private.

Ключевое         слово private          используется       для     объявления     закрытых   членов          класса.

В   следующем            примере      показана      эквивалентная       программа, которая        использует  вместо типа            struct тип     class.

//  Использование     типа   class   вместо          типа   struct. #include <iostream> using   namespace   std;

class        cl         {

  int  i;          //         закрытый    член   по       умолчанию

 public:

  int  get_i();

  void           put_i(int        j);

};

int     cl::get_i() {

 return            i;

}

void         cl::put_i(int  j) {

 i           =          j;

}

int    main() {

 cl         s;

 s.put_i(10);

 cout    <<       s.get_i();

 return            0;

}

Иногда  С++-программисты       к          структурам,        которые     не содержат  функции-члены, применяют термин       POD-struct.

С++-программисты  тип     class   используют            главным      образом       для определения         формы объекта,   который       содержит     функции-члены,   а          тип struct —        для     создания     объектов,    которые содержат          только          члены данных.        Иногда         для     описания     структуры,  которая        не       содержит функции-члены,   используется         аббревиатура        POD    (Plain Old     Data).

Сравнение      структур   с          классами

Тот           факт,  что      и          структуры,  и          классы          обладают    практически идентичными возможностями,         создает         впечатление           избыточности. Многие         новички       в          программировании на C++     недоумевают,       почему в          нем    существует  такое очевидное  дублирование.      Нередко приходится слышать       предложения        отказаться   от        ненужного  ключевого  слова (class  или struct)            и          оставить       только          одно  из        них.

Ответ      на       эту      цепь   рассуждений         лежит            в          происхождении языка            C++     от        С          и          намерении сохранить   C++     совместимым снизу вверх с          С.        В          соответствии          с          современным        определением C++     стандартная           С-структура одновременно      является       совершенно законной     С++-структурой. В            языке            С,        который       не       содержит ключевых    слов   public или     private,         все      члены           структуры являются открытыми.            Вот     почему         и          члены           С++-структур          по умолчанию являются открытыми     (а        не       закрытыми).          Поскольку конструкция           типа   class   специально            разработана для  поддержки инкапсуляции,      есть    определенный      смысл           в          том,   чтобы            по умолчанию ее члены     были закрытыми.            Следовательно,    чтобы            избежать несовместимости             с          языком         С          в этом           вопросе,      аспекты доступа,       действующие         по       умолчанию,           менять          было нельзя, поэтому       и решено     было добавить     новое            ключевое    слово.           Но       в перспективе           можно          говорить      о          более веской         причине       для отделения   структур       от        классов.       Поскольку   тип     class   синтаксически отделен        от        типа   struct,            определение         класса           вполне          открыто для     эволюционных      изменений, которые      синтаксически       могут оказаться несовместимыми             с          С-подобными        структурами. Поскольку           мы имеем           дело  с          двумя           отдельными           типами,        будущее направление          развития языка    C++     не       обременяется        "моральными обязательствами",      связанными           с совместимостью           с          С-структурами.

Под         "занавес"     этой   темы  отметим       следующее.            Структура определяет             тип     класса. Следовательно,            структура     является       классом. На       этом   настаивал    создатель    языка            C++,   Бьерн Страуструп.           Он полагал,       что      если   структура     и          классы          будут более            или     менее эквивалентны, то переход       от        С          к          C++     станет            проще.          И история        доказала     его      правоту!

Объединения           и          классы         —        родственные        типы

Тот           факт,  что      структуры            и          классы          —        родственны, обычно         никого          не       удивляет;    однако вы   можете         удивиться,  узнав, что      объединения         также            связаны        с          классами     "близкими отношениями".     Согласно      определению        C++     объединение         —        это,     по сути,  тот      же       класс,            в котором   все      члены           данных         хранятся       в одной           и          той     же       области.       (Таким          образом, объединение также            определяет             тип     класса.)        Объединение        может содержать   конструктор           и деструктор,         а          также            функции-члены. Конечно       же,     члены           объединения         по       умолчанию открыты      (public), а          не       закрыты       (private).

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

//  Создание     класса           на       основе     объединения. #include  <iostream> using     namespace   std;

union      u_type           {

 u_type(short           int       a);       //         открытые     члены           по       умолчанию

 void    showchars();

 short  int       i;

 char    ch[2];

};

//  конструктор u_type::u_type(short            int     a) {

 i           =          a;

}

//  Отображение         символов,   составляющих                   значение      типа   short     int. void        u_type::showchars() {

 cout    <<       ch[0]  <<       "          ";

 cout    <<       ch[1]  <<       "\n";

}

int    main() {

 u_type           u(1000);

 u.showchars();

 return            0;

}

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

Встраиваемые       функции

Прежде чем    мы      продолжим            освоение     класса,          сделаем небольшое,            но       важное отступление.      Оно    не       относится    конкретно   к объектно-ориентированному программированию,    но       является       очень полезным   средством   C++,   которое        довольно    часто используется         в определениях       классов.       Речь   идет   о          встраиваемой,    или подставляемой, функции       (inline            function).     Встраиваемой        называется функция,      код     которой        подставляется       в то    место            строки программы,           из        которого      она     вызывается,           т.е.      вызов            такой функции заменяется      ее        кодом.          Существует два     способа        создания встраиваемой        функции.     Первый состоит    в          использовании     модификатора inline. Например,  чтобы            создать         встраиваемую функцию           f(),      которая возвращает             int-значение           и          не       принимает  ни       одного параметра, достаточно объявить      ее        таким            образом.

inline       int     f() {

 //         ...

}

Модификатор            inline  должен        предварять все      остальные   аспекты     объявления            функции.

Встраиваемая            функция      —        это    небольшая (по      объему         кода) функция,     код     которой подставляется        вместо        ее        вызова.

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

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

#include <iostream> using     namespace   std;

class        cl         {

  int  i;          //         закрытый    член   по       умолчанию

 public:

  int  get_i();

  void           put_i(int        j);

};

inline       int       cl::get_i()

{

 return            i;

}

inline       void    cl::put_i(int  j) {

 i     =     j; }

int    main() {

 cl         s;

 s.put_i(10);

 cout    <<       s.get_i();

 return            0;

}

Здесь      вместо          вызова          функций       get_i()           и          put_i() подставляется       их        код.   Так,    в          функции main()     строка

s.put_i(10); функционально         эквивалентна         следующей     инструкции присваивания:

s.i =          10;

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

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

■  Некоторые  компиляторы        не       генерируют            встраиваемый       код, если   соответствующая функция      содержит     цикл, конструкцию          switch            или инструкцию            goto.

■  Чаще всего встраиваемыми    не       могут быть  рекурсивные          функции.

■  Как     правило,      встраивание           "не     проходит"   для     функций,     которые содержат     статические (static)         переменные.

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

Использование        встраиваемых    функций      в          определении   класса

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

#include <iostream> using     namespace   std;

class        cl         {

  int  i;          //         закрытый    член   по       умолчанию  public:

  //    автоматически      встраиваемые       функции

  int  get_i()            {           return            i;          }

  void           put_i(int        j)         {           i           =          j;          }

};

int    main() {

 s.put_i(10);

 cout    <<       s.get_i();

 return            0;

}

Здесь     функции       get_i()           и          put_i()           определены           в          теле     объявления            класса           cl         и          автоматически являются     встраиваемыми.

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

class        cl         {

  int  i;          //         закрытый    член   по       умолчанию

 public:

  //    встраиваемые       функции

  int  get_i()

  {

   return  i;

  }

  void           put_i(int        j)

  {

   i  =          j;

  }

};

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

Важно!  Определение         небольших функций-членов   в          объявлении класса          —        обычная практика        в          С++-программировании.         И дело  даже не       в          средстве     автоматического встраивания,   а          просто в          удобстве.   Вряд  ли       вы       встретите           в          профессиональных программах, чтобы      короткие    функции-члены    определялись        вне     их класса.

Массивы        объектов

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

//  Пример        использования      массива        объектов.

#include <iostream> using     namespace   std;

enum      resolution     {low,  medium,       high}

class        display     {   int     width;

  int  height;   resolution          res;

 public:

  void           set_dim(int  w,       int       h)        {width=w;    height=h;}

  void           get_dim(int  &w,    int       &h)     {w=width;    h=height;}

  void           set_res(resolution  r)         {res     =          r;}

  resolution            get_res()       {return          res;}

};

char         names[3][8] =          {

 "низкий",

 "средний",

 "высокий", };

int    main() {

 display           display_mode[3];

 int       i,          w,       h;

 display_mode[0].set_res(low);

 display_mode[0].set_dim(640, 480);

 display_mode[1].set_res(medium);

 display_mode[1].set_dim(800, 600);

 display_mode[2].set_res(high);

 display_mode[2].set_dim(1600,           1200);

 cout    <<       "Возможные          режимы       отображения         данных:\n\n";

 for(i=0;          i<3;     i++)     {

  cout           <<       names[display_mode[i].get_res()]       <<       ":";

  display_mode[i].get_dim(w, h);

  cout           <<       w         <<       "          x          "          <<       h          <<       "\n";

 }

 return            0;

}

При         выполнении           эта      программа генерирует  такие     результаты. Возможные           режимы       отображения     данных:

низкий:  640     х          480 средний:           800     х     600

высокий:           1600  х          1200

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

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

Инициализация      массивов     объектов

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

//  Инициализация    массива     объектов. #include          <iostream> using       namespace   std;

class        samp  {

  int  a;

 public:

  samp(int   n)        {           a          =          n;        }

  int  get_a()          {           return            a;         }

};

int    main() {

 samp  sampArray[4]          =          {           -1,       -2,       -3,       -4        };

 int       i;

 for(i=0;          i<4;     i++)     cout    <<       sampArray[i].get_a()        <<       '           ';

 cout    <<       "\n";

 return            0;

}

Результаты      выполнения           этой   программы

-1 -2        -3        -4

подтверждают,          что      конструктору         samp  действительно      были переданы     значения      от        -1        до

-4.

В  действительности            синтаксис    инициализации     массива,      выраженный     строкой

samp       sampArray[4]          =          {           -1,       -2,       -3,       -4        }; представляет  собой            сокращенный        вариант        следующего     (более           длинного)   формата:

samp       sampArray[4]          =          {           samp(-1),      samp(-2),      samp(-3),      samp(-4)     };

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

#include <iostream> using     namespace   std;

class        samp     {   int     a,         b;

 public:

  samp(int   n,        int       m)       {           a          =          n;        b          =          m;       }

  int  get_a()          {           return     a;         }   int       get_b()          {     return            b;        }

};

int            main()

{

 samp  sampArray[4][2]    =          {

  samp(1,    2),

  samp(3,    4),

  samp(5,    6),

  samp(7,    8),

  samp(9,    10),

  samp(11, 12),

  samp(13, 14),

  samp(15, 16)

 };

 int       i;

 for(i=0;          i<4;     i++)     {

  cout           <<       sampArray[i][0].get_a()   <<       '           ';

  cout           <<       sampArray[i][0].get_b()  <<       "\n";   cout         <<       sampArray[i][1].get_a()   <<       '           ';

  cout           <<       sampArray[i][1].get_b()  <<       "\n";

 }

 cout    <<       "\n";

 return            0;

}

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

1   2

3   4

5   6

7   8

9   10

11 12

13 14

15 16

Указатели     на       объекты

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

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

//  Простой       пример         использования      указателя    на     объект. #include   <iostream> using   namespace   std;

class        P_example   {

  int  num;

 public:

  void           set_num(int val)     {num  =          val;}

  void           show_num();

};

void     P_example::show_num() {

 cout    <<       num   <<       "\n";

}

int    main() {

 P_example   ob,      *p;      //         Объявляем объект          и                      указатель    на     него.

 ob.set_num(1);       //         Получаем    прямой         доступ          к          объекту        ob.

 ob.show_num();

 р          =          &ob;   //         Присваиваем         указателю   р          адрес            объекта     ob.

 p->show_num();    //         Получаем    доступ          к          объекту        ob       с            помощью указателя.

 return            0;

}

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

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

//  Инкрементация    и          декрементация     указателя    на     объект. #include   <iostream> using   namespace   std;

class        P_example   {

  int  num;

 public:

  void           set_num(int val)     {num  =          val;}

  void           show_num();

};

void     P_example::show_num() {

 cout    <<       num   <<       "\n";

}

int    main() {

 P_example   ob[2], *p;

 ob[0].set_num(10);           //         прямой         доступ          к          объектам

 ob[1].set_num(20);

 p          =          &ob[0];         //         Получаем    указатель    на       первый         элемент.

 p->show_num();    //         Отображаем          значение      элемента     ob[0]  с            помощью указателя.

 p++;    //         Переходим к          следующему          объекту.

 p->show_num();    //         Отображаем          значение      элемента     ob[1]  с            помощью указателя.

 p--;      //         Возвращаемся       к          предыдущему       объекту.

 p->show_num();    //         Снова            отображаем           значение      элемента     ob[0].

 return            0;

}

Вот          как     выглядят     результаты выполнения           этой   программы.

10

20

10

Как          будет показано     ниже  в          этой   книге,            указатели    на       объекты играют          главную       роль  в реализации         одного          из        важнейших принципов  C++:   полиморфизма.

Ссылки            на       объекты

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

Глава     12:  О     классах  подробнее

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

Завершает   главу описание     ключевого  слова this.

Функции-"друзья"

В   C++     существует  возможность         разрешить   доступ          к          закрытым членам         класса           функциям, которые        не       являются     членами       этого класса.          Для    этого  достаточно объявить      эти      функции "дружественными" (или   "друзьями")           по       отношению             к          рассматриваемому         классу. Чтобы сделать      функцию      "другом"     класса,          включите     ее        прототип      в public-раздел         объявления            класса и       предварите             его      ключевым словом         friend.           Например,  в          этом   фрагменте   кода  функция       frnd() объявляется           "другом"     класса           cl.

class     cl         {   //  .           .     .

 public:

  friend        void    frnd(cl           ob);

  //    .           .           .

};

Ключевое         слово friend предоставляет   функции,     которая      не       является членом         класса, доступ     к          его     закрытым членам.

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

Рассмотрим     короткий     пример,       в          котором       функция-"друг" используется         для     доступа        к закрытым            членам         класса           myclass.

//  Демонстрация       использования      функции-"друга". #include <iostream> using   namespace   std;

class        myclass     {   int       a,     b;  public:

  myclass(int          i,          int       j)         {           a=i;     b=j;     }

  friend        int       sum(myclass            x);       //         Функция       sum() -           "друг"            класса myclass.

};

// Обратите     внимание    на       то,       что      функция       sum() не       является            членом ни  одного          класса int     sum(myclass            х) {

 /*        Поскольку   функция       sum() —        "друг"           класса           myclass,        она            имеет право          на       прямой         доступ          к          его      членам         данных            а          и          b.        */

 return            x.a       +          x.b;

}

int     main     () {

 myclass          n          (3,       4);

 cout    <<       sum(n);

 return            0;

}

В   этом   примере      функция       sum() не       является       членом         класса myclass.        Тем    не       менее            она имеет    полный        доступ          к          private-членам         класса           myclass.        В          частности,   она     может непосредственно использовать         значения      х.а      и          х.b.     Обратите     также            внимание    на то,       что функция           sum() вызывается             обычным     образом,     т.е.      без привязки     к          объекту        (и        без использования         оператора   "точка"). Поскольку   она     не       функция-член,       то        при     вызове          ее        не нужно квалифицировать            с          указанием   имени           объекта.       (Точнее,       при     ее вызове          нельзя          задавать имя         объекта.)     Обычно        функции-"другу"  в качестве       параметра   передается один  или несколько      объектов      класса,          для которого      она     является       "другом",    как     в          случае           функции       sum().

Несмотря         на       то        что      в          данном        примере      мы      не     извлекаем   никакой       пользы         из        объявления функции     sum() "другом",     а          не       членом         класса           myclass,        существуют             определенные обстоятельства,         при     которых       статус            функции-"друга" имеет     большое      значение.    Во-первых, функции-"друзья"           могут быть  полезны     для     перегрузки операторов             определенных      типов.           Вовторых,     функции-"друзья"           упрощают   создание     некоторых   функций       ввода-вывода. Об      этом речь    впереди.

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

//  Использование     функции-"друга". #include <iostream> using   namespace     std;

const       int       IDLE=0; const          int     INUSE=1; class        С2;      //     опережающее       объявление class C1     {

  int  status;            //         IDLE    если   сообщение  неактивно,  INUSE            если сообщение  выведено    на       экран.

  //    ...

 public:

  void           set_status(int          state);

  friend        int       idle(C1           a,         C2       b);

};

class        C2       {

  int  status;            //         IDLE    если   сообщение  неактивно,  INUSE            если сообщение  выведено    на       экран.

  //    ...

 public:

  void           set_status(int          state);

  friend        int       idle(C1           a,         C2       b);

};

void         C1::set_status(int   state) {

 status =          state;

}

void         C2::set_status(int   state) {

 status =          state;

}

//  Функция       idle()  -           "друг"           для     классов        C1     и          C2. int            idle(C1           a,         C2       b) {

 if(a.status      ||        b.status)        return            0;

 else     return            1;

}

int    main() {

 C1       x;

 C2       y;

 x.set_status(IDLE);

 у.set_status(IDLE);

 if(idle(x,         y))       cout    <<       "Экран          свободен.\n";

 else     cout    <<       "Отображается      сообщение.\n";

 x.set_status(INUSE);

 if(idle(x,         y))       cout    <<       "Экран          свободен.\n";

 else     cout    <<       "Отображается      сообщение.\n";

 return            0;

}

При         выполнении           программа генерирует  такие результаты.

Экран     свободен.

Отображается             сообщение.

Поскольку        функция       idle()  является       "другом"     как     для     класса           С1, так      и          для     класса           С2,      она имеет    доступ          к          закрытому  члену status,           определенному    в          обоих            классах.        Таким            образом, состояние    объекта        каждого       класса           одновременно      можно проверить   всего одним обращением       к          функции       idle().

Опережающее           объявление            предназначено     для     объявления имени           классового типа до определения   самого         класса.

Обратите           внимание    на       то,       что      в          этой   программе используется опережающее      объявление (также         именуемое опережающей      ссылкой)      для класса           С2.      Его      необходимость     обусловлена тем,            что      объявление функции       idle()  в          классе           С1       использует  ссылку          на       класс С2       до его объявления.   Чтобы           создать         опережающее       объявление            для класса,          достаточно использовать формат,   представленный  в          этой программе.

"Друг"    одного          класса           может           быть  членом         другого        класса. Перепишем            предыдущую программу         так,     чтобы            функция       idle() стала членом         класса           С1.      Обратите     внимание    на использование оператора   разрешения            области        видимости  (или   оператора   разрешения контекста)   при     объявлении            функции       idle()  в          качестве       "друга" класса           С2.

/* Функция       может           быть  членом         одного          класса           и            одновременно "другом"         другого.

*/ #include     <iostream> using     namespace   std;

const       int       IDLE=0; const       int       INUSE=1;

class        C2;      //         опережающее       объявление

class        C1       {

  int  status;            //         IDLE,  если   сообщение  неактивно,  INUSE,           если сообщение  выведено    на       экран.

  //    ...

 public:

  void           set_status(int          state);

  int  idle(C2           b);       //         теперь           это      член   класса           C1

};

class        C2       {

  int  status;            //         IDLE,  если   сообщение  неактивно,  INUSE,           если сообщение  выведено    на       экран.

  //    .           .           .

 public:

  void           set_status(int          state);

  friend        int       C1::idle(C2   b);       //         функция-"друг"

};

void         C1::set_status(int   state) {

 status =          state;

}

void         C2::set_status(int   state) {

 status =          state;

}

//  Функция       idle()  --         член   класса           С1       и          "друг"           класса           С2.

int            C1::idle(С2     b) {

 if(status         ||        b.status)        return            0;

 else     return            1;

}

int    main() {

 C1       x;

 C2       y;

 x.set_status(IDLE);

 y.set_status(IDLE);

 if(x.idle(y))    cout    <<       "Экран          свободен.\n";

 else     cout    <<       "Отображается      сообщение.\n";

 x.set_status(INUSE);

 if(x.idle(y))    cout    <<       "Экран          свободен.\n";

 else     cout    <<       "Отображается      сообщение.\n";

 return            0;

}

Поскольку        функция       idle()  является       членом         класса           C1,      она имеет            прямой         доступ          к переменной        status объектов      типа   С1. Следовательно,    в          качестве       параметра   необходимо передавать          функции idle()  только          объекты       типа   С2.

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

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

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

clock_t   clock();

Тип          clock_t          представляет         собой            разновидность      длинного целочисленного   типа. Операция    деления       значения,    возвращаемого    функцией clock(),          на       значение CLOCKS_PER_SEC      позволяет    преобразовать      результат в          секунды.      Как     прототип      для функции          clock(),          так      и определение         константы   CLOCKS_PER_SEC  принадлежат         заголовку <ctime>.

//  Использование     перегруженных    конструкторов.

#include <iostream>

#include <cstdlib> #include <ctime> using     namespace   std;

class     timer{   int     seconds;

 public:

  //    секунды,      задаваемые           в          виде  строки

  timer(char           *t)       {           seconds         =          atoi     (t);      }

  //    секунды,      задаваемые           в          виде  целого          числа

  timer(int   t)         {           seconds         =          t;         }

  //    время,          задаваемое            в          минутах        и          секундах

  timer(int   min,    int       sec)     {           seconds         =          min*60         +          sec;     }

  void           run();

};

void     timer::run() {

 clock_t           t1;

 t1        =          clock();

 while( (clock()/CLOCKS_PER_SEC          -           t1/CLOCKS_PER_SEC)<seconds);

 cout    <<       "\a";   //         звуковой      сигнал

}

int                 main() {

 timer  a          (10),   b("20"),        c(1,     10);

 a.run();          //         отсчет            10       секунд

 b.run();          //         отсчет            20       секунд

 c.run();          //         отсчет            1          минуты         и          10       секунд

 return            0;

}

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

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

Динамическая         инициализация

В   C++     как     локальные, так      и          глобальные            переменные           можно инициализировать          во       время выполнения         программы.           Этот    процесс иногда          называют    динамической       инициализацией. До сих           пор     в большинстве          инструкций инициализации,   представленных   в          этой   книге, использовались    константы.  Однако         переменную          можно          также инициализировать          во       время выполнения         программы,           используя любое           С++-выражение,   действительное    на       момент объявления       этой переменной.          Это     означает,     что      переменную          можно инициализировать          с помощью других          переменных           и/или            вызовов функций       при     условии,      что      в          момент выполнения      инструкции объявления            общее           выражение инициализации     имеет действительное значение.    Например,  следующие варианты     инициализации     переменных абсолютно  допустимы в          C++, int         n          =          strlen(str); double arc      = sin(theta); float      d          =          1.02   *          count /          deltax;

Применение  динамической      инициализации    к          конструкторам

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

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

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

//  Демонстрация       динамической       инициализации.

#include <iostream>

#include <cstdlib> #include <ctime> using     namespace   std;

class     timer{   int     seconds;

 public:

  //    секунды,      задаваемые           в          виде  строки

  timer(char           *t)       {           seconds         =          atoi(t);           }

  //    секунды,      задаваемые           в          виде  целого          числа

  timer(int   t)         {           seconds         =          t;         }

  //    время,          задаваемое            в          минутах        и          секундах

  timer(int   min,    int       sec)     {           seconds         =          min*60         +          sec;     }

  void           run();

};

void     timer::run() {

 clock_t           t1;

 t1        =          clock();

 while((clock()/CLOCKS_PER_SEC          -           t1/CLOCKS_PER_SEC)<seconds);  cout     <<       "\a";   //         звуковой      сигнал

}

int    main() {

 timer  a(10);

 a.run();

 cout    <<       "Введите      количество секунд:         ";

 char    str[80];

 cin       >>       str;

 timer  b(str); //         инициализация     в          динамике

 b.run();

 cout    <<       "Введите      минуты         и          секунды:      ";

 int       min,    sec;

 cin       >>       min     >>       sec;

 timer  с(min,            sec);   //         инициализация     в          динамике

 c.run();

 return            0;

}

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

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

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

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

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

//  Демонстрация       присваивания     объектов. #include          <iostream> using     namespace   std;

class        myclass         {

  int  a,         b;

 public:

  void           setab(int       i,          int       j)         {           a          =          i,          b          =          j;     }

  void           showab();

};

void         myclass::showab()

{

 cout    <<       "а        равно            "          <<       a     <<       '\n';  cout  <<       "b        равно            "     <<       b          <<       '\n';

}

int    main() {

 myclass          ob1,   ob2;

 ob1.setab(10,          20);

 ob2.setab(0,            0);

 cout    <<       "Объект        ob1     до       присваивания:       \n";

 ob1.showab();

 cout    <<       "Объект        ob2     до       присваивания:       \n";

 ob2.showab();

 cout    <<       '           \n';

 ob2     =          ob1;   //         Присваиваем         объект          ob1     объекту        ob2.

 cout    <<       "Объект        ob1     после присваивания:       \n";

 ob1.showab();

 cout    <<       "Объект        ob2     после присваивания:       \n";

 ob2.showab();

 return            0;

}

При         выполнении           программа генерирует  такие результаты.

Объект   ob1     до       присваивания:

а   равно            10 b    равно     20 Объект    ob2     до     присваивания:

а   равно            0 b      равно            0 Объект   ob1     после     присваивания:

а   равно            10 b    равно            20 Объект   ob2     после     присваивания:

а   равно            10

b   равно            20

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

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

Передача        объектов   функциям

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

Этот    механизм    демонстрируется в          следующей программе.

#include <iostream> using     namespace   std;

class        OBJ     {

  int  i;

 public:

  void           set_i(int         x)         {           i           =          x;         }

  void           out_i()           {           cout    <<       i           <<       "          ";         }

};

void     f(OBJ  x) {

 x.out_i();       //         Выводит       число.

 х.set_i(100);            //         Устанавливает       только          локальную  копию.

 x.out_i();       //         Выводит       число 100.

}

int    main() {

 OBJ     о;

 о.set_i(10);

 f(о);

 o.out_i();      //         По-прежнему        выводит       число 10,      значение переменной           i           не       изменилось.

 return            0;

}

Вот          как     выглядят     результаты выполнения           этой   программы.

10 100     10

Как          подтверждают      эти      результаты,            модификация        объекта        x в          функции       f()        не       влияет           на объект    o          в          функции       main().

Конструкторы,     деструкторы      и          передача     объектов

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

//  Конструкторы,       деструкторы          и          передача     объектов. #include          <iostream> using   namespace   std;

class        myclass         {

  int  val;

 public:

  myclass(int          i)          {           val       =          i;          cout    <<       "Создание\n";       }

  ~myclass()           {           cout    <<       "Разрушение\n";  }

  int  getval()          {           return            val;     }

};

void         display(myclass     ob) {

 cout    <<       ob.getval()   <<       '\n';

}

int    main() {

 myclass          a(10);

 display(a);

 return            0;

}

При         выполнении           эта      программа выводит       следующие неожиданные     результаты.

Создание

10

Разрушение

Разрушение

Как          видите,         здесь выполняется          одно  обращение к          функции конструктора         (при   создании объекта            a),       но       почему-то   два обращения к          функции       деструктора.          Давайте        разбираться,          в          чем тут       дело.

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

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

Но            когда функция       завершается           и          разрушается           копия объекта,       используемая        в          качестве аргумента,       вызывается деструктор  этого  объекта.       Необходимость    вызова          деструктора связана      с выходом      объекта        из        области        видимости. Именно        поэтому предыдущая          программа имела           два     обращения к          деструктору. Первое         произошло при     выходе         из        области        видимости параметра функции       display(),      а          второе—      при     разрушении           объекта        a          в функции       main()            по завершении     программы.

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

Потенциальные     проблемы  при     передаче     параметров

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

// Демонстрация       проблемы,  возможной при     передаче     объектов функциям.

#include <iostream> #include <cstdlib> using     namespace   std;

class        myclass         {

  int  *p;

 public:

  myclass(int          i);

  ~myclass();

  int  getval()          {           return            *p;      }

};

myclass::myclass(int  i)

{

 cout    <<       "Выделение           памяти,        адресуемой            указателем p.\n";

 р          =          new    int;

 *p     =     i; }

myclass::~myclass() {

 cout    <<"Освобождение           памяти,        адресуемой            указателем p.\n";

 delete             p;

}

//  При    выполнении           этой   функции       и          возникает     проблема. void     display(myclass       ob) {

 cout    <<       ob.getval()   <<       '\n';

}

int    main() {

 myclass          a(10);

 display(a);

 return            0;

}

Вот          как     выглядят     результаты выполнения           этой   программы.

Выделение      памяти,        адресуемой            указателем р.

10 Освобождение     памяти,        адресуемой     указателем р.

Освобождение           памяти,        адресуемой            указателем р.

Эта           программа содержит     принципиальную ошибку.       И         вот      почему: при     создании     в функции   main()           объекта        a          выделяется             область памяти,        адрес            которой        присваивается указателю        а.р      .           При передаче     функции       display()        объект          a          копируется в          параметр     ob. Это означает,         что      оба     объекта        (a        и          ob)      будут иметь одинаковое            значение      для     указателя    р.

Другими            словами,      в          обоих            объектах      (в        оригинале   и          его копии)          член   данных         p          будет указывать   на       одну  и          ту        же динамически         выделенную          область        памяти.        По       завершении функции display()        объект          ob       разрушается,         и          его      разрушение сопровождается   вызовом деструктора.   Деструктор освобождает          область памяти,        адресуемую           указателем ob.р.  Но       ведь эта       (уже освобожденная)   область        памяти          —        та        же       самая            область,       на которую       все      еще указывает      член   данных         (исходного  объекта)       a.p! Налицо         серьезная    ошибка.

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

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

//  Одно из        решений      проблемы   передачи     объектов.

#include <iostream> #include <cstdlib> using     namespace   std;

class        myclass         {

  int  *p;

 public:

  myclass(int          i);

  ~myclass();

  int  getval()          {           return            *p;      }

};

myclass::myclass(int  i) {

 cout    <<       "Выделение           памяти,        адресуемой            указателем p.\n";

 р          =          new    int;

 *p     =     i; }

myclass::~myclass() {

 cout    <<"Освобождение           памяти,        адресуемой            указателем p.\n";

 delete             p;

}

/* Эта      функция       HE       создает         проблем.     Поскольку   объект          ob            теперь передается          по       ссылке,         копия            аргумента   не            создается,   а следовательно, объект          не       выходит       из        области            видимости  по завершении     функции       display().

*/ void    display(myclass     &ob) {

 cout    <<       ob.getval()   <<       '\n';

}

int    main() {

 myclass          a(10);

 display(a);

 return            0;

}

Результаты      выполнения           этой   версии          программы выглядят     гораздо     лучше           предыдущих.

Выделение      памяти,        адресуемой            указателем р.

10

Освобождение           памяти,        адресуемой            указателем р.

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

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

Возвращение           объектов   функциями

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

//  Использование     функции,     которая        возвращает             объект.

#include <iostream> #include <cstring> using     namespace   std;

class        sample           {

  char           s[80];

 public:

  void           show()           {           cout    <<       s          <<       "\n";   }

  void           set(char         *str)   {           strcpy(s,        str);    }

};

//  Эта      функция       возвращает             объект          типа   sample.

sample     input() {

 char    instr[80];

 sample           str;

 cout    <<       "Введите      строку:          ";

  cin  >>       instr;

 str.set(instr);

 return     str; }

int    main() {

 sample           ob;

 //         Присваиваем         объект,     возвращаемый  //       функцией    input(),     объекту        ob.

 ob       =          input();

 ob.show();

 return            0;

}

В   этом   примере      функция       input()           создает         локальный  объект          str класса           sample,         а          затем считывает   строку           с          клавиатуры.           Эта строка           копируется в          строку           str.s,   после чего   объект          str возвращается         функцией    input()           и          присваивается       объекту        ob       в функции       main().

Потенциальная     проблема   при     возвращении        объектов   функциями

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

//  Ошибка,       генерируемая        при     возвращении         объекта        функцией.

#include <iostream>

#include <cstring> #include <cstdlib> using     namespace   std;

class        sample           {

  char           *s;

 public:

  sample()   {           s          =          0;        }

  ~sample()            {

   if(s)       delete            []         s;

   cout      <<       "Освобождение    s-памяти.\n";

  }

  void           show()           {           cout    <<       s          <<       "\n";   }

  void           set(char         *str);

};

//  Загрузка       строки.

void         sample::set(char     *str) {

 s           =          new    char[strlen(str)+1];

 strcpy(s,        str);

}

//  Эта      функция       возвращает             объект          типа     sample. sample      input() {

 char    instr[80];

 sample           str;

 cout    <<       "Введите      строку:          ";

  cin  >>       instr;

 str.set(instr);

 return            str;

}

int    main() {

 sample           ob;

 //         Присваиваем         объект,         возвращаемый

 //         функцией    input(),          объекту        ob.

 ob       =          input();          //         Эта      инструкция генерирует  ошибку!!!!

 ob.show();    //         Отображение         "мусора".

 return            0;

}

Результаты      выполнения           этой   программы выглядят     таким            образом.

Введите строку:          Привет Освобождение           s-памяти.

Освобождение           s-памяти.

Здесь      мусор

Освобождение           s-памяти.

Обратите           внимание    на       то,       что      деструктор  класса           sample вызывается             три     раза!  В          первый раз он       вызывается             при     выходе локального объекта        str       из        области        видимости  в          момент возвращения из        функции       input().          Второй          вызов            деструктора           ~sample() происходит             тогда, когда           разрушается           временный объект, возвращаемый     функцией    input().          Когда функция возвращает      объект, автоматически      генерируется         невидимый            (для   вас)    временный объект, который       хранит           возвращаемое      значение.    В          данном        случае           этот объект          просто          представляет собой       побитовую  копию           объекта        str, который       является       значением, возвращаемым     из функции.            Следовательно, после возвращения         из        функции       выполняется          деструктор временного объекта.       Поскольку   область        памяти,        выделенная           для     хранения строки,         вводимой пользователем,      уже    была  освобождена         (причем дважды!),   при     вызове          функции       show()           на экран       выведется   "мусор". (Вы     можете         не       увидеть        вывод           на       экран "мусора".    Это     зависит от того,         как     ваш    компилятор            реализует    динамическое       выделение памяти.        Однако         ошибка         все равно    здесь присутствует.)        Наконец,      по завершении            программы вызывается             деструктор объекта        ob       (в функции       main()).         Ситуация      здесь осложняется          тем,    что      при     первом вызове деструктора       освобождается     память,         выделенная           для     хранения строки,         получаемой           функцией input(). Таким            образом,     само  по себе   плохо            не       только          то,       что      остальные   два     обращения к деструктору            класса           sample          попытаются            освободить             уже освобожденную   область динамически    выделяемой          памяти,        но       они также            могут разрушить   систему        динамического распределения        памяти.

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

Создание        и          использование     конструктора     копии

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

Для          начала          еще    раз     сформулируем     проблемы,  для     решения которых       мы      хотим определить           конструктор           копии.           При    передаче объекта        функции       создается    побитовая   (т.е. точная)            копия            этого объекта,       которая        передается параметру   этой   функции.     Однако возможны ситуации,     когда такая идентичная             копия            нежелательна.      Например, если оригинальный        объект          содержит     указатель    на       выделяемую динамически         память,         то        и указатель,           принадлежащий  копии, также            будет ссылаться    на       ту        же       область        памяти. Следовательно, если   копия            внесет           изменения  в          содержимое          этой   области памяти,        эти изменения      коснутся       также            оригинального      объекта! Более            того,  при     завершении            функции копия     будет разрушена  (с вызовом      деструктора).         Это     может           нежелательным   образом сказаться на       исходном    объекте.

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

Конструктор копии            позволяет  управлять действиями, составляющими процесс создания            копии            объекта.

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

Прежде чем    подробнее  знакомиться           с          использованием  конструктора копии,           важно понимать, что      в          C++     определено            два     отдельных вида  ситуаций,     в          которых       значение      одного объекта    передается другому. Первой         такой ситуацией    является       присваивание,       а          второй          — инициализация.    Инициализация    может           выполняться          тремя способами, т.е.      в          случаях,       когда: ■       один  объект          явно инициализирует   другой          объект,         как,    например,   в          объявлении;

■  копия            объекта        передается параметру   функции;

■  генерируется         временный объект          (чаще            всего в          качестве значения,    возвращаемого функцией).

Конструктор    копии            применяется          только          к          инициализациям. Он      не       применяется          к присваиваниям.

Узелок   на       память.        Конструкторы    копии            не       оказывают никакого     влияния       на       операции присваивания.

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

имя_класса     (const            имя_класса            &obj) {

 //         тело   конструктора

}

Здесь      элемент        obj      означает      ссылку          на       объект,         которая используется         для     инициализации другого          объекта.       Например, предположим,      у          нас     есть    класс myclass         и          объект          y          типа myclass, тогда       при     выполнении           следующих инструкций будет вызван конструктор           копии            класса myclass.

myclass  х          =          у;         //         Объект          у          явно   инициализирует   объект     x

х.func1(у);        //         Объект          у          передается в          качестве       аргумента.

          у       =          func2();           //        Объект        у        принимает          объект, возвращаемый

функцией.

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

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

Конструкторы       копии            и          параметры           функции

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

//  Использование     конструктора         копии     для //            определения         параметра.

#include <iostream>

#include <cstdlib>

using       namespace   std;

class        myclass         {

  int  *p;

 public:

  myclass(int          i);        //         обычный     конструктор

  myclass(const     myclass         &ob); //         конструктор           копии

  ~myclass();

  int  getval()          {           return            *p;      }

};

//  Конструктор           копии. myclass::myclass(const         myclass         &obj) {

 p          =          new    int;

 *p       =          *obj.p;          //         значение      копии

 cout    <<       "Вызван       конструктор           копии.\n";

}

//  Обычный     конструктор. myclass::myclass(int  i) {

 cout    <<       "Выделение           памяти,        адресуемой            указателем p.\n";

 р          =          new    int;

 *p     =     i; }

myclass::~myclass() {

 cout    <<"Освобождение           памяти,        адресуемой            указателем p.\n";

 delete             p;

}

//  Эта      функция       принимает  один  объект-параметр. void         display(myclass       ob) {

 cout    <<       ob.getval()   <<       '\n';

}

int    main() {

 myclass          a(10);

 display(a);

 return            0;

}

Эта          программа генерирует  такие результаты.

Выделение      памяти,        адресуемой            указателем р.

Вызван  конструктор           копии.

10

Освобождение           памяти,        адресуемой            указателем р.

Освобождение           памяти,        адресуемой            указателем р.

При         выполнении           этой   программы здесь происходит             следующее: когда в          функции       main() создается  объект          а,        "стараниями"      обычного конструктора         выделяется             память,         и          адрес            этой области памяти          присваивается       указателю   а.р.     Затем            объект          а передается функции       display(), а   именно—    ее        параметру   ob.      В          этом случае           вызывается             конструктор           копии,           который       создает копию объекта        а.        Конструктор           копии            выделяет     память          для     этой копии,           а          значение      указателя на          выделенную          область        памяти присваивает           члену р          объекта-копии.     Затем            значение, адресуемое указателем р          исходного   объекта,       записывается         в          область        памяти, адрес            которой хранится            в          указателе    р          объекта-копии.     Таким образом,     области        памяти,        адресуемые указателями        а.р      и          ob.р, раздельны  и          независимы           одна  от        другой,         но       хранимые    в          них значения      (на      которые       указывают  а.р      и          ob.р)  одинаковы.            Если   бы конструктор           копии            не       был определен,    то        в          результате  создания по       умолчанию побитовой   копии            члены           а.р      и          ob.р указывали     бы на       одну  и          ту        же       область        памяти.

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

Использование        конструкторов   копии            при     инициализации   объектов

Конструктор    копии            также            вызывается             в          случае,         когда один  объект          используется         для инициализации        другого.

Рассмотрим    следующую            простую       программу.

//  Вызов            конструктора         копии            для     инициализации     объекта.

#include <iostream> #include <cstdlib> using     namespace   std;

class        myclass         {

  int  *p;

 public:

  myclass(int          i);        //         обычный     конструктор

  myclass(const     myclass         &ob); //         конструктор           копии

  ~myclass();

  int  getval()          {           return            *p;      }

};

//  Конструктор           копии. myclass::myclass(const         myclass         &ob) {

 p          =          new    int;

 *p       =          *ob.p;            //         значение      копии

 cout    <<       "Выделение           p-памяти      конструктором      копии.\n";

}

//  Обычный     конструктор. myclass::myclass(int  i) {

 cout    <<       "Выделение           p-памяти      обычным     конструктором.\n";

 р          =          new    int;

 *р     =     i; }

myclass::~myclass() {

 cout    <<       "Освобождение    р-памяти.\n";

 delete             p;

}

int    main() {

 myclass          a(10); //         Вызывается            обычный     конструктор.

 myclass          b          =          a;         //         Вызывается            конструктор           копии.

 return            0;

}

Результаты      выполнения           этой   программы таковы.

Выделение      p-памяти      обычным     конструктором.

Выделение      p-памяти      конструктором      копии.

Освобождение           р-памяти.

Освобождение           р-памяти.

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

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

myclass  а(2),   b(3); //     ...

b   =          а;

Здесь     инструкция b          =          а          выполняет  операцию    присваивания,       а     не       операцию    копирования.

Использование        конструктора     копии            при     возвращении   функцией    объекта

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

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

*/ #include     <iostream> using     namespace   std;

class        myclass         {

 public:

  myclass()  {           cout    <<       "Обычный   конструктор.\n";   }

  myclass(const     myclass         &obj) {cout  <<       "Конструктор         копии.\n";

} };

myclass     f() {

 myclass          ob;      //         Вызывается            обычный     конструктор.

 return            ob;      //         Неявно         вызывается             конструктор           копии.

}

int    main() {

 myclass          a;         //         Вызывается            обычный     конструктор.

 а          =          f();      //         Вызывается            конструктор           копии.

 return            0;

}

Эта          программа генерирует  такие результаты.

Обычный          конструктор.

Обычный          конструктор.

Конструктор    копии.

Здесь      обычный     конструктор           вызывается             дважды:      первый         раз при     создании     объекта        а          в функции   main(),          второй          —        при создании     объекта        ob       в          функции       f().      Конструктор           копии вызывается в          момент,       когда генерируется         временный объект          в качестве       значения, возвращаемого       из        функции       f().

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

Конструкторы       копии            —        а          нельзя          ли       найти          что-то         попроще?

Как          уже    неоднократно       упоминалось         в          этой   книге,            C++     — очень            мощный       язык. Он      имеет множество            средств,       которые наделяют    его      широкими   возможностями,  но       при     этом   его можно  назвать сложным     языком.       Конструкторы        копии            представляют        собой механизм,   на который ссылаются   многие         программисты      как     на       основной пример         сложности   языка, поскольку это      средство      не       воспринимается   на интуитивном          уровне.         Начинающие программисты  часто не       понимают, почему         так      важен            конструктор           копии.           Для    многих          не сразу становится  очевидным ответ  на       вопрос:         когда нужен           конструктор копии,           а          когда — нет.           Эта      ситуация      часто выражается            в такой форме:         "А       не       существует          ли       более            простого способа?". Ответ также            непрост:       и          да,      и          нет!

Такие      языки,           как     Java    и          С#,      не       имеют           конструкторов копии,           поскольку   ни       в          одном           из        них не           создаются побитовые  копии            объектов.    Дело  в          том,   что      как     Java,   так      и          C# динамически выделяют           память          для     всех    объектов,    а          программист оперирует   этими            объектами исключительно     через ссылки.        Поэтому       при передаче     объектов      в          качестве       параметров функции     или     при     возврате их        из        функций       в          копиях          объектов      нет      никакой необходимости.

Тот           факт,  что      ни       Java,   ни       C#       не       нуждаются  в          конструкторах копии,           делает          эти      языки проще,        но       за        простоту      тоже  нужно платить.       Работа          с          объектами  исключительно     посредством ссылок      (а не       напрямую,  как     в          C++)   налагает       ограничения          на       тип операций,   которые       может выполнять            программист.        Более            того, такое использование      объектных   ссылок          в          Java    и          C#       не позволяет точно определить,           когда объект          будет разрушен.   В          C++     же       объект всегда разрушается        при     выходе         из        области        видимости.

Язык       C++     предоставляет       программисту       полный        контроль     над ситуациями, складывающимися      в          программе,            поэтому       он несколько   сложнее,      чем    Java    и          С#.      Это     —        цена, которую       мы платим         за        мощность    программирования.

Ключевое       слово            this

Ключевое  слово            this     —        это    указатель  на       объект,       который          вызывает   функцию-член.

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

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

Например,       у          нас     определен  такой класс.

class        cl         {

 int       i;

 void    f()        {           ...        };

 //         .           .           .

};

В   функции       f()        можно          использовать         следующую            инструкцию для     присваивания        члену i значения   10.

i    =          10;

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

this->i     =          10;

Чтобы    понять,         как     работает      указатель    this,    рассмотрим     следующую            короткую     программу.

#include <iostream> using     namespace   std;

class        cl         {

  int  i;

 public:

  void           load_i(int      val)     {           this->i            =          val;     }           //         то        же            самое,           что i   =          val

  int  get_i()            {           return            this->i;           }           //         то        же       самое,     что      return            i

};

int    main() {

 cl         o;

 o.load_i         (100);

 cout    <<       о.get_i();

 return            0;

}

При         выполнений           эта      программа отображает             число 100.

Безусловно,     предыдущий         пример         тривиален,  но       в          нем    показано, как     можно          использовать указатель           this.    Скоро            вы       поймете, почему         указатель    this     так      важен            для     программирования        на C++.

Важно!  Функции-"друзья"           не       имеют         указателя  this,    поскольку   они не       являются   членами класса.  Только          функции-члены    имеют указатель  this.

Глава     13:  Перегрузка  операторов

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

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

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

тип          имя_класса::operator#(список_аргументов) {

 операция_над_классом

}

Операторы    перегружаются  с          помощью    функции      operator.

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

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

Перегрузка    операторов          с          использованием  функций-членов

Начнем  с          простого      примера.     В          следующей программе создается класс three_d,        который поддерживает            координаты           объекта        в трехмерном           пространстве.        Для    класса           three_d перегружаются операторы  "+"      и          "=".    Итак,  рассмотрим            внимательно          код     этой программы.

//  Перегрузка операторов             с          помощью    функций-членов.

#include <iostream> using     namespace   std;

class        three_d         {

  int  x,         y,         z;         //         3-мерные    координаты

 public:

  three_d() {           x          =          у          =          z          =          0;        }

  three_d(int          i,          int       j,          int       k)        {x         =          i;          у          =          j;     z          =          k;         }

  three_d    operator+(three_d            op2);  //         Операнд      op1     передается неявно.

  three_d    operator=(three_d            op2);  //         Операнд      op1     передается неявно.

  void           show();

};

//  Перегрузка оператора   "+". three_d     three_d::operator+(three_d      op2) {

 three_d         temp;

 temp.x           =          x          +          op2.x;            //         Операции    сложения     целочисленных  temp.у        =          у          +          ор2.у;            //         значений     сохраняют   оригинальный  temp.z           =          z          +          op2.z; //     смысл.

 return            temp;

}

//  Перегрузка оператора   присваивания. three_d     three_d::operator=(three_d      op2) {

 x          =          op2.x;            //         Операции    присваивания        целочисленных

 у          =          ор2.у;            //         значений     сохраняют     оригинальный  z          =          op2.z; //         смысл.

 return            *this;

}

//  Отображение         координат   X,        Y,         Z.

void     three_d::show() {

 cout    <<       x          <<       ",         ";

 cout    <<       у          <<     ",         ";  cout      <<     z          <<       "\n";

}

int    main() {

 three_d         a(1,     2,        3),       b(10,  10,      10),    c;

 a.show();

 b.show();

 c=a+b;           //         сложение    объектов      а          и          b

 c.show();

 c=a+b+c;       //         сложение    объектов      a,         b          и          с

 с.show();

 c=b=a;           //         демонстрация       множественного  присваивания

 с.show();

 b.show();

 return            0;

}

При         выполнении           эта      программа генерирует  такие результаты.

1, 2,        3

10,           10,      10

11,           12,      13

22,           24,      26

1, 2,        3

1, 2,        3

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

temp.x    =          х          +          ор2.х;

под          членом         х          подразумевается член   this->x,          т.е.      член   х связывается            с          объектом,   который вызывает          данную        операторную функцию.    Во       всех    случаях         неявно          передается объект, указываемый слева от        символа       операции,   который       стал    причиной    вызова операторной функции. Объект,        располагаемый    с          правой          стороны       от символа       операции,   передается этой функции        в          качестве       аргумента.  В общем          случае           при     использовании     функции-члена     для перегрузки унарного     оператора   параметры  не       используются        вообще,       а          для перегрузки бинарного  —        только          один  параметр.    (Тернарный            оператор "?"      перегружать           нельзя.)        В любом      случае           объект,         который вызывает     операторную         функцию,    неявно          передается через указатель    this.

Чтобы     понять,         как     работает      механизм    перегрузки операторов, рассмотрим            внимательно предыдущую    программу,            начиная        с перегруженного   оператора   "+".    При    обработке   двух объектов       типа   three_d оператором            "+"      выполняется          сложение    значений     соответствующих координат,  как     показано     в          функции       operator+(). Но       заметьте,     что      эта функция       не модифицирует            значение      ни       одного          операнда.   В качестве       результата  операции     эта      функция возвращает      объект          типа three_d,        который       содержит     результаты попарного   сложения координат двух   объектов.    Чтобы           понять,         почему         операция     "+"      не       изменяет содержимое          ни одного   из        объектов-участников,    рассмотрим стандартную          арифметическую  операцию сложения,     примененную, например,   к          числам         10       и          12.      Результат     операции     10+12 равен            22, но            при     его      получении  ни       10,      ни       12       не       были изменены.  Хотя   не       существует  правила,      которое бы не       позволяло перегруженному  оператору   изменять     значение      одного          из        его операндов, все же           лучше,          чтобы            он       не       противоречил общепринятым     нормам        и          оставался    в          согласии      со своим оригинальным      назначением.

Обратите           внимание    на       то,       что      функция       operator+() возвращает объект          типа   three_d. Несмотря           на       то        что      она     могла            бы возвращать             значение      любого         допустимого          в          C++     типа,  тот факт, что      она     возвращает             объект          типа   three_d,        позволяет    использовать оператор     "+"      в          таких составных   выражениях,          как     a+b+с.           Часть этого  выражения,            а+Ь,   генерирует  результат     типа three_d,         который затем суммируется          с          объектом     с.         И         если   бы      эта      часть выражения генерировала       значение      иного типа   (а        не       типа   three_d), такое составное    выражение попросту не            работало     бы.

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

а   =          b          =          с          =          d;

необходимо,   чтобы            операторная          функция       operator=() возвращала объект,         адресуемый указателем          this,    и          чтобы            этот    объект располагался         слева от        оператора   "=".    Это     позволит выполнить      любую цепочку        присваиваний.      Операция    присваивания        —        это      одно  из самых важных       применений           указателя    this.

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

Использование        функций-членов  для     перегрузки унарных   операторов

Можно   также            перегружать           такие унарные       операторы, как     "++""--", или     унарные       "-"       и "+". Как     упоминалось         выше,            при     перегрузке унарного     оператора   с          помощью    функции-члена операторной функции       ни один  объект          не       передается явным           образом.     Операция    же выполняется          над     объектом,   который       генерирует  вызов            этой   функции через неявно переданный       указатель    this.    Например,  рассмотрим расширенную        версию         предыдущего примера программы.           В          этом варианте      для     объектов      типа   three_d         определяется        операция инкремента.

//  Перегрузка унарного     оператора. #include <iostream> using   namespace     std;

class        three_d         {

  int  x,         y,         z;         //         3-мерные    координаты

 public:

  three_d() {           x          =          у          =          z          =          0;        }

  three_d(int          i,          int       j,          int       k)        {x         =          i;          у          =          j;     z          =          k;         }

  three_d    operator+(three_d            op2);  //         Операнд      op1     передается неявно.

  three_d    operator=(three_d            op2);  //         Операнд      op1     передается неявно.

  three_d    operator++();          //         префиксная            версия          оператора   ++

  void           show();

};

//  Перегрузка оператора   "          +          ".

three_d  three_d::operator+(three_d      op2) {

 three_d         temp;

 temp.x           =          x          +          op2.x;            //         Операции    сложения     целочисленных  temp.у        =          у          +          ор2.у;            //         значений     сохраняют   оригинальный

 temp.z           =          z          +          op2.z; //         смысл.

 return            temp;

}

//  Перегрузка оператора   присваивания. three_d     three_d::operator=(three_d      op2) {

 x          =          op2.x;            //         Операции    присваивания        целочисленных

 у          =          ор2.у;            //         значений     сохраняют   оригинальный

 z          =          op2.z; //         смысл.

 return            *this;

}

//  Перегруженная     префиксная            версия          оператора     "++". three_d          three_d::operator++() {

 х++;    //         инкремент  координат   х,         у          и          z

 у++;

 z++;

 return            *this;

}

//  Отображение         координат   X,        Y,         Z.

void     three_d::show() {

 cout    <<       x          <<       ",         ";

 cout    <<       у          <<     ",         ";  cout      <<     z          <<       "\n";

}

int    main() {

 three_d         a(1,     2,        3),       b(10,  10,      10),    c;

 a.show();

 b.show();

 с          =          a          +          b;        //         сложение    объектов      а          и          b

 c.show();

 c=a+b+c;       //         сложение    объектов      a,         b          и          с

 с.show();

 с          =          b          =          a;         //         множественное    присваивание  с.show();

 b.show();

 ++c;    //         инкремент  с

 c.show();

 return            0;

}

Эта          версия          программы генерирует  такие результаты.

1, 2,        3

10,           10,      10

11,           12,      13

22,           24,      26

1, 2,        3

1, 2,        3

2, 3,        4

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

Как          вы       знаете,          операторы  "++"   и          "--"     имеют           префиксную и          постфиксную         формы. Например,         оператор     инкремента            можно использовать         в          форме

++0;

и  в          форме

0++;.

Как          отмечено     в          комментариях       к          предыдущей          программе, функция       operator++() определяет           префиксную           форму           оператора   "++" для     класса           three_d.        Но       нам    ничего          не мешает   перегрузить            и постфиксную         форму.         Прототип     постфиксной          формы          оператора   "++" для     класса           three_d         имеет            следующий            вид.

three_d  three_d::operator++(int  notused);

Операторы    инкремента          и          декремента          имеют         как     префиксную,         так    и          постфиксную формы.

Параметр          notused         не       используется         самой            функцией.   Он служит          индикатором         для компилятора,            позволяющим       отличить префиксную           форму           оператора   инкремента            от постфиксной.   (Этот параметр     также            используется         в          качестве       признака     постфиксной формы и      для     оператора   декремента.)         Ниже приводится             один  из возможных способов     реализации постфиксной        версии          оператора   "++"   для класса           three_d.

//  Перегрузка постфиксной          версии          оператора   "++".

three_d  three_d::operator++(int  notused) {

 three_d         temp  =          *this;  //         сохранение             исходного   значения

 x++;    //         инкремент  координат   х,         у          и          z

 у++;

 z++;

 return            temp; //         возврат         исходного   значения

}

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

three_d  temp  =          *this;

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

В  следующей версии          исходной     программы реализованы         обе     формы     оператора   "++".

//  Демонстрация       перегрузки оператора   "++"   с //     использованием  его      префиксной            и          постфиксной     форм. #include      <iostream> using   namespace   std;

class        three_d         {

  int  x,         у,         z;         //         3-мерные    координаты

 public:

  three_d() {           x          =          у          =          z          =          0;        }

  three_d(int          i,          int       j,          int       k)        {x         =          i;          у          =          j;     z          =          k;         }

  three_d    operator+(three_d            op2);  //         Операнд      op1     передается неявно.

  three_d    operator=(three_d            op2);  //         Операнд      op1     передается неявно.

  three_d    operator++();          //         префиксная            версия

  three_d    operator++(int        notused);      //         постфиксная           версия

  void           show();

};

//  Перегрузка оператора   "          +          ".

three_d  three_d::operator+(three_d      op2) {

 three_d         temp;

 temp.x           =          x          +          op2.x;            //         Операции    сложения     целочисленных  temp.у        =          у          +          ор2.у;            //         значений     сохраняют   оригинальный

 temp.z           =          z          +          op2.z; //         смысл.

 return            temp;

}

//  Перегрузка оператора   присваивания. three_d     three_d::operator=(three_d      op2) {

 x          =          op2.x;            //         Операции    присваивания        целочисленных

 у          =          ор2.у;            //         значений     сохраняют   оригинальный

 z          =          ор2.z; //         смысл.

 return            *this;

}

//  Перегрузка префиксной            версии          оператора     "++". three_d          three_d::operator++() {

 х++;    //         инкремент  координат   х,         у          и          z

 У++;

 z++;

 return            *this;

}

//  Перегрузка постфиксной          версии          оператора   "++".

three_d  three_d::operator++        (int      notused) {

 three_d         temp  =          *this;  //         сохранение             исходного   значения

 х++;    //         инкремент  координат   х,         у          и          z

 у++;

 z++;

 return            temp; //         возврат         исходного   значения

}

//  Отображение         координат   X,        Y,         Z.

void     three_d::show() {

 cout    <<       x          <<       ",         ";

 cout    <<       у          <<     ",         ";  cout      <<     z          <<       "\n";

}

int    main() {

 three_d         a(1,     2,        3),       b(10,  10,      10),    c;

 a.show();

 b.show();

 с          =          a          +          b;        //         сложение    объектов      а          и          b

 c.show();

 c=a+b+c;       //         сложение    объектов      a,         b          и          с

 с.show();

 с          =          b          =          a;         //         множественное    присваивание  с.show();

 b.show();

 ++c;    //         префиксная            форма           инкремента

 c.show();

 с++;    //         постфиксная           форма           инкремента

 с.show();

 а          =          ++с;    //         Объект          а          получает      значение      объекта        с            после его инкрементирования.

 a.show();      //         Теперь          объекты       а          и          с

 с.show();       //         имеют           одинаковые           значения.

 а          =          с++;    //         Объект          а          получает      значение      объекта        с            до       его инкрементирования.

 a.show();      //         Теперь          объекты       а          и          с

 с.show();       //         имеют           различные  значения.

 return            0;

}

Вот          как     выглядят     результаты выполнения           этой   версии     программы.

1, 2,        3

10,           10,      10

11,           12,      13

22,           24,      26

1, 2,        3

1, 2,        3

2, 3,        4

3, 4,        5

4, 5,        6

4, 5,        6

4, 5,        б

5, 6,        7

Как          подтверждают      последние  четыре          строки           результатов программы,           при     префиксном инкрементировании    значение      объекта        c увеличивается       до       выполнения           присваивания объекту  a,        при постфиксном         инкрементировании       —        после присваивания.

Помните,          что      если   символ         "++"   стоит  перед            операндом, вызывается             операторная функция   operator++(),          а          если   после операнда —        операторная          функция       operator++(int notused).Тот      же       подход используется         и          для     перегрузки префиксной            и          постфиксной форм оператора   декремента            для     любого         класса.          В          качестве упражнения           определите            оператор декремента    для     класса           three_d.

Важно!  Ранние         версии          языка            C++     не       содержали различий между          префиксной           и постфиксной    формами    операторов инкремента          и          декремента.         Тогда в          обоих            случаях вызывалась префиксная            форма          операторной        функции.     Это   следует       иметь в          виду, если   вам придется       работать  со        старыми    С++-программами.

Советы           по       реализации            перегрузки операторов

Действие           перегруженного   оператора   применительно    к          классу,          для которого      он определяется, не       обязательно           должно        иметь отношение  к          стандартному        действию     этого оператора   применительно    к встроенным           С++-типам.  Например,  операторы  "<<"   и          ">>", применяемые к          объектам     cout    и          cin,     имеют           мало  общего         с          аналогичными операторами, применяемыми          к          значениям  целочисленного   типа.  Но       для улучшения  структурированности и читабельности       программного      кода создаваемый         перегруженный    оператор     должен        по возможности   отражать исходное     назначение             того    или     иного оператора.  Например,  оператор "+", перегруженный    для     класса           three_d,        концептуально      подобен оператору   "+", определенному       для     целочисленных     типов.           Ведь  вряд  ли есть    логика          в          определении         для класса, например,   оператора   "+", который       по       своему          действию     больше         напоминает оператор   деления (/).      Таким            образом,     основная     идея  создания     перегруженного оператора   — наделить            его      новыми        (нужными   для     вас) возможностями,  которые,      тем     не       менее,          связаны        с его первоначальным назначением.

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

.                                        ::                                              .*                                            ?

Оператор          ".*"    —        это      оператор     специального        назначения             (он рассматривается  ниже  в этой            книге).

О            значении     порядка       операндов

Перегружая     бинарные    операторы, помните,      что      во       многих          случаях порядок       следования операндов имеет            значение.    Например,  выражение А+В коммутативно,      а          выражение А-В     — нет.           (Другими     словами,      А         - В          не       то        же       самое,           что      В          -           А!)      Следовательно,    реализуя перегруженные    версии          некоммутативных            операторов,           необходимо помнить,      какой операнд стоит       слева от        символа       операции,   а          какой — справа          от        него.  Например,  в          следующем фрагменте кода демонстрируется перегрузка  оператора   вычитания   для     класса           three_d.

//  Перегрузка оператора   вычитания.

three_d  three_d::operator-(three_d       op2) {

 three_d         temp;

 temp.x           =          x          -           op2.x;

 temp.у           =          у          -           op2.y;

 temp.z           =          z          -           op2.z;

 return            temp;

}

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

х   -           ор2.х.

Перегрузка    операторов          с          использованием  функций-не   членов          класса

Бинарные         операторные       функции,     которые     не       являются   членами класса,         имеют         два параметра,  а          унарные      (тоже          не       члены) —        один.

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

=, (),        []         и          ->.

Например,       в          следующей программе для     перегрузки оператора   "+" вместо          функции-члена используется функция-"друг".

//  Перегрузка оператора   "+"      с          помощью    функции-"друга".

#include <iostream> using     namespace   std;

class        three_d         {

  int  x,         y,         z;         //         3-мерные    координаты

 public:

  three_d() {           x          =          у          =          z          =          0;        }

  three_d(int          i,          int       j,          int       k)        {           x          =          i;          у          =     j;          z          =          k;         }

  friend        three_d         operator+(three_d            op1,   three_d         op2);

  three_d    operator=     (three_d       op2);  //         Операнд      op1     передается неявно.

  void           show();

};

//  Теперь          это      функция-"друг". three_d     operator+(three_d            op1,   three_d         op2) {

 three_d         temp;

 temp.x           =          op1.x +          op2.x;

 temp.у           =          op1.у +     op2.y;  temp.z     =          op1.z +     op2.z;

 return            temp;

}

//  Перегрузка присваивания.

three_d  three_d::operator=(three_d      op2) {

 x          =          op2.x;

 у          =          op2.у;

 z          =          op2.z;

 return            *this;

}

//  Отображение         координат   X,        Y,         Z.

void     three_d::show() {

 cout    <<       x          <<       ",         ";

 cout    <<       у          <<     ",         ";  cout      <<     z          <<       "\n";

}

int    main() {

 three_d         a(1,     2,        3),       b(10,  10,      10),    c;

 a.show();

 b.show();

 с          =          a          +          b;        //         сложение    объектов      а          и          b  c.show();

 c=a+b+c;       //         сложение    объектов      a,         b          и          с

 с.show();

 с          =          b          =          а;         //         демонстрация       множественного     присваивания

 с.show();

 b.show();

 return            0;

}

Как          видите,         операторной          функции       operator+() теперь передаются            два     операнда.   Левый операнд    передается параметру   op1,   а правый         —        параметру   ор2.

Во многих          случаях         при     перегрузке  операторов             с          помощью функций-"друзей"           нет никакого          преимущества       по       сравнению  с использованием  функций-членов   класса.          Однако возможна           ситуация (когда           нужно,          чтобы            слева от        бинарного   оператора   стоял объект встроенного           типа), в          которой        функция-"друг"    оказывается чрезвычайно          полезной.    Чтобы понять        это,     рассмотрим            следующее. Как     вы       знаете,          указатель    на       объект,         который       вызывает операторную         функцию-член,     передается с          помощью    ключевого  слова this. При использовании        бинарного   оператора   функцию      вызывает     объект, расположенный   слева от него.        И         это      замечательно        при     условии,      что левый           объект          определяет             заданную    операцию. Например, предположим,      что      у          нас     есть    некоторый  объект          ob,      для     которого определена операция   сложения    с          целочисленным   значением, тогда следующая запись           представляет собой       вполне          допустимое выражение.

ob            +          10;      //         будет работать

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

10 +          ob;      //         не       будет работать

Дело       в          том,   что      в          этой   инструкции объект,         расположенный слева от        оператора   "+", представляет             собой            целое            число,           т.е. значение      встроенного           типа,  для     которого      не определена      ни       одна операция,    включающая          целое            число и          объект          классового  типа.

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

#include <iostream> using     namespace   std;

class        CL        {

 public:

  int  count;

  CL   operator=(CL           obj);

  friend        CL        operator+(CL           ob,      int       i);   friend      CL        operator+(int          i,          CL        ob);

};

CL CL::operator=(CL    obj)

{

 count =          obj.count;

 return            *this;

}

//  Эта      версия          обрабатывает     аргументы //         объект          +          int-значение. CL   operator+(CL           ob,      int     i)

{

 CL        temp;

 temp.count  =          ob.count       +          i;

 return            temp;

}

//  Эта      версия          обрабатывает     аргументы //         int-значение           +     объект. CL   operator+(int          i,          CL     ob)

{

 CL        temp;

 temp.count  =          ob.count       +          i;

 return            temp;

}

int    main() {

 CL        o;

 o.count          =          10;

 cout    <<       o.count         <<       "          ";         //         выводит       число 10

 o=10+o;        //         сложение    числа с          объектом

 cout    <<       o.count         <<       "          ";         //         выводит       число 20

 o=o+12;        //         сложение    объекта        с          числом

 cout    <<       0.count;        //         выводит       число 32

 return            0;

}

Как          видите,         операторная          функция       operator+() перегружается дважды,      позволяя     тем самым предусмотреть      два     возможных способа участия         целого          числа и          объекта        типа   CL        в операции сложения.

Использование        функций-"друзей"          для     перегрузки унарных   операторов

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

//  Перегрузка префиксной            формы          оператора   "++".

three_d  three_d::operator++() {

 х++;

 у++;

 z++;

 return            *this;

}

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

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

//  ЭТОТ  ВАРИАНТ     РАБОТАТЬ   НЕ       БУДЕТ

three_d  operator++(three_d         op1) {

 op1.x++;

 op1.y++;

 op1.z++;

 return            op1;

}

Эта           функция       неработоспособна,         поскольку   только          копия объекта,       активизировавшего вызов      функции       operator++(),          передается функции       через параметр     op1.   Таким            образом, изменения      в          теле функции       operator++()           не       повлияют    на       вызывающий         объект,         они изменяют только локальный  параметр.

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

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

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

//  В          этой   программе используются     перегруженные //           операторные         функции-"друзья"           operator++()           . #include      <iostream> using       namespace   std;

class        three_d         {

  int  x,         y,         z;         //         3-мерные    координаты

 public:

  three_d() {           x          =          у          =          z          =          0;        }

  three_d(int          i,          int       j,          int       k)        {x         =          i;          у          =          j;     z          =          k;         }

  friend        three_d         operator+(three_d            op1,   three_d         op2);

  three_d    operator=(three_d            op2);

  //    Эти     функции       для     перегрузки

  //    оператора   "++"   используют            ссылочные  параметры.

  friend        three_d         operator++(three_d         &op1);

  friend        three_d         operator++(three_d         &op1,            int       notused);

  void           show();

};

//  Теперь          это      функция-"друг". three_d     operator+(three_d            op1,   three_d         op2) {

 three_d         temp;

 temp.x           =          op1.x +          op2.x;

 temp.у           =          op1.у +          op2.y;

 temp.z           =          op1.z +          op2.z;

 return            temp;

} //           Перегрузка оператора     "=". three_d     three_d::operator=(three_d     op2) {

 x          =          op2.x;

 у          =          op2.y;

 z          =          op2.z;

 return            *this;

}

/* Перегрузка префиксной            версии          оператора   "++"   с использованием функции-"друга". Для    этого  необходимо           использование ссылочного параметра.

*/             three_d     operator++(three_d         &op1) {

 op1.х++;

 op1.у++;

 op1.z++;

 return            op1;

}

/* Перегрузка постфиксной          версии          оператора   "++"   с использованием функции-"друга". Для    этого  необходимо           использование ссылочного параметра.

*/             three_d         operator++(three_d         &op1,     int       notused)

{

 three_d         temp  =          op1;

 op1.x++;

 op1.у++;

 op1.z++;

 return            temp;

}

//  Отображение         координат   X,        Y,         Z.

void         three_d::     show() {

 cout    <<       x          <<       ",         ";

 cout    <<       у          <<     ",         ";  cout      <<     z          <<       "\n";

}

int    main() {

 three_d         a(1,     2,        3),       b(10,  10,      10),    c;

 a.show();

 b.show();

 с          =          a          +          b;        //         сложение    объектов      а          и          b

 c.show();

 c=a+b+c;       //         сложение    объектов      a,         b          и          с  с.show();

 с          =          b          =          a;         //         демонстрация       множественного     присваивания

 с.show();

 b.show();

 ++c;    //         префиксная            версия          инкремента

 c.show();

 с++;    //         постфиксная           версия          инкремента

 с.         show();

 а          =          ++с;    //         Объект          а          получает      значение      объекта        с            после инкрементирования.

 a.show();      //         В          этом   случае           объекты       а          и          с

 с.show();       //         имеют           одинаковые           значения      координат.

 а          =          C++;   //         Объект          а          получает      значение      объекта        с            до инкрементирования.

 a.show();      //         В          этом   случае           объекты       а          и          с

 с.show();       //         имеют           различные  значения      координат.

 return            0;

}

Узелок   на       память.        Для    реализации            перегрузки операторов следует       использовать функции-члены.        Функции-"друзья"           используются в          C++     в          основном    для     обработки специальных         ситуаций.

Перегрузка    операторов          отношения           и          логических   операторов

Операторы       отношений  (например, "=="   или     "<")    и          логические операторы  (например, "&&" или     "||")  также            можно          перегружать, причем         делать          это      совсем          нетрудно.    Как     правило, перегруженная операторная          функция       отношения  возвращает             объект          класса,          для которого      она перегружается.         А         перегруженный    оператор     отношения  или логический оператор     возвращает одно из        двух   возможных значений:    true    или false.  Это     соответствует         обычному   применению этих            операторов             и позволяет    использовать         их        в          условных     выражениях.

Рассмотрим    пример         перегрузки оператора   "=="   для     уже    знакомого   нам     класса           three_d.

//  Перегрузка оператора   "==" bool     three_d::operator==(three_d    op2) {

 if((x     ==       op2.x)            &&      (y        ==       op2.y)            &&      (z         ==       op2.z))     return            true;

 else     return            false;

}

Если        считать,        что      операторная          функция       operator==()           уже реализована,         следующий фрагмент    кода  совершенно           корректен.

three_d  а,         b;

 //         ...

if(а           ==       b)        cout    <<       "а        равно            b\n";

else          cout    <<       "а        не       равно            b\n";

Поскольку        операторная          функция       operator==()           возвращает результат     типа   bool,  ее        можно использовать     для     управления инструкцией           if.        В          качестве       упражнения           попробуйте реализовать и          другие          операторы  отношений  и          логические операторы  для     класса three_d.

Подробнее     об       операторе присваивания

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

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

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

//  Ошибка,       генерируемая        при     возврате      объекта        из        функции.

#include <iostream>

#include <cstring> #include <cstdlib> using     namespace   std;

class        sample           {

  char           *s;

 public:

  sample()   {           s          =          0;        }

  sample(const      sample           &ob); //         конструктор           копии

  ~sample()            {

   if(s)       delete            []         s;

   cout      <<       "Освобождение    s-памяти.\n";

  }

  void           show()           {           cout    <<       s          <<       "\n";   }

  void           set(char         *str);

}; //          Конструктор     копии. sample::sample(const     sample           &ob) {

 s           =          new    char[strlen(ob.s)    +1];

 strcpy(s,        ob.s);

}

//  Загрузка       строки.

void         sample::set(char     *str) {

 s           =          new    char[strlen(str)       +1];

 strcpy(s,        str);

}

//  Эта      функция       возвращает             объект          типа     sample. sample      input() {

 char    instr[80];

 sample           str;

 cout    <<       "Введите      строку:          ";

  cin  >>       instr;

 str.set(instr);

 return     str; }

int            main()

{

 sample           ob;

 //         Присваиваем         объект,         возвращаемый

 //         функцией    input(),          объекту        ob.

 ob       =          input();          //         Эта      инструкция генерирует  ошибку!!!!

 ob.show();

 return            0;

}

Возможные     результаты выполнения           этой   программы выглядят     так.

Введите строку:          Привет Освобождение           s-памяти.

Освобождение           s-памяти.

Здесь      "мусор"

Освобождение           s-памяти.

В   зависимости           от        используемого     компилятора,        вы       можете увидеть        "мусор"        или     нет. Программа    может           также            сгенерировать ошибку         во       время            выполнения.          В          любом          случае ошибки      не миновать.    И         вот      почему.

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

Однако  ошибки        не       миновать,    если   объект,         возвращаемый функцией,   присваивается объекту ob,      поскольку   при     выполнении присваивания        по       умолчанию создается    побитовая копия.            В          данном случае           временный объект,         возвращаемый     функцией    input(), копируется в объект       ob.      В          результате  член   ob.s    указывает    на       ту        же самую           область        памяти,        что      и          член   s временного         объекта.       Но после присваивания        в          процессе      разрушения            временного            объекта эта память  освобождается.    Следовательно,    член   ob.s    теперь           будет указывать на       уже освобожденную      память!        Более            того,  память,         адресуемая членом         ob.s,   должна        быть освобождена          и          по       завершении программы,           т.е.      во       второй          раз.    Чтобы           предотвратить возникновение      этой   проблемы,  необходимо           перегрузить            оператор присваивания        так,     чтобы объект,        располагаемый    слева от        оператора присваивания,       выделял      собственную          область памяти.

Реализация     этого  решения      показана      в          следующей откорректированной     программе.

//  Эта      программа работает      корректно.

#include <iostream>

#include <cstring> #include <cstdlib> using     namespace   std;

class        sample           {

  char           *s;

 public:

  sample();  //         обычный     конструктор

  sample(const      sample           &ob); //         конструктор           копии

  ~sample()            {

   if(s)       delete            []         s;

   cout      <<       "Освобождение    s-памяти.\n";

  }

  void           show()           {           cout    <<       s          <<       "\n";   }

  void           set(char         *str);

  sample      operator=(sample  &ob); //         перегруженный    оператор присваивания

};

//  Обычный     конструктор.

sample::sample() {

 s           =          new    char('\0');     //         Член  s          указывает    на       null-строку.

}

//  Конструктор           копии. sample::sample(const           sample     &ob) {

 s           =          new    char[strlen(ob.s)+1];

 strcpy(s,        ob.s);

}

//  Загрузка       строки.

void         sample::set(char     *str) {

 s           =          new    char[strlen(str)+1];

 strcpy(s,        str);

}

//  Перегрузка оператора   присваивания. sample    sample::operator=(sample         &ob) {

 /*        Если   выделенная           область        памяти          имеет            недостаточный            размер, выделяется       новая            область        памяти.        */

 if(strlen          (ob.s) >          strlen(s))       {

  delete        []         s;

  s      =          new    char[strlen(ob.s)+1];

 }

 strcpy(s,        ob.s);

 return            *this;

}

//  Эта      функция       возвращает             объект          типа   sample.

sample     input() {

 char    instr[80];

 sample           str;

 cout    <<       "Введите      строку:          ";

  cin  >>       instr;

 str.set(instr);

 return     str; }

int    main() {

 sample           ob;

 //         Присваиваем         объект,     возвращаемый  //       функцией    input(),     объекту        ob.

 ob       =          input();          //         Теперь          здесь все      в          порядке!

 ob.show();

 return            0;

}

Эта           программа теперь           отображает             такие результаты (в предположении,  что      на приглашение   "Введите    строку:        "          вы       введете "Привет").

Введите строку:          Привет Освобождение           s-памяти.

Освобождение           s-памяти.

Освобождение           s-памяти.

Привет

Освобождение           s-памяти.

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

Перегрузка    оператора             индексации            массивов     ([])

В   дополнение           к          традиционным     операторам            C++     позволяет перегружать           и          более "экзотические",   например,   оператор     индексации массивов     ([]).     В          C++     (с         точки зрения механизма          перегрузки) оператор     "[]"     считается     бинарным.  Его      можно          перегружать           только для     класса           и          только          с          использованием  функции-члена.    Вот     как выглядит     общий           формат операторной     функции-члена     operator[]().

тип          имя_класса::operator[](int       индекс) {

 //         ...

}

Оператор       "[]"     перегружается   как     бинарный   оператор.

Формально      параметр     индекс          необязательно      должен        иметь            тип int,      но       операторные функции  operator[]() обычно         используются        для обеспечения          индексации            массивов,    поэтому в    общем          случае           в качестве       аргумента   этой   функции       передается целочисленное     значение. Предположим,      у          нас     определен  объект          ob,      тогда выражение

ob[3] преобразуется            в          следующий            вызов     операторной          функции       operator[]():

ob.operator[](3)

Другими            словами,      значение      выражения,            заданного   в операторе   индексации,           передается операторной         функции       operator[]() в качестве       явно   заданного   аргумента.  При    этом   указатель this        будет указывать на       объект          ob,      т.е.      объект,         который       генерирует  вызов            этой функции.

В   следующей программе в          классе           atype объявляется           массив          для хранения     трех    intзначений.           Его      конструктор           инициализирует   каждый член   этого  массива.      Перегруженная операторная функция       operator[]() возвращает             значение      элемента,    заданного   его      параметром.

//  Перегрузка оператора   индексации            массивов #include <iostream> using   namespace   std;

const       int       SIZE    =          3;

class        atype {   int     a[SIZE];  public:

  atype()      {

   register            int       i;

   for(i=0; i<SIZE;           i++)     a[i]      =          i;

  }

  int  operator[](int          i)          {return          a[i];}

};

int            main()

{

 atype  ob;

 cout    <<       ob[2]; //         отображает             число 2

 return            0;

}

Здесь      функция       operator[]() возвращает             значение      i-го     элемента массива        a.        Таким            образом, выражение     ob[2]  возвращает             число 2, которое        отображается        инструкцией           cout. Инициализация     массива        a с          помощью    конструктора         (в        этой   и          следующей программах) выполняется          лишь в          иллюстративных  целях.

Можно   разработать            операторную         функцию      operator[]() так,     чтобы оператор     "[]"     можно было          использовать         как     слева,            так      и справа          от        оператора   присваивания.       Для    этого  достаточно указать,       что значение,    возвращаемое      операторной          функцией    operator[](),            является ссылкой. Эта          возможность         демонстрируется в          следующей программе.

//  Возврат        ссылки          из        операторной          функции     operator()[]. #include      <iostream> using   namespace   std;

const       int       SIZE    =          3;

class        atype {   int     a[SIZE];  public:

  atype()      {

   register            int       i;

   for(i=0; i<SIZE;           i++)     a[i]      =          i;

  }

  int  &operator[](int      i)          {return          a[i];}

};

int    main() {

 atype  ob;

 cout    <<       ob[2]; //         Отображается        число 2.

 cout    <<"     ";

 ob[2]  =          25;      //         Оператор     "[]"     стоит  слева от        оператора   "=".

 cout    <<       ob[2]; //         Теперь          отображается        число 25.

 return            0;

}

При         выполнении           эта      программа генерирует  такие результаты.

2   25

Поскольку        функция       operator[]() теперь           возвращает             ссылку          на элемент        массива, индексируемый        параметром           i,          оператор     "[]" можно          использовать         слева от        оператора присваивания,        что      позволит модифицировать любой           элемент        массива.      (Конечно      же,     его попрежнему          можно          использовать         и          справа          от        оператора присваивания.)

Одно      из        достоинств  перегрузки оператора   "[]"     состоит         в          том,   что с          его      помощью    мы можем  обеспечить средство      реализации безопасной             индексации            массивов.    Как     вы       знаете,          в C++ возможен    выход           за        границы       массива        во       время            выполнения программы без Соответствующего   уведомления         (т.е.    без     генерирования сообщения  о          динамической ошибке).           Но       если   создать         класс, который       содержит     массив,         и          разрешить   доступ          к          этому массиву только          через перегруженный    оператор     индексации            "[]",    то возможен    перехват индекса,           значение      которого      вышло          за дозволенные         пределы.     Например,  следующая программа (в        основу которой        положен      код     предыдущей)        оснащена    средством   контроля попадания  в          допустимый           интервал.

//  Пример        организации          безопасного           массива.

#include <iostream>

#include <cstdlib>

using       namespace   std;

const       int       SIZE    =          3;

class        atype {   int     a[SIZE];  public:

  atype()      {

   register            int       i;

   for(i=0; i<SIZE;           i++)     a[i]      =          i;

  }

 int       &operator[] (int      i);

};

// Обеспечение          контроля     попадания  в          допустимый           интервал      для класса           atype.

int            &atype::        operator       [](int     i) {

 if(i<0  ||        i>SIZE-1)       {

  cout           <<       "\n      Значение     индекса        ";

  cout           <<       i           <<       "          выходит       за        границы       массива.      \n";

  exit(1);

 }

 return            a[i];

}

int    main() {

 atype  ob;

 cout    <<       ob[2]; //         Отображается        число 2.

 cout    <<       "          ";

 ob[2]  =25;   //         Оператор     "[]"     стоит  в          левой            части.

 cout    <<       ob[2]; //         Отображается        число 25.

 ob[3]  =          44;      //         Генерируется         ошибка         времени       выполнения.

 //         поскольку   значение      3          выходит       за        границы       массива.

 return            0;

}

При         выполнении           эта      программа выводит       такие результаты.

2   25

Значение          индекса        3          выходит       за        границы     массива. При         выполнении           инструкции

ob[3]       =          44;

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

Перегрузка    оператора             "()"

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

int            operator()(float      f,         char    *p);

И  если   в          программе создается    объект          ob       этого  класса,          то     инструкция

ob            (99.57,          "перегрузка"); преобразуется            в     следующий            вызов            операторной          функции     operator():

operator()         (99.57,          "перегрузка");

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

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

//  Демонстрация       перегрузки оператора   "()". #include <iostream> using   namespace   std;

class        three_d         {

  int  x,         y,         z;         //         3-мерные    координаты

 public:

  three_d() {           x          =          у          =          z          =          0;        }

  three_d(int          i,          int       j,          int       k)        {x         =          i;          у          =          j;     z          =          k;         }

  three_d    operator()(int          a,         int       b,        int       c);

  void           show();

};

//  Перегрузка оператора   "()". three_d     three_d::operator()(int    a,         int       b,        int       c) {

 three_d         temp;

 temp.x           =          x          +          a;

 temp.у           =          у          +          b;

 temp.z           =          z          +          c;

 return            temp;

}

//  Отображение         координат   x,         y,         z.

void     three_d::show() {

 cout    <<       x          <<       ",         ";

 cout    <<       у          <<     ",         ";  cout      <<     z          <<       "\n";

}

int    main() {

 three_d         ob1(1,           2,        3),       ob2;

 ob2     =          ob1(10,         11,      12);    //         вызов            функции       operator()

 cout    <<       "ob1: ";

 ob1.show();

 cout    <<       "ob2: ";

 ob2.show();

 return            0;

}

Эта          программа генерирует  такие результаты.

ob1:        1,        2,        3

ob2:        11,      13,      15

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

Выбор           типа   должен        диктоваться            потребностями     конкретных программ.

Перегрузка    других          операторов

За исключением        таких операторов,           как     new,   delete,           ->,       ->*      и "запятая", остальные   С++операторы       можно          перегружать           таким            же способом,   который       был    показан        в          предыдущих примерах. Перегрузка операторов             new    и          delete            требует         применения специальных          методов, полное описание     которых       приводится             в главе 17       (она   посвящена  обработке   исключительных ситуаций).   Операторы ->, ->*      и          "запятая"  —        это      специальные         операторы, подробное рассмотрение        которых       выходит       за        рамки           этой   книги.           Читатели, которых       интересуют другие примеры  перегрузки операторов,           могут обратиться  к          моей  книге Полный        справочник по C++.

Еще       один  пример        перегрузки операторов

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

Сначала            определим "классовый"           тип     str_type.

#include <iostream> #include <cstring> using     namespace   std;

class        str_type     {   char     string[80];

 public:

  str_type(char     *str     =          "")       {           strcpy(string,           str);    }

  str_type   operator+(str_type           str);    //         конкатенация        строк   str_type operator=(str_type           str);    //         присваивание        строк

  //    Вывод           строки

  void           show_str()   {           cout    <<       string;            }

};

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

Этот         класс имеет            один  конструктор,          который       можно использовать         для     инициализации массива          string с          использованием заданного   значения      или     для     присваивания        ему    пустой строки        в случае           отсутствия   инициализатора.  В          этом   классе           также объявляются          два перегруженных        оператора,  которые       выполняют конкатенацию       и          присваивание.       Наконец, класс     str_type        содержит функцию      show_str(),  которая        выводит       строку           на       экран.           Вот     как выглядит     код     операторных          функций       operator+() и          operator=().

//  Конкатенация        двух   строк.

str_type str_type::operator+(str_type     str)      {

 str_type        temp;

 strcpy(temp.string,            string);

 strcat(temp.string,             str.string);

 return            temp;

}

//  Присваивание       одной           строки           другой.

str_type str_type::operator=(str_type     str)      {

 strcpy(string,           str.string);

 return            *this;

}

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

int            main()

{

 str_type        а("Всем        "),       b("привет"),            с;

 с          =          а          +          b;

 с.show_str();

 return            0;

}

При         выполнении           эта      программа выводит       на       экран строку Всем  привет.       Сначала        она конкатенирует          строки           (объекты      класса str_type)       а          и          b,        а          затем присваивает           результат конкатенации строке           c.

Следует иметь            в          виду, что      операторы  "="      и          "+"      определены только          для     объектов      типа str_type.        Например,  следующая инструкция неработоспособна,         поскольку   она     представляет собой       попытку присвоить   объекту        а          строку           с          завершающим      нулем.

а   =          "Этого           пока   делать          нельзя.";

Но            класс str_type,       как     будет показано     ниже,            можно усовершенствовать         и          разрешить выполнение            таких инструкций.

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

class        str_type     {   char     string{80];

 public:

  str_type(char     *str     =          "")       {           strcpy(string,           str);    }

  str_type   operator+(str_type           str);    /*        конкатенация        объектов типа            str_type*/

  str_type   operator+(char       *str);  /*        конкатенация        объекта класса            str_type        со        строкой        с          завершающим      нулем           */

  str_type   operator=(str_type           str);    /*        присваивание        одного объекта            типа   str_type        другому       */

  char           *operator=(char    *str);  /*        присваивание        строки           с завершающим      нулём           объекту        типа   str_type        */

  void           show_str()   {           cout    <<       string;            }

};

Затем     реализуем  перегрузку  операторных          функций       operator+() и     operator=().

// Присваивание       строки           с          завершающим      нулем           объекту            типа str_type.

str_type str_type::operator=(char            *str) {

 str_type        temp;

 strcpy(string,           str);

 strcpy(temp.string,            string);

 return            temp;

}

// Конкатенация        строки           с          завершающим      нулем           с          объектом            типа str_type.

str_type str_type::operator+(char            *str) {

 str_type        temp;

 strcpy(temp.string,            string);

 strcat(temp.string,             str);

 return            temp;

}

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

str_type a,         b,        c;

a   =          "Привет        всем";           /*        присваивание        строки           с     завершающим      нулем

объекту        */

          с      =      а     +      "         Георгий";         /*         конкатенация          объекта        со   строкой        с

завершающим      нулем           */

Следующая     программа включает     дополнительные  определения     операторов             "="      и          "+".

//  Усовершенствование     строкового  класса.

#include <iostream> #include <cstring> using     namespace   std;

class        str_type     {   char     string[80];

 public:

  str_type(char     *str     =          "")       {           strcpy(string,           str);    }

  str_type   operator+(str_type           str);

  str_type   operator+(char       *str);

  str_type   operator=(str_type           str);

  str_type   operator=(char       *str);

  void           show_str()   {           cout    <<       string;            }

};

str_type str_type::operator+(str_type     str)

{

 str_type        temp;

 strcpy(temp.string,            string);

 strcat(temp.string,             str.string);

 return     temp; }

str_type str_type::operator=(str_type     str) {

 strcpy(string,           str.string);

 return            *this;

}

str_type str_type::operator=(char            *str) {

 str_type        temp;

 strcpy(string,           str);

 strcpy(temp.string,            string);

 return     temp; }

str_type str_type::operator+(char            *str) {

 str_type        temp;

 strcpy(temp.string,            string);

 strcat(temp.string,             str);

 return            temp;

}

int    main() {

 str_type        а("Привет    "),       b("всем"),    с;

 с          =          а          +          b;

 с.show_str();

 cout    <<       "\n";

 а          =          "для   программирования,       потому         что";

 а.show_str();

 cout    <<       "\n";

 b          =          с          =          "C++   это      супер";

 с          =          c          +          "          "          +          а          +          "          "          +b;

 с.show_str();

 return            0;

}

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

C++          это      супер для     программирования,       потому         что      C++     это     супер

Прежде чем    переходить к          следующей главе,            убедитесь    в          том,   что до       конца            понимаете, как     получены    эти      результаты.            Теперь          вы можете         сами  определять             другие          операции     над строками. Попытайтесь,         например,   определить            операцию    удаления     подстроки   на основе оператора            "-".     Так,    если   строка           объекта        А         содержит фразу            "Это трудный-трудный        тест",         а строка       объекта        В          — фразу            "трудный",            то        вычисление            выражения А-В     даст   в результате  "Это -           тест".         В          данном        случае           из        исходной строки           были удалены      все      вхождения  подстроки "трудный". Определите также            "дружественную"          функцию,    которая        бы      позволяла   строке с завершающим   нулем           находиться слева от        оператора   "+".    Наконец, добавьте      в          программу код,   обеспечивающий             контроль     за ошибками.

Важно!  Для    создаваемых         вами  классов        всегда           имеет          смысл экспериментировать  с перегрузкой        операторов.         Как     показывают примеры     этой главы,          механизм    перегрузки операторов          можно использовать       для     добавления            новых            типов           данных         в среду программирования.      Это   одно  из        самых           мощных       средств       C++.

Глава     14:  Наследование

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

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

Понятие        о          наследовании

Базовый           класс наследуется         производным        классом.

Язык       C++     поддерживает       механизм    наследования,      позволяя     в объявление            класса встраивать            другой          класс.            Для    этого  базовый класс задается       при     объявлении            производного. Лучше   всего начать           с примера.     Рассмотрим            класс road_vehicle,          который       в          самых общих чертах         определяет             дорожное   транспортное         средство.     Его члены           данных         позволяют  хранить количество        колес и          число пассажиров,           которое        может           перевозить транспортное         средство.

class        road_vehicle     {   int     wheels;   int     passengers;

 public:

  void           set_wheels(int        num)  {           wheels           =          num;  }

  int  get_wheels()           {           return            wheels;          }

  void           set_pass(int num)  {           passengers   =          num;  }

  int  get_pass()    {           return            passengers;  }

};

Это           общее           определение         дорожного транспортного       средства можно          использовать         для определения             конкретных            типов транспортных        средств.       Например,  в          следующем            фрагменте путем наследования        класса           road_vehicle           создается    класс truck  (грузовых автомобилей).

class        truck  :           public road_vehicle           {

  int  cargo;

 public:

  void           set_cargo(int           size)    {           cargo =          size;    }

  int  get_cargo()  {           return            cargo;            }

  void           show();

};

Тот           факт,  что      класс truck  наследует    класс road_vehicle,          означает,     что класс truck наследует     все      содержимое          класса           road_vehicle.          К содержимому       класса           road_vehicle           класс truck  добавляет   член   данных cargo,            а          также            функции-члены,   необходимые        для     поддержки члена cargo.

Обратите           внимание    на       то,       как     наследуется           класс road_vehicle. Общий          формат         для обеспечения  наследования        имеет            следующий вид.

class        имя_производного_класса     :           доступ     имя_базового_класса {

 тело   нового          класса

}

Здесь      элемент        доступ         необязателен.       При    необходимости     он может           быть  выражен      одним из     спецификаторов   доступа:       public, private          или     protected.    Подробнее об       этих спецификаторах      доступа        вы узнаете         ниже  в          этой   главе.            А         пока   в          определениях       всех наследуемых         классов        мы      будем           использовать         спецификатор public.           Это     означает,     что      все public-члены   базового      класса           также будут public-членами      производного       класса. Следовательно,            в предыдущем         примере      члены           класса           truck  имеют           доступ          к открытым функциям-членам класса           road_vehicle,          как     будто они     (эти функции)     были объявлены  в          теле класса truck. Однако         класс truck  не имеет            доступа        к          private-членам      класса           road_vehicle. Например, для     класса           truck  закрыт          доступ          к          члену данных         wheels.

Рассмотрим     программу,            которая        использует  механизм    наследования для     создания     двух подклассов  класса           road_vehicle:          truck  и automobile.

//  Демонстрация     наследования. #include     <iostream> using   namespace     std;

//  Определяем          базовый       класс транспортных        средств.

class        road_vehicle           {

  int  wheels;   int     passengers;

 public:

  void           set_wheels(int        num)  {           wheels           =          num;  }

  int  get_wheels()           {           return            wheels;          }

  void           set_pass(int num)  {           passengers   =          num;  }

  int  get_pass()    {           return            passengers;  }

};

//  Определяем          класс грузовиков. class     truck  :           public road_vehicle           {

  int  cargo;

 public:

  void           set_cargo(int           size)    {           cargo =          size;    }

  int  get_cargo()  {           return            cargo;            }

  void           show();

};

enum      type    {car,   van,    wagon};

//  Определяем          класс автомобилей.

class        automobile  :           public road_vehicle           {

  enum        type    car_type;

 public:

  void           set_type(type          t)         {           car_type       =          t;         }

  enum        type    get_type()    {           return            car_type;      }

  void           show();

};

void     truck::show() {

 cout    <<       "колес:         "          <<       get_wheels()           <<       "\n";

 cout    <<       "пассажиров:         "          <<       get_pass()    <<       "\n";

 cout    <<       "грузовместимость         в          кубических футах:            "          <<     cargo <<

"\n"; }

void     automobile::show() {

 cout    <<       "колес:         "          <<       get_wheels()           <<       "\n";

 cout    <<       "пассажиров:         "          <<       get_pass()    <<       "\n";

 cout    <<       "тип:  ";

 switch(get_type()) {

  case           van:    cout    <<       "автофургон\n";

   break;

  case           car:     cout    <<       "легковой\n";

   break;

  case           wagon:          cout    <<       "фура\n";

 }

}

int    main() {

 truck  t1,       t2;

 automobile   c;

 t1.set_wheels(18);

 t1.set_pass(2);

 t1.set_cargo(3200);

 t2.set_wheels(6);

 t2.set_pass(3);

 t2.set_cargo(1200);

 t1.show();

 cout    <<       "\n";

 t2.show();

 cout    <<       "\n";

 с.set_wheels(4);

 с.set_pass(6);

 с.set_type(van);

 с.show();

 return            0;

}

При         выполнении           эта      программа генерирует  такие результаты.

колес:    18 пассажиров:     2 грузовместимость     в          кубических футах:            3200

колес:    6 пассажиров:       3 грузовместимость     в          кубических футах:            1200

колес:    4 пассажиров:    6

тип:         автофургон

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

И  еще.   Обратите     внимание    на       то,       что      оба     класса           truck  и automobile  включают    функциючлен        show(),          которая        отображает информацию         об       объекте.       Эта      функция       демонстрирует     еще один аспект           объектно-ориентированного  программирования        —        полиморфизм. Поскольку каждая          функция       show()           связана         с          собственным классом,      компилятор            может           легко "понять", какую   именно         функцию нужно           вызвать        для     данного       объекта.

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

Управление   доступом   к          членам         базового     класса

Если        один  класс наследует    другой,         члены           базового      класса становятся   членами производного.           Статус            доступа        членов          базового класса           в          производном        классе           определяется спецификатором доступа,       используемым      для     наследования        базового      класса. Спецификатор доступа базового      класса           выражается            одним           из ключевых    слов:  public,           private          или     protected. Если      спецификатор доступа        не       указан,         то        по       умолчанию используется         спецификатор private, если           речь   идет   о          наследовании       типа   class.  Если   же наследуется           тип     struct,            то        при     отсутствии явно    заданного спецификатора     доступа        по       умолчанию используется         спецификатор public. Рассмотрим          рамификацию       (разветвление)      использования спецификаторов   public или private. (Спецификатор      protected     описан          в следующем            разделе.)

Если        базовый       класс наследуется         как     public-класс,           его     public-члены            становятся          publicчленами       производного       класса.

Если        базовый       класс наследуется           как     publie-класс,           все      его public-члены          становятся   publicчленами       производного       класса.          Во       всех случаях         private-члены        базового      класса           остаются закрытыми      в рамках          этого  класса           и          не       доступны     для     членов          производного. Например,  в следующей         программе public-члены          класса           base становятся   public-членами      класса           derived. Следовательно,           они     будут доступны     и          для     других          частей           программы.

#include <iostream> using     namespace   std;

class        base     {   int     i,          j;

 public:

  void           set       (int      a,         int       b)        {           i           =          a;         j           =          b;     }

  void           show()           {           cout    <<       i           <<       "          "          <<       j           <<     "\n";   }

};

class        derived          :           public base   {

  int  k;

 public:

  derived(int          x)         {           k          =          x;         }

  void           showk()         {           cout    <<       k          <<       "\n";   }

};

int            main()

{

 derived          ob(3);

 ob.set(1,       2);       //         доступ          к          членам         класса     base  ob.show();            //         доступ          к          членам         класса     base

 ob.showk(); //         доступ          к          члену класса           derived

 return            0;

}

Поскольку        функции       set()    и          show()           (члены          класса           base) унаследованы       классом       derived          как public-члены, их        можно          вызывать для     объекта        типа   derived          в          функции       main().          Поскольку члены данных         i           и          j           определены           как     private-члены,       они     остаются закрытыми в          рамках          своего класса        base.

Если        базовый       класс наследуется         как     private-класс,        его     public-члены            становятся          privateчленами     производного       класса.

Противоположностью         открытому  (public)          наследованию      является закрытое     (private). Если        базовый       класс наследуется           как     private-класс, все      его      public-члены          становятся   privateчленами     производного       класса. Например,  следующая программа не       скомпилируется, поскольку   обе     функции set()    и          show()           теперь           стали private-членами   класса           derived,        и поэтому        их        нельзя          вызывать     из        функции       main().

//  Эта      программа не     скомпилируется. #include     <iostream> using   namespace   std;

class        base     {   int     i,          j;

 public:

  void           set       (int      a,         int       b)        {           i           =          a;         j           =          b;     }

  void           show()           {           cout    <<       i           <<       "          "          <<       j           <<     "\n";   }

};

//  Открытые    члены           класса           base   теперь           становятся

//  закрытыми членами       класса           derived.

class        derived          :           private           base   {

  int  k;

 public:

  derived(int          x)         {           k          =          x;         }

  void           showk()         {           cout    <<       k          <<       "\n";   }

};

int    main() {

 derived          ob       (3);

 ob.set(1,       2);       //         Ошибка,       доступа        к          функции       set()    нет.

 ob.show();    //         Ошибка,       доступа        к          функции       show()           нет.

 return            0;

}

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

Использование        защищенных        членов

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

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

Спецификатор          доступа      protected     объявляет защищенные         члены или     обеспечивает наследование            защищенного       класса.

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

Рассмотрим    следующий            пример         программы.

#include <iostream> using     namespace   std;

class        base   {

 protected:

  int  i,          j;          //         Эти     члены           закрыты       в          классе           base,  но            доступны для        класса           derived.

 public:

  void           set(int            a,         int       b)        {           i           =          a;         j           =          b;     }

  void           show()           {           cout    <<       i           <<       "          "          <<       j           <<     "\n";   }

};

class        derived          :           public base   {

  int  k;

 public:

  //    Класс derived          имеет            доступ          к          членам         класса     base   i           и          j.

  void           setk() {           k          =          i*j;      }

  void           showk()         {           cout    <<       k          <<       "\n";   }

};

int    main() {

 derived          ob;

 ob.set(2,       3);       //         OK,     классу           derived          это      позволено.

 ob.show();    //         OK,     классу           derived          это      позволено.

 ob.setk();

 ob.showk();

 return            0;

}

Поскольку        класс base   унаследован          классом       derived          открытым способом    (т.е.    как     publicкласс),           и          поскольку   члены           i           и          j объявлены  защищенными      в          классе           base,  функция       setk() (член класса derived)        может           получать      к          ним    доступ.         Если   бы      члены           i и          j           были объявлены  в          классе base закрытыми,            то        класс derived не       мог     бы      обращаться            к          ним,   и          эта      программа не скомпилировалась          бы.

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

Если        некоторый  производный        класс используется         в          качестве базового      для     другого производного  класса,          то        любой           защищенный член   исходного   базового      класса,          который наследуется     (открытым способом)   первым        производным        классом,      может           быть  унаследован еще    раз     (в        качестве       защищенного        члена)           вторым         производным классом.      Например,  в следующей         (вполне        корректной)           программе класс derived2       имеет            законный    доступ          к          членам i       и          j.

#include <iostream> using     namespace   std;

class        base     {  protected:   int            i,     j;

 public:

  void           set(int            a,         int       b)        {           i           =          a;         j           =          b;     }

  void           show()           {           cout    <<       i           <<       "          "          <<       j           <<     "\n";   }

};

//  Члены           i           и          j           наследуются          как     protected-члены.

class        derived1:      public base   {

  int  k;

 public:

  void           setk() {           к          =          i*j;      }           //         правомерный        доступ

  void           showk()         {           cout    <<       к          <<       "\n";   }

};

//  Члены           i           и          j           наследуются          косвенно     через класс derived1.

class        derived2       :           public derived1       {

  int  m;

 public :

  void           setm()            {           m        =          i-j;       }           //         правомерный     доступ

  void           showm()       {           cout    <<       m        <<       "\n";   }

};

int    main() {

 derived1       ob1;

 derived2       ob2;

 ob1.set          (2,       3);

 ob1.show();

 ob1.setk();

 ob1.showk();

 ob2.set          (3,       4);

 ob2.show();

 ob2.setk();

 ob2.setm();

 ob2.showk();

 ob2.showm();

 return            0;

}

Если        базовый       класс наследуется           закрытым    способом    (т.е.    с использованием спецификатора      private),        защищенные          (derived)       члены этого  базового      класса           становятся закрытыми  (private)       членами производного       класса.          Следовательно,    если   бы      в          предыдущем примере       класс base   наследовался        закрытым    способом,   то        все      его члены           стали бы      privateчленами     класса           derived1,      и          в          этом случае           они     не       были бы      доступны     для     класса           derived2. (Однако члены           i           и          j           по-прежнему         остаются      доступными           для класса           derived1.)    Эта      ситуация иллюстрируется        в          следующей программе,            которая        поэтому       некорректна           (и        не скомпилируется). Все     ошибки        отмечены    в          комментариях.

//  Эта      программа не     скомпилируется. #include     <iostream> using   namespace   std;

class        base   {  protected:

  int  i,          j;

 public:

  void           set       (int      a,         int       b)        {           i           =          a;         j           =          b;     }

  void           show()           {           cout    <<       i           <<       "          "          <<       j           <<     "\n";   }

};

//  Теперь          все      элементы    класса           base   будут     закрыты //  в          рамках          класса           derived1. class     derived1       :           private           base   {

  int  k;

 public:

  //    Вызовы        этих    функций       вполне          законны,      поскольку

  //    переменные           i           и          j           являются     одновременно

  //    private-членами    класса           derived1.

  void           setk() {           k          =          i*j;      }           //         OK

  void           showk()         {           cout    <<       k          <<       "\n";   }

};

//  Доступ          к          членам         i,          j,          set()    и          show()           не     наследуется.

class        derived2       :           public derived1       {

  int  m;

 public :

  //    Неверно,      поскольку   члены           i           и          j           закрыты       в     рамках

  //    класса           derived1.

  void           setm()            {           m        =          i-j;       }           //         ошибка

  void           showm()       {           cout    <<       m        <<       "\n";   }

};

int    main() {

 derived1       ob1;

 derived2       ob2;

 ob1.set(1,     2);       //         Ошибка:       нельзя          вызывать     функцию      set().

 ob1.show(); //         Ошибка:       нельзя          вызывать     функцию      show().

 ob2.set(3,     4);       //         Ошибка:       нельзя          вызывать     функцию      set().

 ob2.show(); //         Ошибка:       нельзя          вызывать     функцию      show().

 return            0;

}

Несмотря          на       то        что      класс base   наследуется           классом       derived1 закрытым    способом,   класс derived1,      тем     не       менее,          имеет            доступ к          public-           и          protected-членам класса           base.  Однако         он       не может эту      привилегию           передать      дальше,       т.е.      вниз   по       иерархии     классов. Ключевое    слово protected—            это      часть  языка            C++.   Оно    обеспечивает механизм    защиты         определенных элементов       класса           от        модификации функциями,            которые       не       являются     членами       этого  класса, но позволяет    передавать их        "по     наследству".

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

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

class        имя_класса            {

  private-члены  protected:   protected-члены  public:

  public-члены

};

Напомню,        что      раздел          защищенных          членов          необязателен.

Использование        спецификатора  protected     для     наследования   базового     класса

Спецификатор            protected     можно          использовать         не       только          для придания    членам         класса статуса       "защищенности", но       и          для наследования        базового      класса.          Если   базовый       класс наследуется           как защищенный,        все      его      открытые     и          закрытые     члены           становятся защищенными членами           производного       класса.          Рассмотрим            пример.

//  Демонстрация       наследования        защищенного        базового     класса. #include    <iostream> using   namespace   std;

class        base   {

  int  i;

 protected:

  int  j;  public:

  int  k;

  void           seti(int           a)        {           i           =          a;         }

  int  geti()  {           return            i;          }

}; //          Наследуем  класс base   как     protected-класс. class        derived          :           protected     base   {

 public:

  void           setj(int           a)        {           j           =          a;         }           //         j           —     здесь protected-член   void          setk(int          a)        {           k          =          a;         }     //         k          —        здесь protected-член

  int  getj()  {           return            j;     }   int       getk() {           return     k;         }

};

int    main() {

 derived          ob;

 /*        Следующая            строка           неправомочна,     поскольку   функция            seti() является        protected-членом             класса           derived,        что      делает            ее недоступной    за        его      пределами.            */

 //         ob.seti           (10);

 //         cout    <<       ob.geti();       //         Неверно,      поскольку   функция       geti()  — protected-член.

 //ob.k=10;   //         Неверно,      поскольку   переменная            k          —            protectedчлен.

 //         Следующие            инструкции правомочны.

  ob.setk(10);

  cout           <<       ob.getk()       <<       '           ';

  ob.setj(12);

  cout           <<       ob.getj()        <<       '           ';

  return       0;

}

Как          отмечено     в          комментариях       к          этой   программе,            члены (класса         base)  k,         j,          seti()   и          geti() становятся   protected-членами класса           derived.        Это     означает,     что      к          ним    нельзя          получить доступ           из        кода, "прописанного"    вне     класса           derived.        Поэтому ссылки          на       эти      члены           в          функции main()     (через           объект          ob) неправомочны.

Об          использовании     спецификаторов            public,           protected     и   private

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

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

Если        базовый       класс наследуется           с          использованием  ключевого слова public,           его      publicчлены            становятся   public-членами      производного класса,          а          его      protected-члены   —        protectedчленами            производного класса.

Если        базовый       класс наследуется           с          использованием  спецификатора protected,    его      publicи          protected-члены   становятся   protected-членами производного       класса.

Если        базовый       класс наследуется           с          использованием  ключевого слова private,         его      public-           и protected-члены           становятся   private-членами       производного       класса.

Во всех    случаях         private-члены        базового      класса           остаются закрытыми в          рамках          этого класса           и          не       наследуются.

По            мере  увеличения             вашего          опыта            в          программировании на       C++     применение спецификаторов            public,           protected     и          private не       будет доставлять  вам    хлопот.         А         пока, если   вы еще         не       уверены в          правильности        использования      того    или     иного спецификатора     доступа, напишите     простую       экспериментальную       программу и          проанализируйте полученные результаты.

Наследование          нескольких базовых       классов

Производный класс может           наследовать           два     или     больше         базовых классов.       Например,  в этой            короткой     программе класс derived наследует    оба     класса           base1 и          base2.

//  Пример        использования      нескольких базовых       классов. #include <iostream> using   namespace   std;

class        base1 {  protected:

  int  x;

 public:

  void           showx()         {           cout    <<       x          <<       "\n";   }

};

class        base2 {  protected:

  int  y;

 public:

  void           showy()         {           cout    <<       у          <<       "\n";   }

};

//  Наследование       двух   базовых       классов.

class        derived:        public base1,            public base2 {  public:

  void           set(int            i,          int       j)         {           x          =          i;          у          =          j;     }

};

int    main() {

 derived          ob;

 ob.set (10,    20);    //         член   класса           derived  ob.showx();     //         функция     из        класса           base1  ob.showy();        //         функция       из        класса     base2

 return            0;

}

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

Конструкторы,     деструкторы      и          наследование

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

Когда   выполняются      конструкторы    и          деструкторы

Базовый            и/или            производный        класс может           содержать конструктор           и/или            деструктор. Важно          понимать     порядок,      в котором       выполняются         эти      функции       при     создании     объекта производного        класса           и          его      (объекта)     разрушении. Рассмотрим короткую     программу. #include       <iostream> using   namespace   std;

class        base   {

 public:

  base()        {           cout    <<"Создание          basе-объекта.\n";             }

  ~base()     {           cout    <<"Разрушение     bаsе-объекта.\n";             }

};

class        derived:        public base   {

 public:

  derived()  {           cout    <<"Создание          derived-объекта.\n";       }

  ~derived()           {           cout    <<"Разрушение     derived-объекта.\n";       }

};

int    main() {

 derived          ob;

 //         Никаких       действий,    кроме           создания     и          разрушения     объекта        ob.

 return            0;

}

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

Создание          base-объекта.

Создание          derived-объекта.

Разрушение     derived-объекта.

Разрушение     base-объекта.

Судя        по       результатам,          сначала        выполняется          конструктор класса           base,  а          за        ним    — конструктор      класса           derived.        Затем (по      причине       немедленного       разрушения            объекта        ob       в          этой программе)            вызывается             деструктор  класса           derived,        а          за        ним —        деструктор  класса           base.

Конструкторы         вызываются         в          порядке       происхождения    классов, а          деструкторы       —        в обратном           порядке.

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

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

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

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

#include <iostream> using     namespace   std;

class        base   {

 public:

  base()        {           cout    <<"Создание          base-объекта.\n";             }

  ~base(){    cout    <<"Разрушение     base-объекта.\n";             }

};

class        derived1       :           public base   {

 public:

  derived1()           {           cout    <<"Создание          derived1-объекта.\n";     }

  ~derived1(){       cout    <<"Разрушение     derived1-объекта.\n";     }

};

class        derived2:      public derived1       {

 public:

  derived2()           {           cout    <<"Создание          derived2-oбъeктa.\n";     }

  ~derived2(){       cout    <<"Разрушение     derived2-oбъeктa.\n";     }

};

int    main() {

 derived2       ob;

 //         Создание     и          разрушение            объекта        ob.

 return            0;

}

отображаются            такие результаты:

Создание          base-объекта.

Создание          derived1-объекта.

Создание          derived2-oбъeктa.

Разрушение     derived2-oбъeктa.

Разрушение     derived1-объекта.

Разрушение     base-объекта.

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

#include <iostream> using     namespace   std;

class        base1 {

 public:

  base1()     {           cout    <<"Создание          base1-объекта.\n";           }

  ~base1(){ cout    <<"Разрушение     base1-объекта.\n";           }

};

class        base2 {

 public:

  base2()     {           cout    <<"Создание          bаsе2-объекта.\n";           }

  ~base2(){ cout    <<"Разрушение     basе2-объекта.\n";           }

};

class        derived:        public base1,            public base2 {

 public:

  derived()  {           cout    <<"Создание          derived-объекта.\n";       }

  ~derived(){          cout    <<"Разрушение     derived-объекта.\n";       }

};

int    main() {

 derived          ob;

 //         Создание     и          разрушение            объекта        ob.

 return            0;

}

генерируются такие результаты:

Создание          base1-объекта.

Создание          basе2-объекта.

Создание          derived-объекта.

Разрушение     derived-объекта.

Разрушение     basе2-объекта.

Разрушение     base1-объекта.

Как          видите,         конструкторы        вызываются           в          порядке происхождения    их        классов,       слева направо,     в          порядке       их        задания в          списке           наследования        для     класса           derived.        Деструкторы вызываются            в          обратном    порядке,      справа          налево.         Это     означает, что      если   бы      класс base2 стоял перед            классом       base1 в          списке класса           derived,        т.е.      в          соответствии          со        следующей инструкцией:

class        derived:        public base2,            public base1 {}; то     результаты выполнения           предыдущей          программы     были бы      такими:

Создание          basе2-объекта.

Создание          base1-объекта.

Создание          derived-объекта.

Разрушение     derived-объекта.

Разрушение     base1-объекта.

Разрушение     base2-объекта.

Передача        параметров         конструкторам базового     класса

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

конструктор_производного_класса     (список_аргументов)      :

 base1 (список_аргументов),  base2     (список_аргументов),

 .

 .

 .

 baseN             {список_аргументов);

{

 тело   конструктора         производного       класса

}

Здесь     элементы    base1-baseN           означают     имена           базовых       классов,     наследуемых производным   классом.      Обратите     внимание    на       то,       что     объявление            конструктора производного   класса           отделяется  от     списка           базовых       классов        двоеточием,           а          имена           базовых классов разделяются          запятыми    (в        случае           наследования     нескольких базовых       классов). Рассмотрим    следующую            простую     программу.

#include <iostream> using     namespace   std;

class        base   {  protected:

  int  i;

 public:

  base           (int      x)         {

i          =         x;

   cout      <<       "Создание   bаsе-объекта.\n";

  }

  ~base()     {cout  <<       "Разрушение          base1-объекта.\n";}

};

class        derived:        public base   {

  int  j;

 public:

  //    Класс derived          использует  параметр     x,         а          параметр     у передается конструктору         класса           base.

  derived(int          x,         int       y):       base(y){

j         =         x;

   cout      <<       "Создание   derived-объекта.\n";

  }

  ~derived()           {           cout    <<       "Разрушение          derived-объекта.\n";       }

  void           show()           {           cout    <<       i           <<       "          "          <<       j           <<     "\n";   }

};

int    main() {

 derived          ob(3,  4);

 ob.show();    //         отображает             числа 4          3

 return            0;

}

Здесь      конструктор           класса           derived          объявляется           с          двумя параметрами,        х          и          у.         Однако конструктор       derived()       использует только          параметр     х,         а          параметр     у          передается конструктору base(). В          общем          случае           конструктор           производного       класса           должен объявлять   параметры, которые      принимает  его      класс,            а          также            те, которые       требуются   базовому     классу.          Как     показано в  предыдущем примере,     любые          параметры,            требуемые  базовым      классом, передаются            ему    в списке       аргументов базового      класса,          указываемого после двоеточия.

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

#include <iostream> using     namespace   std;

class        base1 {

 protected:

  int  i;

 public:

  base1(int  x)         {

   i  =          x;

   cout      <<       "Создание   base1-объекта.\n";

  }

  ~base1()   {           cout    <<       "Разрушение          base1-объекта.\n";           }

};

class        base2 {  protected:

  int  k;

 public:

  base2(int  x)         {

   k =          x;

   cout      <<       "Создание   basе2-объекта.\n";

  }

  ~base2()   {           cout    <<       "Разрушение          basе2-объекта.\n";           }

};

class        derived:        public base1,            public base2 {

  int  j;

 public:

  derived(int          x,         int       y,         int       z):       base1(y),      base2(z){

   j =          x;

   cout      <<       "Создание   derived-объекта.\n";

  }

  ~derived()           {           cout    <<       "Разрушение          derived-объекта.\n";       }

  void           show()           {           cout    <<       i           <<       "          "<<     j           <<       "     "<<     k          <<       "          \          n";      }

};

int    main() {

 derived          ob(3,  4,        5);

 ob.show();    //         отображает             числа 4          3          5

 return            0;

}

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

#include <iostream> using     namespace   std;

class        base1 {

 protected:

  int  i;

 public:

  base1(int  x)         {

   i=x;

   cout      <<       "Создание   base1-объекта.\n";

  }

  ~base1()   {           cout    <<       "Разрушение          base1-объекта.\n";           }

};

class        base2 {

 protected:

  int  k;

 public:

  base2(int  x)         {

   k =          x;

   cout      <<       "Создание   basе2-объекта.\n";

  }

  ~base2()   {           cout    <<       "Разрушение          basе2-объекта.\n";           }

};

class        derived:        public base1,            public base2 {

 public:

  /*   Конструктор           класса           derived          не       использует  параметров, но должен  объявить      их,      чтобы            передать      конструкторам      базовых классов.   */

  derived(int          х,         int       у):       base1(х),       base2(у){

   cout      <<       "Создание   derived-объекта.\n";

  }

  ~derived()           {           cout    <<       "Разрушение          derived-объекта.\n";       }

  void           show()           {           cout    <<       i           <<       "          "          <<       k          <<     "\n";   }

};

int    main() {

 derived          ob(3,  4);

 ob.show();    //         отображает             числа 3          4

 return            0;

}

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

class        derived:        public base   {

  int  j;

 public:

  //    Класс derived          использует  оба     параметра   x          и          у          а            также передает    их        классу           base.

  derived(int          х,         int       у):       base(х,           у){

   j =          х*у;

   cout      <<       "Создание   derived-объекта.\n";

  }

//  .           .           .

};

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

Предоставление    доступа

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

Поскольку   они     все      еще    используются        в          С++-коде,    мы      уделим     внимание    этой   теме. Объявление           доступа        имеет            такой формат:

имя_базового_класса::член;

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

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

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

class        base   {

 public:

  int  j;          //         public-доступ         в          классе           base

};

//  Класс base   наследуется           как     private-класс.

class        derived:        private           base   {

 public:

  //    Вот     использование      объявления            доступа:

  base::j;      //         Теперь          член   j           снова стал    открытым.

//  .           .           .

};

Поскольку        класс base   наследуется           классом       derived          закрытым способом,   его      publicпеременная            j           становится  private-переменной класса           derived.        Однако         включение  этого объявления            доступа

base::j; в           классе           derived          под    спецификатором  public     восстанавливает   public-статус           члена            j.

Объявление     доступа        можно          использовать         для     восстановления прав   доступа        public-           и protected-членов.         Однако         для     изменения (повышения           или     понижения)            статуса          доступа        его использовать нельзя.         Например,  член, объявленный         закрытым    в          базовом       классе, нельзя сделать     открытым    в          производном.       (Разрешение          подобных вещей           разрушило  бы инкапсуляцию!)

Использование          объявления            доступа        иллюстрируется   в     следующей программе.

#include <iostream> using     namespace   std;

class        base   {

  int  i;          //         private-член            в          классе           base

 public:

  int  j,          k;

  void           seti      (int      x)         {           i           =          x;         }

  int  geti()  {           return            i;          }

};

//  Класс base   наследуется           как     private-класс.

class        derived:        private           base   {

 public:

  /*   Следующие            три     инструкции переопределяют  privateнаследование            класса           base   и          восстанавливают  public-статус           доступа для            членов          j,          seti()   и          geti(). */

  base::j;     //         Переменная           j           становится  снова public-членом,       а переменная            k          остается       закрытым    членом.

  base::seti;            //         Функция       seti()   становится  public-членом.

  base::geti;            //         Функция       geti()  становится  public-членом.

  //    base::i;           //         Неверно:      нельзя          повышать    уровень        доступа.

  int  а;         //         public-член

};

int    main() {

 derived          ob;

 //ob.i =          10;      //         Неверно,      поскольку   член   i                       закрыт          в            классе derived.

 ob.j     =          20;      //         Допустимо, поскольку   член   j           стал                открытым            в классе       derived.

 //ob.k            =30;   //         Неверно,      поскольку   член   k                      закрыт          в            классе derived.

 ob.a    =          40;      //         Допустимо, поскольку   член   а                      открыт          в            классе derived.

 ob.seti(10);

 cout    <<       ob.geti()        <<       "          "          <<       ob.j     <<       "          "          <<     ob.a;

 return            0;

}

Обратите           внимание    на       то,       как     в          этой   программе используются объявления            доступа        для восстановления        статуса          public у          членов j,          seti()   и          geti(). В          комментариях       отмечены    и          другие ограничения, связанные   со        статусом      доступа.

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

Чтение           С++-графов            наследования

Иногда   иерархии     С++-классов           изображаются       графически,            что облегчает    их        понимание. Но     порой            различные  способы       изображения графов          наследования        классов        вводят          новичков     в заблуждение. Рассмотрим,          например,   ситуацию,    в          которой        класс А         наследуется классом       В, который  в          свою  очередь       наследуется           классом       С. Используя   стандартную          С++-систему обозначений,      эту      ситуацию     можно отобразить  так:

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

Из этого  графа следует,       что      класс Е          выведен       из        обоих            классов С          и          D.        (Другими     словами, класс     Е          имеет            два     базовых класса           С          и          D.)      При    этом   класс С          выведен       из        класса           А, а          класс D         — из  класса           В.        Несмотря     на       то        что      направление стрелок        может           вас      обескураживать   на       первых порах,       все      же лучше           познакомиться      с          этим   стилем          графических           обозначений, поскольку   он широко  используется         в          книгах,          журналах     и документации       на       компиляторы.

Виртуальные          базовые       классы

При         наследовании       нескольких базовых       классов        в          С++-программу может           быть  внесен элемент    неопределенности.         Рассмотрим            эту некорректную       программу.

/* Эта      программа содержит     ошибку         и          не       скомпилируется.

*/ #include     <iostream> using     namespace   std; class        base   {  public:

  int  i;

};

//  Класс derived1       наследует    класс base.

class        derived1       :           public base   {           public:           int       j;};

//  Класс derived2       наследует    класс base. class    derived2     :           public base   {           public:           int       k;};

/* Класс derived3       наследует    оба     класса           derived1       и          derived2.      Это означает,     что      в          классе           derived3       существует  две     копии            класса            base!

*/ class   derived3       :           public derived1,      public derived2       {

 public:

  int  sum;

};

int    main() {

 derived3       ob;

 ob.i     =          10;      //         Это     и          есть    неоднозначность:            какой именно            член   i имеется      в          виду???

 ob.j     =          20;

 ob.k    =          30;

 //И      здесь тоже  неоднозначность с          членом         i.

 ob.sum          =          ob.i     +          ob.j     +          ob.k;

 //         И         здесь тоже  неоднозначность с          членом         i.

 cout    <<       ob.i     <<       "          ";

 cout    <<       ob.      j           <<       "          "          <<       ob.k    <<       "          ";

 cout    <<       ob.sum;

 return            0;

}

Как          отмечено     в          комментариях       этой   программы,           оба     класса derived1       и          derived2       наследуют класс  base.  Но       класс derived3 наследует    как     класс derived1,      так      и          класс             derived2.      В результате в          объекте        типа   derived3       присутствуют         две     копии            класса base,  поэтому,      например, в           таком            выражении

ob.i          =          20;

не ясно,  на       какую            именно         копию           члена            i           здесь дана ссылка:         на       член, унаследованный  от класса     derived1       или     от        класса derived2?     Поскольку   в          объекте        ob       присутствуют         обе     копии класса base,  то        в          нем    существуют             и          два     члена            ob.is!  Потому-то   эта инструкция и          является наследственно           неоднозначной     (существенно неопределенной).

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

/* Эта      программа использует  оператор     разрешения            контекста     для выбора         нужного       члена            i.

*/ #include     <iostream> using     namespace   std; class        base   {  public:

  int  i;

};

//  Класс derived1       наследует    класс base.

class        derived1       :           public base   {           public:           int       j;};

//  Класс derived2       наследует    класс base. class    derived2     :           public base   {           public:           int       k;};

/* Класс derived3       наследует    оба     класса           derived1       и          derived2.      Это означает,     что      в          классе           derived3       существует  две     копии            класса            base!

*/ class   derived3       :           public derived1,      public derived2       {

 public:

  int  sum;

};

int    main() {

 derived3       ob;

 ob.derived1::i          =          10;      //         Контекст       разрешен,   используется     член   i класса         derived1.

 ob.j     =          20;

 ob.k    =          30;

 //         Контекст       разрешен    и          здесь.

 ob.sum          =          ob.derived1::i         +          ob.j     +          ob.k;

 //         Неоднозначность             ликвидирована     и          здесь.

 cout    <<       ob.derived1::i         <<       "          ";

 cout    <<       ob.j     <<       "          "          <<       ob.k    <<       "          ";

 cout    <<       ob.sum;

 return            0;

}

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

Применение    оператора   "::"      позволяет    программе "ручным      способом" выбрать       версию класса      base   (унаследованную            классом       derived1).    Но после такого           решения      возникает интересный  вопрос:         а          что,    если   в действительности            нужна           только          одна  копия            класса           base? Можно          ли       каким-то      образом       предотвратить       включение  двух   копий в          класс derived3?     Ответ, как,  наверное,    вы       догадались,           положителен. Это     решение      достигается            с          помощью виртуальных           базовых классов.

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

Для          иллюстрации         этого  процесса      приведем    еще    одну  версию предыдущей          программы. На     этот    раз     класс derived3       содержит     только одну  копию           класса           base.

//  Эта      программа использует  виртуальные          базовые       классы. #include <iostream> using   namespace   std;

class        base   {

 public:

 int       i;

};

//  Класс derived1       наследует    класс base   как     виртуальный. class     derived1       :           virtual            public base   {           public:           int       j;};

//  Класс derived2       наследует    класс base   как     виртуальный. class     derived2       :           virtual            public base   {           public:           int       k;};

/* Класс derived3       наследует    оба     класса           derived1       и          derived2.      На этот    раз     он       содержит     только          одну  копию           класса           base.

*/ class   derived3       :           public derived1,      public derived2       {

 public:

  int  sum;

};

int    main() {

 derived3       ob;

 ob.i     =          10;      //         Теперь          неоднозначности             нет.

 ob.j     =          20;

 ob.k    =          30;

 //         Теперь          неоднозначности             нет.

 ob.sum          =          ob.i     +          ob.j     +          ob.k;

 //         Теперь          неоднозначности,            нет.

 cout    <<       ob.i     <<       "          ";

 cout    <<       ob.j     <<       "          "          <<       ob.k    <<       "          ";

 cout    <<       ob.sum;

 return            0;

}

Как          видите,         ключевое    слово virtual            предваряет остальную   часть спецификации наследуемого            класса.          Теперь          оба     класса           derived1 и          derived2       наследуют  класс base   как виртуальный,            и          поэтому       при любом          множественном   их        наследовании       в          производный        класс будет включена    только          одна  его      копия.           Следовательно,    в          классе derived3       присутствует          лишь одна  копия            класса           base,  а инструкция ob.i     =          10       теперь           совершенно           допустима  и          не содержит никакой          неоднозначности.

И  еще.   Даже если   оба     класса           derived1       и          derived2       задают класс base   как     virtual-класс,          он по-прежнему   присутствует          в          объекте любого         типа.  Например,  следующая последовательность      инструкций вполне допустима.

//  Определяем          класс типа   derived1.

derived1            myclass;

myclass.i            =          88;

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

Глава     15:  Виртуальные      функции       и      полиморфизм

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

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

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

Указатели     на       производные         типы

Указатель       на       базовый       класс может         ссылаться  на       любой объект,       выведенный           из        этого базового   класса.

Фундаментом для     динамического     полиморфизма     служит          указатель    на базовый       класс. Указатели  на       базовые       и          производные         классы связаны        такими          отношениями,       которые       не свойственны    указателям других          типов.           Как     было отмечено     выше в          этой   книге,            указатель одного          типа,  как     правило,      не       может           указывать    на       объект другого        типа.  Однако         указатели    на базовые классы          и          объекты производных         классов        —        исключения            из        этого  правила.      В          C++ указатель     на       базовый       класс также            можно          использовать         для ссылки          на       объект          любого         класса, выведенного      из        базового. Например,  предположим,      что      у          нас     есть    базовый       класс B_class          и класс D_class,        который       выведен       из        класса           B_class.         В          C++ любой           указатель,   объявленный         как указатель        на       класс B_class, может           быть  также            указателем на       класс D_class.        Следовательно, после этих    объявлений

B_class   *р;      //         указатель    на       объект          типа   B_class

B_class   B_ob; //         объект          типа   B_class

D_class   D_ob; //         объект          типа   D_class обе          следующие инструкции абсолютно     законны:

р   =          &В_ob;          //         р          указывает    на       объект          типа   B_class

р  =          &D_ob;         /*        р          указывает    на       объект          типа   D_class,            который является            объектом,   выведенным          из        класса           B_class.            */

В   этом   примере      указатель    р          можно          использовать         для     доступа ко       всем  элементам  объекта D_ob,       выведенным          из        объекта        В_ob. Однако         к          элементам, которые       составляют специфическую    "надстройку" (над   базой,           т.е.      над     базовым      классом       B_class)         объекта        D_ob, доступ           с          помощью    указателя    р          получить      нельзя.

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

// Использование     указателей  на       базовый       класс для     доступа        к объектам     производных         классов.

#include <iostream> #include <cstring> using     namespace   std;

class        B_class          {    char         author[80];

  public:

   void      put_author(char    *s)      {           strcpy(author,         s);        }

   void      show_author()       {           cout    <<       author           <<       "\n";   }

};

class        D_class          :           public B_class          {

  char           title     [80];

 public:

  void           put_title(char          *num)           {           strcpy(title,  num);}

  void           show_title() {

   cout      <<       "Название:  ";

   cout      <<       title     <<       "\n";

  }

};

int    main() {

 B_class           *p;

 B_class           B_ob;

 D_class          *dp;

 D_class          D_ob;

 p          =          &B_ob;          //         адрес            объекта        базового      класса

 //         Доступ          к          классу           B_class          через указатель.

 p->put_author("Эмиль   Золя");

 //         Доступ          к          классу           D_class          через "базовый"   указатель.

 р          =          &D_ob;

 p->put_author("Уильям Шекспир");

 //         Покажем,    что      каждый        автор относится    к                      соответствующему объекту.

 B_ob.show_author();

 D_ob.show_author();

 cout    <<       "\n";

 /*        Поскольку   функции       put_title()     и          show_title() не       являются частью          базового      класса,          они     недоступны            через "базовый"   указатель р,         и          поэтому       к          ним    нужно           обращаться            либо непосредственно,            либо, как     показано     здесь,            через указатель    на производный        тип.

 */

 dp       =          &D_ob;

 dp->put_title("Буря");

 p->show_author();            //         Здесь можно          использовать         либо  указатель р,        либо  указатель    dp.

 dp->show_title();

 return            0;

}

При         выполнении           эта      программа отображает             следующие     результаты.

Эмиль    Золя

Уильям  Шекспир

Уильям  Шекспир

Название:         Буря

В   этом   примере      указатель    р          определяется        как     указатель    на класс B_class.         Но       он       может также          ссылаться    на       объект производного       класса           D_class,        причем         его      можно          использовать для     доступа        только          к          тем     элементам  производного       класса, которые       унаследованы       от базового.           Однако         следует        помнить,      что через "базовый"   указатель    невозможно           получить доступ   к          тем     членам, которые       специфичны           для     производного       класса.          Вот     почему         к функции show_title()      обращение реализуется            с          помощью    указателя    dp, который       является       указателем на       производный        класс.

Если        вам    нужно           с          помощью    указателя    на       базовый       класс получить      доступ          к          элементам, определенным    производным        классом, необходимо           привести      этот    указатель    к          типу   указателя на производный        тип.    Например,  при     выполнении           этой   строки           кода действительно      будет вызвана       функция       show_title() объекта        D_ob:

((D_class            *)р)->show_title();

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

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

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

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

Ссылки            на       производные         типы

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

Виртуальные          функции

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

Виртуальная функция       —        это      функция,      которая        объявляется           в базовом       классе           с использованием           ключевого  слова virtual            и переопределяется           в          одном           или     нескольких производных        классах. Таким            образом,     каждый        производный        класс может           иметь собственную          версию         виртуальной          функции.     Интересно   рассмотреть ситуацию,    когда виртуальная          функция       вызывается             через указатель (или   ссылку)        на       базовый       класс.            В          этом случае            C++ определяет,           какую            именно         версию         виртуальной          функции необходимо           вызвать, по типу объекта,       адресуемого          этим   указателем. Причем        следует        иметь            в          виду, что      это решение           принимается во       время            выполнения           программы.           Следовательно,    при     указании на различные        объекты       будут вызываться и          различные  версии виртуальной          функции.     Другими словами,           именно         по       типу адресуемого          объекта        (а        не       по       типу   самого          указателя) определяется, какая      версия          виртуальной          функции       будет выполнена. Таким            образом,     если   базовый       класс содержит    виртуальную          функцию и          если   из        этого  базового      класса           выведено    два     (или   больше) других класса,          то        при     адресации   различных  типов объектов      через указатель    на базовый класс      будут выполняться          и          различные  версии          виртуальной функции.     Аналогичный         механизм работает         и          при     использовании ссылки          на       базовый       класс.

Чтобы   объявить    функцию     виртуальной,       достаточно         предварить ее        объявление ключевым словом         virtual.

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

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

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

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

//  Пример        использования      виртуальных     функций. #include            <iostream> using   namespace     std;

class        base   {

 public:

  virtual       void    who() {

   //           объявление            виртуальной          функции

   cout      <<       "Базовый     класс.\n";

  }

};

class        first_d            :           public base   {

 public:

  void           who() {

   //           Переопределение           функции       who() для

   //           класса           first_d.

   cout      <<       "Первый      производный        класс.\n";

  }

};

class        second_d      :           public base   {

 public:

  void           who() {

   //           Переопределение           функции     who() для    //         класса           second_d.

   cout      <<       "Второй        производный        класс.\n";

  }

};

int    main() {

 base    base_obj;

 base    *p;

 first_d            first_obj;

 second_d      second_obj;

 p          =          &base_obj;

 p->who();     //         доступ          к          функции       who() класса           base

 p          =          &first_obj;

 p->who();     //         доступ          к          функции       who() класса           first_d

 p          =          &second_obj;

 p->who();     //         доступ          к          функции       who() класса           second_d

 return            0;

}

При         выполнении           эта      программа генерирует  такие результаты.

Базовый            класс.

Первый производный        класс.

Второй   производный        класс.

Теперь   рассмотрим            код     этой   программы подробно,   чтобы            понять,     как     она     работает.

В   классе           base   функция       who() объявлена   виртуальной.         Это     означает, что      ее        можно переопределить           в          производном        классе           (в классе,          выведенном          из        base). И         она     действительно переопределяется           в          обоих            производных         классах         first_d            и second_d.    В          функции       main() объявляются        четыре          переменные: base_obj      (объект         типа   base), p          (указатель   на       объект          класса base), а          также            два     объекта        first_obj        и          second_obj  двух   производных классов        first_d            и          second_d соответственно.        Затем            указателю   p присваивается       адрес            объекта        base_obj,     и          вызывается функция who().           Поскольку   функция       who() объявлена   виртуальной,         C++     во время            выполнения программы          определяет,           к          какой именно версии          функции       who() здесь нужно           обратиться, причем        решение принимается          путем            анализа        типа   объекта,       адресуемого указателем p.        В данном     случае           р          указывает    на       объект          типа base,  поэтому       сначала        выполняется          та        версия функции   who(), которая        объявлена   в          классе           base.  Затем            указателю   р присваивается       адрес объекта       first_obj.       Вспомните, что      с          помощью указателя    на       базовый       класс можно          обращаться к        объекту        любого его      производного       класса.          Поэтому,      когда функция       who() вызывается во второй    раз,    C++     снова выясняет      тип     объекта,       адресуемого указателем р,        и,        исходя          из        этого типа,  определяет,           какую версию         функции       who() нужно           вызвать.       Поскольку   р          здесь указывает на         объект          типа   first_d,          то        выполняется          версия функции       who(),           определенная       в          классе           first_d. Аналогично после присвоения            р          адреса          объекта        second_obj  вызывается версия          функции who(),    объявленная          в          классе           second_d.

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

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

first_obj.who();

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

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

Поначалу          может           показаться, что      переопределение            виртуальной функции       в          производном классе      представляет         собой            специальную форму           перегрузки функций.     Но       это      не       так.     В действительности        мы имеем           дело  с          двумя           принципиально    разными      процессами. Прежде всего,       версии          перегруженной     функции       должны       отличаться друг   от        друга типом            и/или количеством         параметров,           в          то время            как     тип     и          количество параметров            у          версий переопределенной         функции       должны       в          точности      совпадать.  И         в самом           деле, прототипы виртуальной           функции       и          ее переопределений            должны       быть  абсолютно  одинаковыми.      Если прототипы будут различными,         то        такая функция       будет попросту      считаться перегруженной,   и ее    "виртуальная      сущность" утратится.    Кроме           того, виртуальная           функция       должна        быть членом          класса,          для     которого она     определяется,       а          не       его      "другом".    Но       в          то        же       время виртуальная           функция       может           быть  "другом"     другого        класса.          И еще:   функциям    деструкторов разрешается      быть  виртуальными,     а          функциям конструкторов       —        нет.

Наследование          виртуальных       функций

Атрибут         virtual            передается           "по     наследству".

Если        функция       объявляется           как     виртуальная,          она     остается такой независимо            от        того, через  сколько        уровней       производных классов        она     может           пройти.         Например,  если   бы      класс second_d     был выведен       из        класса           first_d,          а          не       из        класса           base,  как показано     в          следующем примере,   то        функция       who() по-прежнему оставалась  бы      виртуальной,         и          механизм    выбора соответствующей версии          по-прежнему         работал        бы      корректно.

//  Этот    класс выведен       из        класса           first_d,          а          не       из        base.

class        second_d      :           public first_d            {

 public:

  void           who() {

   //           Переопределение           функции       who() для     класса           second_d.

   cout      <<       "Второй        производный        класс.\n";

  }

};

Если        производный        класс не       переопределяет   виртуальную          функцию, то        используется функция, определенная       в          базовом       классе. Например,  проверим,   как     поведет        себя   версия предыдущей      программы, если   в          классе           second_d      не       будет переопределена  функция       who(). #include        <iostream> using   namespace   std;

class        base   {

 public:

  virtual       void    who() {

   cout      <<       "Базовый     класс.\n";

  }

};

class        first_d            :           public base   {

 public:

  void           who() {

   cout      <<       "Первый      производный        класс.\n";

  }

};

class        second_d      :           public base   {

 //         Функция       who() здесь не       определена            вообще.

};

int    main() {

 base    base_obj;

 base    *p;

 first_d            first_obj;

 second_d      second_obj;

 p          =          &base_obj;

 p->who();     //         доступ          к          функции       who() класса           base

 р          =          &first_obj;

 p->who();     //         доступ          к          функции       who() класса           first_d

 р          =          &second_obj;

 p->who();     /*        Здесь выполняется          обращение к          функции       who() класса           base,  поскольку   в          классе           second_d      она     не            переопределена. */

 return            0;

}

Теперь   при     выполнении           этой   программы на       экран выводится     следующее.

Базовый            класс.

Первый производный        класс.

Базовый            класс.

Как          подтверждают      результаты выполнения           этой   программы, поскольку   функция       who() не переопределена        классом       second_d,    то        при ее        вызове          с          помощью    инструкции p->who()      (когда член р указывает    на       объект          second_obj) выполняется          та        версия          функции who(),           которая определена      в          классе           base.

Следует иметь            в          виду, что      наследуемые         свойства       спецификатора virtual            являются иерархическими.     Поэтому,      если   предыдущий         пример изменить     так,     чтобы            класс second_d был        выведен       из        класса first_d,          а          не       из        класса           base,  то        при     обращении к          функции who() через объект          типа   second_d      будет вызвана       та        ее        версия, которая        объявлена   в          классе           first_d, поскольку            этот    класс является "ближайшим"       (по      иерархическим     "меркам")   к          классу second_d,  а          не функция       who() из        тела   класса           base.  Эти     иерархические      зависимости демонстрируются            на       примере      следующей программы.

#include <iostream> using     namespace   std;

class        base   {

 public:

  virtual       void    who() {

   cout      <<       "Базовый     класс.\n";

  }

};

class        first_d            :           public base   {

 public:

  void           who() {

   cout      <<       "Первый      производный        класс.\n";

  }

};

// Класс second_d      теперь           выведен       из        класса           first_d,          а          не            из класса     base.

class        second_d      :           public first_d            {

 //         Функция       who() не       определена.

};

int            main()

{

 base    base_obj;

 base    *p;

 first_d            first_obj;

 second_d      second_obj;

 р          =          &base_obj;

 p->who();     //         доступ          к          функции       who() класса           base

 р          =          &first_obj;

 p->who();     //         доступ          к          функции       who() класса           first_d

 р          =          &second_obj;

 p->who();     /*        Здесь выполняется          обращение к          функции       who() класса           first_d,          поскольку   в          классе           second_d      она     не            переопределена.

*/

 return            0;

}

Эта          программа генерирует  такие результаты.

Базовый            класс.

Первый производный        класс.

Первый производный        класс.

Как          видите,         класс second_d      теперь           использует  версию         функции who(),           которая определена      в          классе           first_d,          поскольку   она ближе           всех    в          иерархической      цепочке        классов.

Зачем   нужны         виртуальные       функции

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

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

Теперь   у          вас      может           возникнуть вопрос:         почему         же       так важен            общий           интерфейс   со множеством     реализаций?          Ответ снова возвращает             нас     к          основной     побудительной     причине возникновения объектно-ориентированного  программирования:       такой интерфейс   позволяет программисту       справляться            со        все      возрастающей       сложностью программ.  Например,  если корректно    разработать            программу,            то можно          быть  уверенным в          том,   что      ко       всем  объектам, выведенным из        базового      класса,          можно          будет получить      доступ          единым (общим        для     всех) способом,    несмотря     на       то,       что      конкретные действия      у          одного          производного       класса           могут отличаться от действий      у          другого.       Это     означает,     что      программисту       придется помнить       только один           интерфейс,  а          не       великое        их        множество. Кроме           того,  производный        класс волен использовать       любые          или     все функции,     предоставленные            базовым      классом.      Другими словами, разработчику         производного       класса           не       нужно           заново изобретать  элементы,   уже имеющиеся   в          базовом       классе.          Более того,  отделение   интерфейса             от        реализации            позволяет создавать библиотеки            классов,       написанием            которых       могут заниматься сторонние организации.          Корректно   реализованные     библиотеки            должны предоставлять       общий интерфейс,           который       программист         может использовать         для     выведения  классов        в соответствии       со        своими конкретными         потребностями.    Например,  как     библиотека             базовых классов         Microsoft      (Microsoft    Foundation  Classes           —        MFC), так      и более            новая            библиотека классов       .NET   Framework  Windows      Forms поддерживают     Windows-программирование. Использование  этих    классов позволяет    писать           программы,           которые       могут унаследовать множество функций,     нужных        любой           Windows-программе.     Вам    понадобится лишь добавить     в нее  средства,     уникальные            для     вашего          приложения. Это     —        большое      подспорье  при программировании           сложных      систем.

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

Чтобы     вы       могли            получить      представление      о          силе   принципа "один            интерфейс,  множество методов",    рассмотрим            следующую короткую     программу.            Она    создает         базовый       класс figure, предназначенный            для     хранения     размеров     различных  двумерных объектов и          вычисления            их площадей.        Функция       set_dim()      является стандартной           функцией-членом,          поскольку   эта операция         подходит     для всех    производных         классов.       Однако         функция       show_area() объявлена как      виртуальная,          так      как     методы        вычисления            площади различных  объектов      будут разными. Программа    использует  базовый       класс figure для     выведения  двух   специальных          классов rectangle и          triangle.

#include <iostream> using     namespace   std;

class        figure {  protected:   double    x,         y;

 public:

  void           set_dim(double      i,          double           j)         {

   x =          i;

   у =          j;

  }

  virtual       void    show_area()            {

   cout      <<       "Для  этого  класса           выражение вычисления            ";

   cout      <<       "площади    не       определено.\n";

  }

};

class        triangle          :           public figure {

 public:

  void           show_area()            {

   cout      <<       "Треугольник         с          высотой       ";

   cout      <<       x          <<       "          и          основанием            "          <<       у;

   cout      <<       "          имеет            площадь      ";

   cout      <<       х          *          0.5      *          у          <<       ".\n";

  }

};

class        rectangle      :           public figure {

 public:

  void           show_area()            {

   cout      <<       "Прямоугольник  с          размерами ";

   cout      <<       x          <<       "          x          "          <<       у;

   cout      <<       "          имеет            площадь      ";

   cout      <<       х          *          у          <<       ".\n";

  }

};

int    main() {

 figure *р;      //         создаем       указатель    на       базовый       тип

 triangle          t;         //         создаем       объекты       производных         типов

 rectangle       r;

 р          =          &t;

 p->set_dim(10.0,   5.0);

 p->show_area();

 р          =          &r;

 p->set_dim(10.0,   5.0);

 p->show_area();

 return            0;

}

Вот          как     выглядят     результаты выполнения           этой   программы.

Треугольник    с          высотой       10       и          основанием            5          имеет     площадь      25.

Прямоугольник         с          размерами 10       х          5          имеет            площадь      50.

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

Как          вы       думаете,      используя   объявление            класса           figure, можно          вывести        класс circle  для вычисления   площади      круга по заданному  значению    радиуса?     Ответ:            да.      Для    этого достаточно  создать новый           производный        тип,    который       бы      вычислял     площадь      круга. Могущество            виртуальных          функций       опирается    на       тот      факт,  что программист         может           легко вывести       новый           тип,    который       будет разделять    общий           интерфейс   с          другими       "родственными" объектами. Вот,    например,   как     это      можно          сделать        в          нашем          случае:

class        circle  :           public figure {

 public:

  void           show_area()            {    cout         <<       "Круг с          радиусом     ";

   cout      <<       x;

   cout      <<       "          имеет            площадь      ";

   cout      <<       3.14   *          x          *          x;

  }

};

Прежде чем    опробовать             класс circle  в          "деле",         рассмотрим внимательно          определение функции  show_area().          Обратите     внимание    на то,       что      в          нем    используется         только          одно значение      переменной x,         которая        должна        содержать   радиус          круга.            (Вспомните,            что площадь круга      вычисляется           по       формуле      пR2.)   Однако         согласно определению        функции       set_dim()      в классе       figure ей       передается два значения,    а          не       одно. Поскольку   классу           circle  не       нужно второе значение,    то        что      мы      можем          предпринять?

Есть         два     способа        решить         эту      проблему.   Первый        (и одновременно      наихудшим)           состоит         в том,            что      мы      могли            бы, работая        с          объектом     класса           circle, просто          вызывать     функцию set_dim(), передавая      ей       в          качестве       второго        параметра   фиктивное значение.    Основной    недостаток  этого метода          —        отсутствие   четкости       в задании       параметров            и          необходимость     помнить       о специальном исключении,          которое        нарушает     действие      принципа:   "один интерфейс, множество         методов".

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

#include <iostream> using     namespace   std;

class        figure {  protected:   double    x,         y;

 public:

  void           set_dim(double      i,          double           j=0)    {

   x =          i;

   у =          j;

  }

  virtual       void    show_area()            {

   cout      <<       "Для  этого  класса           выражение вычисления            ";

   cout      <<       "площади    не       определено.\n";

  }

};

class        triangle          :           public figure {

 public:

  void           show_area()            {

   cout      <<       "Треугольник         с          высотой       ";

   cout      <<       x          <<       "          и          основанием            "          <<       у;

   cout      <<       "          имеет            площадь      ";

   cout      <<       х          *          0.5      *          у          <<       ".\n";

  }

};

class        rectangle      :           public figure {

 public:

  void           show_area()            {

   cout      <<       "Прямоугольник  с          размерами ";

   cout      <<       x          <<       "          x          "          <<       у;

   cout      <<       "          имеет            площадь      ";

   cout      <<       х          *          у          <<       ".\n";

  }

};

class        circle  :           public figure {

 public:

  void           show_area()            {    cout         <<       "Круг с          радиусом     ";

   cout      <<       x;

   cout      <<       "          имеет            площадь      ";

   cout      <<       3.14   *          x          *          x          <<       ".\n";

  }

};

int    main() {

 figure *p;      //         создаем       указатель    на       базовый       тип

 triangle          t;         //         создаем       объекты       производных         типов

 rectangle       r;

 circle  с;

 р          =          &t;

 p->set_dim(10.0,   5.0);

 p->show_area();

 р          =          &r;

 p->set_dim(10.0,   5.0);

 p->show_area();

 р          =          &с;

 p->set_dim(9.0);

 p->show_area();

 return            0;

}

При         выполнении           эта      программа генерирует  такие результаты.

Треугольник    с          высотой       10       и          основанием            5          имеет     площадь      25.

Прямоугольник         с          размерами 10       х          5          имеет            площадь      50.

Круг        с          радиусом    9          имеет            площадь      254.34.

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

Чисто  виртуальные       функции      и          абстрактные      классы

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

Чисто    виртуальная        функция       —        это    виртуальная        функция, которая      не       имеет определения      в          базовом       классе.

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

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

virtual     тип     имя_функции(список_параметров)             =          0;

Здесь      под    элементом  тип   подразумевается тип     значения,    возвращаемого функцией,   а элемент    имя_функции—    ее        имя.   Обозначение         =          0 является       признаком  того,  что      функция здесь      объявляется           как     чисто виртуальная.          Например,  в          следующей версии          определения класса figure функция       show_area()           уже    представлена        как     чисто виртуальная.

class        figure {   double    х,         у;

 public:

  void           set_dim(double      i,          double           j           =0)      {

   x =          i;

   У            =          j;

  }

  virtual       void    show_area()            =0;      //         чисто виртуальная           функция

};

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

/* Эта      программа не       скомпилируется,  поскольку   в          классе           circle нет            переопределения            функции       show_area().

*/ #include     <iostream> using     namespace   std;

class        figure {  protected:

  double      x,         y;

 public:

  void           set_dim(double      i,          double           j)         {

   x =          i;

   у =          j;

  }

  virtual       void    show_area()            =          0;        //         чисто виртуальная     функция

};

class        triangle          :           public figure {

 public:

  void           show_area()            {

   cout      <<       "Треугольник         с          высотой       ";

   cout      <<       x          <<       "          и          основанием            "          <<       у;

   cout      <<       "          имеет            площадь      ";

   cout      <<       х          *          0.5      *          у          <<       ".\n";

  }

};

class        rectangle      :           public figure {

 public:

  void           show_area()            {

   cout      <<       "Прямоугольник  с          размерами ";

   cout      <<       x          <<       "x"      <<       у;

   cout      <<       "          имеет            площадь      ";

   cout      <<       x          *          у          <<       ".\n";

  }

};

class        circle  :           public figure {

 //         Отсутствие  определения         функции       show_area()

 //         вызовет        сообщение  об       ошибке.

};

int    main() {

 figure *р;      //         создаем       указатель    на       базовый       тип

 triangle          t;         //         создаем       объекты       производных         классов

 rectangle       r;

 circle  с;         //         Ошибка:       создание     этого  объекта        невозможно!

 р          =          &         t;

 p->set_dim(10.0,   5.0);

 p->show_area();

 р          =          &         r;

 p->set_dim(10.0,   5.0);

 p->show_area();

 return            0;

}

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

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

Сравнение      раннего       связывания            с          поздним

При         обсуждении           объектно-ориентированных   языков          обычно используются        два     термина: раннее  связывание            (early binding)        и позднее        связывание            (late    binding).       В          C++     эти      термины связывают с          событиями,            которые       происходят во       время            компиляции           и в          период         выполнения программы          соответственно.

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

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

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

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

Полиморфизм         и          пуризм

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

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

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

Глава     16:  Шаблоны

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

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

Обобщенные            функции

Обобщенная  функция      —        это    функция,     перегружающая  сама  себя.

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

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

template            <class Ttype>           тип     имя_функции     (список_параметров) {

 //         тело   функции

}

Определение  обобщенной          функции      начинается           с          ключевого     слова template.

Здесь      элемент        Ttype представляет         собой            "заполнитель"    для типа   данных, обрабатываемых        функцией.   Это     имя    может           быть использовано        в          теле   функции.     Но       оно означает          всего лишь заполнитель,         вместо          которого      компилятор            автоматически      подставит реальный    тип     данных         при     создании     конкретной             версии          функции. И         хотя    для     задания обобщенного   типа   в          template-объявлении     по традиции     применяется          ключевое    слово class, можно          также использовать         ключевое    слово typename.

В   следующем            примере      создается    обобщенная           функция,      которая меняет          местами значения           двух   переменных,          используемых       при     ее вызове.        Поскольку   общий           процесс        обмена значениями       переменных не       зависит         от        их        типа,  он       является       прекрасным           кандидатом для создания         обобщенной          функции.

//  Пример        шаблонной     функции. #include     <iostream> using   namespace     std;

//  Определение         шаблонной функции. template     <class X>       void    swapargs(X  &a,     X          &b) {

 X          temp;

 temp  =          a;

a       =         b;

b       =         temp;

}

int    main() {

 int       i           =          10,      j=20;

 double           x=10.1,          y=23.3;

 char    a='x',  b='z';

 cout    <<       "Исходные  значения      i,          j:          "          <<       i           <<       '           '<<     j           <<       '           \          n          ';

 cout    <<       "Исходные  значения      x,         у:         "          <<       x          <<       '           '<<     у          <<       '\n';

cout           <<       "Исходные  значения      a,         b:        "          <<       a          <<       '           '<<       b          <<       '           \n';

 swapargs(i,   j);        //         перестановка         целых            чисел

 swapargs(x,  у);       //         перестановка         значений     с          плавающей     точкой

 swapargs(a, b);       //         перестановка         символов

 cout    <<       "После          перестановки         i,          j:          "          <<       i           <<       '     '<<      j           <<       '           \          n

';

 cout    <<       "После          перестановки         x,         у:         "          <<       x          <<       '     '<<      у          <<       '\n';

 cout    <<       "После          перестановки         a,         b:        "          <<       a          <<       '     '<<      b          <<       '           \n';

 return            0;

}

Вот          как     выглядят     результаты выполнения           этой   программы.

Исходные         значения      i,          j:          10       20

Исходные         значения      х,         у:         10.1   23.3

Исходные         значения      a,         b:        х          z

После     перестановки         i,          j:          20       10

После     перестановки         х,         у:         23.3   10.1

После     перестановки         a,         b:        z          х

Итак,      рассмотрим            внимательно          код     программы.           Строка

template            <class Х>       void    swapargs(X  &а,     X          &b)

сообщает          компилятору,        во-первых,  что      создается    шаблон,       и,        во-вторых,         что      здесь начинается обобщенное           определение.        Обозначение X          представляет         собой            обобщенный          тип, который          используется в          качестве       "заполнителя".   За        template-заголовком      следует объявление функции     swapargs(), в          котором       символ         X          означает      тип данных         для     значений,    которые       будут меняться     местами.      В          функции main()           демонстрируется вызов            функции       swapargs()  с использованием трех    различных  типов данных:        int,      float   и          char.  Поскольку   функция swapargs()   является       обобщенной,         компилятор            автоматически      создает три     версии          функции swapargs():      одну  для     обмена         целых            чисел, вторую         для     обмена         значений     с          плавающей             точкой и      третью для     обмена         символов.

Здесь      необходимо           уточнить      некоторые  важные        термины, связанные   с          шаблонами.           Вопервых,   обобщенная           функция       (т.е. функция,      объявление            которой        предваряется         templateинструкцией) также            называется шаблонной функцией.   Оба    термина       используются        в этой книге   взаимозаменяемо.          Когда компилятор            создает         конкретную версию         этой   функции,     то говорят,  что      создается    ее        специализация (или   конкретизация). Специализация     также называется            порожденной функцией    (generated   function).     Действие      порождения           функции определяют как     ее        реализацию           (instantiating).        Другими      словами,      порождаемая функция является            конкретным           экземпляром         шаблонной функции.

Поскольку        C++     не       распознает  символ         конца            строки           в качестве       признака     конца инструкции,          template-часть       определения обобщенной          функции       может           не       находиться в          одной строке         с именем        этой   функции.     В          следующем            примере      показан        еще один  (довольно распространенный)          способ          форматирования  функции swapargs().

template            <class Х> void     swapargs(X  &a,     X     &b) {

 X          temp;

 temp  =          a;

a       =         b;

b       =         temp;

}

При         использовании     этого  формата       важно           понимать,   что      между template-инструкцией    и началом   определения         обобщенной          функции никакие        другие          инструкции находиться не могут.     Например,  следующий фрагмент     кода  не       скомпилируется.

//  Этот    код     не     скомпилируется. template     <class Х> int i;          //         Здесь     ошибка! void         swapargs(X     &а,     X          &b) {

 X          temp;

 temp  =          a;

a       =      b;

b       =      temp;

}

Как          отмечено     в          комментарии,       template-спецификация            должна стоять            непосредственно перед           определением      функции.     Между ними не       может           находиться ни       инструкция объявления переменной,        ни какая-либо другая          инструкция.

Функция         с          двумя           обобщенными     типами

В   template-инструкции      можно          определить            несколько   обобщенных типов данных, используя          список          элементов,  разделенных         запятой. Например,  в          следующей программе создается    шаблонная функция       с двумя           обобщенными      типами.

#include <iostream> using     namespace   std;

template            <class type1,            class     type2> void myfunc(type1         x,     type2 y) {

 cout    <<       x          <<       '           '           <<       у          <<       '\n';

}

int    main() {

 myfunc(10,  "Привет");

 myfunc(0.23,          10L);

 return            0;

}

В   этом   примере      при     выполнении           функции       main(),          когда компилятор            генерирует конкретные            экземпляры           функции       myfunc(), заполнители          типов type1 и          type2 заменяются сначала      парой            типов данных         int       и          char*,            а          затем парой            double           и          long соответственно.

Узелок   на       память.        Создавая     шаблонную            функцию,    вы,     по сути, разрешаете компилятору    генерировать      столько      различных  версий этой функции,     сколько        необходимо для  обработки различных  способов, которые     использует            программа для     ее        вызова.

Явно     заданная     перегрузка обобщенной          функции

"Вручную"        перегруженная    версия          обобщенной          функции называется           явной специализацией.

Несмотря          на       то        что      обобщенная           функция       сама  перегружается по       мере  необходимости, это       можно          делать          и          явным           образом. Формально этот    процесс        называется явной специализацией.            При перегрузке  обобщенная           функция       переопределяется           "в        пользу" этой конкретной  версии.         Рассмотрим,          например,   следующую программу,            которая        представляет собой       переработанную  версию первого        примера      из        этой   главы.

//  Перегрузка шаблонной функции. #include <iostream> using   namespace     std;

template            <class X> void     swapargs(X  &a,     X     &b) {

 X          temp;

 temp  =          a;

a       =         b;

b       =         temp;

 cout    <<       "Выполняется        шаблонная функция       swapargs.\n";

}

// Эта      функция       переопределяет   обобщенную          версию         функции swapargs()   для     int-параметров. void      swapargs(int            &а,     int       &b) {

 int       temp;

temp  =          a;

a       =         b;

b       =         temp;

 cout    <<       "Это   int-специализация           функции       swapargs.\n";

}

int    main() {

 int       i=10,  j           =20;

 double           x=10.1,          y=23.3;

 char    a='x',  b='z';

 cout    <<       "Исходные  значения      i,          j:          "          <<       i           <<       '           '<<     j           <<       '\n';

 cout    <<       "Исходные  значения      x,         у:         "          <<       x          <<       '           '<<     у          <<       '\n';

 cout    <<       "Исходные  значения      a,         b:        "          <<       a          <<       '           '<<     b          <<       '\n';

 swapargs(i,  j);        //         Вызывается            явно   перегруженная     функция swapargs().

 swapargs(x,  у);       //         Вызывается            обобщенная           функция     swapargs().

 swapargs(a, b);       //         Вызывается            обобщенная           функция     swapargs().

 cout    <<       "После          перестановки         i,          j:          "          <<       i           <<       '     '<<      j           <<       '\n';

 cout    <<       "После          перестановки         x,         у:         "          <<       x          <<       '     '<<      у          <<       '\n';

 cout    <<       "После          перестановки         a,         b:        "          <<       a          <<       '     '<<      b          <<       '\n';  return          0;

}

При         выполнении           эта      программа генерирует  такие результаты.

Исходные         значения      i,          j:          10       20

Исходные         значения      х,         у:         10.1   23.3

Исходные         значения      a,         b:        х          z Это          int-специализация           функции     swapargs.

Выполняется   шаблонная функция       swapargs.

Выполняется   шаблонная функция       swapargs.

После     перестановки         i,          j:          20       10

После     перестановки         х,         у:         23.3   10.1

После     перестановки         а,         b:        z          х

Как          отмечено     в          комментариях,      при     вызове          функции swapargs(i,  j)         выполняется          явно перегруженная      версия          функции swapargs(), определенная       в          программе.            Компилятор           в          этом случае           не       генерирует  эту      версию         обобщенной          функции swapargs(), поскольку   обобщенная функция    переопределяется           явно заданным   вариантом  перегруженной     функции.

Для          обозначения          явной            специализации     функции       можно использовать         новый альтернативный             синтаксис,   содержащий          ключевое слово template.      Например,  если   задать специализацию  с          использованием этого  альтернативного  синтаксиса, перегруженная     версия функции   swapargs()  из предыдущей          программы будет выглядеть   так.

//  Использование     нового          синтаксиса  задания        специализации. template<> void         swapargs<int>        (int      &a,     int       &b) {

 int       temp;

 temp  =          a;

a       =         b;

b       =         temp;

 cout    <<       "Это   int-специализация           функции       swapargs.\n";

}

Как          видите,         в          новом           синтаксисе  для     обозначения специализации     используется конструкция      template<>. Тип     данных,        для которых       создается    эта      специализация, указывается  в          угловых        скобках после имени           функции.     Для    задания        любого         типа   обобщенной функции       используется         один  и          тот      же       синтаксис.   На       данный момент         ни       один  из синтаксических           способов     задания        специализации не       имеет            никаких        преимуществ         перед другим,      но       с          точки зрения          перспективы          развития      языка,           возможно,  все      же       лучше использовать         новый           стиль.

Явная     специализация      шаблона      позволяет    спроектировать    версию обобщенной          функции       в расчете     на       некоторую  уникальную ситуацию,    чтобы,           возможно,  воспользоваться преимуществами  повышенного быстродействия   программы только          для     одного          типа   данных. Но,            как правило,      если   вам    нужно           иметь            различные  версии          функции       для разных          типов данных,       имеет            смысл           использовать         перегруженные функции,     а          не       шаблоны.

Перегрузка    шаблона     функции

Помимо            создания     явным           образом       перегруженных    версий обобщенной          функции,     можно также         перегружать           саму  спецификацию шаблона      функции.     Для    этого  достаточно создать еще           одну  версию шаблона,     которая        будет отличаться  от        остальных   списком       параметров. Рассмотрим            пример.

//  Объявление           перегруженного   шаблона     функции. #include            <iostream> using   namespace     std;

//  Первая          версия     шаблона      f(). template     <class X> void          f(X       a) {

 cout    <<       "Выполняется        функция       f(X       a)\n";

}

//  Вторая          версия          шаблона     f(). template            <class X,     class   Y> void          f(X       a,         Y     b)

{

 cout    <<       "Выполняется        функция       f(X       a,         Y          b)\n";

}

int    main() {

 f(10);  //         Вызывается            функция       f(X).

 f(10,   20);    //         Вызывается            функция       f(X,     Y).

 return            0;

}

Здесь      шаблон        для     функции       f()        перегружается,     чтобы обеспечить возможность         приема         как одного, так      и          двух   параметров.

Использование        стандартных      параметров         в          шаблонных   функциях

В   шаблонных функциях     можно          смешивать  стандартные          параметры  с обобщенными параметрами типа.  Эти     параметры  работают     так      же,     как     в любой           другой          функции. Рассмотрим   пример.

//  Использование     стандартных           параметров                        в     шаблонной функции. #include            <iostream> using   namespace   std;

//  Отображение         данных         заданное     количество     раз. template<class           Х> void          repeat(X        data,     int       times) {

 do       {

  cout           <<       data    <<       "\n";

  times--;

 }while(times);

}

int    main() {

 repeat("Это  тест.",            3);

 repeat(100,  5);

 repeat(99.0/2,        4);

 return            0;

}

Вот          какие результаты генерирует  эта      программа.

Это          тест.

Это          тест.

Это          тест.

100

100

100

100

100

49.5

49.5

49.5

49.5

В   этой   программе функция       repeat()        отображает             свой   первый аргумент      столько        раз, сколько           задано          ее        вторым         аргументом. Поскольку   первый         аргумент      имеет            обобщенный          тип, функцию repeat()        можно          использовать         для     отображения         данных         любого типа.  Параметр times     —        стандартный,         он       передается по       значению. Смешанное             задание        обобщенных          и необобщенных  параметров,           как правило,      не       вызывает     никаких        проблем      и          является       обычной практикой   программирования.

Ограничения            при     использовании     обобщенных         функций

Обобщенные  функции       подобны      перегруженным   функциям,  но       имеют больше ограничений     по       применению.         При    перегрузке  функций       в теле   каждой         из        них     обычно задаются различные  действия.     Но обобщенная           функция       должна        выполнять  одно  и          то        же действие для     всех    версий          —        отличие        между          версиями     состоит         только в          типе   данных. Рассмотрим      пример,       в          котором       перегруженные функции       нельзя          заменить     обобщенной функцией,           поскольку   они выполняют различные  действия, void       outdata(int   i) {

 cout    <<       i;

}

void         outdata(double     d) {

 cout    <<       d          *          3.1416;

}

Создание        обобщенной          функции      abs()

Давайте-ка       снова обратимся   к          функции       abs(). Вспомните, что      в главе 8          стандартные библиотечные   функции       abs(), labs() и          fabs() были сгруппированы     в          три     перегруженные функции         с          общим          именем myabs().       Каждая         из        перегруженных    версий          функции       myabs() предназначена      для     возврата      абсолютного          значения      для     данных "своего"       типа.  Несмотря     на то  что      показанную            в          главе 8 перегрузку  функции       abs()  можно          считать         шагом           вперед          по сравнению  с          использованием  трех    различных  библиотечных       функций       (с различными именами),            все      же       это      не       лучший         способ          создания функции,     которая        возвращает             абсолютное значение    заданного аргумента.  Поскольку   процедура  возврата      абсолютного          значения      числа одинакова   для     всех    типов числовых     значений,    функция       abs()  может послужить   прекрасным поводом   для     создания     шаблонной функции.     При наличии       обобщенной          версии          функции       abs() компилятор сможет автоматически      создавать    необходимую        ее        версию.        Программист         в этом случае            освобождается     от        написания   отдельных  версий          для каждого       типа   данных.        (Кроме того,          исходный    код     программы не будет загромождаться   несколькими         "вручную" перегруженными  версиями.)

В  следующей программе содержится            обобщенная           версия          функции     myabs().       Имеет            смысл сравнить    ее        с          перегруженными     версиями,   приведенными     в          главе 8.        Нетрудно     убедиться,  что обобщенная    версия          короче          и          обладает     большей      гибкостью.

//  Обобщенная          версия          функции     myabs(). #include  <iostream> using     namespace   std;

template     <class X> X     myabs(X        val)

{

 return            val       <          0          ?          -val     :           val;

}

int    main() {

 cout    <<       myabs(-10)  <<       "\n";   //         для     типа   int

 cout    <<       myabs(-10.0)          <<       "\n";   //         для     типа   double  cout         <<       myabs(-10L)            <<       "\n";   //         для     типа   long

 cout    <<       myabs(-10.0F)        <<       "\n";   //         для     типа   float

 return            0;

}

В   качестве       упражнения           было бы      неплохо,      если   бы      вы попытались            найти             другие библиотечные    функции-кандидаты       для переделки  их        в          обобщенные          функции.     Помните, главное            здесь то, что      один  и          тот      же       алгоритм     должен        быть  применим   к широкому   диапазону данных.

Обобщенные            классы

Помимо            обобщенных          функций,     можно          также            определить обобщенный          класс.            Для    этого создается     класс,            в          котором определяются       все      используемые       им      алгоритмы; при     этом реальный     тип обрабатываемых в          нем    данных         будет задан как     параметр     при     создании объектов      этого  класса.

Обобщенные  классы          особенно     полезны       в          случае,         когда в          них используется         логика, которую   можно          обобщить.   Например,  алгоритмы, которые       поддерживают функционирование            очереди       целочисленных значений,    также            подходят     и          для     очереди символов.         Механизм, который       обеспечивает         поддержку связного      списка           почтовых     адресов, также годится         для     поддержки связного      списка,         предназначенного          для хранения     данных         о запчастях к          автомобилям.       После            создания обобщенный          класс сможет         выполнять определенную       программистом операцию    (например, поддержку очереди       или     связного списка)  для     любого типа   данных.        Компилятор           автоматически      сгенерирует            корректный тип объекта            на       основе          типа,  заданного   при     создании     объекта.

Общий   формат         объявления            обобщенного         класса           имеет     следующий            вид:

template            <class Ttype>           class   имя_класса            {

 .

 .

 .

}

Здесь      элемент        Ttype представляет         собой            "заполнитель"    для имени           типа,  который       будет задан            при     реализации            класса.          При необходимости     можно          определить            несколько обобщенных            типов данных,        используя   список          элементов,  разделенных         запятыми.

Создав   обобщенный          класс,            можно          создать         его      конкретный экземпляр, используя следующий  общий           формат.

имя_класса     <тип> имя_объекта;

Здесь      элемент        тип   означает      имя    типа   данных,        которые       будут обрабатываться экземпляром           обобщенного         класса.          Функции-члены обобщенного         класса           автоматически являются          обобщенными.     Поэтому вам    не       нужно           использовать         ключевое    слово template       для явного определения         их        таковыми.

В   следующей программе класс queue            (впервые     представленный  в главе 11)      переделан  в обобщенный.     Это     значит,          что      его      можно использовать         для     организации          очереди       объектов любого типа.  В данном        примере      создаются   две     очереди:      для     целых            чисел и значений     с плавающей         точкой,         но       можно          использовать         данные и          любого         другого        типа.

//  Демонстрация       использования      обобщенного         класса     очереди. #include            <iostream> using   namespace   std;

const       int       SIZE=100;

//  Создание     обобщенного         класса           queue.

template            <class QType> class        queue{

  QType     q[SIZE];   int     sloc,    rloc;

 public:

  queue()    {           sloc     =          rloc     =0;      }

  void           qput(QType i);

  QType       qget();

};

//  Занесение   объекта        в     очередь. template            <class     QType> void     queue<QType>::qput(QType     i) {

 if(sloc==SIZE)           {

  cout           <<       "Очередь     заполнена.\n";

  return;

 }

 sloc++;

 q[sloc]            =          i;

}

//  Извлечение            объекта        из        очереди.

template            <class QType> QType     queue<QType>::qget()

{

 if(rloc ==       sloc)   {

  cout           <<       "Очередь     пуста.\n";

  return       0;

 }

 rloc++;

 return            q[rloc];

}

int    main() {

 queue<int>  a,         b;        //         Создаем       две     очереди       для     целых     чисел.

 a.qput(10);

 a.qput(20);

 b.qput(19);

 b.qput(1);

 cout    <<       a.qget()         <<       "          ";

 cout    <<       a.qget()         <<       "          ";

 cout    <<       b.qget()         <<       "          ";

 cout    <<       b.qget()         <<       "\n";

 queue<double>      с,         d;        //         Создаем       две     очереди       для            doubleзначений.

 c.qput(10.12);

 c.qput(-20.0);

 d.qput(19.99);

 d.qput(0.986);

 cout    <<       с.qget()         <<       "          ";

 cout    <<       с.qget()         <<       "          ";

 cout    <<       d.qget()<<    "          ";

 cout    <<       d.qget()         <<       "\n";

 return            0;

}

При         выполнении           этой   программы получаем    следующие результаты.

10       20    19       1

10.12      -20      19.99 0.986

В   этой   программе объявление            обобщенного         класса           подобно объявлению           обобщенной функции.  Тип     данных,        хранимых    в          очереди, обобщен      в          объявлении            класса.          Он      неизвестен  до тех            пор, пока   не       будет объявлен     объект          класса           queue,           который       и определит   реальный    тип данных.            После            объявления            конкретного экземпляра            класса           queue            компилятор            автоматически сгенерирует все      функции       и          переменные,         необходимые        для     обработки   реальных данных.        В данном     примере      объявляются          два     различных  типа   очереди: две     очереди       для     хранения     целых чисел           и          две     очереди       для значений     типа   double.          Обратите     особое          внимание    на       эти объявления:

queue<int>       а,         b;

queue<double>           с,         d;

Заметьте,          как     указывается           нужный        тип     данных:        он заключается           в          угловые       скобки. Изменяя  тип     данных         при     создании объектов      класса           queue,           можно          изменить     тип     данных, хранимых в          очереди.      Например,  используя   следующее объявление,           можно создать         еще    одну очередь,       которая        будет содержать   указатели    на символы:

queue<char      *>       chrptrQ;

Можно   также            создавать    очереди       для     хранения     данных,        тип которых       создан программистом.           Например,  предположим,      вы используете           следующую            структуру     для хранения         информации          об адресе.

struct      addr   {

 char    name[40];

 char    street[40];

 char    city[30];

 char    state[3];

 char    zip[12];

};

Тогда      для     того,  чтобы            с          помощью    класса           queue сгенерировать       очередь       для     хранения объектов         типа   addr,  достаточно использовать         такое объявление.

queue<addr>   obj;

На            примере      класса           queue            нетрудно     убедиться,  что обобщенные          функции       и          классы представляют    собой            мощные средства,     которые       помогут        увеличить    эффективность      работы программиста,      поскольку   они     позволяют  определить            общий           формат объекта,       который можно   затем использовать         с          любым         типом данных.        Обобщенные         функции       и          классы избавляют           вас      от утомительного      труда по       созданию    отдельных  реализаций            для     каждого типа данных,         подлежащих          обработке   единым        алгоритмом.          Эту работу          сделает        за        вас      компилятор: он    автоматически      создаст конкретные            версии          определенного     вами  класса.

Пример           класса          с          двумя           обобщенными     типами   данных

Шаблонный     класс может           иметь            несколько   обобщенных          типов данных.        Для    этого достаточно  объявить      все      нужные        типы  данных         в template-спецификации            в          виде  элементов списка,          разделяемых запятыми.   Например,  в          следующей программе создается    класс, который использует  два     обобщенных          типа   данных.

/* Здесь используется         два     обобщенных          типа   данных         в            определении класса.

*/ #include     <iostream> using     namespace   std; template            <class     Type1,           class     Type2> class     myclass         {   Type1     i;

  Type2        j;

 public:

  myclass(Type1   a,         Type2 b)        {           i           =          a;         j           =          b;        }

  void           show()           {           cout    <<       i           <<       '           '           <<       j           <<     '\n';     }

};

int    main() {

 myclass<int, double>        ob1(10,         0.23);

 myclass<char,         char    *>       ob2('x',          "Это   тест.");

 ob1.show(); //         отображение         int-      и          double-значений

 ob2.show(); //         отображение         значений     типа   char    и          char    *

 return            0;

}

Эта          программа генерирует  такие результаты.

10 0.23

X   Это     тест.

В   данной         программе объявляется           два     вида  объектов.    Объект          ob1 использует  данные         типа int         и          double,          а          объект          ob2     — символ         и          указатель    на       символ.        Для    этих    ситуаций      компилятор автоматически       генерирует  данные         и          функции,     соответствующие способу        создания     объектов.

Создание        обобщенного        класса          безопасного          массива

Прежде чем    двигаться    дальше,       рассмотрим            еще    одно  приложение для     обобщенного класса.    Как     было показано     в          главе 13,      можно перегружать           оператор     "[]",    что      позволяет создавать      собственные реализации            массивов,    в          том     числе и          "безопасные          массивы", которые обеспечивают динамическую      проверку     нарушения  границ.         Как     вы знаете,          в          C++     во       время выполнения         программы возможен    выход за        границы       массива        без     выдачи         сообщения  об ошибке. Но       если создать         класс,            который       бы      содержал    массив,         и          разрешить доступ          к          этому массиву       только          через перегруженный    оператор индексации            ("[]"), то        можно          перехватить индекс,      соответствующий адресу          за        пределами адресного   пространства         массива.

Объединив      перегрузку  оператора   с          обобщенным         классом,      можно создать         обобщенный тип безопасного           массива,      который       затем будет использован          для     создания     безопасных массивов,  предназначенных            для хранения     данных         любого         типа.  Такой            тип     массива        и создается в          следующей программе.

// Пример        создания     и          использования      обобщенного         безопасного массива.

#include <iostream> #include <cstdlib> using     namespace   std;

const       int       SIZE    =          10;

template            <class AType> class        atype {

  AType       a[SIZE];

 public:

  atype()      {

   register            int       i;

   for(i=0; i<SIZE;           i++)     a[i]      =          i;

  }

  AType       &operator[](int      i);

};

//  Обеспечение          контроля     границ          для     класса     atype.            template       <class АТуре> АТуре     &atype<AType>::operator[](int            i)

{

 if(i<0  ||        i>         SIZE-1)           {

  cout           <<       "\n      Значение     индекса        ";

  cout           <<       i           <<       "          за        пределами границ          массива.\n";

 }

 return            a          [i];

}

int    main() {

 atype<int>    intob; //         массив          int-значений

 atype<double>       doubleob;     //         массив          double-значений

 int       i;

 cout    <<       "Массив       int-значений:          ";

 for(i=0;          i<SIZE;           i++)     intob[i]          =          i;

 for(i=0;          i<SIZE;           i++)     cout    <<       intob[i]          <<       "          ";  cout      <<     '\n';

 cout    <<       "Массив       double-значений: ";

 for(i=0;          i<SIZE;           i++)     doubleob[i]  =          (double)        i/3;  for(i=0;     i<SIZE;           i++)     cout    <<       doubleob[i]  <<       "          ";

 cout    <<       '\n';

 intob[12]      =          100;   //         ошибка         времени       выполнения!

 return            0;

}

В   этой   программе сначала        создается    обобщенный          тип     безопасного массива,      а          затем демонстрируется            его      использование      путем создания     массива        целых            чисел и          массива        doubleзначений.   Было бы неплохо,      если   бы      вы       попробовали         создать         массивы       других типов.           Как доказывает    этот    пример,       одно  из        наибольших достоинств  обобщенных          классов        состоит         в том,            что      они позволяют  только          один  раз     написать      код,   отладить      его,    а          затем применять  его      к данным    любого         типа,  не       переписывая          его      для каждого       конкретного           приложения.

Использование        в          обобщенных         классах        аргументов,         не   являющихся          типами

В   template-спецификации            для     обобщенного         класса           можно также            задавать      аргументы, не являющиеся     типами.        Это     значит,          что в          шаблонной спецификации       можно          указывать    то,       что обычно принимается          в          качестве       стандартного         аргумента,  например,   аргумент типа   int       или аргумент-указатель.          Синтаксис    (он      практически           такой же, как     при     задании       обычных параметров     функции)     включает     определение типа   и          имени           аргумента.  Вот,    например,   как можно  по-другому реализовать           класс безопасного           массива,      представленного в предыдущем разделе.

// Использование     в          шаблоне      аргументов,            которые       не       являются типами.

#include <iostream> #include         <cstdlib> using       namespace   std; //            Здесь     элемент        int       size     -           это      аргумент,    не       являющийся           типом. template            <class AType,           int       size> class     atype {

  AType       a[size];           //         В          аргументе   size     передается длина     массива.

 public:

  atype()      {

   register            int       i;

   for(i=0; i<size;            i++)     a[i]      =          i;

  }

  AType       &operator[](int      i);

};

//  Обеспечение          контроля     границ          для     класса     atype. template      <class АТуре,           int       size> AType     &atype<AType,      size>::operator[](int          i)

{

 if(i<0  ||        i>         size-1)            {

  cout           <<       "\n      Значение     индекса        ";

  cout           <<       i           <<       "          за        пределами границ          массива.\n";

  exit(1);

 }

 return            a[i];

}

int            main()

{

 atype<int,     10>     intob; //         10-элементный     массив          целых            чисел

 atype<double,        15>     doubleob;     //         15-элементный     массив            doubleзначений

 int       i;

 cout    <<       "Массив       целых            чисел:           ";

 for(i=0;          i<10;  i++)     intob[i]          =          i;

 for(i=0;          i<10;  i++)     cout    <<       intob[i]          <<       "          ";

 cout    <<       '\n';

 cout    <<       "Массив       double-значений: ";

 for(i=0;          i<15;  i++)     doubleob[i]  =          (double)        i/3;  for(i=0;     i<15;  i++)     cout    <<       doubleob[i]  <<       "          ";

 cout    <<       '\n';

 intob[12]      =          100;   //         ошибка         времени       выполнения!

 return            0;

}

Рассмотрим     внимательно          template-спецификацию           для     класса atype.            Обратите     внимание на          то,       что      аргумент      size     объявлен     с указанием   типа   int.      Этот    параметр     затем используется         в теле            класса atype для     объявления            размера       массива        a.        Несмотря     на       то        что в          исходном    коде программы член   size     имеет            вид     "переменной",      его значение      известно      уже    во       время компиляции.        Поэтому       его      можно успешно       использовать         для     установки    размера       массива. Кроме    того, значение      "переменной"       size     используется         для     контроля     выхода         за границы массива в          операторной          функции       operator[]().            Обратите также            внимание    на       то,       как     в функции   main()            создается    массив целых            чисел и          массив          значений     с          плавающей             точкой.         При этом   размер         каждого       из        них     определяется        вторым         параметром template-спецификации.

На            тип     параметров,           которые       не       представляют        типы, налагаются ограничения.         В          этом случае            разрешено  использовать только          целочисленные    типы, указатели    и          ссылки.        Другие типы (например, float)  не       допускаются.         Аргументы, которые       передаются параметру, не являющемуся  типом,          должны       содержать   либо  целочисленную константу,   либо  указатель    или ссылку на       глобальную            функцию      или объект.         Таким            образом,     эти      "нетиповые"          параметры следует рассматривать       как     константы,  поскольку   их        значения      не       могут быть изменены. Например,   в          теле   функции       operator[]() следующая инструкция недопустима:

size          =          10;      //         ошибка

Поскольку        параметры-"нетипы"   обрабатываются   как     константы,  их можно          использовать для            установки    размерности          массива,      что существенно          облегчает    жизнь            программисту.

Как          показывает пример         создания     безопасного           массива, использование      "нетиповых" параметров       весьма          расширяет   сферу применения           шаблонных классов.       И         хотя    информация, передаваемая через "нетиповой"         аргумент,    должна        быть  известна       во       время компиляции, малость    этого  ограничения          несравнима            с          достоинствами, предлагаемыми   такими параметрами.

Шаблонный     класс queue,           представленный  выше в          этой   главе, также            выиграл       бы      от применения      к          нему  "нетипового" параметра, задающего размер         очереди.      В          качестве упражнения попробуйте            усовершенствовать         класс queue            самостоятельно.

Использование        в          шаблонных            классах        аргументов          по   умолчанию

Шаблонный     класс может           по       умолчанию определять             аргумент, соответствующий обобщенному      типу.  Например,  в          результате  такой template-спецификации

template            <class X=int> class        myclass         {           //...

};

         будет   использован   тип   int,   если   при  создании   объекта   класса   myclass  отсутствует

задание        какого-то     бы      то        ни       было типа.

Для          аргументов,            которые       не       представляют        тип     в          template-спецификации,      также разрешается         задавать      значений     по       умолчанию. Они    используются        в          случае,         если   при реализации    класса           значение для     такого           аргумента   явно   не       указано.       Аргументы  по умолчанию      для "нетиповых"         параметров            задаются     с          помощью    синтаксиса, аналогичного используемому           при     задании       аргументов по       умолчанию для параметров            функций.

Рассмотрим     еще    одну  версию         класса           безопасного           массива,      в котором       используются аргументы         по       умолчанию как     для     типа   данных, так      и          для     размера       массива.

//  Демонстрация       использования      шаблонных аргументов по       умолчанию.

#include <iostream> #include <cstdlib> using     namespace   std; /*     Здесь параметр     AType            по     умолчанию     принимает  тип     int.     а параметр size     по     умолчанию     устанавливается     равным        10.

*/ template       <class AType=int,   int       size=10> class        atype{

  AType       a[size];           //         Через            параметр     size     передается размер массива.

 public:

  atype()      {

   register            int       i;

   for(i=0; i<size;            i++)     a[i]      =          i;

  }

  AType       &operator[](int      i);

};

//  Обеспечение          контроля     границ          для     класса     atype. template      <class АТуре,           int       size> AType     &atype<AType,      size>::operator[](int          i)

{

 if(        i<0      ||        i>         size-1)            {

  cout           <<       "\n      Значение     индекса        ";

  cout           <<       i           <<       "          за        пределами границ          массива.\n";

  exit(1);

 }

 return            a[i];

}

int    main() {

 atype<int,     100>  intarray;        /*        100-элементный  массив          целых            чисел */

 atype<double>       doublearray;           /*        10-элементный     массив            doubleзначений    (размер        массива        установлен по       умолчанию)           */

 atype<>        defarray;      /*        10-элементный     массив          int-значений            (размер и    тип     int       установлены          по       умолчанию)           */

 int       i;

 cout    <<       "Массив       целых            чисел:           ";

 for(i=0;          i<100;            i++      )           intarray[i]     =          i;

 for(i=0;          i<100;            i++)     cout    <<       intarray[i]     <<       "          ";

 cout    <<       '\n';

 cout    <<       "Массив       double-значений: ";

 for(i=0;          i<10;  i++)     doublearray[i]         =          (double)        i/3;  for(i=0;     i<10;  i++)     cout    <<       doublearray[i]         <<       "          ";

 cout    <<       '\n';

 cout    <<       "Массив       по       умолчанию:           ";

 for(i=0;          i<10;  i++)     defarray[i]    =          i;

 for(i=0;          i<10;  i++)     cout    <<       defarray[i]    <<       "          ";

 cout    <<       '\n';

 return            0;

}

Обратите          особое          внимание    на       эту      строку:

template            <class AType=int,   int       size=10>

class        atype {

Здесь      параметр     AType            по       умолчанию заменяется типом            int,      а параметр     size     по умолчанию      устанавливается   равным        числу 10.      Как показано     в          этой   программе,            объекты класса    atype можно          создать тремя            способами:

■  путем            явного          задания        как     типа,  так      и          размера       массива;

■  задав явно   лишь тип     массива,      при     этом   его      размер         по умолчанию устанавливается равным         10       элементам;

■  вообще         без     задания        типа   и          размера       массива,      при     этом   он по       умолчанию будет хранить элементы           типа   int,      а          его      размер         по умолчанию устанавливается   равным        10.

Использование          аргументов по       умолчанию (особенно   типов)           делает шаблонные            классы еще более            гибкими.      Тип,    используемый      по умолчанию,           можно          предусмотреть      для     наиболее употребительного типа   данных,        позволяя     при     этом   пользователю       ваших            классов задавать при         необходимости     нужный        тип     данных.

Явно     задаваемые          специализации    классов

Подобно           шаблонным           функциям    можно          создавать    и специализации     обобщенных          классов. Для          этого  используется конструкция           template<>, которая        работает      по       аналогии     с          явно задаваемыми        специализациями            функций.     Рассмотрим            пример.

//  Демонстрация       специализации     класса. #include    <iostream> using     namespace   std;

template            <class T> class        myclass         {

  T     x;

 public:

  myclass(T a)        {

   cout      <<       "В        теле   обобщенного         класса           myclass.\n";

   x =          a;

  }

  T     getx() {           return            x;         }

};

//  Явная            специализация      для     типа     int. template            <> class     myclass<int>           {

  int  x;

 public:

  myclass(int          a)        {

   cout      <<       "В        теле   специализации     myclass<int>.\n";

   x =          a          *          a;

  }

  int  getx() {           return            x;         }

};

int    main() {

 myclass<double>   d(10.1);

 cout    <<       "double:        "          <<       d.getx()         <<       "\n\n";  myclass<int>   i(5);

 cout    <<       "int:    "          <<       i.getx()           <<       "\n";

 return            0;

}

При         выполнении           данная          программа отображает             такие     результаты.

В   теле   обобщенного         класса           myclass.

double:   10.1 В            теле     специализации     myclass<int>.

int:           25

В  этой   программе обратите      особое          внимание    на       следующую     строку.

template            <>

class        myclass<int>           {

Она         уведомляет            компилятор            о          том,   что      создается    явная int-специализация      класса myclass.     Тот      же       синтаксис    используется         и          для любого         другого        типа   специализации     класса.

Явная     специализация      классов        расширяет   диапазон     применения обобщенных          классов, поскольку         она     позволяет    легко обрабатывать один  или     два     специальных          случая,         оставляя      все остальные       варианты для     автоматической    обработки   компилятором.     Но       если   вы       заметите, что у          вас      создается    слишком      много            специализаций,    то        тогда, возможно,  лучше           вообще отказаться          от        создания     шаблонного класса.

Глава     17:  Обработка   исключительных       ситуации

Эта           глава посвящена  обработке   исключительных  ситуаций. Исключительная            ситуация (или      исключение)          —        это      ошибка, которая        возникает    во       время            выполнения           программы. Используя С++-подсистему обработки   исключительных  ситуаций,     с          такими          ошибками вполне          можно          справляться.          При    их        возникновении     во       время работы         программы автоматически     вызывается             так      называемый обработчик          исключений.          Теперь          программист         не должен обеспечивать         проверку     результата  выполнения           каждой         конкретной операции     или функции          вручную.     В          этом-то         и          состоит принципиальное  преимущество       системы       обработки исключений, поскольку   именно         она     "отвечает"           за        код     обработки   ошибок, который       прежде приходилось     "вручную"   вводить        в          и          без     того объемные   программы.

В   этой   главе мы      также            возвращаемся       к          С++-операторам динамического     распределения памяти:           new    и          delete.           Как разъяснялось         выше в          этой   книге,            если   оператор     new    не       может выделить     требуемую  память,         он       генерирует  исключение.          И         здесь мы узнаем,        как     именно обрабатывается           такое исключение.          Кроме того,  вы       научитесь    перегружать           операторы  new    и delete,       что      позволит вам    определять             собственные          схемы           выделения  памяти.

Основы           обработки             исключительных            ситуаций

Обработка     исключений           —        это    системные            средства,   с помощью    которых     программа может        справиться            с          ошибками времени       выполнения.

Управление     С++-механизмом обработки   исключений           зиждется     на трех    ключевых    словах: try, catch  и          throw.           Они    образуют взаимосвязанную            подсистему,           в          которой        использование одного из них     предполагает        применение           другого.       Для    начала          будет полезно получить      общее представление   о          роли, которую       они     играют          в обработке   исключительных  ситуаций.     Если кратко,           то        их        работа состоит         в          следующем.           Программные       инструкции,            которые       вы считаете нужным            проконтролировать        на       предмет       исключений, помещаются          в          try-блок.      Если исключение (т.е.    ошибка)       таки возникает    в          этом   блоке,           оно     дает   знать о          себе   выбросом определенного     рода  информации          (с         помощью    ключевого  слова throw). Это     выброшенное исключение     может           быть  перехвачено           программным путем            с          помощью    catch-блока            и обработано         соответствующим образом.     А         теперь           подробнее.

Инструкция    throw генерирует           исключение,          которое перехватывается          catchинструкцией.

Итак,       код,   в          котором       возможно   возникновение     исключительных ситуаций,     должен выполняться     в          рамках          try-блока.    (Любая         функция, вызываемая           из        этого  try-блока,    также подвергается        контролю.) Исключения,          которые       могут быть  выброшены            контролируемым кодом, перехватываются             catch-инструкцией,         непосредственно             следующей за try-блоком, в котором   фиксируются          эти      "выбросы"  исключений.          Общий формат         try-     и          catch-блоков выглядит так.

try            {

 //    try-блок       (блок кода, подлежащий          проверке     на       наличие       ошибок)

}

catch       (type1            arg)    {

 //         catch-блок   (обработчик           исключения            типа   type1)

}

catch       {type2            arg)    {

 //         catch-блок   (обработчик           исключения            типа   type2)

}

catch       {type3            arg)    {

 //         catch-блок   (обработчик           исключения            типа   type3)

}  //   ...

catch       (typeN           arg)    {

 //         catch-блок   (обработчик           исключения            типа   typeN)

}

Блок        try       должен        содержать   код,   который,      по       вашему        мнению, должен        проверяться           на предмет возникновения     ошибок.       Этот    блок может           включать     лишь несколько   инструкций некоторой  функции       либо охватывать  весь   код     функции       main()            (в        этом   случае,         по       сути, "под колпаком"   системы       обработки   исключений           будет находиться вся программа).

После     "выброса"   исключение            перехватывается  соответствующей инструкцией           catch, которая       выполняет  его      обработку.  С          одним           try-блоком         может           быть  связана         не       одна, а несколько            catch-инструкций.            Какая именно         из        них     будет выполнена,            определяется типом исключения.        Другими      словами,      будет выполнена  та        catch-инструкция,            тип     исключения которой     (т.е.    тип     данных,        заданный    в catch-инструкции)           совпадает    с          типом сгенерированного         исключения (а        все      остальные   будут проигнорированы).        После            перехвата исключения            параметр     arg     примет         его      значение.    Таким            путем могут перехватываться  данные любого    типа,  включая       объекты       классов, созданных  программистом.

Чтобы  исключение            было перехвачено,         необходимо           обеспечить     его     "выброс"     в          try-блоке.

Общий   формат         инструкции throw выглядит     так:

throw     exception;

Здесь      с          помощью    элемента     exception      задается       исключение, сгенерированное инструкцией         throw.           Если   это      исключение            подлежит перехвату,   то        инструкция throw должна быть         выполнена  либо  в          самом блоке            try,     либо  в          любой           вызываемой          из        него   функции       (т.е. прямо           или     косвенно).

На            заметку.      Если   в          программе обеспечивается  "выброс" исключения,          для     которого    не предусмотрена         соответствующая catch-инструкция,          произойдет           аварийное  завершение программы, вызываемое          стандартной       библиотечной     функцией    terminate(). По умолчанию функция      terminate()  вызывает   функцию     abort()          для остановки программы,           но       при     желании можно  определить собственный        обработчик          ее        завершения.          За       подробностями относительно     обработки этой ситуации    следует       обратиться          к документации, прилагаемой           к          вашему        компилятору.

Рассмотрим    простой        пример         обработки   исключений           средствами     языка            C++.

//  Простой       пример         обработки     исключений. #include    <iostream> using     namespace   std;

int    main() {

 cout    <<       "HAЧAЛО\n";

 try       {

  //    начало          try-блока

  cout           <<       "В        trу-блоке\n";

  throw        99;      //         генерирование      ошибки

  cout           <<       "Эта    инструкция не       будет выполнена.";

 }

 catch  (int      i)          {

  //    перехват      ошибки

  cout           <<       "Перехват    исключения.          Его      значение      равно:           ";

  cout           <<       i           <<       "\n";

 }

 cout    <<       "КОНЕЦ";

 return            0;

}

При         выполнении           эта      программа отображает             следующие     результаты.

НАЧАЛО            В          try-блоке Перехват     исключения.          Его      значение      равно:           99

КОНЕЦ

Рассмотрим     внимательно          код     этой   программы.           Как     видите, здесь try-блок       содержит     три инструкции,    а          инструкция catch(int       i) предназначена     для     обработки   исключения целочисленного типа.  В          этом   try-блоке            выполняются         только          две     из        трех    инструкций:            cout    и throw.           После            генерирования      исключения            управление передается catch-выражению,           при     этом выполнение            try-блока прекращается.       Необходимо          понимать,   что      catch-инструкция не вызывается, а          просто          с          нее     продолжается       выполнение           программы после "выброса" исключения.            (Стек  программы автоматически      настраивается        в соответствии          с          создавшейся ситуацией.)         Поэтому       cout-инструкция, следующая после throw-инструкции,          никогда        не выполнится.

После     выполнения           catch-блока            управление            программой передается инструкции, следующей          за        этим   блоком.       Поэтому       ваш обработчик             исключения            должен        исправить ошибку,         вызвавшую его возникновение,    чтобы            программа могла            нормально продолжить выполнение.          В          случаях,       когда ошибку         исправить    нельзя,         catch-блок   обычно         завершается обращением       к          функциям    exit()  или     abort(). (Функции     exit()  и          abort()          описаны       в          разделе "Копнем глубже" ниже  в          этой   главе.)

Как          упоминалось         выше,            тип     исключения            должен совпадать    с          типом,          заданным   в          catchинструкции. Например,  если   в предыдущей          программе тип     int,      указанный  в          catchвыражении, заменить типом            double,          то        исключение            перехвачено           не       будет,           и произойдет аварийное завершение            программы.           Вот     как     выглядят последствия           внесения      такого изменения.

//  Этот    пример         работать      не       будет.

#include <iostream>

using       namespace   std;

int    main() {

 cout    <<       "НАЧАЛО\n";

 try       {

  //    начало          try-блока

  cout           <<       "В        trу-блоке\n";

  throw        99;      //         генерирование      ошибки

  cout           <<       "Эта    инструкция не       будет выполнена.";

 }

 catch  (double         i)          {

  //    Перехват      исключения            типа   int       не       состоится.

  cout           <<       "Перехват    исключения.          Его      значение      равно:           ";

  cout           <<       i           <<       "\n";

 }

 cout    <<       "КОНЕЦ";

 return            0;

}

Такие      результаты выполнения           этой   программы объясняются          тем,    что исключение целочисленного             типа   не       перехватывается  инструкцией catch  (double         i).

НАЧАЛО

В   try-блоке

Abnormal          program       termination

Функции         exit()  и          abort()

Функции            exit()  и          abort()          входят           в          состав           стандартной библиотеки            C++     и          часто используются        в          программировании        на C++.   Обе    они     обеспечивают        завершение            программы, но     по-разному.

Вызов     функции       exit()  немедленно           приводит     к          "правильному" прекращению        программы. ("Правильное"    окончание   означает      выполнение стандартной           последовательности       действий по           завершению           работы.) Обычно        этот    способ          завершения            работы         используется         для остановки    программы при     возникновении     неисправимой       ошибки,       которая делает дальнейшее        ее        выполнение           бессмысленным   или     опасным.     Для использования      функции       exit() требуется     включить     в          программу заголовок <cstdlib>.      Ее        прототип      выглядит     так.

void         exit(int           status);

Поскольку        функция       exit()  вызывает     немедленное         завершение программы,           она     не передает            управление            вызывающему      процессу и          не       возвращает             никакого      значения.    Тем    не менее      вызывающему процессу      в          качестве       кода  завершения            передается значение параметра status.            По       соглашению           нулевое        значение      параметра status говорит        об       успешном   окончании работы          программы.           Любое другое          его      значение      свидетельствует   о          завершении            программы по ошибке.        Для    индикации  успешного  окончания   можно          также использовать         константу EXIT_SUCCESS,          а          для     индикации  ошибки— константу    EXIT_FAILURE.        Эти     константы определены            в          заголовке <cstdlib>.

Прототип          функции       abort()          выглядит     так:

void         abort();

Аналогично     exit()  функция       abort()          вызывает     немедленное завершение            программы.           Но       в отличие    от        функции       exit()  она     не возвращает             операционной       системе        никакой       информации          о статусе завершения            и          не       выполняет  стандартной           ("правильной") последовательности действий          при     остановке    программы.           Для использования      функции       abort()          требуется включить        в          программу заголовок    <cstdlib>.      Функцию     abort()          можно          назвать         аварийным "стоп-краном"     для     С++-программы.  Ее        следует        использовать         только после возникновения неисправимой          ошибки.

Последнее       сообщение  об       аварийном  завершении            программы (Abnormal program termination)      может           отличаться  от        приведенного       в результатах            выполнения           предыдущего примера.            Это     зависит         от используемого     вами  компилятора.

Исключение,   сгенерированное             функцией,   вызванной  из        try-блока, может           быть перехвачено            этим   же       try-блоком. Рассмотрим, например,   следующую            вполне          корректную программу.

/* Генерирование     исключения            из        функции,     вызываемой          из        try-блока.

*/

#include <iostream>

using       namespace   std;

void         Xtest(int     test) {

 cout    <<       "В        функции       Xtest(),          значение      test     равно:           "<<     test     <<

"\n";

 if(test)            throw test;

}

int    main() {

 cout    <<       "НАЧАЛО\n";

 try       {

  //    начало          try-блока

  cout           <<       "В        trу-блоке\n";

  Xtest          (0);

  Xtest          (1);

  Xtest          (2);

 }

 catch  (int      i)          {

  //    перехват      ошибки

  cout           <<       "Перехват    исключения.          Его      значение      равно:           ";

  cout           <<       i           <<       "\n";

 }

 cout    <<       "КОНЕЦ";

 return            0;

}

Эта          программа генерирует  такие результаты.

НАЧАЛО            В          try-блоке

В   функции       Xtest(),          значение      test     равно:           0

В   функции       Xtest(),          значение      test     равно:           1 Перехват           исключения.          Его      значение      равно:     1

КОНЕЦ

Блок        try       может           быть  локализован          в          рамках          функции.     В этом   случае           при     каждом        ее выполнении     запускается            и обработка   исключений,          связанная    с          этой   функцией.   Рассмотрим следующую            простую       программу. #include       <iostream> using   namespace   std;

/* Функционирование         блоков          try/catch       возобновляется    при     каждом входе            в          функцию.

*/ void    Xhandler(int     test) {

 try       {

  if(test)       throw test;

 }

 catch(int        i)          {

  cout           <<       "Перехват!  Исключение           №:      "          <<       i           <<       '\n';

 }

}

int    main() {

 cout    <<       "HAЧАЛО\n ";

 Xhandler       (1);

 Xhandler       (2);

 Xhandler       (0);

 Xhandler       (3);

 cout    <<       "КОНЕЦ";

 return            0;

}

При         выполнении           этой   программы отображаются       такие результаты.

НАЧАЛО

Перехват!         Исключение           №:1

Перехват!         Исключение           №:2 Перехват!         Исключение           №:3

КОНЕЦ

Как          видите,         программа сгенерировала      три     исключения.          После каждого       исключения функция     Xhandler()    передавала            управление            в функцию      main().          Когда она     снова вызывалась, возобновлялась             и обработка   исключения.

В   общем          случае           try-блок       возобновляет        свое   функционирование при     каждом        входе            в          него. Поэтому       try-блок,      который       является частью          цикла,           будет запускаться            при     каждом повторении       этого цикла.

Перехват       исключений           классового типа

Исключение    может           иметь            любой           тип,    в          том     числе и          тип класса,          созданного программистом.  В          реальных     программах большинство         исключений           имеют           именно         тип класса,  а          не встроенный            тип.    Вероятно,    тип     класса           больше         всего подходит     для описания ошибки,          которая        потенциально        может           возникнуть в программе.            Как     показано     в          следующем примере,   информация, содержащаяся      в          объекте        класса           исключений,          позволяет    упростить обработку   исключений.

//  Использование     класса           исключений.

#include <iostream> #include <cstring> using     namespace   std;

class        MyException           {  public:

  char           str_what[80];

  MyException()    {           *str_what    =0;      }

  MyException(char         *s)      {           strcpy(str_what,    s);}

};

int    main() {

 int       a,         b;

 try       {

  cout           <<       "Введите      числитель    и          знаменатель:         ";

   cin         >>       а          >>       b;

  if(   !b)       throw MyException("Делить      на       нуль   нельзя!");

  else

   cout      <<       "Частное      равно            "          <<       a/b      <<       "\n";

 }

 catch  (MyException          e)        {

  //    перехват      ошибки

  cout           <<       e.str_what   <<       "\n";

 }

 return            0;

}

Вот          один  из        возможных результатов            выполнения           этой     программы.

Введите числитель    и          знаменатель:         10       0

Делить   на       нуль   нельзя!

После     запуска         программы пользователю       предлагается         ввести числитель    и          знаменатель. Если          знаменатель          равен            нулю, создается объект          класса           MyException,          который       содержит информацию            о попытке       деления       на       нуль. Таким            образом,     класс MyException инкапсулирует информацию об       ошибке,       которая        затем используется обработчиком       исключений           для уведомления            пользователя         о случившемся.

Безусловно,     реальные    классы          исключений           гораздо        сложнее класса           MyException.          Как правило,          создание     классов        исключений имеет            смысл           в          том     случае,         если   они     инкапсулируют информацию,        которая        бы      позволила   обработчику          исключений эффективно            справиться  с ошибкой   и          по       возможности         восстановить работоспособность         программы.

Использование        нескольких catch-инструкций

Как          упоминалось         выше,            с          try-блоком  можно          связывать    не одну, а          несколько   catchинструкций. В          действительности            именно такая практика      и          является       обычной.     Но       при     этом все       catch-инструкции должны       перехватывать       исключения            различных  типов. Например,  в приведенной      ниже  программе обеспечивается     перехват      как целых            чисел,           так      и          указателей  на символы.

#include <iostream> using     namespace   std; //     Здесь возможен     перехват     исключений     различных  типов.

void         Xhandler(int     test) {

 try       {

  if(test)       throw test;

  else            throw "Значение   равно            нулю.";

 }

 catch  (int      i)          {

 cout    <<       "Перехват!  Исключение           №:      "          <<       i           <<       '\n';

 }

 catch(char    *str)   {

  cout           <<       "Перехват    строки:         ";

  cout           <<       str       <<       '\n';

 }

}

int    main() {

 cout    <<       "НАЧАЛО\n";

 Xhandler(1);

 Xhandler(2);

 Xhandler(0);

 Xhandler(3);

 cout    <<       "КОНЕЦ";

 return            0;

}

Эта          программа генерирует  такие результаты.

НАЧАЛО

Перехват!         Исключение           №:      1

Перехват!         Исключение           №:      2 Перехват           строки:         Значение     равно     нулю. Перехват!   Исключение           №:      3

КОНЕЦ

Как          видите,         каждая         catch-инструкция отвечает       только          за исключение            "своего"       типа.  В общем      случае           catch-выражения проверяются          в          порядке       следования,           и          выполняется          только тот      catch-блок, в          котором       тип     заданного   исключения            совпадает    с типом            сгенерированного исключения.       Все     остальные   catch-блоки игнорируются.

Перехват       исключений           базового     класса

Важно    понимать,   как     выполняются         catch-инструкции,            связанные   с производными классами.       Дело  в          том,   что      catch-выражение для     базового класса           "отреагирует     совпадением" на исключение            любого производного       типа   (т.е.    типа,  выведенного         из        этого  базового      класса). Следовательно,    если   нужно           перехватывать       исключения            как     базового, так      и          производного типов,     в          catch-последовательности       catch-инструкцию            для     производного       типа   необходимо поместить            перед catch-инструкцией           для     базового      типа.  В          противном  случае catchвыражение   для     базового      класса           будет перехватывать       (помимо "своих")       и          исключения            всех производных           классов.       Рассмотрим, например,   следующую            программу:

//  Перехват      исключений           базовых       и          производных     типов. #include      <iostream> using   namespace   std;

class     В     { };

class        D:        public В     { };

int    main() {

 D         derived;

 try       {

  throw        derived;

 }

 catch(B          b)        {

  cout           <<       "Перехват    исключения            базового      класса.\n";

 }

 catch(D          d)        {

 cout    <<       "Этот  перехват      никогда        не       произойдет.\n";

 }

 return            0;

}

Поскольку        здесь объект          derived          —        это      объект          класса           D, который       выведен       из        базового класса   В,        то        исключение            типа derived          будет всегда           перехватываться  первым        catchвыражением; вторая           же       catch-инструкция при     этом   никогда        не       выполнится. Одни компиляторы        отреагируют           на       такое положение вещей предупреждающим        сообщением. Другие     могут выдать          сообщение  об ошибке.       В          любом          случае,         чтобы            исправить    ситуацию, достаточно  поменять     порядок       следования            этих    catch-инструкций             на противоположный.

Варианты     обработки             исключений

Помимо            рассмотренных,    существуют             и          другие          С++-средства обработки   исключений, которые   создают       определенные      удобства      для программистов.   О         них     и          пойдет          речь   в          этом разделе.

Перехват       всех   исключений

Иногда   имеет            смысл           создать         обработчик             для     перехвата    всех исключений,          а          не исключений     только          определенного     типа.  Для этого  достаточно использовать         такой формат catch-блока.

catch       (...)     {

 //         Обработка  всех    исключений

}

Здесь      заключенное         в          круглые       скобки          многоточие обеспечивает         совпадение            с          любым типом        данных.

Использование          формата       catch(...)       иллюстрируется   в          следующей     программе.

//  В          этой   программе перехватываются             исключения            всех     типов. #include      <iostream> using   namespace   std;

void         Xhandler(int     test) {

 try       {

  if(test==0)           throw test;    //         генерирует  int-исключение

  if(test==1)           throw 'a';       //         генерирует  char-исключение

  if(test==2)           throw 123.23;         //         генерирует  double-исключение

 }

 catch  (...)     {           //         перехват      всех    исключений

  cout           <<       "Перехват!\n";

 }

}

int    main() {

 cout    <<       "НАЧАЛО\n";

 Xhandler       (0);

 Xhandler       (1);

 Xhandler       (2);

 cout    <<       "КОНЕЦ";

 return            0;

}

Эта          программа генерирует  такие результаты.

НАЧАЛО Перехват!

Перехват!

Перехват!

КОНЕЦ

Как          видите,         все      три     throw-исключения          перехвачены          с     помощью    одной-единственной catch-инетрукции.

Зачастую           имеет            смысл           использовать         инструкцию            catch(...) в          качестве       последнего "рубежа"   catch-последовательности.     В          этом случае           она     обеспечивает         перехват      исключений "всех           остальных" типов (т.е.    не       предусмотренных           предыдущими      catch-выражениями). Например,  рассмотрим            еще    одну  версию         предыдущей          программы, в          которой        явным           образом обеспечивается          перехват      исключений целочисленного   типа,  а          перехват      всех    остальных возможных  исключений "взваливается     на       плечи"          инструкции catch(...).

/* Использование     формата       catch  (...)     в          качестве       варианта      "все остальное".

*/ #include     <iostream> using     namespace   std;

void         Xhandler(int     test) {

 try       {

  if(test==0)           throw test;    //         генерирует  int-исключение

  if(test==1)           throw 'a';       //         генерирует  char-исключение

  if(test==2)           throw 123.23;         //         генерирует  double-исключение

 }

 catch(int        i)          {

  //    перехватывает       int-исключение

  cout           <<       "Перехват    "          <<       i           <<       '\n';

 }

 catch(...)       {

  //    перехватывает       все      остальные   исключения

  cout           <<       "Перехват-перехват!\n";

 }

}

int    main() {

 cout    <<       "НАЧАЛО\n";

 Xhandler(0);

 Xhandler(1);

 Xhandler(2);

 cout    <<       "КОНЕЦ";

 return            0;

}

Результаты,     сгенерированные            при     выполнении           этой   программы,     таковы.

НАЧАЛО

Перехват           0 Перехват-перехват!

Перехват-перехват!

КОНЕЦ

Как          подтверждает       этот    пример,       использование      формата       catch(...) в          качестве       "последнего оплота"     catch-последовательности—  это      удобный способ          перехватить            все      исключения, которые    вам    не       хочется обрабатывать        в          явном           виде. Кроме           того,  перехватывая абсолютно  все исключения,   вы       предотвращаете   возможность         аварийного завершения            программы,           которое может     быть  вызвано       каким-то непредусмотренным     (а        значит,          необработанным) исключением.

Ограничения,           налагаемые          на       тип   исключений,   генерируемых      функциями

Существуют     средства,     которые       позволяют  ограничить тип     исключений, которые       может генерировать      функция       за        пределами своего           тела. Можно         также            оградить      функцию      от генерирования            каких бы      то ни       было исключений           вообще.       Для    формирования      этих ограничений необходимо           внести           в          определение         функции       throw-выражение. Общий          формат определения     функции       с          использованием  throw-выражения выглядит     так.

тип          имя_функции(список_аргументов)  throw(список_имен_типов) {

 //         .           .           .

}

Здесь      элемент        список_имен_типов     должен        включать     только          те имена           типов данных, которые разрешается           генерировать         функции (элементы   списка           разделяются          запятыми). Генерирование     исключения любого         другого        типа   приведет     к          аварийному           окончанию программы.           Если   нужно,          чтобы            функция       вообще         не       могла генерировать         исключения, используйте        в          качестве       этого  элемента пустой           список.

На            заметку.      При    попытке     сгенерировать    исключение,          которое не       поддерживается функцией, вызывается          стандартная       библиотечная функция       unexpected().          По       умолчанию она вызывает      функцию     abort(), которая      обеспечивает       аварийное  завершение           программы.           Но при желании      можно          задать         собственный        обработчик          процесса завершения.          За подробностями         обращайтесь       к          документации, прилагаемой         к          вашему        компилятору.

На            примере      следующей программы показано,    как     можно ограничить типы  исключений, которые   способна     генерировать         функция.

/* Ограничение          типов исключений,          генерируемых       функцией.

*/ #include     <iostream> using     namespace   std;

/* Эта      функция       может           генерировать                     исключения            только            типа   int, char        и          double.

*/ void    Xhandler(int            test)    throw(int,     char,  double) {

 if(test==0)    throw test;    //         генерирует  int-исключение

 if(test==1)    throw 'a';       //         генерирует  char-исключение

 if(test==2)    throw 123.23;         //         генерирует  double-исключение

}

int    main() {

 cout    <<       "НАЧАЛО\n";

 try       {

  Xhandler(0);       //         Попробуйте            также            передать      функции            Xhandler() аргументы     1          и          2.

 }

 catch(int        i)          {

  cout           <<       "Перехват    int-исключения.\n";

 }

 catch(char    c)         {

  cout           <<       "Перехват    char-исключения.\n";

 }

 catch(double           d)        {

  cout           <<       "Перехват    double-исключения.\n";

 }

 cout    <<       "КОНЕЦ";

 return            0;

}

В   этой   программе функция       Xhandler()    может           генерировать исключения            только          типа   int, char        и          double.          При    попытке сгенерировать       исключение            любого         другого        типа   произойдет аварийное   завершение            программы (благодаря вызову          функции unexpected()).        Чтобы убедиться  в          этом, удалите        из        throw-списка, например,   тип     int       и          перезапустите       программу.

Важно    понимать,   что      диапазон     исключений,          разрешенных         для генерирования      функции, можно  ограничивать         только          типами, генерируемыми   ею      в          try-блоке,    из        которого      была вызвана.      Другими словами,      любой           try-блок,      расположенный   в          теле   самой            функции, может генерировать      исключения            любого         типа,  если   они перехватываются             в          теле   той     же       функции. Ограничение применяется только          для     ситуаций,     когда "выброс"     исключений           происходит             за пределы      функции.

Следующее     изменение  помешает    функции       Xhandler()    генерировать     любые          изменения.

//  Эта      функция       вообще         не       может           генерировать     исключения! void            Xhandler(int            test)    throw() {

 /*        Следующие            инструкции больше         не       работают.    Теперь          они            могут вызвать       лишь аварийное   завершение            программы.           */

 if(test==0)    throw test;

 if(test==1)    throw 'a';

 if(test==2)    throw 123.23;

}

На            заметку.      На       момент       написания  этой книги среда Visual C++     не обеспечивала        для функции          запрет         генерировать      исключения,          тип которых     не       задан            в          throw-выражении.          Это говорит        о нестандартном поведении  данной         среды.          Тем    не       менее            вы       все равно            можете задавать         "ограничивающее"        throw-выражение,          но оно     в          этом случае          будет           играть         лишь уведомительную роль.

Повторное    генерирование     исключения

Для          того    чтобы            повторно     сгенерировать       исключение            в          его обработчике,         воспользуйтесь throw-инструкцией            без     указания      типа исключения.          В          этом   случае           текущее        исключение            будет передано во       внешнюю    try/catch-последовательность.          Чаще всего причиной    для такого выполнения         инструкции throw служит          стремление            позволить доступ          к          одному        исключению нескольким         обработчикам. Например,  первый         обработчик             исключений           будет сообщать     об одном аспекте         исключения,          а          второй          —        о          другом.        Исключение можно          повторно сгенерировать          только          в          catch-блоке            (или   в любой           функции,     вызываемой          из        этого  блока).         При повторном генерировании     исключение            не       будет перехватываться  той     же       catch-инструкцией. Оно           распространится  на       ближайшую           try/catch-последовательность.

Повторное        генерирование      исключения            демонстрируется в следующей программе (в данном   случае           повторно     генерируется         тип     char *).

//  Пример        повторного генерирования      исключения. #include <iostream> using   namespace   std;

void     Xhandler() {

 try       {

  throw        "Привет";     //         генерирует  исключение            типа   char    *

 }

 catch(char    *)        {           //         перехватывает       исключение            типа   char    *

  cout           <<       "Перехват    исключения            в          функции       Xhandler.\n";

  throw;      //         Повторное  генерирование      исключения            типа   char    *, которое        будет перехвачено                       вне     функции       Xhandler.

 }

}

int    main() {

 cout    <<       "НАЧАЛО\n";

 try       {

  Xhandler();

 }

 catch(char    *)        {

  cout           <<       "Перехват    исключения            в          функции       main().\n";

 }

 cout    <<       "КОНЕЦ";

 return            0;

}

При         выполнении           эта      программа генерирует  такие результаты.

НАЧАЛО Перехват    исключения            в     функции       Xhandler.

Перехват           исключения            в          функции       main().

КОНЕЦ

Обработка   исключений,          сгенерированных            оператором         new

В   главе 9          вы       узнали,         что      оператор     new    генерирует  исключение, если   не       удается удовлетворить запрос          на       выделение  памяти. Поскольку   тема   исключений           рассматривается только          в          этой   главе, описание     обработки   исключений           этого  типа   было отложено    "на потом". Вот     теперь           настало        время            об       этом   поговорить.

Для          начала          необходимо           отметить,     что      в          этом   разделе описывается           поведение  оператора new     в          соответствии          со стандартом             C++.   Как     было отмечено     в          главе 9,        действия, выполняемые        системой     при     неуспешном           использовании     оператора new,   с          момента изобретения   языка            C++     изменялись            уже несколько   раз.    Сначала        оператор     new    возвращал  при неудаче           значение null.    Позже           такое поведение  было заменено     генерированием  исключения. Кроме           того,  несколько   раз     менялось     имя    этого  исключения.          Наконец, было решено,       что оператор          new    будет генерировать         исключения            по умолчанию,           но       в          качестве       альтернативного варианта      он       может возвращать             и          нулевой       указатель.   Следовательно,    оператор     new    в разное время        был    реализован             различными           способами. И         хотя    все современные         компиляторы реализуют         оператор     new    в          соответствии со        стандартом             C++,   компиляторы        более "почтенного"        возраста могут содержать   отклонения             от        него.  Если   приведенные         здесь примеры программ    не       работают     с          вашим          компилятором,     обратитесь  к документации, прилагаемой к          компилятору,        и          поинтересуйтесь,             как именно         он       реализует функционирование           оператора   new.

Согласно           стандарту    C++     при     невозможности    удовлетворить      запрос на       выделение  памяти, требуемой         оператором            new,   генерируется исключение            типа   bad_alloc.    Если   ваша  программа не       перехватит  его,    она будет досрочно     завершена. Хотя   такое поведение  годится         для коротких примеров    программ,  в          реальных     приложениях         необходимо перехватывать       это исключение    и          разумно       обрабатывать        его.    Чтобы получить      доступ          к          исключению          типа bad_alloc,     нужно           включить в          программу заголовок    <new>.

Рассмотрим     пример         использования      оператора   new,   заключенного       в try/catch-блок       для отслеживания           неудачных  результатов            запроса        на выделение  памяти.

//  Обработка  исключений,          генерируемых       оператором            new.

#include <iostream> #include <new> using     namespace   std;

int    main() {

 int       *p,      i;

 try       {

  p     =          new    int[32];          //         запрос          на       выделение  памяти          для            32элементного     int-массива

 }

 catch  (bad_alloc    ха)      {

  cout           <<       "Память       не       выделена.\n";

  return       1;

 }

 for(i=0;          i<32;  i++)     p[i]      =          i;

for(i=0;          i<32;  i++      )           cout    <<       p[i]      <<       "          ";

 delete             []         p;        //         освобождение      памяти

 return            0;

}

При         неудачном  выполнении           оператора   new    исключение            в этой   программе будет перехвачено          catch-инструкцией.         Этот    же       подход можно          использовать         для     отслеживания любых    ошибок,       связанных   с использованием  оператора   new:   достаточно заключить   каждую new-инструкцию в          try-блок.

Альтернативная  форма          оператора             new    —        nothrow

Стандарт           C++     при     неудачной  попытке       выделения  памяти          вместо генерирования исключения   также            позволяет    оператору   new    возвращать значение      null.    Эта      форма использования   оператора   new    особенно     полезна при     компиляции           старых          программ    с применением     современного       С++-компилятора.        Это     средство      также            очень            полезно       при замене вызовов       функции       malloc()        оператором            new.   (Это    обычная       практика при     переводе     Скода            на       язык  C++.)  Итак,  этот    формат         оператора   new выглядит     следующим            образом.

p_var      =          new(nothrow)        тип;

Здесь      элемент        p_var—        это      указатель    на       переменную          типа тип.  Этот    nothrow-формат оператора    new    работает      подобно      оригинальной версии          оператора   new,   которая использовалась          несколько   лет      назад. Поскольку   оператор     new    (nothrow)    возвращает             при неудаче           значение null,    его      можно          "внедрить" в          старый          код     программы,           не прибегая      к обработке            исключений.          Однако         в          новых программах           на       C++     все      же       лучше           иметь            дело  с исключениями.

В   следующем            примере      показано,    как     используется альтернативный   вариант        new (nothrow).      Нетрудно     догадаться, что      перед вами  вариация     на       тему   предыдущей          программы.

//  Использование     nothrow-версии    оператора   new.

#include <iostream> #include <new> using     namespace   std;

int    main() {

 int       *p,      i;

p          =          new(nothrow)        int[32];          //         использование      nothrow-версии

 if(!p)   {

  cout           <<       "Память       не       выделена.\n";

  return       1;

 }

 for(i=0;          i<32;  i++)     p[i]      =          i;

 for(i=0;          i<32;  i++      )           cout    <<       p[i]      <<       "          ";

 delete             []         p;        //         освобождение      памяти

 return            0;

}

Здесь      при     использовании     nothrow-версии    после каждого       запроса        на выделение  памяти необходимо       проверять   значение      указателя,   возвращаемого оператором            new.

Перегрузка    операторов          new    и          delete

Поскольку        new    и          delete            —        операторы, их        также            можно перегружать.         Несмотря     на       то        что перегрузку      операторов             мы рассматривали      в          главе 13,      тема   перегрузки операторов             new    и delete была  отложена     до       знакомства с          темой            исключений,          поскольку правильно перегруженная      версия          оператора   new    (та,     которая соответствует         стандарту    C++)   должна        в случае       неудачи       генерировать исключение            типа   bad_alloc.    По       ряду  причин         вам    имеет            смысл создать         собственную          версию         оператора   new.   Например,  создайте процедуры выделения памяти,        которые,      если   область        кучи   окажется исчерпанной,         автоматически      начинают использовать            дисковый    файл  в качестве       виртуальной          памяти.        В          любом          случае           реализация перегрузки  этих    операторов             не       сложнее       перегрузки любых          других.

Ниже      приводится             скелет           функций,     которые       перегружают     операторы  new    и          delete.

//  Выделение памяти          для     объекта. void         *operator     new(size_t   size)

{

/*        В          случае           невозможности    выделить     память          генерируется исключение            типа   bad_alloc.    Конструктор           вызывается             автоматически.

*/

 return            pointer_to_memory;

}

//  Удаление     объекта.

void         operator       delete(void     *p) {

 /*        Освобождается     память,         адресуемая            указателем р.            Деструктор вызывается            автоматически.     */

}

Тип          size_t специально            определен, чтобы            обеспечить хранение размера       максимально возможной        области        памяти,        которая        может быть  выделена    для     объекта.       (Тип   size_t,            по       сути, —это   целочисленный тип     без     знака.)          Параметр    size     определяет             количество байтов памяти, необходимых   для     хранения     объекта,       для     которого      выделяется память.         Другими      словами,      это объем   памяти,        который       должна выделить     ваша  версия          оператора   new.   Перегруженная функция          new должна        возвращать             указатель    на       выделяемую          ею      память          или генерировать исключение      типа   bad_alloc     в          случае           возникновении ошибки.       Помимо       этих    ограничений, перегруженная            функция       new может           выполнять  любые          нужные        действия.     При    выделении памяти для     объекта        с          помощью    оператора   new    (его    исходной     версии          или вашей собственной)       автоматически      вызывается             конструктор           объекта.

Функция            delete            получает      указатель    на       область        памяти, которую       необходимо           освободить. Затем          она     должна        вернуть        эту область        памяти          системе.       При    удалении     объекта автоматически вызывается             его      деструктор.

Чтобы     выделить     память          для     массива        объектов,    а          затем освободить             ее,      необходимо использовать      следующие форматы операторов             new    и          delete. //      Выделение памяти          для     массива объектов.

void         *operator     new[](size_t     size) {

 /*        В          случае           невозможности    выделить     память          генерируется исключение            типа   bad_alloc.    Каждый        конструктор           вызывается автоматически.     */

 return            pointer_to_memory;

}

//  Удаление     массива        объектов. void         operator       delete[](void     *p) {

 /*        Освобождается     память,         адресуемая            указателем р.        При            этом автоматически       вызывается             деструктор  для     каждого       элемента            массива.

*/

}

При         выделении памяти          для     массива        автоматически      вызывается конструктор           каждого объекта,            а          при     освобождении      массива автоматически      вызывается             деструктор  каждого объекта.            Это     значит, что      для     выполнения           этих    действий      не       нужно           явным           образом программировать            их.

Операторы       new    и          delete,           как     правило,      перегружаются относительно         класса.          Ради простоты      в          следующем            примере используется         не       новая            схема            распределения     памяти,        а перегруженные    функции       new    и          delete,           которые       просто          вызывают С-ориентированные функции            выделения  памяти          malloc()        и          free(). (В своем            собственном          приложении           вы       вольны реализовать       любой метод            выделения  памяти.)

Чтобы     перегрузить            операторы  new    и          delete            для     конкретного класса,          достаточно сделать эти перегруженные    операторные         функции членами       этого  класса.          В          следующем            примере программы операторы  new    и          delete            перегружаются     для     класса           three_d.        Эта перегрузка позволяет    выделить     память          для     объектов      и          массивов объектов,    а          затем освободить             ее.

//  Демонстрация       перегруженных    операторов             new    и          delete.

#include <iostream>

#include <new> #include     <cstdlib> using     namespace   std;

class        three_d         {

  int  x,         y,         z;         //         3-мерные    координаты

 public:

  three_d() {

   x =          у          =          z          =          0;

   cout      <<       "Создание   объекта        0,        0,        0\n";

  }

  three_d(int          i,          int       j,          int       k)        {

   x =          i;

   у =          j;

   z =          k;

   cout      <<       "Создание   объекта        "          <<       i           <<       ",         ";

   cout      <<       j           <<       ",         "          <<       k;

   cout      <<       '\n';

  }

  ~three_d()           {           cout    <<       "Разрушение          объекта\n";             }

  void           *operator     new(size_t   size);

  void           *operator     new[](size_t            size);

  void           operator       delete(void   *p);

  void           operator       delete[](void            *p);

  void           show();

}; //          Перегрузка оператора   new    для     класса     three_d. void          *three_d::operator           new(size_t     size) {

 void    *p;

 cout    <<"Выделение      памяти          для     объекта        класса           three_d.\n";

 р          =          malloc(size);

 //         Генерирование     исключения            в          случае           неудачного            выделения памяти.

 if(!р)   {

  bad_alloc ba;

  throw        ba;

 }

 return            р;

}

//  Перегрузка оператора   new    для     массива        объектов      типа   three_d. void     *three_d::operator           new[](size_t            size) {

 void    *p;

 cout    <<"Выделение      памяти          для     массива        three_d-oбъeктoв.";

 cout    <<       "\n";

 //         Генерирование     исключения            при     неудаче.

 р          =          malloc(size);

 if(!р)   {

  bad_alloc ba;

  throw        ba;

 }

 return            p;

}

//  Перегрузка оператора   delete            для     класса     three_d. void          three_d::operator delete(void   *p) {

 cout    <<       "Удаление   объекта        класса           three_d.\n";

 free(p);

}

// Перегрузка оператора   delete            для     массива        объектов      типа three_d.

void         three_d::operator delete[](void            *p) {

 cout    <<       "Удаление   массива        объектов      типа   three_d.\n";

 free(р);

} //           Отображение         координат     X,        Y,         Z. void     three_d::show() {

 cout    <<       x          <<       ",         ";

 cout    <<       у          <<     ",         ";  cout      <<     z          <<       "\n";

}

int    main() {

 three_d         *p1,   *p2;

 try       {

  p1  =          new    three_d[3];  //         выделение  памяти          для     массива

  р2  =          new    three_d(5,    6,        7);       //         выделение  памяти          для     объекта

 }

 catch  (bad_alloc    ba)      {

  cout           <<       "Ошибка      при     выделении памяти.\n";

  return       1;

 }

 p1[1].show();

 p2->show();

 delete             []         p1;      //         удаление     массива

 delete             р2;      //         удаление     объекта

 return            0;

}

При         выполнении           эта      программа генерирует  такие результаты.

Выделение      памяти          для     массива        three_d-oбъeктoв.

Создание          объекта        0,        0,        0

Создание          объекта        0,        0,        0

Создание          объекта        0,        0,        0 Выделение     памяти          для     объекта        класса           three_d. Создание          объекта        5,        6,        7

0, 0,        0

5, б,        7

Разрушение     объекта

Разрушение     объекта

Разрушение     объекта Удаление           массива     объектов      типа   three_d.

Разрушение     объекта

Удаление          объекта        класса           three_d.

Первые  три     сообщения  Создание     объекта      0,        0,        0          выданы конструктором      класса           three_d (который не       имеет            параметров)           при выделении памяти          для     трехэлементного  массива.      Как упоминалось выше, при     выделении памяти          для     массива        автоматически      вызывается конструктор            каждого       элемента.    Сообщение Создание     объекта      5,        б, 7          выдано         конструктором класса   three_d         (который     принимает  три аргумента)  при     выделении памяти          для     одного объекта.   Первые         три сообщения  Разрушение           объекта      выданы        деструктором        в результате удаления     трехэлементного  массива,      поскольку   при     этом автоматически      вызывался деструктор  каждого       элемента     массива. Последнее  сообщение  Разрушение           объекта      выдано при            удалении одного          объекта        класса           three_d.        Важно           понимать,   что,    если операторы  new    и delete         перегружены         для     конкретного           класса,          то в          результате  их        использования      для     данных других      типов будут задействованы      оригинальные       версии          операторов             new    и          delete. Это означает,         что      при     добавлении            в          функцию      main() следующей строки           будет выполнена стандартная           версия          оператора new.

int            *f        =          new    int;      //         Используется         стандартная           версия     оператора

new.

И  еще.   Операторы new    и          delete            можно          перегружать глобально.  Для    этого  достаточно объявить     их        операторные         функции       вне классов.       В          этом   случае           стандартные          версии          С++операторов     new и          delete            игнорируются        вообще,       и          во       всех    запросах      на выделение  памяти используются    их        перегруженные    версии.         Безусловно, если   вы       при     этом   определите            версию операторов        new    и          delete для     конкретного           класса,          то        эти      "классовые"           версии          будут применяться          при     выделении памяти          (и        ее        освобождении)     для объектов      этого  класса.          Во       всех же         остальных   случаях         будут использоваться     глобальные            операторные         функции.

Перегрузка    nothrow-версии    оператора             new

Можно   также            создать         перегруженные    nothrow-версии    операторов new    и          delete.           Для    этого используйте           такие схемы.

//  Перегрузка nothrow-версии    оператора   new.

void         *operator     new(size_t   size,    const  nothrow_t   &n) {

 //         Выделение памяти.

 if(success)     return            pointer_to_memory;

 else     return            0;

}

//  Перегрузка nothrow-версии    оператора   new    для     массива. void     *operator     new[](size_t            size,    const  nothrow_t   &n) {

 //         Выделение памяти.

 if(success)     return            pointer_to_memory;

 else     return            0;

}

//  Перегрузка nothrow-версии    оператора   delete. void     operator       delete(void   *p,      const  nothrow_t   &n) {

 //         Освобождение      памяти.

}

//  Перегрузка nothrow-версии    оператора   delete            для     массива.

void         operator       delete[](void            *p,      const  nothrow_t   &n) {

 //         Освобождение      памяти.

}

Тип          nothrow_t   определяется        в          заголовке    <new>.          Параметр типа   nothrow_t   не используется.  В          качестве       упражнения поэкспериментируйте   с          nothrow-версиями          операторов new   и          delete самостоятельно.

Глава     18:  С++-система ввода-вывода

С   самого          начала          книги мы      использовали        С++-систему           ввода-вывода,        но       не       давали подробных         пояснений   по       этому            поводу. Поскольку   С++-система           ввода-вывода       построена   на иерархии           классов, ее        теорию         и          детали          невозможно           освоить,       не       рассмотрев сначала        классы, наследование   и          механизм    обработки   исключений. Теперь          настало        время            для     подробного изучения    С++-средств ввода-вывода.

В   этой   главе рассматриваются средства      как     консольного,         так      и файлового  ввода-вывода. Необходимо   сразу отметить,     что      С++-система ввода-вывода       —        довольно    обширная    тема, и здесь         описаны       лишь самые           важные        и          часто применяемые       средства.     В          частности,   вы узнаете, как           перегрузить            операторы  "<<"   и          ">>"   для     ввода            и вывода         объектов      созданных  вами классов,        а          также            как отформатировать             выводимые            данные         и          использовать манипуляторы ввода-вывода.           Завершает   главу рассмотрение        средств файлового  ввода-вывода.

Сравнение      старой        и          новой            С++-систем           ввода-вывода

В   настоящее   время            существуют             две     версии          библиотеки объектно-ориентированного ввода-вывода,     причем         обе     широко используются        программистами:            более            старая,          основанная на оригинальных       спецификациях     языка            C++,   и          новая,           определенная стандартом             языка            C++. Старая библиотека             ввода-вывода поддерживается  за        счет    заголовочного      файла            <iostream.h>, а      новая —        посредством          заголовка    <iostream>. Новая            библиотека             ввода-вывода,        по       сути, представляет          собой            обновленную        и усовершенствованную  версию         старой.         Основное различие        между ними состоит         в          реализации,           а          не       в          том,   как     их        нужно использовать.

С   точки зрения          программиста,      есть    два     существенных        различия между          старой           и          новой            С ++-библиотеками         ввода-вывода.      Во-первых,        новая            библиотека             содержит     ряд дополнительных     средств и          определяет             несколько   новых            типов данных.        Таким            образом, новую           библиотеку             ввода-вывода       можно          считать супермножеством           старой.         Практически           все программы,   написанные для     старой           библиотеки,           успешно       компилируются    при использовании новой,           не       требуя           внесения      каких-либо значительных        изменений. Во-вторых, старая            библиотека             ввода-вывода       была  определена            в глобальном            пространстве         имен,            а          новая использует пространство         имен  std.     (Вспомните,            что      пространство         имен  std используется всеми       библиотеками       стандарта    C++.)  Поскольку   старая библиотека             ввода-вывода       уже устарела,        в          этой   книге описывается только          новая,           но       большая      часть  информации          применима и        к старой.

Потоки           C++

Поток     —        это    последовательный        логический интерфейс,           который связан           с          физическим файлом.

Принципиальным     для     понимания С++-системы          ввода-вывода       является то,       что      она опирается       на       понятие        потока.         Поток          (stream)        — это      общий           логический интерфейс   с различными       устройствами, составляющими   компьютер.            Поток            либо  синтезирует информацию, либо  потребляет ее        и          связывается            с          любым         физическим устройством           с помощью С++-системы          ввода-вывода.      Характер поведения  всех    потоков        одинаков,   несмотря на           различные  физические устройства, с          которыми    они     связываются.         Поскольку   потоки действуют одинаково, то        практически           ко       всем  типам            устройств     можно применить  одни  и          те же функции       и          операторы  ввода-вывода. Например,  методы,       используемые       для     записи данных      на       экран, также            можно          использовать         для     вывода         их        на       принтер        или для     записи          в дисковый файл.

В   самой            общей           форме           поток можно          назвать         логическим интерфейсом         с          файлом.       С++определение  термина       "файл"         можно отнести         к          дисковому  файлу,          экрану,         клавиатуре,          порту, файлу на       магнитной            ленте           и          пр.      Хотя   файлы           отличаются по форме           и          возможностям,     все потоки  одинаковы.            Достоинство этого  подхода       (с         точки зрения          программиста)      состоит         в том,            что одно  устройство  компьютера           может           "выглядеть"         подобно      любому другому.      Это значит, что      поток обеспечивает         интерфейс,  согласующийся     со всеми            устройствами.

Поток     связывается            с          файлом        при     выполнении           операции открытия     файла,          а отсоединяется   от        него   с          помощью    операции закрытия.

Существует       два     типа   потоков:       текстовый            и          двоичный. Текстовый поток           используется для ввода-вывода       символов.   При    этом могут происходить          некоторые  преобразования символов.    Например,  при выводе         символ         новой            строки           может           быть  преобразован        в последовательность       символов:   возврата      каретки        и          перехода     на новую           строку.          Поэтому может    не       быть  взаимно-однозначного соответствия          между          тем,    что      посылается в          поток,           и тем,            что в          действительности            записывается         в          файл. Двоичный   поток можно          использовать         с данными  любого         типа,  причем         в          этом случае           никакого      преобразования   символов    не выполняется,   и          между тем,    что      посылается в          поток,           и          тем,    что      потом            реально содержится            в файле,       существует  взаимно-однозначное   соответствие.

Текущая            позиция      —        это    место          в          файле,          с          которого будет           выполняться        следующая операция    доступа      к          файлу.

Говоря   о          потоках,       необходимо           понимать,   что      вкладывается        в понятие        "текущей позиции".        Текущая       позиция       —        это      место            в файле,          с          которого      будет выполняться          следующая операция    доступа к          файлу.          Например,  если   длина            файла            равна            100     байт,  и известно,     что      уже прочитана      половина     этого  файла,          то        следующая операция     чтения           произойдет            на       байте 50, который            в          данном случае           и          является       текущей       позицией.

Итак,       в          языке            C++     механизм    ввода-вывода       функционирует     с использованием  логического интерфейса,         именуемого           потоком.      Все потоки          имеют           аналогичные          свойства,     которые позволяют выполнять  одинаковые           функции       ввода-вывода,      независимо            от того,  с          файлом какого     типа   существует  связь.            Под    файлом понимается            реальное     физическое             устройство, которое       содержит данные.       Если   файлы           различаются          между          собой,           то        потоки —        нет. (Конечно,       некоторые  устройства  могут не       поддерживать       все операции     ввода-вывода, например        операции     с          произвольной выборкой,   поэтому       и          связанные   с          ними потоки          тоже  не будут поддерживать       эти      операции.)

Встроенные С++-потоки

В   C++     содержится            ряд     встроенных             потоков        (cin,    coutcerr    и clog), которые автоматически            открываются,         как     только          программа начинает      выполняться.         Как     вы       знаете, cin   —        это      стандартный входной,      а          cout    —        стандартный          выходной    поток.           Потоки          cerr и          clog (они      предназначены    для     вывода         информации          об       ошибках) также            связаны        со        стандартным выводом данных.        Разница        между ними состоит         в          том,   что      поток clog    буферизирован,   а          поток cerr    — нет.    Это     означает,     что      любые          выходные   данные,       посланные  в поток cerr,   будут немедленно          выведены,  а          при     использовании     потока clog    данные         сначала        записываются        в буфер,       и          реальный    их вывод           происходит             только          тогда,            когда буфер            полностью заполняется.

Обычно        потоки          cerr    и          clog    используются        для     записи информации          об       отладке        или     ошибках.

В   C++     также            предусмотрены    двухбайтовые        (16-битовые) символьные           версии стандартных       потоков,       именуемые            wcinwcout, wcerr и          wclog.           Они    предназначены    для поддержки    таких языков,        как китайский,  для     представления      которых       требуются   большие символьные наборы.       В          этой   книге двухбайтовые        стандартные          потоки          не используются.

По            умолчанию стандартные          С++-потоки связываются          с консолью,   но       программным способом          их        можно          перенаправить      на другие          устройства  или     файлы.         Перенаправление может         также выполнить  операционная       система.

Классы потоков

Как          вы       узнали          в          главе 2,        С++-система           ввода-вывода использует  заголовок    <iostream>, в котором   для     поддержки операций     ввода-вывода         определена            довольно    сложная       иерархия классов.           Эта иерархия     начинается  с          системы       шаблонных классов.       Как     отмечалось в          главе 16, шаблонный     класс определяет             форму,         не       задавая        в полном        объеме         данные,       которые       он должен  обрабатывать.       Имея шаблонный            класс,            можно          создавать    его      конкретные экземпляры. Для    библиотеки            ввода-вывода       стандарт      C++     создает         две специализации шаблонных    классов:       одну  для     8-,       а          другую         для     16-битовых        ("широких")           символов.   В          этой книге   описываются         классы только          для     8-битовых   символов,   поскольку   они     используются гораздо чаще.

С++-система    ввода-вывода       построена   на       двух   связанных,  но различных  иерархиях шаблонных  классов.       Первая          выведена    из        класса низкоуровневого ввода-вывода basic_streambuf.        Этот    класс поддерживает базовые       низкоуровневые  операции     ввода            и          вывода и     обеспечивает поддержку для     всей   С++-системы          ввода-вывода.      Если   вы       не собираетесь заниматься          программированием     специализированных     операций ввода-вывода,      то        вам    вряд ли        придется      использовать         напрямую класс basic_streambuf.   Иерархия     классов,       с          которой        С

++-программистам          наверняка   предстоит    работать      вплотную,   выведена    из класса           basic_ios. Это         —        класс высокоуровневого          ввода-вывода, который       обеспечивает         форматирование, контроль    ошибок        и предоставляет       статусную    информацию,        связанную   с          потоками вводавывода.        (Класс           basic_ios       выведен       из        класса           ios_base, который       определяет             ряд     нешаблонных свойств,  используемых       классом basic_ios.)    Класс basic_ios       используется         в          качестве       базового для нескольких производных         классов,       включая       классы          basic_istream, basic_ostream        и basic_iostream.  Эти     классы          используются        для     создания потоков,       предназначенных            для     ввода данных,      вывода         и          ввода-вывода         соответственно.

Как          упоминалось         выше,            библиотека             ввода-вывода       создает две     специализированные иерархии       шаблонных классов:       одну  для     8-,       а другую         для     16-битовых символов.   Ниже приводится            список          имен шаблонных классов        и          соответствующих им      "символьных"       версий.

В   остальной   части этой   книги используются        имена           символьных классов,       поскольку именно          они     применяются         в          программах.          Те же       имена           используются        и          старой           библиотекой ввода-вывода.  Вот поэтому       старая           и          новая            библиотеки            совместимы           на уровне          исходного кода.

И  еще:   класс ios       содержит     множество  функций-членов   и          переменных, которые управляют        основными операциями           над     потоками     или отслеживают          результаты их выполнения.    Поэтому       имя    класса           ios будет употребляться       в          этой   книге довольно    часто.            И помните: если включить     в          программу заголовок    <iostream>, она     будет иметь            доступ к          этому важному     классу.

Перегрузка    операторов          ввода-вывода

В   примерах    из        предыдущих          глав   при     необходимости     выполнить операцию    ввода            или вывода            данных,        связанных   с          классом, создавались           функции-члены,   назначение             которых       и состояло  лишь в том,   чтобы            ввести           или     вывести        эти      данные.       Несмотря     на       то что      в          самом этом            решении      нет      ничего          неправильного,    в          C++ предусмотрен       более            удачный      способ выполнения        операций     ввода-вывода         "классовых"           данных:        путем            перегрузки операторов ввода-вывода         "<<"   и          ">>".

Оператор        "<<"   выводит     информацию         в          поток,         а оператор   ">>"   вводит        информацию         из потока.

В   языке            C++     оператор     "<<"   называется оператором         вывода         или вставки,     поскольку   он вставляет           символы      в          поток.           Аналогично оператор     ">>"   называется оператором         ввода            или извлечения, поскольку   он       извлекает    символы      из        потока.

Как          вы       знаете,          операторы  ввода-вывода       уже    перегружены         (в заголовке    <iostream>), чтобы          они     могли            выполнять  операции потокового ввода            или     вывода         данных         любых встроенных          С++-типов.            Здесь вы       узнаете,        как     определить            эти      операторы  для собственных классов.

Создание        перегруженных   операторов          вывода

В   качестве       простого      примера      рассмотрим            создание     оператора вывода         для     следующей версии         класса           three_d.

class        three_d         {

 public:

  int  x,         у,         z;         //         3-мерные    координаты

  three_d(int          a,         int       b,        int       с)         {           x          =          a;         у          =     b;        z          =          c;         }

};

Чтобы     создать         операторную         функцию      вывода         для     объектов типа   three_d,        необходимо перегрузить         оператор     "<<".  Вот     один  из возможных способов.

/* Отображение         координат   X,        Y,         Z          (оператор    вывода         для            класса three_d).

*/ ostream        &operator<<(ostream      &stream,      three_d     obj) {

 stream           <<       obj.x   <<       ",         ";

 stream           <<       obj.у  <<       ",         ";

 stream           <<       obj.z   <<       "\n";

 return            stream;          //         возвращает             параметр     stream

}

Рассмотрим     внимательно          эту      функцию,    поскольку   ее        содержимое характерно для     многих функций   вывода         данных.        Во-первых,  отметьте,     что согласно      объявлению           она     возвращает ссылку        на       объект          типа ostream.       Это     позволяет    несколько   операторов             вывода         объединить в одном       составном   выражении.            Затем            обратите      внимание    на       то, что      эта      функция       имеет            два параметра.     Первый        представляет собой            ссылку          на       поток,           который       используется         в          левой части  оператора.  Вторым        является       объект,         который       стоит  в          правой части этого  оператора. (При   необходимости     второй          параметр     также может           иметь            тип     ссылки          на       объект.)        Само  тело функции состоит         из        инструкций вывода         трех    значений     координат, содержащихся в          объекте типа         three_d,        и          инструкции возврата      потока          stream.

Перед     вами  короткая      программа,            в          которой        демонстрируется использование      оператора вывода.

//  Использование     перегруженного   оператора   вывода. #include <iostream> using   namespace   std;

class        three_d         {

 public:

  int  x,         y,         z;         //         3-мерные    координаты

  three_d(int          a,         int       b,        int       с)         {           x          =          a;         у          =     b;        z          =          c;         }

};

/* Отображение         координат   X,        Y,         Z          (оператор    вывода         для            класса three_d).

*/ ostream        &operator<<(ostream      &stream,      three_d     obj) {

 stream           <<       obj.x   <<       ",         ";

 stream           <<       obj.у  <<       ",         ";

 stream           <<       obj.z   <<       "\n";

 return            stream;          //         возвращает             параметр     stream

}

int    main() {

 three_d         a(1,     2,        3),       b(3,    4,        5),       c(5,     6,        7);

 cout    <<       a          <<       b          <<       c;

 return            0;

}

При         выполнении           эта      программа возвращает             следующие     результаты:

1, 2,        3

3, 4,        5

5, 6,        7

Если        удалить        код,   относящийся          конкретно   к          классу           three_d, останется     "скелет", подходящий для     любой           функции       вывода         данных.

ostream &operator<<(ostream      &stream,      class_type    obj) {

 //         код,   относящийся          к          конкретному          классу

 return            stream;          //         возвращает             параметр     stream

}

Как          уже    отмечалось,           для     параметра   obj      разрешается использовать         передачу     по       ссылке.         В широком смысле         конкретные действия      функции       вывода         определяются       программистом.  Но если        вы хотите           следовать    профессиональному      стилю            программирования,       то ваша  функция вывода  должна        все-таки       выводить     информацию.        И потом,          всегда           нелишне      убедиться    в          том, что        она     возвращает параметр     stream.

Прежде чем    переходить к          следующему          разделу,      подумайте, почему функция       вывода         для класса  three_d         не       была  закодирована        таким образом.

/* Версия          ограниченного      применения           (использованию   не     подлежит). */ ostream   &operator<<(ostream      &stream,      three_d         obj) {

 cout    <<       obj.x   <<       ",         ";

 cout    <<       obj.у  <<       ",         ";

 cout    <<       obj.z   <<       "\n";

 return            stream;          //         возвращает             параметр     stream

}

В   этой   версии          функции       жестко          закодирован          поток cout.  Это ограничивает         круг   ситуаций, в которых       ее        можно          использовать. Помните,     что      оператор     "<<"   можно          применить  к          любому потоку     и что      поток,           который       использован          в          "<<"-выражении, передается параметру   stream. Следовательно,            вы       должны       передавать функции поток,           который       корректно   работает      во       всех случаях.         Только          так можно          создать         функцию      вывода         данных,        которая        подойдет     для использования      в          любых          выражениях           ввода-вывода.

Использование        функций-"друзей"          для     перегрузки операторов   вывода

В   предыдущей          программе перегруженная     функция       вывода         не была  определена            как     член класса            three_d.        В          действительности ни       функция       вывода,        ни       функция       ввода            не       могут быть членами класса.          Дело  здесь вот      в          чем.   Если   операторная          функция       является членом         класса, левый        операнд       (неявно        передаваемый      с          помощью указателя    this)    должен        быть  объектом класса, который       сгенерировал обращение к          этой   операторной          функции.     И         это      изменить нельзя. Однако         при     перегрузке  операторов             вывода         левый           операнд должен        быть  потоком,      а правый     —        объектом     класса,          данные которого      подлежат    выводу.        Следовательно, перегруженные      операторы вывода         не       могут быть  функциями-членами.

В   связи с          тем,    что      операторные         функции       вывода         не       должны быть  членами       класса,          для которого         они     определяются,      возникает серьезный   вопрос:         как     перегруженный    оператор вывода может           получить доступ          к          закрытым    элементам  класса?         В          предыдущей программе переменные          х,         у          z          были определены           как открытые,   и          поэтому       оператор     вывода         без     проблем мог         получить к          ним    доступ.         Но       ведь   сокрытие     данных         —        важная         часть объектноориентированного   программирования,       и          требовать,   чтобы            все данные         были открытыми, попросту    нелогично.  Однако         существует  решение и          для     этой   проблемы:  оператор     вывода можно      сделать        "другом" класса.          Если   функция       является       "другом"     некоторого класса,          то        она получает      легальный  доступ          к          его      private-данным.   Как     можно объявить      "другом"     класса перегруженную функцию      вывода,        покажем      на примере      класса           three_d.

//  Использование     "дружбы"    для     перегрузки оператора   "<<" #include <iostream> using   namespace   std;

class        three_d         {

  int  x,         y,         z;         //         3-мерные    координаты           (теперь         это            privateчлены)

 public:

  three_d(int          a,         int       b,        int       с)         {           x          =          a;         у          =     b;        z          =          c;         }

  friend        ostream        &operator<<(ostream      &stream,      three_d         obj); };

// Отображение         координат   X,        Y,         Z          (оператор    вывода         для            класса three_d).

ostream &operator<<(ostream      &stream,      three_d         obj) {

 stream           <<       obj.x   <<       ",         ";

 stream           <<       obj.у  <<       ",         ";

 stream           <<       obj.z   <<       "\n";

 return            stream;          //         возвращает             поток

}

int    main() {

 three_d         a(1,     2,        3),       b(3,    4,        5),       с          (5,       6,        7);

 cout    <<       a          <<       b          <<       c;

 return            0;

}

Обратите           внимание    на       то,       что      переменные           х,         у          и          z в          этой   версии          программы являются закрытыми     в          классе           three_d, тем     не       менее,          операторная          функция       вывода         обращается            к ним напрямую.    Вот     где     проявляется           великая        сила   "дружбы":  объявляя операторные         функции ввода     и          вывода         "друзьями" класса,          для которого      они     определяются,      мы      тем     самым поддерживаем  принцип инкапсуляции        объектно-ориентированного  программирования.

Перегрузка    операторов          ввода

Для          перегрузки операторов             ввода            используйте           тот      же метод,          который       мы      применяли при перегрузке     оператора   вывода. Например,  следующий            оператор     ввода            обеспечивает         ввод трехмерных            координат. Обратите     внимание    на       то,       что      он       также выводит       соответствующее сообщение             для     пользователя.

/* Прием           трехмерных            координат   (оператор    ввода            для     класса three_d).

*/ istream          &operator>>(istream       &stream,      three_d     &obj)

{

 cout    <<       "Введите      координаты           X,        Y          и          Z:

 stream           >>       obj.x   >>       obj.у  >>       obj.z;

 return            stream;

}

Оператор          ввода            должен        возвращать             ссылку          на       объект типа   istream.        Кроме           того,  первый параметр            должен        представлять собой            ссылку          на       объект          типа   istream.        Этот    тип     принадлежит потоку,         указанному            слева от        оператора   ">>".  Второй          параметр является       ссылкой       на переменную,   которая        принимает  вводимое    значение. Поскольку   второй          параметр     —        ссылка, он  может           быть модифицирован   при     вводе            информации. Общий     формат         оператора ввода            имеет            следующий            вид.

istream   &operator>>(istream       &stream,      object_type &obj) {

 //         код     операторной          функции       ввода            данных

 return            stream;

}

Использование          функции       ввода            данных         для     объектов      типа three_d         демонстрируется в следующей         программе.

//  Использование     перегруженного   оператора   ввода. #include <iostream> using   namespace   std;

class        three_d         {

  int  x,         y,         z;         //         3-мерные    координаты

 public:

  three_d(int          a,         int       b,        int       с)         {           x          =          a;         у          =     b;        z          =          c;         }

  friend        ostream        &operator<<(ostream      &stream,      three_d         obj);

  friend        istream          &operator>>(istream       &stream,      three_d         &obj);

};

// Отображение         координат   X,        Y,         Z          (оператор    вывода         для            класса three_d).

ostream &operator<<(ostream      &stream,      three_d         obj) {

 stream           <<       obj.x   <<       ",         ";

 stream           <<       obj.у  <<       ",         ";

 stream           <<       obj.z   <<       "\n";

 return            stream;          //         возвращает             параметр     stream

}

// Прием           трехмерных            координат   (оператор    ввода            для     класса three_d).

istream   &operator>>(istream       &stream,      three_d         &obj) {

 cout    <<       "Введите      координаты           X,        Y          и          Z:         ";

 stream           >>       obj.x   >>       obj.у  >>       obj.z;

 return            stream;

}

int    main() {

 three_d         a(1,     2,        3);

 cout    <<       a;

 cin       >>       a;

 cout    <<       a;

 return            0;

}

Вот          как     выглядит     один  из        возможных результатов            выполнения     этой   программы.

1, 2,        3 Введите    координаты           X,     Y          и          Z:         5          6          7

5, 6,        7

Подобно           функциям    вывода,        функции       ввода            не       должны быть  членами       класса,          для обработки      данных         которого      они предназначены.   Они    могут быть  "друзьями" этого  класса или  просто независимыми      функциями.

За исключением        того,  что      функция       ввода            должна        возвращать ссылку          на       объект          типа istream,          тело   этой   функции       может содержать   все,    что      вы       считаете       нужным       в          нее     включить. Но логичнее      использовать         операторы  ввода            все      же       по       прямому назначению,          т.е.      для выполнения   операций     ввода.

Сравнение      С-        и          С++-систем           ввода-вывода

Как          вы       знаете,          предшественник  C++,   язык  С,        оснащен       одной из        самых           гибких           (среди структурированных      языков)        и          при этом   очень            мощных       систем          ввода-вывода.      (Не     будет преувеличением  сказать,        что      среди            всех    известных    структурированных языков          С-система ввода-вывода          не       имеет            себе   равных.)       Почему же       тогда,            спрашивается,       в          C++     определяется собственная      система ввода-вывода,      если   в          ней     продублирована  большая      часть  того,  что содержится             в          С          (имеется      в          виду  мощный       набор            С-функций       ввода-вывода)?    Ответить      на       этот вопрос            нетрудно.    Дело  в том,   что      С-система    ввода-вывода       не       обеспечивает         никакой поддержки для     объектов,    определяемых      пользователем.    Например,  если   создать         в С          такую структуру

struct      my_struct     {

 int       count;

 char    s          [80];

 double           balance;

}    cust;

то существующую     в          С          систему        ввода-вывода       невозможно настроить    так,     чтобы            она     могла выполнять операции     ввода-вывода непосредственно             над     объектами  типа   my_struct.   Но поскольку        центром объектно-ориентированного  программирования        являются     именно объекты, имеет            смысл,          чтобы            в          C++     функционировала           такая система ввода-вывода,      которую можно    было бы      динамически         "обучать" обращению            с          любыми       объектами, создаваемыми программистом. Именно        поэтому       для     C++     и          была  изобретена новая объектноориентированная     система        ввода-вывода.      Как     вы       уже    могли убедиться,  С++-подход к          вводувыводу         позволяет    перегружать операторы  "<<"   и          ">>",  чтобы            они     могли            работать      с классами, создаваемыми      программистами.

И  еще.   Поскольку   C++     является       супермножеством           языка            С,        все содержимое          С-системы ввода-вывода         включено    в          C++.   (См. приложение           А,        в          котором       представлен           обзор Сориентированных         функций       ввода-вывода.)     Поэтому       при     переводе     С-программ    на       язык  C++ вам        не       нужно           изменять     все      инструкции ввода-вывода       подряд.        Работающие           С-инструкции скомпилируются         и будут успешно       работать      и          в          новой            С++-среде.  Просто          вы должны       учесть, что  старая           С-система    ввода-вывода       не       обладает объектно-ориентированными возможностями.

Форматированный          ввод-вывод            данных

До            сих      пор     при     вводе            или     выводе         информации          в наших           примерах    программ    действовали параметры          форматирования, которые       по       умолчанию использует  С++-система           ввода-вывода. Но программист         может           сам     управлять    форматом   представления      данных, причем         двумя способами.           Первый        способ          предполагает использование      функций-членов   класса           ios,      а второй—  функций специального        типа,  именуемых             манипуляторами           (manipulator).        Мы же начнем  освоение     возможностей       форматирования  с          функций-членов класса           ios.

Форматирование  данных        с          использованием  функций-членов   класса          ios

В   системе        ввода-вывода       C++     каждый        поток связан           с          набором флагов          форматирования, которые     управляют  процессом  форматирования информации.         В          классе           ios       объявляется перечисление     fmtflags,       в котором       определены           следующие значения.    (Точнее,       эти      значения определены           в          классе           ios_base,      который,      как     упоминалось выше,            является       базовым      для     класса

ios.)

Эти           значения      используются        для     установки    или     очистки        флагов форматирования  с помощью таких функций,     как     setf()  и          unsetf().        При использовании     старого         компилятора может       оказаться,   что      он       не определяет             тип     перечисления        fmtflags.       В          этом   случае           флаги форматирования  будут кодироваться        как     целочисленные    long-значения.

Если        флаг   skipws           установлен,            то        при     потоковом  вводе данных         ведущие      "пробельные" символы,          или     символы      пропуска     (т.е. пробелы,     символы      табуляции   и          новой            строки), отбрасываются. Если   же       флаг   skipws           сброшен,     пробельные           символы      не отбрасываются.

Если        установлен флаг   left,     выводимые            данные         выравниваются     по левому         краю, а          если установлен   флаг   right   —        по       правому.     Если установлен флаг   internal,        числовое     значение дополняется  пробелами, которыми    заполняется           поле  между          ним    и          знаком         числа или символом    основания   системы       счисления.  Если   ни       один  из        этих    флагов не       установлен, результат   выравнивается      по       правому       краю  по умолчанию.

По            умолчанию числовые     значения      выводятся   в          десятичной системе счисления.  Однако основание          системы       счисления   можно          изменить. Установка    флага oct      приведет     к          выводу результата          в          восьмеричном представлении,    а          установка    флага hex     —        в          шестнадцатеричном. Чтобы            при     отображении         результата  вернуться    к          десятичной системе счисления,  достаточно установить  флаг   dec.

Установка         флага showbase     приводит     к          отображению        обозначения основания   системы счисления,        в          которой        представляются    числовые значения.    Например,  если   используется шестнадцатеричное   представление,     то значение      1F       будет отображено            как     0x1F.

По            умолчанию при     использовании     экспоненциального        представления чисел отображается       строчной      вариант        буквы            "е".     Кроме           того,  при отображении шестнадцатеричного             значения      используется         также строчная      буква "х".     После            установки флага   uppercase    отображается прописной  вариант        этих    символов.

Установка         флага showpos       вызывает     отображение         ведущего     знака "плюс"          перед положительными          значениями.

Установка         флага showpoint    приводит     к          отображению        десятичной точки и          хвостовых нулей  для     всех    чисел с          плавающей             точкой          — нужны          они     или     нет.

После     установки    флага scientific        числовые     значения      с          плавающей точкой          отображаются       в экспоненциальном      представлении.    Если установлен флаг   fixed,  вещественные       значения отображаются           в          обычном представлении.    Если   не       установлен ни       один  из        этих    флагов, компилятор сам     выбирает     соответствующий             метод            представления.

При         установленном     флаге unitbuf          содержимое          буфера сбрасывается         на       диск   после каждой        операции     вывода         данных.

Если        установлен флаг   boolalpha,   значения      булева          типа   можно вводить        или     выводить, используя     ключевые    слова true    и          false.

Поскольку        часто приходится обращаться            к          полям           oct,     dec     и hex,    на       них     допускается коллективная      ссылка          ios::basefield. Аналогично            поля  left,     right   и          internal         можно собирательно     назвать ios::adjustfield.       Наконец,      поля  scientific        и          fixed   можно          назвать ios::floatfield.

Чтобы  установить          флаги           форматирования,          обратитесь          к     функции      setf().

Для          установки    любого         флага используется         функция       setf(), которая является       членом класса      ios.      Вот     как     выглядит     ее        формат.

fmtflags  setf(fmtflags            flags);

Эта           функция       возвращает             значение      предыдущих          установок флагов          форматирования  и устанавливает    их        в          соответствии          со значением, заданным   параметром           flags.  Например, чтобы установить  флаг showbase,    можно          использовать         эту      инструкцию.

stream.setf(ios::showbase);

Здесь      элемент        stream           означает      поток,           параметры форматирования  которого      вы       хотите изменить. Обратите     внимание    на использование      префикса     ios::    для     уточнения принадлежности   параметра showbase.    Поскольку   параметр     showbase     представляет         собой перечислимую константу,   определенную      в          классе           ios,      то        при     обращении к          ней необходимо указывать имя    класса           ios.      Этот    принцип       относится    ко всем  флагам         форматирования.            В следующей         программе функция setf()  используется         для     установки    флагов          showpos       и scientific.

#include <iostream> using     namespace   std;

int    main() {

 cout.setf(ios::showpos);

 cout.setf(ios::scientific);

 cout    <<       123     <<       "          "          <<       123.23          <<       "          ";

 return            0;

}

Вот          как     выглядят     результаты выполнения           этой   программы.

+123       +1.232300е+002

С   помощью    операции     ИЛИ   можно          установить  сразу несколько   нужных флагов форматирования          в          одном           вызове          функции       setf(). Например,  предыдущую         программу можно сократить,            объединив  по ИЛИ   флаги            scientific        и          showpos,      поскольку   в          этом   случае выполняется          только          одно  обращение к          функции       setf().

cout.setf(ios::scientific          |          ios::showpos);

Чтобы    сбросить      флаг, используйте           функцию      unsetf(),        прототип     которой        выглядит     так.

void         unsetf(fmtflags       flags);

Для         очистки      флагов         форматирования           используется       функция     unsetf().

В  этом   случае           будут обнулены    флаги,           заданные    параметром     flags.  (При   этом   все      другие флаги         остаются      в          прежнем     состоянии.)

Чтобы   получить    текущие     установки  флагов         форматирования, используйте         функцию flags().

Для          того    чтобы            узнать           текущие       установки    флагов форматирования,            воспользуйтесь функцией       flags(),           прототип      которой имеет            следующий            вид.

fmtflags  flags();

Эта           функция       возвращает             текущее        значение      флагов форматирования  для     вызывающего потока.

При         использовании     следующего           формата       вызова          функции flags()            устанавливаются значения     флагов          форматирования  в соответствии          с          содержимым         параметра   flags   и возвращаются   их предыдущие          значения.

fmtflags  flags(fmtflags          flags);

Чтобы     понять,         как     работают     функции       flags()            и          unsetf(), рассмотрим            следующую программу.          Она    включает     функцию showflags(), которая        отображает             состояние    флагов форматирования.

#include <iostream> using     namespace   std;

void         showflags(ios::fmtflags    f);

int    main() {

 ios::fmtflags f;

 f           =          cout.flags();

 showflags(f);

 cout.setf(ios::showpos);

 cout.setf(ios::scientific);

 f           =          cout.flags();

 showflags(f);

 cout.unsetf(ios:scientific);

 f           =          cout.flags();

 showflags(f);

 return            0;

}

void         showflags(ios::fmtflags    f) {

 long    i;

 for(i=0x4000;          i;          i=i>>1)

  if(i  &         f)         cout    <<       "1";

  else            cout    <<       "0";

 cout    <<       "\n";

}

При         выполнении           эта      программа отображает             такие результаты. (Между        этими            и          вашими результатами   возможно   расхождение, вызванное  использованием  различных  компиляторов.)

0   0          0          0          0          1          0          0          0          0          0          0          0          0          1

0   0          1          0          0          1          0          0          0          1          0          0          0          0          1

0   0          0          0          0          1          0          0          0          1          0          0          0          0          1

В   предыдущей          программе обратите      внимание    на       то,       что      тип fmtflags        указан           с          префиксом ios       ::.        Дело  в          том,   что      тип fmtflags        определен  в          классе           ios.      В          общем          случае           при использовании      имени           типа   или     перечислимой       константы,  определенной в          некотором классе,          необходимо           указывать    соответствующее имя вместе          с          именем        класса.

Установка     ширины       поля, точности  и          символов    заполнения

Помимо            флагов          форматирования  можно          также            устанавливать ширину        поля, символ заполнения        и          количество цифр  после десятичной точки (точность).  Для    этого  достаточно использовать        следующие функции. streamsize    width(streamsize   len);

char         fill(char          ch);

streamsize         precision(streamsize         num);

Функция            width()          возвращает             текущую      ширину        поля  и устанавливает       новую           равной значению            параметра   len.     Ширина поля, которая        устанавливается   по       умолчанию, определяется      количеством символов,   необходимых        для     хранения     данных         в          каждом конкретном случае.         Функция       fill()     возвращает             текущий       символ         заполнения (по      умолчанию используется         пробел)        и          устанавливает       в          качестве нового          текущего      символа       заполнения значение,   заданное     параметром ch.      Этот    символ         используется         для     дополнения           результата символами,            недостающими     для     достижения            заданной     ширины поля. Функция       precision() возвращает   текущее        количество цифр, отображаемых после десятичной точки,            и устанавливает    новое            текущее        значение точности      равным        содержимому       параметра   num.  (По умолчанию     после десятичной точки отображается        шесть            цифр.)           Тип     streamsize определен как      целочисленный    тип.

Рассмотрим    программу,            которая        демонстрирует     использование      этих     трех    функций.

#include <iostream> using     namespace   std;

int    main() {

 cout.setf(ios::showpos);

 cout.setf(ios::scientific);

 cout    <<       123     <<       "          "          <<       123.23          <<       "\n";

 cout.precision(2);   //         Две     цифры          после десятичной точки.  cout.width(10);           //         Всё     поле  состоит         из        10       символов.

 cout    <<       123     <<       "          ";

 cout.width(10);      //         Установка    ширины       поля  равной          10.

 cout    <<       123.23          <<       "\n";

 cout.fill('#'); //         Для    заполнителя           возьмем      символ         "#"  cout.width(10);           //         и          установим   ширину        поля  равной          10.

 cout    <<       123     <<       "          ";

 cout.width(10);      //         Установка    ширины       поля  равной          10.

 cout    <<       123.23;

 return            0;

}

Эта          программа генерирует  такие результаты.

+123       +1.232300е+002

                         +123  +1.23е+002

######+123    +1.23е+002

В   некоторых   реализациях           необходимо           устанавливать       значение ширины       поля  перед выполнением      каждой         операции     вывода.        Поэтому функция       width()          в          предыдущей          программе вызывалась несколько   раз.

В   системе        ввода-вывода       C++     определены           и          перегруженные версии          функций       width(), precision()           и          fill(),   которые       не       изменяют текущие       значения      соответствующих параметров форматирования            и используются        только          для     их        получения. Вот     как     выглядят     их прототипы,

char         fill(); streamsize     width(); streamsize     precision();

Использование        манипуляторов  ввода-вывода

Манипуляторы        позволяют встраивать          инструкции     форматирования           в          выражение ввода-вывода.

В   С++-системе           ввода-вывода       предусмотрен       и          второй          способ изменения  параметров форматирования,           связанных   с          потоком.      Он реализуется            с          помощью    специальных          функций, называемых манипуляторами,          которые       можно          включать     в          выражение ввода-вывода.

Стандартные          манипуляторы      описаны       в          табл.  18.1.

При         использовании     манипуляторов,   которые       принимают аргументы, необходимо включить  в          программу заголовок    <iomanip>.

Манипулятор  используется         как     часть  выражения ввода-вывода.      Вот пример         программы,           в которой    показано,    как     с          помощью манипуляторов     можно          управлять    форматированием выводимых        данных.

#include <iostream> #include <iomanip> using     namespace   std;

int    main() {

 cout    <<       setprecision (2)       <<       1000.243     <<       endl;

 cout    <<       setw(20)       <<       "Всем            привет!         ";

 return            0;

}

Результаты      выполнения           этой   программы таковы.

1е+003

Всем       привет!

Обратите           внимание    на       то,       как     используются        манипуляторы      в цепочке        операций     вводавывода.        Кроме           того,  отметьте,     что,    если манипулятор         вызывается             без     аргументов (как, например,    манипулятор endl    в          нашей           программе),           то        его      имя    указывается           без пары  круглых скобок.

В   следующей программе используется         манипулятор         setiosflags() для установки    флагов scientific    и          showpos.

#include <iostream> #include <iomanip> using     namespace   std;

int    main() {

 cout    <<       setiosflags(ios::showpos);

 cout    <<       setiosflags(ios::scientific);

 cout    <<       123     <<       "          "          <<       123.23;

 return            0;

}

Вот          результаты выполнения           данной         программы.

+123       +1.232300е+002

А   в          этой   программе демонстрируется использование      манипулятора       ws, который пропускает       ведущие      "пробельные"       символы      при     вводе строки           в          массив          s:

#include <iostream> using     namespace   std;

int    main() {

 char    s[80];

 cin       >>       ws       >>       s;

 cout    <<       s;

 return            0;

}

Создание        собственных        манипуляторных          функций

Программист  может           создавать    собственные          манипуляторные функции. Существует два типа       манипуляторных  функций:     принимающие      и          не принимающие      аргументы. Для    создания параметризованных           манипуляторов используются        методы,       рассмотрение        которых       выходит за рамки этой   книги.           Однако         создание     манипуляторов,   которые       не       имеют параметров,           не вызывает           особых         трудностей.

Все           манипуляторные функции       вывода         данных         без     параметров имеют           следующую структуру.

ostream &manip_name(ostream  &stream) {

 //         код     манипуляторной  функции

 return            stream;

}

Здесь      элемент        manip_name          означает      имя    манипулятора.      Важно понимать,   что,    несмотря на           то,       что      манипулятор         принимает  в качестве       единственного      аргумента   указатель    на       поток, который     он обрабатывает,       при     использовании     манипулятора       в          результирующем выражении ввода-вывода      аргументы  не       указываются          вообще.

В   следующей программе создается    манипулятор         setup(),         который устанавливает       флаг выравнивания        по       левому         краю, ширину        поля равной          10       и          задает           в          качестве       заполняющего символа знак   доллара.

#include <iostream> #include <iomanip> using     namespace   std;

ostream &setup(ostream     &stream) {

 stream.setf(ios::left);

 stream           <<       setw(10)       <<       setfill  ('$');

 return            stream;

}

int    main() {

 cout    <<       10       <<       "          "          <<       setup  <<       10;

 return            0;

}

Собственные   манипуляторы      полезны       по       двум  причинам.   Во-первых, иногда          возникает необходимость       выполнять  операции     ввода-вывода       с использованием  устройства, к          которому ни          один  из        встроенных манипуляторов     не       применяется          (например, плоттер).      В          этом   случае создание      собственных           манипуляторов     сделает        вывод           данных         на это      устройство  более удобным.  Во-вторых,  может           оказаться,   что      у          вас в          программе некоторая   последовательность инструкций      повторяется несколько   раз.    И         тогда вы       можете         объединить            эти      операции     в один манипулятор,         как     показано     в          предыдущей          программе.

Все           манипуляторные функции       ввода            данных         без     параметров имеют           следующую структуру.

istream   &manip_name(istream    &stream) {

 //         код     манипуляторной  функции

 return            stream;

}

Например,       в          следующей программе создается    манипулятор         prompt(). Он      настраивает входной     поток на       прием           данных         в шестнадцатеричном       представлении      и          отображает             для пользователя наводящее  сообщение.

#include <iostream> #include <iomanip> using     namespace   std;

istream   &prompt(istream   &stream)

{

 cin       >>       hex;

 cout    <<       "Введите      число в          шестнадцатеричном       формате:     ";

 return            stream;

}

int    main() {

 int       i;

 cin       >>       prompt          >>       i;

 cout    <<       i;

 return            0;

}

Помните:          очень            важно,          чтобы            ваш    манипулятор возвращал  потоковый  объект          (элемент stream).            В          противном  случае этот    манипулятор         нельзя          будет использовать         в          составном выражении ввода            или     вывода.

Файловый      ввод-вывод

В   С++-системе           ввода-вывода       также            предусмотрены    средства      для выполнения соответствующих           операций     с          использованием  файлов. Файловые   операции     ввода-вывода можно    реализовать           после включения  в программу заголовка    <fstream>,   в          котором определены    все      необходимые для     этого  классы          и          значения.

Как        открыть    и          закрыть     файл

В   C++     файл  открывается           путем            связывания             его      с          потоком. Как     вы       знаете,          существуют потоки         трех    типов:           ввода,           вывода и          ввода-вывода.      Чтобы           открыть        входной       поток, необходимо объявить      потоковый  объект          типа   ifstream.       Для    открытия     выходного потока нужно        объявить      поток класса           ofstream.     Поток,           который предполагается    использовать         для операций        как     ввода,           так      и вывода,        должен        быть  объявлен     как     объект          класса           fstream. Например,  при     выполнении           следующего           фрагмента   кода  будет создан входной       поток, выходной  и          поток,           позволяющий        выполнение операций     в          обоих            направлениях.

ifstream in;       //         входной       поток

ofstream            out;    //         выходной    поток

fstream  both;  //         поток ввода-вывода

Чтобы  открыть    файл,            используйте         функцию     open().

Создав   поток,           его      нужно           связать         с          файлом.       Это     можно сделать        с          помощью    функции open(),   причем         в          каждом        из трех    потоковых   классов        есть    своя   функция-член        open(). Представим         их прототипы.

void         ifstream::open(const        char    *filename,    ios::openmode        mode =

ios::in);

void         ofstream::open(const       char    *filename,    ios::openmode        mode =

ios::out          |          ios::trunc);

void         fstream::open(const         char    *          filename,      ios::openmode        mode =

ios::in |          ios::out);

Здесь      элемент        filename       означает      имя    файла,          которое        может включать     спецификатор пути.       Элемент       mode определяет             способ открытия     файла.          Он      должен        принимать  одно  или несколько      значений перечисления        openmode,  которое        определено            в          классе           ios.

ios::арр ios::ate ios::rbinary ios::in ios::out

ios::trunc

Несколько        значений     перечисления        openmode   можно          объединять посредством логического       сложения    (ИЛИ).

На            заметку.      Параметр  mode для     функции      fstream::open()      может не       устанавливаться          по умолчанию      равным        значению    in         |          out (это  зависит       от      используемого     компилятора). Поэтому        при необходимости   этот            параметр  вам    придется    задавать    в          явном виде.

Включение       значения      ios::арр         в          параметр     mode обеспечит присоединение     к          концу            файла всех  выводимых            данных.        Это значение      можно          применять  только          к          файлам,       открытым    для вывода         данных.        При    открытии     файла            с          использованием  значения ios::ate          поиск будет начинаться             с          конца            файла.          Несмотря     на это,     операции     ввода-вывода       могут по-прежнему выполняться     по       всему файлу.

Значение           ios::in говорит        о          том,   что      данный        файл  открывается для     ввода            данных,        а значение  ios::out          обеспечивает         открытие файла            для     вывода         данных.

Значение           ios::binary    позволяет    открыть        файл  в          двоичном    режиме. По       умолчанию все файлы   открываются          в          текстовом    режиме.       Как упоминалось         выше,            в          текстовом    режиме        могут происходить некоторые  преобразования   символов    (например, последовательность, состоящая из        символов    возврата      каретки        и          перехода     на       новую           строку, может           быть преобразована      в          символ         новой            строки).        При открытии     файла            в          двоичном    режиме        никакого преобразования символов    не       выполняется.         Следует        иметь            в          виду, любой файл, содержащий форматированный      текст  или     еще    необработанные  данные, можно          открыть        как     в          двоичном, так       и          в          текстовом    режиме. Единственное        различие     между          этими            режимами  состоит         в преобразовании   (или   нет)    символов.

Использование          значения      ios::trunc      приводит     к          разрушению содержимого        файла,          имя которого         совпадает    с          параметром filename,      а          сам     этот    файл  усекается     до       нулевой       длины.         При создании     выходного  потока          типа   ofstream       любой           существующий файл  с          именем        filename автоматически            усекается     до       нулевой длины.

При         выполнении           следующего           фрагмента   кода  открывается     обычный     выходной    файл.

ofstream            out;

out.open("тест");

Поскольку        параметр     mode функции       open()           по       умолчанию устанавливается   равным значению,          соответствующему          типу   открываемого потока,         в          предыдущем         примере      вообще         нет необходимости задавать      его      значение.

Не            открытый    в          результате  неудачного выполнения           функции open()           поток при использовании         в          булевом       выражении устанавливается   равным        значению    ЛОЖЬ.          Этот    факт может служить        для подтверждения    успешного  открытия     файла,          например,   с          помощью такой if-инструкции.

if(!mystream)   {

 cout    <<       "He     удается         открыть        файл.\n";

 //         обработка   ошибки

}

Прежде чем    делать          попытку       получения   доступа        к          файлу, следует        всегда           проверять результат      вызова          функции       open().

Можно   также            проверить   факт   успешного  открытия     файла            с помощью    функции       is_open(), которая            является       членом         классов fstream,        ifstream        и          ofstream.     Вот     ее        прототип,

bool         is_open();

Эта           функция       возвращает             значение      ИСТИНА,      если   поток связан с          открытым    файлом,       и ЛОЖЬ        —        в          противном  случае. Например,  используя   следующий            код,   можно          узнать,          открыт ли    в данный        момент         потоковый  объект          mystream.

if(!mystream.is_open())        {

 cout    <<       "Файл            не       открыт.\n";

 //         ...

}

Хотя        вполне          корректно   использовать         функцию      open()           для открытия     файла,          в большинстве      случаев        это      делается      по-другому, поскольку   классы          ifstream,       ofstream       и          fstream включают конструкторы,       которые       автоматически      открывают  заданный    файл. Параметры у этих             конструкторов       и          их        значения      (действующие       по умолчанию)           совпадают  с          параметрами         и соответствующими значениями            функции       open().          Поэтому       чаще  всего файл  открывается так, как         показано     в          следующем            примере,

ifstream mystream("myfile");         //         файл  открывается           для     ввода

Если        по       какой-то       причине       файл  открыть        невозможно, потоковая   переменная, связываемая      с          этим   файлом,       устанавливается равной          значению    ЛОЖЬ.

Чтобы  закрыть      файл,            вызовите   функцию     close().

Чтобы     закрыть        файл, используйте           функцию-член       close(). Например,  чтобы            закрыть        файл, связанный  с          потоковым объектом mystream,   используйте           такую            инструкцию,

mystream.close();

Функция           close()            не       имеет            параметров            и          не     возвращает             никакого      значения.

Чтение           и          запись          текстовых            файлов

Проще    всего считывать    данные         из        текстового   файла            или записывать их        в          него   с          помощью операторов   "<<"   и          ">>". Например,  в          следующей программе выполняется          запись           в          файл  test целого          числа,           значения      с          плавающей             точкой          и          строки.

//  Запись          данных         в          файл.

#include <iostream> #include <fstream> using     namespace   std;

int    main() {

 ofstream       out("test");

 if(!out)           {

  cout           <<       "He     удается         открыть        файл.\n";

  return       1;

 }

 out      <<       10       <<       "          "          <<       123.23          <<       "\n";

 out      <<       "Это   короткий     текстовый    файл.";

 out.close();

 return            0;

}

Следующая      программа считывает    целое            число,           float-значение, символ         и          строку           из        файла, созданного          при     выполнении предыдущей          программой.

//  Считывание            данных         из        файла.

#include <iostream> #include <fstream>      using  namespace   std;

int    main() {

 char    ch;

 int       i;

 float    f;

 char    str[80];

 ifstream         in("test");

 if(!in)  {

  cout           <<       "He     удается         открыть        файл.\n";

  return       1;

 }

 in         >>       i;

 in         >>       f;

 in         >>       ch;

 in         >>       str;

 cout    <<       i           <<       "          "          <<       f           <<       "          "          <<       ch        <<     "\n";

 cout    <<       str;

 in.close();

 return            0;

}

Следует иметь            в          виду, что      при     использовании     оператора   ">>"   для считывания             данных         из текстовых          файлов         происходит преобразование   некоторых   символов.   Например, "пробельные"        символы опускаются.            Если   необходимо           предотвратить       какие бы      то        ни было преобразования   символов,   откройте      файл  в          двоичном    режиме доступа.       Кроме           того,  помните, что          при     использовании     оператора   ">>" для     считывания             строки           ввод  прекращается        при обнаружении первого        "пробельного"      символа.

Неформатированный     ввод-вывод            данных        в          двоичном   режиме

Форматированные   текстовые    файлы           (подобные  тем,    которые использовались    в предыдущих      примерах)   полезны       во       многих ситуациях,   но       они     не       обладают    гибкостью неформатированных       двоичных файлов.        Поэтому       C++     поддерживает       ряд     функций       файлового ввода-вывода         в          двоичном    режиме,       которые       могут выполнять  операции     без форматирования данных.

Для          выполнения           двоичных    операций     файлового  ввода-вывода необходимо           открыть        файл с           использованием  спецификатора     режима ios::binary.   Необходимо          отметить,     что      функции обработки неформатированных      файлов         могут работать      с          файлами,     открытыми в текстовом режиме          доступа,       но       при     этом   могут иметь            место преобразования   символов,   которые       сводят на    нет      основную    цель выполнения           двоичных    файловых    операций.

Функция           get()   считывает            символ         из        файла,          а          функция     put()   записывает           символ         в          файл.

В   общем          случае           существует  два     способа        записи неформатированных      двоичных    данных         в файл          и          считывания             их из        файла.          Первый        состоит         в          использовании     функции-члена put()   (для записи            байта в          файл) и          функции-члена     get()   (для считывания             байта из        файла).         Второй способ      предполагает применение           "блочных"   С++-функций          ввода-вывода       read() и          write().

Рассмотрим            каждый        способ          в          отдельности.

Использование        функций      get()   и          put()

Функции            get()   и          put()   имеют           множество  форматов,   но       чаще всего используются следующие       их        версии:

istream   &get(char     &ch);

ostream &put(char    ch);

Функция            get()   считывает    один  символ         из        соответствующего потока          и          помещает    его значение          в          переменную          ch.      Она возвращает             ссылку          на       поток,           связанный   с          предварительно открытым    файлом.       При    достижении            конца            этого  файла            значение ссылки          станет            равным нулю.       Функция       put()   записывает символ         ch в          поток и          возвращает             ссылку          на       этот    поток.

При         выполнении           следующей программы на       экран будет выведено содержимое          любого заданного           файла.          Здесь используется         функция get().

/* Отображение         содержимого        файла            с          помощью    функции     get().

*/

#include <iostream> #include <fstream> using     namespace   std;

int            main(int        argc,   char     *argv[]) {

 char    ch;

 if(argc!=2)    {

  cout           <<       "Применение:       имя_программы  <имя_файла>\n";

  return       1;

 }

 ifstream         in(argv[1],    ios::in |          ios::binary);

 if(!in)  {

  cout           <<       "He     удается         открыть        файл.\n";

  return       1;

 }

 while(in)        {

  /*   При    достижении            конца            файла            потоковый  объект          in            примет значение false.  */

  in.get(ch);

  if(in)          cout    <<       ch;

 }

 in.close();

 return            0;

}

При         достижении            конца            файла            потоковый  объект          in примет         значение      ЛОЖЬ,           которое остановит          выполнение           цикла while.

Существует       более            короткий     вариант        цикла,           предназначенного для     считывания             и отображения      содержимого        файла.

while(in.get(ch))          cout    <<       ch;

Этот         вариант        также            имеет            право            на       существование, поскольку   функция       get()   возвращает потоковый объект          in,       который       при достижении            конца            файла            примет         значение      false. В следующей программе для     записи          строки           в          файл  используется функция       put().

/* Использование     функции       put()   для     записи          строки           в          файл.

*/

#include <iostream> #include <fstream> using     namespace   std;

int    main() {

 char    *p       =          "Всем            привет!";

 ofstream       out("test",    ios::out          |          ios::binary);

 if(!out)           {

  cout           <<       "He     удается         открыть        файл.\n";

  return       1;

 }

 while(*p)      out.put(*p++);

 out.close();

 return            0;

}

Считывание и          запись          в          файл блоков          данных

Чтобы     считывать    и          записывать в          файл  блоки            двоичных    данных, используйте           функциичлены      read() и          write().          Их       прототипы  имеют следующий            вид.

istream   &read(char  *buf,  streamsize    num);

ostream &write(const           char    *buf,  int       streamsize    num);

Функция            read() считывает    num   байт   данных         из        связанного  с файлом        потока          и          помещает их          в          буфер,          адресуемый параметром           buf.    Функция       write()           записывает num   байт   данных         в связанный   с          файлом        поток из        буфера,        адресуемого          параметром buf.    Как     упоминалось         выше, тип    streamsize    определен  как     некоторая разновидность      целочисленного   типа.  Он      позволяет хранить          самое большое      количество байтов,         которое        может           быть  передано     в процессе      любой операции  ввода-вывода.

Функция           read() вводит        блок  данных,        а          функция       write()     выводит     его.

При         выполнении           следующей программы сначала        в          файл записывается         массив          целых чисел,         а          затем он       же       считывается из        файла.

//  Использование     функций       read() и          write().

#include <iostream> #include <fstream> using     namespace   std;

int    main() {

 int       n[5]    =          {1,       2,        3,        4,        5};

 register          int       i;

 ofstream       out("test",    ios::out          |          ios::binary);

 if(!out)           {

  cout           <<       "He     удается         открыть        файл.\n";

  return       1;

 }

 out.write((char       *)        &n,     sizeof n);

 out.close();

 for(i=0;          i<5;     i++)     //         очищаем      массив

  n[i] =          0;

 ifstream         in         ("test",          ios::in |          ios::binary);

 if(!in)  {

  cout           <<       "He     удается         открыть        файл.\n";

  return       1;

 }

 in.read((char           *)        &n,     sizeof n);

 for(i=0;          i<5;     i++)     //         Отображаем          значения,    считанные   из     файла.

  cout           <<       n[i]      <<       "          ";

 in.close();

 return            0;

}

Обратите           внимание    на       то,       что      в          инструкциях           обращения к функциям    read() и          write() выполняются       операции     приведения            типа, которые       обязательны          при     использовании     буфера, определенного            не в          виде  символьного          массива.

Функция            gcount()        возвращает          количество           символов, считанных при     выполнении последней            операции    ввода            данных.

Если        конец            файла            будет достигнут    до       того,  как     будет считано num   символов,   функция read()     просто          прекратит    выполнение,          а буфер            будет содержать   столько        символов,   сколько удалось  считать         до этого  момента.     Точное          количество считанных   символов    можно          узнать с помощью еще    одной           функции-члена     gcount(),      которая        имеет такой прототип.

streamsize         gcount();

Функция            gcount()        возвращает             количество символов,   считанных   в процессе      выполнения последней            операции     ввода            данных.

Обнаружение          конца            файла

Обнаружить    конец            файла            можно          с          помощью    функции-члена eof(),  которая        имеет            такой прототип.

bool         eof();

Эта           функция       возвращает             значение      true    при     достижении конца            файла;          в          противном случае           она     возвращает             значение false.

Функция           eof()   позволяет  обнаружить         конец            файла.

В   следующей программе для     вывода         на       экран содержимого        файла используется         функция eof().

/* Обнаружение        конца            файла            с          помощью    функции       eof().

*/

#include <iostream> #include <fstream> using     namespace   std;

int            main(int        argc,   char     *argv[]) {

 char    ch;

 if(argc!=2)    {

  cout           <<       "Применение:       имя_программы  <имя_файла>\n";

  return       1;

 }

 ifstream         in(argv[1],    ios::in |          ios::binary);

 if(!in)  {

  cout           <<       "He     удается         открыть        файл.\n";

  return       1;

 }

 while(!in.eof())        {

  //    использование      функции       eof()

  in.get(ch);

  if(   !in.eof())       cout    <<       ch;

 }

 in.close();

 return            0;

}

Пример           сравнения   файлов

Следующая      программа иллюстрирует        мощь и          простоту      применения в          C++     файловой системы.        Здесь сравниваются        два     файла            с помощью    функций       двоичного   ввода-вывода       read(), eof() и          gcount(). Программа сначала        открывает   сравниваемые       файлы           для     выполнения двоичных    операций     (чтобы          не       допустить    преобразования   символов). Затем            из        каждого файла     по       очереди       считываются          блоки информации          в          соответствующие             буферы        и сравнивается      их содержимое.         Поскольку   объем           считанных   данных         может           быть меньше размера  буфера,        в          программе используется         функция       gcount(), которая        точно определяет количество            считанных   в          буфер            байтов. Нетрудно     убедиться    в          том,   что      при     использовании файловых       С++-функций       для     выполнения           этих    операций     потребовалась      совсем небольшая  по размеру программа.

//  Сравнение   файлов.

#include <iostream> #include <fstream> using     namespace   std; int     main(int        argc,   char     *argv[]) {

 register          int       i;

 unsigned       char    buf1[1024], buf2[1024];

 if(argc!=3)    {

  cout           <<       "Применение:       имя_программы  <имя_файла1>      "<<     " <имя_файла2>\n";

  return       1;

 }

 ifstream         f1(argv[1],   ios::in |          ios::binary);

 if(!f1) {

  cout           <<       "He     удается         открыть        первый         файл.\n";

  return       1;

 }

 ifstream         f2(argv[2],   ios::in |          ios::binary);

 if(!f2) {

  cout           <<       "He     удается         открыть        второй          файл.\n";

  return       1;

 }

 cout    <<       "Сравнение             файлов         ...\n";

 do       {

  f1.read((char      *)        buf1,  sizeof buf1);

  f2.read((char      *)        buf2,  sizeof buf2);

  if(f1.gcount()      !=        f2.gcount()) {

   cout      <<       "Файлы        имеют           разные         размеры.\n";

   f1.close();

   f2.close();

   return  0;

  }

  //    Сравнение   содержимого        буферов.

  for(i=0;     i<f1.gcount();          i++)

   if(buf1[i]          !=        buf2[i])          {

    cout <<       "Файлы        различны.\n";

    f1.close();

    f2.close();

    return         0;

   }

 }while(!f1.eof()       &&      !f2.eof());

 cout    <<       "Файлы        одинаковы.\n";

 f1.close();

 f2.close();

 return            0;

}

Проведите        эксперимент.         Размер         буфера         в          этой   программе жестко          установлен равным 1024.        В          качестве       упражнения           замените это      значение      const-переменной           и          опробуйте   другие размеры   буферов. Определите            оптимальный        размер         буфера         для     своей операционной среды.

Использование        других          функций      двоичного   ввода-вывода

Помимо            приведенного       выше формата       использования      функции get()   существуют             и          другие ее    перегруженные    версии.         Приведем прототипы  для     трех    из        них,    которые       используются чаще        всего.

istream   &get(char     *buf,  streamsize    num);

istream   &get(char     *buf,  streamsize    num,  char    delim);

int            get();

Первая   версия          позволяет    считывать    символы      в          массив,         заданный параметром           buf,    до       тех пор,        пока   либо  не       будет считано        num-1 символов,   либо  не       встретится   символ         новой            строки, либо          не будет достигнут    конец            файла.          После            выполнения           функции get()   массив,         адресуемый параметром        buf,    будет иметь            завершающий нуль-символ.         Символ         новой            строки,         если таковой         обнаружится во       входном      потоке,         не       извлекается.           Он      остается       там     до       тех пор,   пока не         выполнится            следующая операция     ввода-вывода.

Вторая    версия          предназначена     для     считывания             символов    в массив,         адресуемый параметром        buf,    до       тех      пор,   пока   либо  не будет считано        num-1           символов,   либо  не обнаружится    символ,        заданный параметром           delim,            либо  не       будет достигнут    конец            файла. После выполнения           функции       get()   массив,         адресуемый           параметром           buf, будет иметь завершающий      нуль-символ.         Символ-разделитель      (заданный параметром           delim),           если   таковой обнаружится    во       входном      потоке, не       извлекается.           Он      остается       там     до       тех      пор,   пока   не выполнится следующая операция     ввода-вывода.

Третья    перегруженная     версия          функции       get()   возвращает             из потока          следующий            символ. Он содержится            в          младшем    байте значения,    возвращаемого    функцией.   Следовательно, значение,       возвращаемое функцией    get(),  можно          присвоить   переменной           типа   char.  При достижении            конца            файла            эта      функция       возвращает             значение EOF,   которое        определено            в заголовке <iostream>.

Функцию           get()   полезно       использовать         для     считывания             строк, содержащих           пробелы.     Как вы          знаете,          если   для     считывания строки           используется         оператор     ">>",  процесс        ввода останавливается  при обнаружении         первого        же       пробельного          символа.      Это     делает оператор ">>"       бесполезным         для     считывания             строк,            содержащих пробелы.     Но       эту      проблему,   как показано         в          следующей программе, можно          обойти          с          помощью    функции       get(buf,num).

/* Использование     функции       get()   для     считывания             строк содержащих пробелы.

*/

#include <iostream> #include <fstream> using     namespace   std;

int    main() {

 char    str[80];

 cout    <<       "Введите      имя:   ";

  cin.get       (str,    79);

 cout    <<       str       <<       '\n';

 return            0;

}

Здесь      в          качестве       символа-разделителя    при     считывании            строки с          помощью    функции       get() используется           символ         новой            строки. Это     делает          поведение  функции       get()   во       многом        сходным с поведением           стандартной           функции       gets().            Однако         преимущество функции       get()   состоит         в том,            что      она     позволяет    предотвратить возможный            выход           за        границы       массива,      который принимает вводимые   пользователем     символы,     поскольку   в          программе задано максимальное количество      считываемых         символов.   Это     делает          функцию get()   гораздо        безопаснее функции gets().

Рассмотрим     еще    одну  функцию,    которая        позволяет    вводить        данные. Речь   идет   о          функции getline(),            которая        является       членом         каждого потокового класса,          предназначенного          для     ввода информации.        Вот     как выглядят     прототипы  версий          этой   функции,

istream   &getline(char          *buf,  streamsize    num);

istream   &getline(char          *buf,  streamsize    num,  char    delim);

Функция           getline()        представляет     собой            еще    один  способ     ввода            данных.

При         использовании     первой          версии          символы      считываются          в массив,         адресуемый указателем          buf,    до       тех      пор,   пока   либо  не будет считано        num-1           символов,   либо  не       встретится символ          новой строки,         либо  не       будет достигнут    конец            файла.          После выполнения           функции getline() массив,         адресуемый           параметром           buf, будет иметь            завершающий       нуль-символ. Символ    новой            строки, если   таковой        обнаружится          во       входном      потоке,         при     этом извлекается, но    не       помещается            в          массив          buf.

Вторая    версия          предназначена     для     считывания             символов    в массив,         адресуемый параметром        buf,    до       тех      пор,   пока   либо  не будет считано        num-1           символов,   либо  не обнаружится    символ,        заданный параметром           delim,            либо  не       будет достигнут    конец            файла. После выполнения           функции       getline()        массив,         адресуемый           параметром buf,    будет иметь завершающий      нуль-символ.         Символ-разделитель (заданный   параметром           delim),           если   таковой обнаружится    во       входном потоке,         извлекается,           но       не       помещается            в          массив          buf.

Как          видите,         эти      две     версии          функции       getline()        практически идентичны  версиям       get      (buf, num)    и          get      (buf,   num,  delim)            функции get().  Обе    считывают   символы      из        входного     потока          и помещают           их в          массив,         адресуемый           параметром           buf,    до       тех      пор,   пока либо  не       будет считано num-1      символов,   либо  не       обнаружится          символ, заданный    параметром           delim.            Различие     между функциями          get()   и getline()        состоит         в          том,   что      функция       getline()        считывает    и удаляет        символразделитель        из        входного     потока,         а          функция get()   этого  не       делает.

Функция           реек()            считывает            следующий символ         из        входного     потока,       не       удаляя          его.

Следующий     символ         из        входного     потока          можно          получить      и не       удалять        его      из        потока          с помощью функции       реек().           Вот     как выглядит     ее        прототип.

int            peek();

Функция            peek()            возвращает             следующий            символ         потока, или     значение      EOF,   если достигнут      конец            файла.          Считанный  символ возвращается        в          младшем    байте значения, возвращаемого       функцией. Поэтому       значение,    возвращаемое      функцией    реек(),           можно присвоить переменной           типа   char.

Функция           putback()     возвращает          считанный символ         во       входной     поток.

Последний       символ,        считанный   из        потока,         можно          вернуть        в поток,           используя   функцию putback().        Ее        прототип      выглядит     так.

istream   &putback(char       с);

Здесь     параметр     с          содержит     символ,        считанный   из        потока     последним.

Функция           flush()            сбрасывает           на       диск   содержимое     файловых   буферов.

При         выводе         данных         немедленной        их        записи          на физическое             устройство, связанное    с потоком,  не       происходит. Подлежащая          выводу         информация          накапливается       во       внутреннем буфере          до       тех      пор,   пока   этот    буфер            не       заполнится целиком.     И только          тогда его      содержимое переписывается            на       диск. Однако существует  возможность         немедленной        перезаписи             на       диск хранимой в          буфере         информации,         не       дожидаясь  его      заполнения.           Это средство      состоит         в вызове      функции       flush().           Ее        прототип      имеет такой вид.

ostream &flush();

К   вызовам      функции       flush()            следует        прибегать    в          случае, если   программа предназначена для        выполнения           в          неблагоприятных средах          (для   которых       характерны             частые          отключения электричества, например).

Произвольный         доступ

До           сих      пор     мы      использовали        файлы,         доступ          к     содержимому       которых       был    организован строго        последовательно,     байт   за        байтом.        Но       в          C++     также            можно          получать     доступ          к          файлу            в произвольном   порядке.      В          этом   случае     необходимо           использовать         функции       seekg()          и          seekp(). Вот их     прототипы.

istream   &seekg(off_type    offset,            seekdir          origin);

ostream &seekp(off_type    offset,            seekdir          origin);

Используемый           здесь целочисленный    тип     off_type        (он      определен  в классе           ios)     позволяет хранить          самое            большое      допустимое значение,    которое        может           иметь            параметр     offset.            Тип seekdir определен  как     перечисление,       которое        имеет            следующие значения.

Функция            seekg()          перемещает         указатель, "отвечающий"    за ввод   данных,        а          функция seekp()   —        указатель, "отвечающий"    за вывод.

В   С++-системе           ввода-вывода       предусмотрена     возможность управления            двумя указателями,        связанными           с          файлом.       Эти     так называемые           cin-     и          put-указатели        определяют,          в каком        месте файла            должна        выполниться          следующая операция     ввода            и вывода соответственно.            При    каждом        выполнении           операции     ввода или     вывода         соответствующий указатель   автоматически      перемещается       в указанную  позицию.     Используя   функции       seekg()          и seekp(),     можно получать      доступ          к          файлу            в          произвольном      порядке.

Функция            seekg()          перемещает           текущий       get-указатель соответствующего           файла            на       offset байт   относительно         позиции, заданной     параметром           origin.            Функция       seekp()          перемещает текущий put-указатель        соответствующего           файла            на       offset байт   относительно позиции, заданной         параметром           origin.

В   общем          случае           произвольный      доступ          для     операций     ввода-вывода         должен        выполняться только       для     файлов,        открытых     в двоичном    режиме.       Преобразования  символов,   которые       могут происходить в          текстовых    файлах,        могут привести      к          тому, что      запрашиваемая позиция       файла не      будет соответствовать    его      реальному  содержимому.

В   следующей программе демонстрируется использование      функции       seekp(). Она позволяет      задать           имя    файла            в          командной строке,          а          за ним    —        конкретный            байт,  который нужно    в          нем    изменить. Программа затем записывает в          указанную  позицию      символ         "X". Обратите внимание    на       то,       что      обрабатываемый             файл  должен        быть  открыт для     выполнения операций  чтения-записи.

/* Демонстрация       произвольного     доступа        к          файлу.

*/

#include <iostream>

#include <fstream> #include <cstdlib> using     namespace   std;

int            main(int        argc,   char     *argv[]) {

 if(argc!=3)    {

  cout           <<       "Применение:       имя_программы  "          <<       "<имя_файла> <байт>\n";

  return       1;

 }

 fstream          out(argv[1], ios::in |          ios::out          |          ios::binary);

 if(!out)           {

  cout           <<       "He     удается         открыть        файл.\n";

  return       1;

 }

 out.seekp(atoi(argv[2]),   ios::beg);

 out.put('X');

 out.close();

 return            0;

}

В   следующей программе показано     использование      функции       seekg().         Она отображает содержимое         файла,          начиная        с          позиции,      заданной     в командной строке.

/* Отображение         содержимого        файла            с          заданной     стартовой     позиции.

*/

#include <iostream>

#include <fstream> #include <cstdlib> using     namespace   std;

int            main(int        argc,   char     *argv[]) {

 char    ch;

 if(argc!=3)    {

  cout           <<       "Применение:       имя_программы  "<<     "<имя_файла> <стартовая_позиция>\n";

  return       1;

 }

 ifstream         in(argv[1],    ios::in |          ios::binary);

 if(!in)  {

  cout           <<       "He     удается         открыть        файл.\n";

  return       1;

 }

 in.seekg(atoi(argv[2]),      ios::beg);

 while(in.get  (ch))   cout    <<       ch;

 return            0;

}

Функция            tellg() возвращает          текущую    позицию      get-указателя,     а функция       tellp() — текущую           позицию      put-указателя.

Текущую           позицию      каждого       файлового  указателя    можно определить            с          помощью    этих    двух функций. pos_type            tellg();

pos_type           tellp();

Здесь      используется         тип     pos_type      (он      определен  в          классе           ios), позволяющий        хранить самое      большое      значение,    которое        может возвратить  любая           из        этих    функций.

Существуют     перегруженные    версии          функций       seekg()          и          seekp(), которые       перемещают файловые            указатели    в          позиции,      заданные значениями,           возвращаемыми  функциями tellg() и tellp()         соответственно.    Вот как     выглядят     их        прототипы,

istream   &seekg(pos_type   position); ostream     &seekp(pos_type   position);

Проверка        статуса     ввода-вывода

С++-система    ввода-вывода       поддерживает       статусную    информацию         о результатах выполнения          каждой         операции     ввода-вывода.      Текущий статус            потока          ввода-вывода описывается    в          объекте        типа   iostate, который       представляет         собой            перечисление        (оно определено в классе           ios),    включающее          следующие члены.

Статусную         информацию         о          результате  выполнения           операций ввода-вывода       можно получать  двумя           способами. Во-первых,  можно вызвать        функцию      rdstate(),      которая        является членом  класса           ios.      Она имеет            такой прототип.

iostate    rdstate();

Функция            rdstate()       возвращает             текущий       статус            флагов ошибок.       Нетрудно     догадаться, что, судя      по       приведенному      выше списку флагов,         функция       rdstate()       возвратит    значение      goodbit при отсутствии каких бы      то        ни       было ошибок.       В          противном  случае           она возвращает соответствующий           флаг   ошибки.

Во-вторых,       о          наличии       ошибки        можно          узнать           с          помощью     одной           или     нескольких следующих            функций-членов   класса           ios.

bool     bad(); bool     eof(); bool     fail();

bool         good();

Функция            eof()   рассматривалась  выше.            Функция       bad()  возвращает значение      ИСТИНА, если        в          результате  выполнения           операции     ввода-вывода         был    установлен флаг   badbit.          Функция fail()        возвращает значение      ИСТИНА,      если   в          результате  выполнения           операции     ввода-вывода был            установлен флаг   failbit.            Функция       good()           возвращает значение      ИСТИНА,      если   при выполнении  операции     ввода-вывода       ошибок не       произошло.            В          противном  случае           они возвращают   значение ЛОЖЬ.

Если        при     выполнении           операции     ввода-вывода       произошла ошибка, то,       возможно,  прежде чем           продолжать           выполнение           программы, имеет            смысл           сбросить      флаги            ошибок.       Для    этого используйте функцию      clear()            (член класса           ios),    прототип      которой        выглядит     так.

void         clear   (iostate          flags   =          ios::goodbit);

Если        параметр     flags   равен            значению    goodbit         (оно устанавливается   по       умолчанию),          все флаги    ошибок        очищаются.            В противном  случае           флаги            устанавливаются  в          соответствии          с заданным    вами  значением.

Прежде чем    переходить к          следующему          разделу,      стоит  опробовать функции,     которые сообщают         данные         о          состоянии    флагов          ошибок, внеся в          предыдущие          примеры     программ    код проверки        ошибок.

Использование        перегруженных   операторов          ввода-вывода       при   работе        с          файлами

Выше      в          этой   главе вы       узнали,         как     перегружать           операторы ввода            и          вывода         для собственных  классов,       а          также            как создавать    собственные          манипуляторы.     В          приведенных выше        примерах программ    выполнялись         только          операции     консольного          ввода-вывода. Но поскольку        все      С++-потоки одинаковы,            одну  и          ту        же перегруженную    функцию      вывода данных,   например,   можно          использовать для     вывода         информации          как     на       экран,           так      и          в          файл, не внося при     этом   никаких        существенных        изменений.             Именно        в этом   и          заключаются основные            достоинства           С++-системы          ввода-вывода.

В   следующей программе используется         перегруженный    (для   класса three_d)       оператор вывода для     записи          значений     координат   в          файл threed.

/* Использование     перегруженного   оператора   ввода-вывода       для записи            объектов      класса           three_d         в          файл.

*/

#include <iostream>

#include <fstream>

using       namespace   std;

class        three_d         {

  int  x,         y,         z;         //         3-мерные    координаты;          они     теперь     закрыты  public:

  three_d(int          a,         int       b,        int       с)         {           x          =          a;         у          =     b;        z          =          c;         }

  friend        ostream        &operator<<(ostream      &stream,      three_d         obj);   /* Отображение         координат   X,        Y,         Z          (оператор    вывода         для     класса three_d).      */

};

ostream &operator<<(ostream      &stream,      three_d         obj) {

 stream           <<       obj.x   <<       ",         ";

 stream           <<       obj.у  <<       ",         ";

 stream           <<       obj.z   <<       "\n";

 return            stream;          //         возвращает             поток

}

int    main() {

 three_d         a(1,     2,        3),       b(3,    4,        5),       c(5,     6,        7);

 ofstream       out("threed");

 if(!out)           {

  cout           <<       "He     удается         открыть        файл.";

  return       1;

 }

 out      <<       a          <<       b          <<       c;

 out.close();

 return            0;

}

Если        сравнить      эту      версию         операторной          функции       вывода данных         для     класса           three_d         с          той, что        была  представлена        в начале          этой   главы,           можно          убедиться    в          том,   что      для "настройки"           ее на работу          с          дисковыми файлами      никаких изменений  вносить        не       пришлось.   Если   операторы ввода и          вывода определены           корректно,  они     будут успешно       работать      с          любым потоком.

Важно!  Прежде        чем    переходить           к          следующей главе,           не пожалейте            времени       и поработайте   с          С++-функциями    ввода-вывода. Создайте    собственный        класс,            а          затем определите        для     него операторы            ввода            и          вывода.        А         еще    создайте    собственные манипуляторы.

Глава     19:  Динамическая   идентификация типов     и операторы приведения  типа

В   этой   главе рассматриваются два     средства      C++,   которые       поддерживают современное объектно-ориентированное           программирование:       динамическая идентификация  типов           (runtime       type    identification           -           RTTI)  и набор            дополнительных  операторов          приведения            типа.            Ни одно из        этих    средств         не       было частью          оригинальной       спецификации       C++, но       оба     они     были добавлены с          целью           усиления     поддержки полиморфизма     времени       выполнения.          Под    RTTI понимается   возможность проведения            идентификации    типа   объекта        во       время            выполнения программы.           Рассматриваемые            здесь операторы  приведения            типа предлагают            программисту более      безопасные            способы       выполнения этой   операции.   Как     будет показано     ниже,            один  из них,          dynamic_cast, непосредственно             связан           с          RTTI-идентификацией,   поэтому операторы приведения            типа   и          RTTI    имеет            смысл           рассматривать в          одной           главе.

Динамическая         идентификация  типов           (RTTI)

С   динамической       идентификацией  типов вы,     возможно,  незнакомы, поскольку   это средство          отсутствует  в          таких неполиморфных  языках,         как С.        В          неполиморфных  языках попросту  нет      необходимости     в получении  информации          о          типе   во       время            выполнения программы, так      как     тип     каждого       объекта        известен       во       время            компиляции (т.е.    еще    при написании      программы).          Но       в          таких полиморфных языках,         как     C++,   возможны   ситуации,     в которых    тип     объекта неизвестен  в          период         компиляции,          поскольку   точная           природа этого объекта        не       будет определена            до       тех      пор,   пока   программа на начнет           выполняться.         Как     вы знаете,   C++     реализует    полиморфизм посредством          использования      иерархии     классов, виртуальных    функций       и указателей  на       базовые       классы.         Указатель    на       базовый       класс можно использовать         для     ссылки          на       члены           как     этого  базового      класса, так      и          на       члены           любого объекта,  выведенного         из        него. Следовательно,    не       всегда           заранее        известно,     на       объект          какого типа   будет ссылаться    указатель    на       базовый       класс в          произвольный момент         времени.     Это выяснится       только          при     выполнении программы —        при     использовании     одного          из        средств динамической идентификации    типов.

Для          получения   типа объекта      во       время            выполнения программы            используйте         оператор typeid.

Чтобы     получить      тип     объекта        во       время            выполнения программы,           используйте           оператор typeid.   Для    этого  необходимо включить     в          программу заголовок    <typeinfo>.  Самый распространенный формат         использования      оператора   typeid            таков.

typeid(object)

Здесь      элемент        object            означает      объект,         тип     которого      нужно получить.    Можно запрашивать      не       только          встроенный            тип,    но       и тип     класса,          созданного программистом. Оператор     typeid            возвращает ссылку          на       объект          типа   type_infо,    который       описывает   тип объекта object.

В  классе           type_info      определены           следующие public-члены.

bool         operator       =          (const            type_info      &ob);

bool         operator       !=(const        type_info      &ob);

bool         before(const            type_info      &ob);

const       char    *name();

Перегруженные         операторы  "=="   и          "!="    служат          для     сравнения типов.           Функция       before() возвращает        значение      true,   если   вызывающий объект          в          порядке       сопоставления      стоит  перед объектом   (элементом ob),    используемым      в          качестве       параметра. (Эта    функция       предназначена в          основном    для     внутреннего           использования.    Возвращаемое      ею значение      результата  не       имеет ничего         общего         с          наследованием    или иерархией   классов.)      Функция       name()          возвращает указатель   на       имя типа.

Рассмотрим    простой        пример         использования      оператора   typeid.

//  Пример        использования      оператора   typeid.

#include <iostream> #include <typeinfo> using     namespace   std;

class        myclass         {

 //         .     .           . };

int    main() {

 int       i,          j;

 float    f;

 myclass          ob;

 cout    <<       "Тип   переменной           i:          "          <<       typeid(i).name();

 cout    <<       endl;

 cout    <<       "Тип   переменной           f:         "          <<       typeid(f).name();

 cout    <<       endl;

 cout    <<       "Тип   переменной           ob:      "          <<       typeid(ob).name();

 cout    <<       "\n\n";

 if(typeid(i)    ==       typeid(j))

  cout           <<       "Типы            переменных           i           и          j           одинаковы.\n";

 if(typeid(i)    !=        typeid(f))

  cout           <<       "Типы            переменных           i           и          f     неодинаковы.\n";

 return            0;

}

При         выполнении           этой   программы получены    такие результаты.

Тип          переменной           i:          int

Тип          переменной           f:         float

Тип          переменной           ob:      class   myclass

Типы       переменных           i           и          j           одинаковы.

Типы       переменных           i           и          f           неодинаковы.

Если        оператор     typeid            применяется          к          указателю   на полиморфный       базовый       класс (вспомните:            полиморфный       класс —        это класс,            который       содержит     хотя    бы      одну  виртуальную функцию),           он автоматически      возвращает             тип     реального   объекта,       на       который       тот указывает: будь   то        объект          базового      класса           или     объект          класса, выведенного         из        базового.

Следовательно,         оператор     typeid            можно          использовать         для динамического     определения типа           объекта,       адресуемого          указателем на базовый       класс.            Применение           этой   возможности демонстрируется        в следующей программе.

/* Пример        применения           оператора   typeid            к          иерархии            полиморфных классов.

*/

#include <iostream> #include <typeinfo> using     namespace   std;

class        Base   {

 virtual            void    f()        {};        //         делаем         класс Base   полиморфным

 //         .     .           . };

class        Derived1:     public Base   {

 //         .     .           . };

class        Derived2:     public Base   {

 //     ... };

int    main() {

 Base    *p,      baseob;

 Derived1       ob1;

 Derived2       ob2;

 p          =          &baseob;

 cout    <<       "Переменная         p          указывает    на       объект          типа   ";

 cout    <<       typeid(*p).name() <<       endl;

 p          =          &ob1;

 cout    <<       "Переменная         p          указывает    на       объект          типа   ";

 cout    <<       typeid(*p).name() <<       endl;

 p          =          &ob2;

 cout    <<       "Переменная         p          указывает    на       объект          типа   ";

 cout    <<       typeid(*p).name() <<       endl;

 return            0;

}

Вот          как     выглядят     результаты выполнения           этой   программы:

Переменная    р          указывает    на       объект          типа   Base

Переменная    р          указывает    на       объект          типа   Derived1

Переменная    р          указывает    на       объект          типа   Derived2

Если        оператор     typeid            применяется          к          указателю   на       базовый класс полиморфного      типа, тип      реально       адресуемого          объекта,       как подтверждают      эти      результаты,            будет определен  во время      выполнения программы.

Во всех    случаях         применения           оператора   typeid            к          указателю   на неполиморфную  иерархию классов           будет получен       указатель    на       базовый тип,    т.е.      то,       на       что      этот    указатель    реально указывает,        определить нельзя.         В          качестве       эксперимента        превратите  в          комментарий виртуальную          функцию      f()        в          классе           Base   и          посмотрите             на результат.   Вы      увидите,       что      тип каждого           объекта        после внесения      в программу этого  изменения  будет определен  как     Base, поскольку    именно         этот тип     имеет            указатель    p.

Поскольку       оператор     typeid            обычно         применяется          к     разыменованному          указателю   (т.е.    к указателю,           к          которому     уже     применен    оператор     "*"),   для     обработки   ситуации,     когда этот разыменованный     указатель    оказывается           нулевым,     создано     специальное          исключение.          В          этом случае            оператор     typeid     генерирует  исключение            типа   bad_typeid.

Ссылки  на       объекты       иерархии     полиморфных       классов        работают подобно      указателям.            Если оператор       typeid            применяется          к ссылке          на       полиморфный       класс,            он       возвращает             тип     объекта, на        который       она     реально       ссылается,   и          это      может           быть  объект не       базового,     а          производного типа.        Описанное  средство      чаще  всего используется         при     передаче     объектов      функциям    по ссылке.  Например,  в следующей программе функция       WhatType() объявляет   ссылочный параметр     на объекты       типа   Base.  Это     значит,          что      функции       WhatType() можно передавать ссылки         на       объекты       типа   Base   или     ссылки          на       объекты любых          классов,       производных         от        Base. Оператор     typeid, примененный        к          такому          параметру, возвратит    реальный    тип     объекта, переданного          функции.

/* Применение           оператора   typeid            к          ссылочному           параметру.

*/

#include <iostream> #include <typeinfo> using     namespace   std;

class        Base   {

 virtual            void    f()        {};        //         делаем         класс Base   полиморфным

 //         .     .           . };

class        Derived1:     public Base   {

 //         .     .           . };

class        Derived2:     public Base   {

 //         .           .           .

};

/* Демонстрируем    применение           оператора   typeid            к          ссылочному параметру.

*/ void    WhatType(Base     &ob) {

 cout    <<       "Параметр  ob       ссылается    на       объект          типа   ";

 cout    <<       typeid(ob).name() <<       endl;

}

int    main() {

 int       i;

 Base    baseob;

 Derived1       obi;

 Derived2       ob2;

 WhatType(baseob);

 WhatType(ob1);

 WhatType(ob2);

 return            0;

}

Эта          программа генерирует  такие результаты.

Параметр         ob       ссылается    на       объект          типа   Base

Параметр         ob       ссылается    на       объект          типа   Derived1

Параметр         ob       ссылается    на       объект          типа   Derived2

Существует       еще    одна  версия          применения           оператора   typeid, которая        в          качестве       аргумента принимает    имя    типа.  Формат         ее таков.

tуре         id(имя_типа)

Например,       следующая инструкция совершенно           допустима.

cout         <<       typeid(int).name();

Назначение      этой   версии          оператора   typeid            —        получить      объект типа   type_info      (который описывает       заданный    тип     данных),      чтобы            его можно          было использовать         в          инструкции сравнения  типов.

Пример           RTTI-приложения

В   следующей программе показано,    насколько   полезной     может           быть средство динамической           идентификации    типов (RTTI).            Здесь используется модифицированная        версия иерархии  классов        геометрических    фигур            из главы 15,      которая        вычисляет   площадь      круга, треугольника        и прямоугольника. В          данной         программе определена            функция       fасtorу(), предназначенная             для     создания     экземпляра            круга,            треугольника или     прямоугольника. Эта функция           возвращает             указатель    на созданный  объект.         (Функция,    которая        генерирует  объекты, иногда называется фабрикой    объектов.) Конкретный            тип     создаваемого        объекта определяется         в          результате  обращения к          функции       rand()            С++-генератора  случайных   чисел. Таким          образом,     мы      не       можем          знать заранее,       объект          какого           типа   будет сгенерирован. Программа      создает десять           объектов      и          подсчитывает        количество созданных  фигур каждого типа.       Поскольку   при     вызове          функции       fасtorу()       может быть  сгенерирована      фигура          любого типа,         для     определения         типа реально       созданного объекта        в          программе используется         оператор typeid.

/* Демонстрация       использования      средства      динамической идентификации            типов.

*/

#include <iostream> #include <cstdlib> using     namespace   std;

class        figure { protected:

double      x,         y;

 public:

  figure(double     i,          double           j)         {

   x =          i;

   У            =          j;

  }

  virtual       double           area() =          0;

};

class        triangle          :           public figure {

 public:

  triangle(double  i,          double           j)         :           figure(i,         j)         {}

  double      area() {

   return  x          *          0.5      *          y;

  }

};

class        rectangle      :           public figure {

 public:

  rectangle(double           i,          double           j)         :           figure (i,        j)         {}

  double      area() {           return            x          *          y;}

};

class        circle  :           public figure {

 public:

  circle(double       i,          double           j=0)    :           figure(i,         j)         {}

  double      area() {return          3.14   *          x          *          x;}

};

//  Фабрика       объектов      класса     figure. figure           *factory() {

 switch(rand()           %         3          )           {

  case           0:        return            new    circle  (10.0);

  case           1:        return            new    triangle          (10.1,     5.3);   case        2:        return            new    rectangle     (4.3,   5.7);

 }

 return     0; }

int    main() {

 figure *p;      //         указатель    на       базовый       класс

 int       i;

 int       t=0,    r=0,    c=0;

 //         генерируем            и          подсчитываем объекты for(i=0;   i<10;  i++)     { p       = factory();      //         генерируем            объект   cout    <<       "Объект        имеет            тип     " <<       typeid(*р).name();

  cout           <<       ".         ";

  //    учитываем  этот    объект

  if(typeid(*р)       ==       typeid(triangle))     t++;

  if(typeid(*p)       ==       typeid(rectangle))  r++;

  if(typeid(*p)       ==       typeid(circle))          c++;

  //    отображаем           площадь      фигуры

  cout           <<       "Площадь   равна            "          <<       p->area()      <<       endl;

 }

 cout    <<       endl;

 cout    <<       "Сгенерированы   такие объекты:\n";

 cout    <<       "          треугольников:     "          <<       t           <<       endl;

 cout    <<       "          прямоугольников:           "          <<       r          <<       endl;

 cout    <<       "          кругов:         "          <<       с          <<       endl;

 return            0;

}

Возможный     результат     выполнения           этой   программы таков.

Объект   имеет            тип     class   rectangle.     Площадь     равна            24.51

Объект   имеет            тип     class   rectangle.     Площадь     равна            24.51

Объект   имеет            тип     class   triangle.        Площадь     равна            26.765

Объект   имеет            тип     class   triangle.        Площадь     равна            26.765 Объект     имеет            тип     class   rectangle.     Площадь     равна            24.51

Объект   имеет            тип     class   triangle.        Площадь     равна            26.765

Объект   имеет            тип     class   circle. Площадь     равна            314

Объект   имеет            тип     class   circle. Площадь     равна            314

Объект   имеет            тип     class   triangle.        Площадь     равна            26.765 Объект   имеет            тип     class   rectangle.     Площадь     равна            24.51

Сгенерированы          такие объекты:

 треугольников:     4

 прямоугольников:           4

 кругов:          2

Применение  оператора             typeid            к          шаблонным          классам

Оператор          typeid            можно          применить  и          к          шаблонным классам.      Тип     объекта,       который является            экземпляром         шаблонного класса,          определяется        частично      на       основании   того,  какие именно данные         используются        для     его      обобщенных          данных         при реализации            объекта.       Таким образом,   два     экземпляра            одного          и того    же       шаблонного           класса,          которые       создаются   с использованием различных  данных,        имеют           различный  тип.    Рассмотрим            простой пример.

/* Использование     оператора   typeid            с          шаблонными         классами.

*/ #include     <iostream> using     namespace   std;

template            <class T> class        myclass         {

  T     a;

public:

myclass(T i)          {           a          =          i;          }

  //    .     .           . };

int    main() {

 myclass<int>            o1(10),          o2(9);

 myclass<double>   o3(7.2);

 cout    <<       "Объект        o1       имеет            тип     ";

 cout    <<       typeid(o1).name() <<       endl;

 cout    <<       "Объект        o2       имеет            тип     ";

 cout    <<       typeid(o2).name() <<       endl;

 cout    <<       "Объект        o3       имеет            тип     ";

 cout    <<       typeid(o3).name() <<       endl;

 cout    <<       endl;

 if(typeid(o1)            ==       typeid(o2))

  cout           <<       "Объекты    o1       и          o2       имеют           одинаковый     тип.\n";

 if(typeid(o1)            ==       typeid(o3))   cout    <<       "Ошибка\n";

 else     cout    <<       "Объекты    o1       и          o3       имеют           разные         типы.\n";  return     0;

}

Эта          программа генерирует  такие результаты.

Объект   o1       имеет            тип     class   myclass<int>

Объект   o2       имеет            тип     class   myclass<int>

Объект   o3       имеет            тип     class   myclass<double>

Объекты           o1       и          o2       имеют           одинаковый           тип.

Объекты           o1       и          o3       имеют           разные         типы.

Как          видите,         несмотря     на       то,       что      два     объекта        являются экземплярами       одного          и          того    же шаблонного    класса,          если   их параметризованные       данные         не       совпадают, они     не       эквивалентны по типу.  В          этой   программе объект          o1       имеет            тип     myclass<int>,          а объект          o3       —        тип myclass<double>.      Таким            образом,     это      объекты разного        типа.

Рассмотрим     еще    один  пример         применения           оператора   typeid            к шаблонным           классам,      а именно     модифицированную      версию программы определения         геометрических    фигур            из предыдущего  раздела. На       этот    раз     класс figure мы      сделали       шаблонным.

//  Шаблонная версия          figure-иерархии.

#include <iostream> #include <cstdlib> using     namespace   std;

template     <class T> class     figure {

 protected:

  T     x,         y;

 public:

  figure(T    i,          T          j)         {

   x =          i;

   у =          j;

  }

  virtual       T          area() =          0;

};

template            <class T> class     triangle          :           public figure<T> {

 public:

  triangle(T i,          T          j)         :           figure<T>(i,  j)         {}

  T     area() {

   return  x          *          0.5      *          y;

  }

};

template            <class T> class     rectangle      :           public figure<T> {

 public:

  rectangle(T          i,          T          j)         :           figure<T>(i,  j)         {}

  T     area() {

   return  x          *          y;

  }

};

template            <class T> class     circle  :           public figure<T> {

 public:

  circle(T      i,          T          j=0)    :           figure<T>(i,  j)         {}

  T     area() {

   return  3.14   *          x          *          x;

  }

};

//  Фабрика       объектов,    генерируемых       из        класса     figure. figure<double>     *generator() {

 switch(rand()           %         3          )           {

  case           0:        return            new    circle<double>        (10.0);

  case           1:        return            new    triangle<double>(10.1,    5.3);

  case           2:        return            new    rectangle<double>            (4.3,   5.7);

 }

 return     0; }

int            main()

{

 figure<double>       *p;

 int       i;

 int       t=0,    c=0,    r=0;

 //         генерируем            и          подсчитываем       объекты

 for(i=0;          i<10;  i++)     {

  p     =          generator();

  cout           <<       "Объект        имеет            тип     "          <<       typeid(*p).name();

  cout           <<       ".         ";

  //    учитываем  объект

  if(typeid(*p)       ==       typeid(triangle<double>))           t++;

  if(typeid(*p)       ==       typeid(rectangle<double>))        r++;

  if(typeid(*p)       ==       typeid(circle<double>))   c++;

  cout           <<       "Площадь   равна            "          <<       p->area()      <<       endl;

 }

 cout    <<       endl;

 cout    <<       "Сгенерированы   такие объекты:\n";

 cout    <<       "          треугольников:     "          <<       t           <<       endl;

 cout    <<       "          прямоугольников:           "          <<       r          <<       endl;

 cout    <<       "          кругов:         "          <<       с          <<       endl;

 return            0;

}

Вот          как     выглядит     возможный            результат     выполнения           этой     программы.

Объект   имеет            тип     class   rectangle<double>.           Площадь     равна     24.51

Объект   имеет            тип     class   rectangle<double>.           Площадь     равна     24.51

Объект   имеет            тип     class   triangle<double>.  Площадь     равна            26.765

Объект   имеет            тип     class   triangle<double>.  Площадь     равна            26.765

Объект   имеет            тип     class   rectangle<double>.           Площадь     равна     24.51

Объект   имеет            тип     class   triangle<double>.  Площадь     равна            26.765

Объект   имеет            тип     class   circle<double>.       Площадь     равна            314

Объект   имеет            тип     class   circle<double>.       Площадь     равна            314

Объект   имеет            тип     class   triangle<double>.  Площадь     равна            26.765 Объект   имеет            тип     class   rectangle<double>.           Площадь     равна     24.51

Сгенерированы          такие объекты:

 треугольников:     4

 прямоугольников:           4

 кругов:          2

Динамическая            идентификация     типов используется         не       в          каждой программе.            Но       при работе  с          полиморфными   типами         это      средство позволяет    узнать           тип     объекта, обрабатываемого     в          любой произвольный      момент         времени.

Операторы  приведения            типов

В   C++     определено            пять   операторов             приведения            типов. Первый        оператор     (он      описан          выше в         этой   книге),          применяемый в          обычном     (традиционном)   стиле,            был    с          самого          начала встроен в    C++.   Остальные  четыре          (dynamic_cast,       const_castreinterpret_cast и          static_cast)  были добавлены в          язык  всего несколько   лет      назад.           Эти операторы  предоставляют     дополнительные "рычаги       управления" характером             выполнения           операций     приведения            типа.  Рассмотрим каждый        из        них     в          отдельности.

Оператор      dynamic_cast

Оператор        dynamic_cast         выполняет операцию   приведения полиморфных       типов           во       время выполнения         программы.

Возможно,       самым          важным       из        новых            операторов             является оператор     динамического приведения   типов dynamic_cast.        Во       время выполнения           программы он       проверяет обоснованность     предлагаемой операции.   Если   в          момент         его      вызова          заданная     операция оказывается           недопустимой,      приведение            типов не       производится. Общий          формат         применения оператора            dynamic_cast         таков.

dynamic_cast<type> (expr)

Здесь      элемент        type    означает      новый           тип,    который       является целью           выполнения           этой операции,     а          элемент        expr— выражение,            приводимое           к          этому            новому         типу.  Тип     type должен быть         представлен           указателем или     ссылкой,      а          выражение expr должно        приводиться          к указателю            или     ссылке.         Таким            образом, оператор     dynamic_cast         можно          использовать         для преобразования указателя    одного          типа   в          указатель    другого        или     ссылки          одного типа   в          ссылку другого.

Этот         оператор     в          основном    используется         для     динамического выполнения           операций приведения   типа   среди            полиморфных       типов. Например,  если   даны полиморфные       классы          В          и D,    причем         класс D выведен       из        класса           В,        то        с          помощью    оператора   dynamic_cast всегда           можно преобразовать   указатель    D*       в          указатель    В*, поскольку   указатель    на       базовый       класс всегда можно        использовать         для указания      на       объект          класса,          выведенного         из        базового.     Однако оператор      dynamic_cast         может           преобразовать      указатель    В*       в указатель    D*       только          в          том случае, если   адресуемым          объектом действительно      является       объект          класса           D.        И,        вообще, оператор dynamic_cast         будет успешно       выполнен    только          при     условии,      если разрешено полиморфное        приведение            типов,           т.е.      если   указатель (или   ссылка),       приводимый          к          новому типу,         может           указывать (или   ссылаться)  на       объект          этого  нового          типа   или     объект, выведенный из    него.  В          противном  случае,         т.е.      если   заданную операцию    приведения            типов выполнить нельзя,          результат     действия оператора   dynamic_cast         оценивается           как     нулевой,      если   в          этой операции     участвуют    указатели.   (Если же       попытка       выполнить  эту операцию    оказалась неудачной     при     участии         в          ней     ссылок, генерируется         исключение            типа   bad_cast.)

Рассмотрим     простой        пример.       Предположим,     что      класс Base   — полиморфный,     а          класс Derived         выведен       из        класса           Base.

Base        *bp,   b_ob; Derived  *dp,   d_ob;

bp       =          &d_ob;          //         Указатель    на       базовый       класс указывает    на            объект

класса           Derived.

dp            =          dynamic_cast<Derived    *>       (bp);   //         Приведение           к            указателю   на производный  класс разрешено.

if(dp)       cout    <<       "Приведение         типа   прошло        успешно!";

Здесь      приведение            указателя    bp       (на      базовый       класс)            к указателю   dp       (на      производный класс)      успешно       выполняется, поскольку   bp       действительно   указывает    на       объект          класса Derived. Поэтому       при     выполнении           этого  фрагмента   кода  будет выведено сообщение Приведение           типа прошло        успешно!.    Но       в          следующем фрагменте   кода  попытка       совершить операцию    приведения            типа   будет неудачной, поскольку   bp       в          действительности            указывает    на объект класса           Base,  и          неправомерно      приводить   указатель    на       базовый класс к          типу   указателя на          производный,       если   адресуемый           им объект          не       является       на       самом           деле  объектом производного класса.

bp            =          &b_ob;          /*        Указатель    на       базовый       класс ссылается    на            объект класса        Base.  */

dp            =          dynamic_cast<Derived    *>       (bp);   //         ошибка!

if(!dp)     cout    <<       "Приведение         типа   выполнить  не       удалось";

Поскольку        попытка       выполнить  операцию    приведения            типа   оказалась неудачной, при выполнении  этого  фрагмента   кода  будет выведено    сообщение Приведение            типа выполнить не      удалось.

В   следующей программе демонстрируются            различные  ситуации применения           оператора dynamic_cast.

//  Использование     оператора   dynamic_cast. #include <iostream> using   namespace   std;

class        Base   {

 public:

  virtual       void    f()        {           cout    <<       "В        классе           Base.\n";       }

  //    .           .           .

};

class        Derived         :           public Base   {

 public:

  void           f()        {           cout    <<       "В        классе           Derived.\n";            }

};

int    main() {

 Base    *bp,   b_ob;

 Derived         *dp,   d_ob;

 dp       =          dynamic_cast<Derived    *>       (&d_ob);

 if(dp)  {

  cout           <<       "Приведение         типов "          <<"(из           Derived         *          в            Derived         *) реализовано.\n";

  dp->f();

 }

 else                 cout    <<"Ошибка\n";

 cout    <<       endl;

 bp       =          dynamic_cast<Base           *>       (&d_ob);

 if(bp)  {

  cout           <<       "Приведение         типов "          <<"(из           Derived         *          в            Base   *) реализовано.\n";

  bp->f();

 }

 else                 cout    <<       "Ошибка\n";

 cout    <<       endl;

 bp       =          dynamic_cast<Base           *>       (&b_ob);

 if(bp)  {

  cout           <<       "Приведение         типов "          <<"(из           Base   *          в            Base   *) реализовано.\n";

  bp->f();

 }

 else                 cout    <<       "Ошибка\n";

 cout    <<       endl;

 dp       =          dynamic_cast<Derived    *>       (&b_ob);

 if(dp)  cout    <<"Ошибка\n";

 else

  cout           <<"Приведение    типов "          <<"(из           Base   *          в          Derived            *)        не реализовано.\n";

 cout    <<       endl;

 bp       =          &d_ob;          //         bp       указывает    на       объект          класса     Derived

 dp       =          dynamic_cast<Derived    *>       (bp);

 if(dp)  {

  cout           <<       "Приведение         bp       к          типу   Derived         *\n"   << "реализовано, поскольку        bp       действительно\n"            <<       "указывает  на объект          класса Derived.\n";

  dp->f();

 }

 else                 cout    <<       "Ошибка\n";

 cout    <<       endl;

 bp       =          &b_ob;          //         bр       указывает    на       объект          класса     Base

 dp       =          dynamic_cast<Derived    *>       (bp);

 if(dp)  cout    <<       "Ошибка";

 else     {

  cout           <<"Теперь   приведение            bp       к          типу   Derived         *\n" <<"не реализовано,        поскольку   bp\n" <<"в   действительности            указывает    на объект\n"    <<"класса    Base.\n";

 }

 cout    <<       endl;

 dp       =          &d_ob;          //         dp       указывает    на       объект          класса     Derived

 bp       =          dynamic_cast<Base           *>       (dp);

 if(bp)  {

  cout           <<"Приведение    dp       к          типу   Base   *          реализовано.\n";

  bp->f();

 }

 else                 cout    <<"Ошибка\n";

 return            0;

}

Программа      генерирует  такие результаты.

Приведение    типов (из      Derived*       в          Derived*)     реализовано.

В   классе           Derived.

Приведение    типов (из      Derived*       в          Base*)           реализовано.

В   классе           Derived.

Приведение    типов (из      Base* в          Base*)           реализовано.

В   классе           Base.

Приведение    типов (из      Base* в          Derived*)     не       реализовано.

Приведение    bр       к          типу   Derived*       реализовано,         поскольку   bр действительно      указывает    на       объект          класса           Derived.

В   классе           Derived.

Теперь   приведение            bр       к          типу   Derived*       не       реализовано,            поскольку bр         в          действительности            указывает    на       объект            класса           Base.

Приведение    dp       к          типу   Base   *          реализовано.

В   классе           Derived.

Оператор          dynamic_cast         можно          иногда          использовать         вместо оператора   typeid.           Например, предположим,      что      класс Base   — полиморфный       и          является       базовым      для     класса           Derived,        тогда при выполнении           следующего           фрагмента   кода  указателю   dp       будет присвоен адрес            объекта, адресуемого   указателем bp,      но       только          в          том случае,         если   этот    объект          действительно      является объектом          класса Derived.

Base        *bp;

Derived     *dp; //     .           .     . if(typeid(*bp)     == typeid(Derived))     dp       =     (Derived     *)        bp;

В   этом   случае           используется         обычная       операция     приведения типов.           Здесь это      вполне безопасно,          поскольку   инструкция if проверяет   законность  операции     приведения            типов с помощью оператора typeid            до       ее        реального   выполнения.          То       же       самое            можно сделать        более эффективно,         заменив       операторы  typeid            и инструкцию            if          оператором

dynamic_cast:

dp            =          dynamic_cast<Derived    *>       (bp);

Поскольку        оператор     dynamic_cast         успешно       выполняется          только в          том     случае,         если объект,          подвергаемый      операции     приведения к          типу,  уже    является       объектом     либо  заданного типа,   либо  типа, выведенного         из        заданного,  то        после завершения            этой   инструкции указатель dp          будет содержать   либо  нулевое        значение,    либо  указатель    на объект          типа   Derived.        Кроме того,            поскольку   оператор     dynamic_cast успешно       выполняется          только          в          том     случае,         если заданная операция     приведения            типов законна,       то        в          определенных      ситуациях ее        логику можно       упростить.   В          следующей программе показано,    как оператор     typeid            можно          заменить оператором   dynamic_cast.        Здесь выполняется          один  и          тот      же       набор            операций     дважды:      с использованием   сначала        оператора   typeid,           а          затем оператора dynamic_cast.

/* Использование     оператора   dynamic_cast          вместо          оператора   typeid.

*/

#include <iostream> #include <typeinfo> using     namespace   std;

class        Base   {

 public:

  virtual       void    f()        {}

};

class        Derived         :           public Base   {

 public:

  void           derivedOnly()          {

   cout      <<       "Это   объект          класса           Derived.\n";

  }

};

int    main() {

 Base    *bp,   b_ob;

 Derived         *dp,   d_ob;

 //------------------------------- //     Использование     оператора   typeid

 //--------------------------------

 bp       =          &b_ob;

 if(typeid(*bp)          ==       typeid(Derived))     {

  dp  =          (Derived        *)        bp;

  dp->derivedOnly();

 }

 else                 cout    <<"Операция         приведения            объекта        типа   Base   к            " <<"типу     Derived         не       выполнилась.\n";

 bp       =          &d_ob;

 if(typeid(*bp)          ==       typeid(Derived))     {

  dp  =          (Derived        *)        bp;

  dp->derivedOnly();

 }

 else                 cout    <<"Ошибка,            приведение            типа   должно        "     <<"быть реализовано!\n";

 //------------------------------------- //     Использование     оператора   dynamic_cast

 //--------------------------------------

 bp       =          &b_ob;

 dp       =          dynamic_cast<Derived    *>       (bp);

 if(dp)  dp->derivedOnly();

 else                 cout    <<       "Операция  приведения            объекта        типа   Base   к            "          <<" типу       Derived         не       выполнилась.\n";            bp       =          &d_ob;

 dp       =          dynamic_cast<Derived    *>       (bp);

 if(dp)  dp->derivedOnly();

 else                 cout    <<       "Ошибка,     приведение            типа   должно        "          <<            "быть реализовано!\n";

 return            0;

}

Как          видите,         использование      оператора   dynamic_cast         упрощает логику,         необходимую        для преобразования      указателя    на       базовый класс в          указатель    на       производный        класс.            Вот     как выглядят результаты выполнения           этой   программы.

Операция         приведения            объекта        типа   Base   к          типу   Derived         не выполнилась.

Это          объект          класса           Derived.        Операция    приведения            объекта            типа   Base к            типу   Derived         не       выполнилась.        Это     объект            класса           Derived.

И  еще.   Оператор     dynamic_cast         можно          также            использовать применительно    к          шаблонным классам.

Оператор      const_cast

Оператор       const_cast    переопределяет модификаторы   const  и/или volatile.

Оператор          const_cast    используется         для     явного          переопределения модификаторов    const  и/ или           volatile.         Новый           тип     должен совпадать    с          исходным,  за        исключением        его      атрибутов    const или volatile.         Чаще всего оператор     const_cast    используется         для     удаления признака постоянства    (атрибута     const).           Его      общий           формат         имеет следующий            вид.

const_cast<type>       (expr)

Здесь      элемент        type    задает           новый           тип     операции     приведения, а          элемент        expr    означает выражение,    которое        приводится             к новому         типу.

Использование          оператора   const_cast    демонстрируется в          следующей     программе.

//  Демонстрация       использования      оператора   const_cast. #include <iostream> using   namespace   std;

void         f           (const     int       *p) {

 int       *v;

 //         Переопределение           const-атрибута.

 v          =          const_cast<int        *>       (p);

 *v        =          100;   //         теперь           объект          можно          модифицировать

}

int    main() {

 int       x          =          99;

 cout    <<       "Значение   x          до       вызова          функции       f():      "          <<       x<<     endl;

 f           (&x);

 cout    <<"Значение          x          после вызова          функции       f():      "          <<       x<<     endl;  return        0;

}

Результаты      выполнения           этой   программы таковы.

Значение          х          до       вызова          функции       f():      99

Значение          х          после функции       f():      100

Как          видите,         переменная            x          была  модифицирована            функцией f(),      хотя    параметр, принимаемый         ею,     задается       как     const-указатель.

Необходимо    подчеркнуть,         что      использование      оператора   const_cast    для удаления     constатрибута        является       потенциально        опасным      средством. Поэтому       обращайтесь          с          ним    очень осторожно.

И  еще.   Удалять        const-атрибут         способен     только          оператор const_cast.  Другими      словами,      ни dynamic_cast,  ни       static_cast,  ни reinterpret_cast     нельзя          использовать         для     изменения  constатрибута объекта.

Оператор      static_cast

Оператор       static_cast    выполняет операцию   неполиморфного приведения     типов.

Оператор          static_cast    выполняет  операцию    неполиморфного приведения типов.           Его можно  использовать         для     любого         стандартного преобразования.  При    этом   во       время            работы программы        никаких проверок     на       допустимость        не       выполняется.         Оператор     static_cast имеет следующий           общий           формат         записи.

static_cast<type>        (expr)

Здесь      элемент        type    задает           новый           тип     операции     приведения, а          элемент        expr    означает выражение,    которое        приводится             к этому            новому         типу.

Оператор          static_cast,  по       сути,  является       заменителем         оригинального оператора приведения типов.           Он      лишь выполняет  неполиморфное преобразование.  Например,  при выполнении  следующей программы переменная типа   float   приводится             к          типу   int.

//  Использование     оператора   static_cast. #include <iostream> using   namespace   std;

int    main() {

 int       i;

 float    f;

 f           =          199.22F;

 i           =          static_cast<int>      (f);

 cout    <<       i;

 return            0;

}

Оператор      reinterpret_cast

Оператор       reinterpret_cast     выполняет фундаментальное         изменение     типа.

Оператор          reinterpret_cast     преобразует           один  тип     в          принципиально другой.         Например,  его можно  использовать         для     преобразования   указателя в          целое            значение      и          целого          значения      —        в указатель.            Его также            можно          использовать         для     приведения            наследственно несовместимых типов   указателей.             Этот    оператор     имеет            следующий общий           формат         записи.

reinterpret_cast<type>         (expr)

Здесь      элемент        type    задает           новый           тип     операции     приведения, а          элемент        expr    означает выражение,    которое        приводится             к этому            новому         типу.

Использование          оператора   reinterpret_cast     демонстрируется в     следующей программе.

//  Пример        использования      оператора   reinterpret_cast. #include <iostream> using   namespace   std;

int    main() {

 int       i;

 char    *p       =          "Это   короткая      строка.";

 i           =          reinterpret_cast<int>       (p);     //         Приводим   указатель    к            типу int.

 cout    <<       i;

 return            0;

}

         Здесь  оператор   reinterpret_cast   преобразует  указатель   p   в   целочисленное   значение.

Данное         преобразование   представляет         фундаментальное           изменение типа.

Сравнение      обычной      операции    приведения            типов           с   новыми       четырьмя  castоператорами

Кому-то из        читателей    могло            бы      показаться, что      описанные  выше четыре          cast-оператора полностью      заменяют    традиционную      операцию приведения            типов.           И         тогда у          них     может возникнуть           такой вопрос:         "Стоит       ли       всегда           вместо        обычной      операции приведения            типов использовать    более            новые           средства?". Дело  в          том,   что      общего         правила       для     всех программистов       не существует.             Поскольку   новые           операторы  были созданы       для повышения безопасности        довольно    рискованной          операции     приведения одного          типа   данных         к          другому, многие  С++-программисты убеждены,  что      их        следует        использовать         исключительно     с          этой целью.          И         здесь трудно          что-либо      возразить.   Другие          же программисты      считают,       что поскольку       традиционная       операция приведения            типов служила       им      "верой          и          правдой"     в течение многих          лет,    то        от        нее     не       стоит  так      легко отказываться. Например,  для     выполнения простых     и          относительно         безопасных операций     приведения            типов (как    те,       что      требуются   при вызове функций ввода-вывода       read() и          write(),          описанных  в          предыдущей          главе) "старое доброе" средство      вполне          приемлемо.

Существует       еще    одна  точка зрения,         с          которой        трудно          не согласиться:           при     выполнении операций  приведения            полиморфных типов определенно         стоит  использовать         оператор dynamic_cast.

Глава     20:  Пространства     имен      и      другие  темы

В   этой   главе описаны       пространства     имен  и          такие эффективные средства,     как     explicitконструкторы,      указатели  на       функции,     static-члены, const-функции-члены,   альтернативный синтаксис     инициализации     членов класса,          операторы  указания      на       члены,          ключевое    слово asm, спецификация       компоновки           и          функции       преобразования.

Пространства        имен

Пространство         имен  определяет           некоторую            декларативную     область.

Пространства  имен  мы      кратко           рассмотрели          в          главе 2.        Они позволяют  локализовать имена      идентификаторов,           чтобы            избежать конфликтных         ситуаций      с          ними.            В          С++-среде программирования используется         огромное    количество имен  переменных,          функций       и имен классов.       До       введения     пространств            имен  все      эти      имена конкурировали     за        память          в глобальном        пространстве         имен,            что и          было причиной    возникновения     многих          конфликтов. Например, если   бы      в          вашей           программе была  определена            функция       toupper(), она     могла            бы      (в зависимости      от        списка           параметров) переопределить   стандартную          библиотечную       функцию toupper(), поскольку   оба     имени           должны       были бы      храниться    в          глобальном пространстве имен.        Конфликты с          именами      возникали   также            при использовании     одной           программой нескольких          библиотек   сторонних производителей.  В          этом   случае           имя,   определенное       в          одной библиотеке,            конфликтовало     с          таким            же       именем        из        другой библиотеки.           Подобная    ситуация особенно         неприятна   при     использовании одноименных        классов.       Например,  если   в          вашей программе определен  класс VideoMode, и          в          библиотеке,           используемой       вашей программой, определен          класс с          таким            же       именем,       конфликта   не избежать.

Для          решения      описанной  проблемы   было создано        ключевое    слово namespace. Поскольку оно      локализует видимость  объявленных         в          нем имен,            это      значит,          что      пространство         имен позволяет    использовать одно  и          то        же       имя    в          различных  контекстах, не       вызывая       при этом конфликта    имен.            Возможно,  больше         всего от        нововведения "повезло"    С++-библиотеке стандартных            функций.     До       появления ключевого  слова namespace  вся      С++-библиотека    была определена            в глобальном            пространстве         имен  (которое      было,            конечно       же, единственным).    С наступлением    namespace-"эры" С++-библиотека    определяется в          собственном          пространстве имен,        именуемом            std,     которое значительно           понизило     вероятность            возникновения конфликтов   имен. В          своей программе программист         волен            создавать    собственные пространства          имен,            чтобы            локализовать         видимость  тех      имен, которые,      по       его      мнению,      могут стать причиной    конфликта. Это     особенно важно,          если   вы       занимаетесь           созданием  библиотек классов         или функций.

Понятие        пространства     имен

Ключевое         слово namespace  позволяет    разделить   глобальное пространство имен  путем создания    некоторой   декларативной     области.       По       сути, пространство         имен  определяет область       видимости. Общий          формат задания        пространства         имен  таков.

namespace     name {  //     объявления

}

Все,         что    определено            в          границах     инструкции           namespace, находится  в          области      видимости этого          пространства     имен.

В   следующей программе приведен     пример         использования      namespace-инструкции.            Она локализует    имена,          используемые       для     реализации простого      класса           счета  в          обратном направлении.           В          созданном здесь пространстве         имен  определяется        класс counter,        который реализует счетчик,        и          переменные           upperbound и          lowerbound,           содержащие значения      верхней        и нижней     границ,         применяемых        для     всех счетчиков.

namespace        CounterNameSpace          {

 int       upperbound;

 int       lowerbound;

 class    counter         {

   int          count;

  public:

   counter(int     n)        {

    if(n   <=       upperbound)           count =          n;

    else  count =          upperbound;

   }

   void      reset(int        n)        {

    if(n   <=       upperbound)           count =          n;

   }

   int          run()  {

    if(count       >          lowerbound)           return            count--;

    else  return            lowerbound;

   }

 };

}

Здесь      переменные           upperbound и          lowerbound,           а          также класс counter         являются     частью области     видимости, определенной пространством      имен  CounterNameSpace.

В   любом          пространстве         имен  к          идентификаторам,          которые       в нем    объявлены,            можно обращаться         напрямую,  т.е.      без     указания этого  пространства         имен.            Например,  в          функции run(),      которая находится    в          пространстве         имен  CounterNameSpace,        можно напрямую обращаться  к          переменной           lowerbound:

if(count  >          lowerbound)           return            count--;

Но            поскольку   инструкция namespace  определяет             область видимости, то        при     обращении к объектам,            объявленным        в пространстве         имен,            извне этого  пространства         необходимо использовать оператор     разрешения            области        видимости. Например,  чтобы присвоить значение       10       переменной           upperbound из        кода, который является       внешним     по       отношению             к пространству      имен CounterNameSpace,        нужно           использовать         такую            инструкцию.

CounterNameSpace::upperbound            =          10;

Чтобы     объявить      объект          типа   counter         вне     пространства         имен CounterNameSpace, используйте      инструкцию,           подобную   следующей.

CounterNameSpace::counter         ob;

В   общем          случае,         чтобы            получить      доступ          к          некоторому члену пространства         имен  извне этого пространства,        необходимо предварить             имя    этого  члена            именем        пространства         и разделить эти      имена           оператором            разрешения            области        видимости.

Рассмотрим     программу,            в          которой        демонстрируется использование пространства         имен CounterNameSpace.

//  Демонстрация       использования      пространства     имен. #include       <iostream> using   namespace   std;

namespace        CounterNameSpace          {

 int       upperbound;

 int       lowerbound;

 class    counter         {

   int          count;

  public:

   counter            (int      n)        {

    if(n   <=       upperbound)           count =          n;

    else  count =          upperbound;

   }

   void      reset   (int      n)        {

    if(n   <=       upperbound)           count =          n;

   }

   int          run()  {

    if(count       >          lowerbound)           return            count--;

    else  return            lowerbound;

   }

 };

}

int    main() {

 CounterNameSpace::upperbound       =          100;

 CounterNameSpace::lowerbound        =          0;

 CounterNameSpace::counter    ob1(10);

 int       i;

 do       {

  i      =          ob1.run();

  cout           <<       i           <<       "          ";

 }while(i          >          CounterNameSpace          ::          lowerbound);

 cout    <<       endl;

 CounterNameSpace::counter    ob2(20);

 do       {

  i      =          ob2.run();

  cout           <<       i           <<       "          ";

 }while(i          >          CounterNameSpace::lowerbound);

 cout    <<       endl;

 ob2.reset(100);

 CounterNameSpace::lowerbound        =          90;

 do       {

  i      =          ob2.run();

  cout           <<       i           <<       "          ";

 }while(i          >          CounterNameSpace::lowerbound);

 return            0;

}

Обратите           внимание    на       то,       что      при     объявлении            объекта класса           counter         и          обращении к переменным      upperbound и lowerbound используется         имя    пространства         имен CounterNameSpace.        Но после объявления            объекта        типа   counter         уже    нет      необходимости     в полной         квалификации       его      самого          или     его      членов.         Поскольку пространство         имен  однозначно определено,         функцию      run()  объекта        ob1 можно          вызывать     напрямую,  т.е.      без     указания      (в качестве  префикса) пространства         имен  (ob1.run()).

Программа      может           содержать   несколько   объявлений            пространств имен  с          одинаковыми именами.          Это     означает,     что      пространство имен  можно          разбить        на       несколько   файлов         или     на несколько частей           в          рамках          одного          файла.          Вот     пример.

namespace        NS       {

 int       i;

}

 //         .           .     . namespace     NS       {

 int       j;

}

Здесь      пространство         имен  NS       разделено   на       две     части.            Однако содержимое          каждой         части относится    к          одному        и          тому  же пространству         имен  NS.

Любое    пространство         имен  должно        быть  объявлено  вне     всех остальных   областей видимости.     Это     означает,     что      нельзя          объявлять пространства         имен,            которые       локализованы, например,       в          рамках функции.     При    этом   одно  пространство         имен  может           быть  вложено      в другое.

Инструкция using

Инструкция   using  делает        заданное     пространство     имен  "видимым",     т.е.    действующим.

Если        программа включает     множество  ссылок          на       члены некоторого пространства         имен, то       нетрудно     представить,          что необходимость     указывать    имя    этого  пространства         имен  при каждом           к ним    обращении,            очень            скоро утомит          вас.    Эту      проблему позволяет    решить инструкция         using, которая        применяется          в          двух форматах.

using       namespace   имя;

using       name::член;

В   первой          форме           элемент        имя    задает           название      пространства имен,            к          которому     вы       хотите получить   доступ.         Все     члены, определенные      внутри          заданного   пространства         имен,            попадают в "поле видимости",         т.е.      становятся   частью          текущего      пространства имен  и          их        можно          затем использовать        без     квалификации (уточнения  пространства         имен).           Во       второй          форме           делается "видимым" только          указанный  член   пространства         имен.            Например, предполагая,         что пространство имен  CounterNameSpace          определено            (как показано     выше),          следующие инструкции            using  и          присваивания будут вполне          законными.

using       CounterNameSpace::lowerbound;      /*        Видимым    стал    только     член lowerbound  */

lowerbound     =          10;      /*        Все     в          порядке,      поскольку   член            lowerbound находится   в          области        видимости. */

using       namespace   CounterNameSpace;         //         Все     члены           видимы.

upperbound     =          100;   //         Все     в          порядке,      поскольку   все      члены     видимы.

Использование          инструкции using  демонстрируется в          следующей программе (которая представляет  собой            новый           вариант        счетчика       из предыдущего        раздела).

//  Использование     инструкции using. #include <iostream> using   namespace     std;

namespace        CounterNameSpace          {

 int       upperbound;

 int       lowerbound;

 class    counter         {

   int          count;

  public:

   counter            (int      n)        {

    if(n   <=       upperbound)           count =          n;

    else  count =          upperbound;

   }

   void      reset(int        n)        {

    if(n   <=       upperbound)           count =          n;

   }

   int          run()  {

    if(count       >          lowerbound)           return            count--;

    else  return            lowerbound;

   }

 };

}

int    main() {

 /*        Используется         только          член   upperbound            из        пространства            имен CounterNameSpace.         */

 using  CounterNameSpace::upperbound;

 /*        Теперь          для     установки    значения      переменной           upperbound            не нужно     указывать    пространство         имен.            */

 upperbound            =          100;

 /*        Но       при     обращении к          переменной           lowerbound и          другим            объектам по-прежнему            необходимо           указывать    пространство            имен.            */

 CounterNameSpace::lowerbound        =          0;

 CounterNameSpace::counter    ob1(10);

 int       i;

 do       {

  i      =          ob1.run();

  cout           <<       i           <<       "          ";

 }while(i          >          CounterNameSpace::lowerbound);

 cout.   <<       endl;

 /*        Теперь          используем            все      пространство         имен     CounterNameSpace.

*/

 using  namespace   CounterNameSpace;

 counter         ob2(20);

 do       {

  i      =          ob2.run();

  cout           <<       i           <<       "          ";

 }while(i          >          lowerbound);

 cout    <<       endl;

 ob2.reset(100);

 lowerbound =          90;

 do       {

  i      =          ob2.run();

  cout           <<       i           <<       "          ";

 }while(i          >          lowerbound);

 return            0;

}

Эта           программа иллюстрирует        еще    один  важный        момент. Использование     одного пространства      имен  не       переопределяет   другое. Если   некоторое   пространство         имен становится  "видимым",           это      значит, что      оно     просто          добавляет   свои   имена           к          именам        других, уже действующих         пространств.          Поэтому       к          концу            этой   программы к глобальному пространству     имен  добавились            и          std,     и CounterNameSpace.

Неименованные     пространства     имен

Неименованное         пространство     имен  ограничивает      идентификаторы рамками     файла,          в котором они     объявлены.

Существует       неименованное    пространство         имен  специального        типа, которое        позволяет создавать      идентификаторы,             уникальные            для данного       файла.          Общий          формат         его      объявления выглядит   так.

namespace        {

 //         объявления

}

Неименованные        пространства         имен  позволяют  устанавливать уникальные идентификаторы,           которые       известны      только          в          области видимости  одного          файла.          Другими словами,           члены           файла, который       содержит     неименованное    пространство         имен,            можно использовать         напрямую,  без     уточняющего         префикса.    Но       вне     файла эти      идентификаторы неизвестны.

Как          упоминалось         выше в          этой   книге,            использование модификатора      типа   static  также позволяет  ограничить область        видимости глобального           пространства         имен  файлом,       в котором   он       объявлен. Например,  рассмотрим            следующие два     файла,          которые       являются частью          одной           и          той     же       программы.

Поскольку        переменная            k          определена            в          файле            One,   ее и          можно          использовать         в          файле            One. В           файле            Two переменная            k          определена            как     внешняя       (extern-переменная),      а это      значит,          что      ее имя          и          тип     известны,    но       сама  переменная k          в          действительности            не       определена.           Когда эти      два файла будут скомпонованы,     попытка       использовать         переменную          k          в файле            Two    приведет     к возникновению             ошибки,       поскольку   в          нем нет      определения         для     переменной           k.         Тот      факт,  что k  объявлена static-переменной           в          файле            One,   означает,     что      ее        область видимости ограничивается     этим   файлом,       и          поэтому       она     недоступна для файла            Two.

                                                                Несмотря                                                       на                                                        то                                                      что                                          использование                                              глобальных                                                                                                static-объявлений                             все                                                      еще                                              разрешено                                                                                                          в

C++,    для     локализации          идентификатора   в          рамках          одного          файла лучше           использовать неименованное           пространство         имен. Рассмотрим            пример.

Здесь      переменная            k          также            ограничена рамками      файла One.   Для    новых            программ рекомендуется        использовать         вместо модификатора      static  неименованное    пространство имен.

Обычно для     большинства          коротких      программ    и          программ    среднего размера       нет необходимости         в          создании     пространств            имен.            Но, формируя   библиотеки            многократно используемых   функций       или     классов, имеет            смысл           заключить   свой   код     (если хотите обеспечить           его максимальную     переносимость)    в          собственное           пространство         имен.

Пространство        имен std

Пространство         имен  std      используется       библиотекой        C++.

Стандарт           C++     определяет             всю    свою  библиотеку             в собственном          пространстве         имен, именуемом           std.     Именно        по этой   причине       большинство         программ    в          этой   книге включает следующую инструкцию:

using       namespace   std;

При         выполнении           этой   инструкции пространство         имен  std становится  текущим,     что открывает        прямой         доступ          к          именам функций       и          классов,       определенных      в          этой   библиотеке, т.е.   при обращении к          ним    отпадает      необходимость     в          использовании     префикса std::.

Конечно,           при     желании      можно          явным           образом квалифицировать            каждое         библиотечное имя          префиксом std::. Например,  следующая программа не       привносит   библиотеку             в глобальное пространство         имен.

//  Использование     явно   заданной     квалификации       std::. #include     <iostream>

int    main() {

 double           val;

 std::cout        <<       "Введите      число:           ";

  std::cin      >>       val;

 std::cout        <<       "Вы    ввели            число ";

 std::cout        <<       val;

 return            0;

}

Здесь      имена           cout    и          cin       явно   дополнены именами      своих пространств            имен.            Итак,  чтобы записать     данные         в          стандартный выходной    поток,           следует        использовать         не       просто          имя потока cout,  а          имя    с          префиксом std::cout,      а          чтобы            считать         данные из        стандартного         входного потока, нужно           применить  "префиксное"        имя std::cin.

Если        ваша  программа использует  стандартную          библиотеку             только в          ограниченных пределах,         то,       возможно,  ее        и          не       стоит  вносить в          глобальное пространство         имен.            Но       если ваша   программа содержит сотни ссылок          на       библиотечные       имена,          то        гораздо        проще сделать пространство    имен  std      текущим,     чем    полностью  квалифицировать каждое         имя    в отдельности.

Если        вы       используете           только          несколько   имен  из        стандартной библиотеки,           то,       вероятно, имеет   смысл           использовать         инструкцию using  для     каждого       из        них     в          отдельности. Преимущество  этого  подхода состоит         в          том,   что      эти      имена           можно          по-прежнему использовать без     префикса     std::,   не       внося при     этом   всю    библиотеку             стандартных функций       в глобальное         пространство         имен.            Рассмотрим            пример.

/* Внесение     в          глобальное пространство         имен  лишь нескольких имен.

*/

#include <iostream>

//  Получаем    доступ          к          именам        потоков        cout    и          cin.

using       std::cout; using       std::cin;

int            main()

{

 double           val;

 cout    <<       "Введите      число:           ";

  cin  >>       val;

 cout    <<       "Вы    ввели            число ";

 cout    <<       val;

 return            0;

}

Здесь      имена           потоков        cin       и          cout    можно          использовать напрямую,  но       остальная    часть пространства          имен  std      не       внесена        в область        видимости.

Как          упоминалось         выше,            исходная     библиотека             C++     была определена            в          глобальном пространстве        имен.            Если   вам    придется модернизировать            старые          С++-программы,  то        вы должны либо  включить в          них     инструкцию            using  namespace  std,     либо  дополнить  каждое обращение  к          члену библиотеки            префиксом std::.   Это     особенно     важно, если   вам    придется заменять          старые          заголовочные        *.h-файлы современными     заголовками.         Помните,     что      старые заголовочные    *.h-файлы           помещают   свое   содержимое          в          глобальное пространство имен,            а современные     заголовки    —        в          пространство         имен  std.

Указатели     на       функции

Указатель      на       функцию     ссылается  на       входную       точку           этой     функции.

Указатель         на       функцию— это      довольное  сложное,     но       очень мощное        средство      C++. Несмотря       на       то        что      функция       не       является переменной,          она,    тем     не       менее,          занимает физическую   область памяти,        некоторый  адрес            которой        можно          присвоить   указателю. Адрес, присваиваемый указателю,  является       входной       точкой          функции. (Именно       этот    адрес используется        при     вызове          функции.)    Если некоторый  указатель    ссылается    на       функцию,    то        ее (функцию)         можно вызвать        с          помощью    этого  указателя.

Указатели         на       функции       также            позволяют  передавать функции       в качестве       аргументов другим        функциям.  Адрес            функции       можно получить,    используя   имя    функции       без     круглых скобок     и          аргументов. (Этот  процесс        подобен       получению адреса          массива,      когда также используется          только          его      имя    без     индекса.)     Если   присвоить   адрес функции       указателю,  то        эту функцию          можно          вызвать        через указатель.   Например,  рассмотрим            следующую            программу. Она   содержит две     функции,     vline()            и          hline(),           которые       рисуют          на       экране вертикальные        и горизонтальные           линии            заданной     длины.

#include <iostream> using     namespace   std;

void         vline(int         i),        hline(int        i);

int    main() {

 void    (*p)(int          i);

 p          =          vline;  //         указатель    на       функцию      vline()

 (*p)(4);          //         вызов            функции       vline()

 p          =          hline;  //         указатель    на       функцию      hline()

 (*p)(3);          //         вызов            функции       hline()

 return     0; }

void         hline(int     i) {

 for(     ;           i;          i--)       cout    <<       "-";

 cout    <<       "\n";

}

void         vline(int         i)

{

 for(     ;           i;          i--)       cout    <<       "|\n";

}

Вот          как     выглядят     результаты выполнения           этой   программы.

I

I

I

I

-    -           -

Рассмотрим     эту      программу в          деталях.       В          первой          строке тела   функции       main()            объявляется переменная         р          как     указатель    на функцию,    которая        принимает  один  целочисленный    аргумент и  не возвращает             никакого      значения.    Это     объявление            не       определяет, какая функция       имеется        в виду.          Оно    лишь создает         указатель,   который можно          использовать         для     адресации   любой функции    этого  типа. Необходимость    круглых        скобок,         в          которые       заключен     указатель    , следует         из        С++-правил предшествования.

В   следующей строке           указателю   р          присваивается       адрес            функции vline().           Затем выполняется         вызов            функции       vline()            с аргументом            4.        После            этого  указателю   р          присваивается адрес функции       hline(),           и          с          помощью    этого  указателя    реализуется            ее вызов.

В   этой   программе при     вызове          функций       посредством          указателя используется         следующий формат:

(*p)         (4);

Однако  функцию,    адресуемую           указателем р,        можно          вызвать        с использованием  более простого     синтаксиса:

p   (4);

Единственная  причина,      по       которой        чаще  используется         первый вариант        вызова          функции, состоит в          том,   что      всем, кто      станет разбирать    вашу  программу,            станет            ясно,  что      здесь реализован вызов            функции       через указатель    р,        а          не       вызов            функции       с именем        р.        Во       всем остальном   эти      варианты     эквивалентны.

Несмотря          на       то        что      в          предыдущем         примере      указатель    на функцию      используется         только ради           иллюстрации,        зачастую      такое его применение           играет           очень            важную        роль. Указатель    на функцию позволяет    передавать ее        адрес            другой          функции.     В          качестве показательного примера         можно          привести      функцию      qsort()           из стандартной           С++-библиотеки.  Функция       qsort() —      это      функция       быстрой сортировки,            основанная             на       алгоритме   Quicksort,    который упорядочивает      содержимое          массива.      Вот     как     выглядит     ее        прототип.

            void            qsort(void          *          start,            size_t            length,            size_t            size,           int

(*compare)  (const            void    *,        const  void    *));

Функция          qsort()           —        это    функция       сортировки           из     стандартной       С++-библиотеки.

Прототип          функции       qsort()           "прописан" в          заголовке    <cstdlib>,      в     котором       также            определен

тип      size_t (как    тип     unsigned       int).    Чтобы           использовать         функцию qsort(),          необходимо           передать ей           указатель    на       начало          массива объектов,    который       вы       хотите           отсортировать       (параметр   start), длину этого  массива        (параметр   length),         размер         в          байтах           каждого элемента     (параметр   size)    и указатель на       функцию      сравнения   элементов массива        (параметр   *compare).

Функция            сравнения,  используемая        функцией    qsort(),          сопоставляя два     элемента     массива, должна возвратить  отрицательное      значение,    если   ее первый         аргумент      указывает    на значение,          которое        меньше        второго, нуль, если   эти      аргументы  равны,          и          положительное значение,       если первый         аргумент      указывает    на       значение,    которое        больше         второго.

Чтобы     понять,         как     можно          использовать         функцию      qsort(), рассмотрим            следующую программу.

#include <iostream>

#include <cstdlib> #include <cstring> using     namespace   std;

int            comp(const  void    *a,      const  void    *b);

int    main() {

 char    str[]    =          "Указатели  на       функции       дают  гибкость.";

 qsort(str,       strlen(str),    1,        comp);

 cout    <<       "Отсортированная           строка:          "          <<       str;

 return            0;

}

int            comp(const  void    *a,      const  void    *b)

{

 return            *          (char  *)        a          -           *          (char  *)        b;

}

Вот          как     выглядят     результаты выполнения           этой   программы.

Отсортированная      строка:          Уаааабгдезииииккклнностттуфцью

Эта           программа сортирует    строку           str       в          возрастающем      порядке. Поскольку   функции       qsort() передается           вся      необходимая         ей информация,         включая       указатель    на       функцию      сравнения,  ее можно использовать         для     сортировки данных         любого         типа.  Например, следующая программа сортирует    массив          целых            чисел.           Для    гарантии переносимости     при     определении         размера целочисленного         значения      в ней     используется         оператор     sizeof.

#include <iostream> #include <cstdlib> using     namespace   std;

int            comp(const  void    *a,      const  void    *b);

int    main() {

 int       num[] =          {10,    4,        3,        6,        5,        7,        8};

 int       i;

 qsort(num,   7,        sizeof(int),    comp);

 for(i=0;          i<7;     i++)

  cout           <<       num[i]           <<       '           ';

 return            0;

}

int            comp(const  void    *a,      const  void    *b) {

 return            *          (int      *)        a          -           *          (int      *)        b;

}

He            стану утверждать,            что      указатели    на       функции       не       так просты          для     понимания,            но практика            поможет      и          "с        ними найти          общий          язык".           В          отношении  указателей  на       функции необходимо           рассмотреть           еще    один  аспект,          связанный   с перегруженными             функциями.

Как        найти          адрес            перегруженной    функции

Получить          адрес            перегруженной     функции       немного       сложнее,      чем найти             адрес            обычной "одиночной"  функции.     Если   же       существует несколько   версий          перегруженной     функции,     то должен   существовать механизм,   который       бы      определял, адрес            какой именно         версии          мы получаем.   При    получении  адреса          перегруженной     функции       именно способ          объявления указателя   определяет,           адрес            какой ее        версии будет получен.      По       сути,  объявление            указателя    в этом           случае сравнивается         с          соответствующими          объявлениями      указателей перегруженных функций.        Функция,     объявление            которой        обнаружит совпадение,           и          будет той     функцией, адрес  которой        мы      получили.

В   следующем            примере      программы содержится            две     версии функции       space().         Первая версия      выводит       на       экран count пробелов,   а вторая—      count символов,   переданных           в          качестве аргумента        ch.      В функции       main()            объявляются          два     указателя    на       функции.     Первый задан как указатель        на       функцию      с          одним           целочисленным параметром,          а          второй          —        как     указатель    на функцию            с двумя           параметрами.

/* Использование     указателей  на       перегруженные    функции.

*/ #include     <iostream> using     namespace   std;

//  Вывод           на       экран count     пробелов. void      space(int       count) {

 for(     ;           count;            count--)         cout    <<       '           ';

}

//  Вывод           на       экран count символов,   переданных           в          ch.

void         space(int       count,            char     ch) {

 for(     ;           count;            count--)         cout    <<       ch;

}

int    main() {

 /*        Создание     указателя    на       void-функцию        с          одним           int-параметром.

*/

 void    (*fp1) (int);

 /*        Создание     указателя    на       void-функцию        с          одним           int-параметром           и одним       параметром           типа   char.  */

 void    (*fp2)(int,    char);

 fp1      =          space;            //         получаем    адрес            функции       space(int)

 fp2      =          space;            //         получаем    адрес            функции       space(int,     char)

 fp1(22);        //         Выводим     22       пробела       (этот   вызов            аналогичен            вызову (*    fp1)    (22))   .

 cout    <<       "|\n";

 fp2(30,          'х');     //         Выводим     30       символов    "х"      (этот   вызов аналогичен вызову          (*fp2) (30,    'x').

 cout    <<       "|\n";

 return            0;

}

Вот          как     выглядят     результаты выполнения           этой   программы.

                       I

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ  I

Как          отмечено     в          комментариях,      компилятор            способен определить,           адрес            какой перегруженной    функции       он       получает,     на основе          того,  как     объявлены  указатели    fp1      и          fp2.

Итак,       когда адрес            перегруженной     функции       присваивается указателю   на       функцию,    то именно   это      объявление            указателя    служит основой       для     определения         того,  адрес            какой функции      был    присвоен. При    этом   объявление            указателя    на       функцию      должно соответствовать одной           (и        только          одной)          из        перегруженных    функций.     В противном  случае           в программу          вносится      неоднозначность,            которая вызовет        ошибку         компиляции.

Статические           члены           класса

Один      статический        член   класса          разделяется         всеми     объектами            класса.

Ключевое         слово static  можно          применять  и          к          членам         класса. Объявляя    член   класса статическим,        мы      тем     самым          уведомляем компилятор            о          том,   что      независимо            от        того,  сколько объектов этого  класса           будет создано,      существует  только          одна  копия            этого static-члена. Другими    словами,      static-член   разделяется           всеми объектами  класса.          Все     статические            данные при            первом         создании объекта        инициализируются          нулевыми   значениями,           если   не представлено        других          значений     инициализации.

При         объявлении            статического          члена            данных         в          классе программист         не       должен        его определять.    Необходимо          обеспечить его глобальное определение         вне     этого  класса.          Это реализуется   путем повторного объявления            этой   статической            переменной           с          помощью оператора   разрешения            области        видимости, который       позволяет идентифицировать,         к          какому классу       она     принадлежит.        Только          в этом   случае           для     этой   статической            переменной           будет выделена память.

Рассмотрим     пример         использования      static-члена             класса.          Изучите код     этой   программы и постарайтесь      понять,         как     она     работает.

#include <iostream> using     namespace   std;

class        ShareVar      {   static       int       num;

 public:

  void           setnum(int   i)          {           num   =          i;          };

  void           shownum()  {           cout    <<       num   <<       "          ";         }

};

int            ShareVar::num;      //         определяем           static-член   num

int    main() {

 ShareVar       a,         b;

 a.shownum();          //         выводится   0

 b.shownum();         //         выводится   0

 a.setnum(10);         //         устанавливаем      static-член   num   равным        10

 a.shownum();          //         выводится   10

 b.shownum();         //         также            выводится   10

 return            0;

}

Обратите           внимание    на       то,       что      статический            целочисленный член   num   объявлен     и          в          классе ShareVar,  и          определен  в          качестве глобальной            переменной.          Как     было заявлено      выше, необходимость такого           двойного     объявления            вызвана       тем,    что      при     объявлении члена            num   в классе       ShareVar      память          для     него   не       выделяется. C++     инициализирует   переменную          num значением    0,        поскольку   никакой другой          инициализации     в          программе нет.    Поэтому       в результате двух   первых         вызовов       функции       shownum()  для     объектов      а          и          b отображается значение            0.        Затем            объект          а          устанавливает член   num   равным        10,      после чего   объекты       а          и          b снова         выводят на       экран его      значение      с          помощью    функции       shownum(). Но       так      как существует только          одна  копия            переменной           num,  разделяемая объектами  а          и          b,        значение      10       будет выведено   при     вызове функции       shownum()  для     обоих            объектов.

Узелок   на       память.        При    объявлении            члена            класса статическим       вы       обеспечиваете создание        только        одной            его копии,           которая      будет           совместно использоваться  всеми объектами этого           класса.

Если        static-переменная            является       открытой     (т.е.    public-переменной), к          ней     можно обращаться         напрямую   через имя    ее        класса,          без ссылки          на       какой-либо конкретный            объект. (Безусловно,      обращаться можно          также            и          через имя    объекта.)     Рассмотрим,          например,   эту версию         класса           ShareVar.

class        ShareVar      {  public:

  static         int       num;

  void           setnum(int   i)          {           num   =          i;          };

  void           shownum()  {           cout    <<       num   <<       "          ";         }

};

В   данной         версии          переменная            num   является       public-членом данных.        Это     позволяет    нам обращаться   к          ней     напрямую,  как     показано в          следующий            инструкции.

ShareVar::num            =          100;

Здесь      значение      переменной           num   устанавливается   независимо            от объекта,       а          для обращения     к          ней     достаточно использовать         имя класса           и          оператор     разрешения            области видимости.       Более того,  эта      инструкция законна        даже  до       создания     каких-либо объектов типа ShareVar!      Таким            образом,     получить      или     установить  значение static-члена             класса           можно          до того,        как     будут созданы       какие-либо  объекты.

И  хотя    вы,     возможно,  пока   не       почувствовали      необходимости     в static-членах           класса,          по мере        программирования        на       C++     вам придется      столкнуться            с          ситуациями,           когда они окажутся         весьма полезными,            позволив     избежать     применения           глобальных переменных.

Можно   также            объявить      статической            и          функцию-член,     но       это —        нераспространенная практика.         К          статической            функции-члену могут получить      доступ          только          другие          static-члены этого           класса. (Конечно      же,     статическая             функция-член        может           получать      доступ к нестатическим   глобальным           данным        и          функциям.) Статическая функция-член        не       имеет указателя   this.    Создание     виртуальных статических             функций-членов   не       разрешено.             Кроме того,            их нельзя          объявлять   с          модификаторами            const  или     volatile. Статическую           функцию-член можно   вызвать        для     объекта        ее        класса или     независимо            от        какого           бы      то        ни       было объекта,       а          для обращения  к          ней     достаточно использовать         имя    класса           и          оператор разрешения            области видимости.

Применение  к          функциям-членам         модификаторов             const и   mutable

Константная            (const-)         функция-член        не       может модифицировать           объект,       который     ее вызвал.

Функции-члены         класса           могут быть  объявлены  с          использованием модификатора      const. Это     означает,     что      с          указателем this     в          этом случае           необходимо           обращаться            как     с          constуказателем.  Другими словами,      const-функция       не       может           модифицировать объект,         для которого      она     вызвана.      Кроме           того,  const-объект           не       может вызвать        не       const-функцию-член.

Но       const-функцию-член       могут вызывать     как     const-,           так      и          не const-объекты.

Чтобы     определить            функцию      как     const-член   класса,          используйте формат,        представленный в          следующем            примере. class       X          {   int some_var;

 public:

  int  f1()     const; //         const-функция-член

};

Как          видите,         модификатор        const  располагается       после объявления списка           параметров функции.

Цель       объявления            функции       как     const-члена             —        не       допустить модификацию       объекта, который            ее        вызывает.    Например,  рассмотрим следующую            программу.

/* Демонстрация       использования      const-функций-членов.  Эта программа     не            скомпилируется.

*/ #include     <iostream> using     namespace   std;

class        Demo {

  int  i;

 public:

  int  geti()  const  {

   return  i;          //         все      в          порядке

  }

  void           seti      (int      x)         const  {           i           =          x;         //         ошибка!       }

};

int            main()

{

 Demo ob;

 ob.seti(1900);

 cout    <<       ob.geti();

 return            0;

}

Эта           программа не       скомпилируется,  поскольку   функция       seti() объявлена   как     const-член. Это     означает,     что      ей       не       разрешено модифицировать вызывающий         объект.         Ее        попытка изменить содержимое          переменной           i           приводит     к          возникновению    ошибки. В          отличие        от функции seti(), функция       geti()  не       пытается модифицировать переменную          i,          и          потому         она совершенно приемлема.

Возможны        ситуации,     когда нужно,          чтобы            const-функция       могла изменить     один  или несколько      членов          класса,          но       никак            не могла            повлиять      на       остальные.  Это     можно          реализовать с       помощью модификатора      mutable,       который       переопределяет   атрибут         функции const. Другими      словами,      mutable-член         может           быть  модифицирован const-функцией-членом. Рассмотрим        пример.

/* Демонстрация       использования      модификатора      mutable.

*/ #include     <iostream> using     namespace   std;

class        Demo {

  mutable    int       i;

  int  j;

 public:

  int  geti()  const  {

   return  i;          //         все      в          порядке

  }

  void           seti(int           x)         const  {

i          =         x;         //         теперь           все      в          порядке

  }

  /*   Следующая            функция       не       скомпилируется,

   void      setj     (int      х)         const  {

j         =         x;         //         Это     по-прежнему         неверно!

   }

  */

};

int    main() {

 Demo ob;

 ob.seti(1900);

 cout    <<       ob.geti();

 return            0;

}

Здесь      член   i           определен  с          использованием  модификатора      mutable, поэтому       его      можно изменить  с          помощью    функции       seti(). Однако переменная            j           не       является       mutable-членом, поэтому        функции setj()  не       разрешено  модифицировать его      значение.

Использование        explicit-конструкторов

Для         создания     "неконвертирующего"            конструктора     используйте     спецификатор     explicit.

В   C++     определено            ключевое    слово explicit,          которое        применяется для     обработки специальных           ситуаций,     когда используются        конструкторы определенных      типов.           Чтобы понять        назначение             спецификатора explicit,          рассмотрим            следующую            программу.

#include <iostream> using     namespace   std;

class        myclass         {

  int  a;

 public:

  myclass(int          x)         {           a          =          x;         }

  int  geta() {           return            a;         }

};

int    main() {

 myclass          ob(4);

 cout    <<       ob.geta();

 return            0;

}

Здесь      конструктор           класса           myclass         принимает  один  параметр. Обратите     внимание    на       то, как           объявлен     объект          ob       в          функции main().          Значение     4,        заданное     в          круглых        скобках        после имени ob,      представляет         собой            аргумент,    который       передается параметру   x конструктора myclass(),            а          параметр     x          в          свою  очередь используется         для     инициализации     члена            a.        Именно таким       способом мы      инициализируем  члены           класса           с          начала          этой   книги. Однако         существует и          альтернативный   вариант        инициализации. Например,  при     выполнении           следующей инструкции            член   a          также получит        значение      4.

             myclass        ob      =       4;       /*       Этот       формат         инициализации   автоматически

преобразуется       в          формат         myclass(4).   */

Как          отмечено     в          комментарии,       этот    формат         инициализации     автоматически      преобразуется

в          вызов            конструктора         класса           myclass,        а          число 4 используется         в          качестве       аргумента.  Другими словами,           предыдущая инструкция обрабатывается    компилятором      так,     как     если   бы      она     была записана:

myclass  ob(4);

В   общем          случае           всегда,          когда у          вас      есть    конструктор, который       принимает  только          один аргумент,     для     инициализации     объекта можно          использовать         любой           из        форматов:   либо  ob(х), либо  ob=х. Дело  в          том,   что      при     создании     конструктора         класса           с          одним аргументом            вами неявно          создается    преобразование   из        типа аргумента   в          тип     этого  класса.

Если        вам    не       нужно,          чтобы            такое неявное        преобразование имело           место,           можно предотвратить   его      с          помощью    спецификатора explicit.          Ключевое    слово explicit           применяется только      к          конструкторам. Конструктор,          определенный      с          помощью    спецификатора     explicit, будет задействован         только          в          том     случае,         если   для     инициализации членов          класса используется       обычный     синтаксис    конструктора.        Никаких автоматических    преобразований выполнено  не       будет.           Например,  объявляя конструктор           класса           myclass         с          использованием спецификатора explicit,          мы      тем     самым          отменяем    поддержку автоматического преобразования   типов.           В          этом   варианте      определения         класса функция       myclass()      объявляется как   explicit-конструктор.

#include <iostream> using     namespace   std;

class        myclass         {

  int  a;

 public:

  explicit      myclass(int   x)         {           a          =          x;         }

  int  geta() {           return            a;         }

};

Теперь   будут разрешены к          применению          только          конструкторы, заданные    в          таком формате.

myclass  ob(110);

Чем       интересно неявное       преобразование  конструктора

Автоматическое         преобразование   из        типа   аргумента   конструктора         в вызов            конструктора само         по       себе   имеет            интересные последствия.          Рассмотрим,          например,   следующий            код.

#include <iostream>

using       namespace   std;

class        myclass         {

  int  num;

 public:

  myclass(int          i)          {           num   =          i;          }

  int  getnum()      {           return            num;  }

};

int    main() {

 myclass          о(10);

 cout    <<       o.getnum()   <<       endl;   //         отображает             10

 /*        Теперь          используем            неявное        преобразование   для            присвоения нового         значения.    */

 о          =          1000;

 cout    <<       o.getnum()   <<       endl;   //         отображает             1000

 return            0;

}

Обратите           внимание    на       то,       что      новое            значение      присваивается объекту        о          с          помощью    такой инструкции:

о   =          1000;

Использование          данного       формата       возможно   благодаря   неявному преобразованию  из        типа int         в          тип     myclass,        которое        создается конструктором      myclass().     Конечно       же,     если   бы конструктор    myclass()      был объявлен     с          помощью    спецификатора     explicit,          то        предыдущая инструкция не       могла            бы      выполниться.

Синтаксис     инициализации    членов          класса

В   примерах    программ    из        предыдущих          глав   члены           данных получали     начальные  значения      в конструкторах    своих классов.       Например, следующая программа содержит     класс myclass, который включает     два     члена данных         numA и          numB.            Эти     члены           инициализируются          в конструкторе         myclass().

#include <iostream> using     namespace   std;

class        myclass     {   int     numA;

  int  numB;

 public:

  /*   Инициализируем члены           numA и          numB в          конструкторе myclass(),     используя   обычный     синтаксис.   */

  myclass(int          х,         int       у)        {           numA =          х;         numA =          y;}

  int  getNumA()   {           return     numA;           }   int       getNumB()   {     return            numB;            }

};

int    main() {

 myclass          ob1(7,           9),       ob2     (5,       2);

 cout    <<       "Значения   членов          данных         объекта        ob1     равны           "            << ob1.getNumB()             <<       "          и          "          <<       ob1.getNumA()      <<            endl;

 cout    <<       "Значения   членов          данных         объекта        ob2     равны           "            << ob2.getNumB()             <<       "          и          "          <<       ob2.getNumA()      <<            endl;

 return            0;

}

Результаты      выполнения           этой   программы таковы.

Значения          членов          данных         объекта        ob1     равны           9          и          7

Значения          членов          данных         объекта        ob2     равны           2          и          5

Присвоение     начальных  значений     членам         данных         numA и          numB в конструкторе,        как     это делается          в          конструкторе         myclass(),     — обычная       практика,     которая        применяется          для     многих классов.   Но       этот метод            годится         не       для     всех    случаев.       Например,  если   бы      члены numA и          numB были заданы         как     const-переменные,          т.е.      таким образом:

class        myclass         {

 const  int       numA;           //         const-член  const       int       numB;            //         const-член

};

то им      нельзя          было бы      присвоить   значения      с          помощью конструктора         класса           myclass, поскольку         const-переменные           должны быть  инициализированы         однократно,           после чего   им уже         нельзя придать        другие          значения.    Подобные   проблемы   возникают   при использовании ссылочных     членов,         которые       должны       быть инициализированы,       и          при     использовании     членов класса,      которые       не имеют           конструкторов       по       умолчанию.           Для    решения      проблем такого           рода в           C++     предусмотрена     поддержка альтернативного синтаксиса  инициализации     членов класса,      который       позволяет    присваивать им      начальные  значения      при     создании     объекта класса.

Синтаксис         инициализации     членов          класса           аналогичен тому, который используется         для вызова конструктора         базового      класса.          Вот     как выглядит     общий           формат         такой инициализации.

constructor(список_аргументов):

 член1(инициализатор),  член2(инициализатор),  //  ...

 членN            (инициализатор)

{

 //         тело   конструктора

}

Члены,   подлежащие          инициализации,   указываются          после конструктора класса,          и отделяются         от        имени           конструктора         и          списка           его аргументов двоеточием.           При    этом   в          одном и       том     же       списке можно          смешивать  обращения к          конструкторам      базового      класса           с инициализацией   членов.

Ниже      представлена        предыдущая          программа,            но       переделанная так,     чтобы            члены           numA и numB         были объявлены  с использованием  модификатора      const, и          получали     свои   начальные значения с          помощью    альтернативного  синтаксиса  инициализации     членов          класса.

#include <iostream> using     namespace   std;

class        myclass         {

  const         int       numA;           //         const-член   const          int       numB;            //     const-член

 public:

  /*   Инициализируем члены           numA и          numB с          использованием альтернативного  синтаксиса  инициализации.   */

  myclass(int          х,         int       у)        :           numA(x),      numB(y)        {           }

  int  getNumA()   {           return     numA;           }   int       getNumB()   {     return            numB;            }

};

int    main() {

 myclass          ob1     (7,       9),       ob2(5,           2);

 cout    <<       "Значения   членов          данных         объекта        ob1     равны           "            << ob1.getNumB()             <<       "          и          "          <<       ob1.getNumA()<< endl;

 cout    <<       "Значения   членов          данных         объекта        ob2     равны           "            << ob2.getNumB()             <<       "          и          "          <<       ob2.getNumA()<< endl;

 return            0;

}

Эта           программа генерирует  такие же       результаты,            как     и          ее предыдущая          версия.         Однако обратите внимание    на       то,       как инициализированы         члены           numA и          numB.

myclass(int        х,         int       у)        :           numA(x),      numB(у)        {           }

Здесь      член   numA инициализируется           значением, переданным          в аргументе   х,         а          член   numB —       значением, переданным          в аргументе   у.         И         хотя    члены           numA и          numB сейчас           определены как const-переменные,  они     могут получить      свои   начальные  значения      при создании     объекта класса     myclass,        поскольку   здесь используется альтернативный   синтаксис    инициализации членов            класса.

Использование        ключевого  слова            asm

С   помощью    ключевого  слова asm    в          С++-программу    встраивается      код, написанный           на языке      ассемблера.

Несмотря          на       то        что      C++     —        всеобъемлющий  и          мощный язык  программирования, возможны        ситуации,     обработка   которых       для него   оказывается           весьма          затруднительной. (Например,           в          C++     не предусмотрена     инструкция,            которая        могла            бы      запретить прерывания.) Чтобы      справиться  с          подобными            специальными ситуациями,           C++     предоставляет       средство, которое           позволяет    войти в код,   написанный           на       языке            ассемблера,           совершенно игнорируя   С ++-компилятор.            Этим  средством   и          является       инструкция asm,   используя   которую       можно встроить   ассемблерный      код непосредственно             в          С++-программу.   Этот    код     скомпилируется без каких-либо изменений  и          станет            частью          кода  вашей           программы, начиная        с          места нахождения          инструкции asm.

Общий   формат         использования      ключевого  слова asm    имеет     следующий            вид.

asm         ("код");

Здесь      элемент        код     означает      инструкцию,           написанную           на языке            ассемблера,           которая будет       встроена      в          программу.            При этом   некоторые  компиляторы        также            позволяют использовать          и другие          форматы      записи          инструкции asm.

asm         инструкция;

asm         инструкция newline

asm         {

 последовательность       инструкций

}

Здесь      элемент        инструкция           означает      любую          допустимую инструкцию            языка            ассемблера. Поскольку            использование инструкции asm    зависит         от        конкретной             реализации            среды программирования,       то        за        подробностями    обратитесь  к          документации, прилагаемой         к          вашему компилятору.

На            момент         написания   этой   книги в          среде            Visual C++ (Microsoft)   для     встраивания           кода, написанного          на       языке ассемблера,           предлагалось        использовать         инструкцию            _          _asm. Во всём остальном   этот    формат         аналогичен описанию    инструкции asm.

Осторожно!     Для    использования     инструкции           asm    необходимо обладать   доскональными знаниями      языка            ассемблера.           Если   вы       не считаете   себя   специалистом     по       этому          языку,           то лучше    пока избегать    использования     инструкции           asm,   поскольку   неосторожное     ее применение           может         вызвать      тяжелые    последствия         для     вашей системы.

Спецификация        компоновки

Спецификатор         компоновки           позволяет  определить           способ     компоновки           функции.

В   C++     можно          определить,           как     функция       связывается            с вашей           программой.         По       умолчанию функции      компонуются         как     С++-функции.      Но,     используя   спецификацию      компоновки,          можно обеспечить компоновку           функций,     написанных            на       других          языках программирования.       Общий формат     спецификатора     компоновки           выглядит так:

extern     "язык"          прототип_функции

Здесь      элемент        язык  означает      нужный        язык  программирования.       Все С++-компиляторы поддерживают   как     С-,       так      и          С++-компоновку. Некоторые  компиляторы        также            позволяют использовать          спецификаторы компоновки           для     таких языков,        как     Fortran,        Pascal            или     BASIC. (Эту    информацию         необходимо           уточнить      в          документации, прилагаемой         к          вашему компилятору.)

Следующая     программа позволяет    скомпоновать        функцию      myCfunc()    как     С-функцию.

#include <iostream> using     namespace   std;

extern     "C"      void    myCfunc();

int    main() {

 myCfunc();

 return            0;

}

//  Эта      функция       будет скомпонована       как     С-функция.

void         myCfunc()    {

 cout    <<       "Эта    функция       скомпонована       как     С-функция.\n";

}

На            заметку.      Ключевое    слово extern            —        необходимая составляющая    спецификации компоновки.   Более            того, спецификация компоновки           должна        быть глобальной;           ее        нельзя использовать    в теле какой-либо функции.

Используя        следующий            формат         спецификации       компоновки, можно          задать           не       одну, а          сразу несколько   функций. extern    "язык" {

 прототипы_функций

}

Спецификации            компоновки           используются        довольно    редко,           и вам,   возможно,  никогда        не придется            их        применять. Основное    их назначение             —        позволить   применение           в          С++программах    кода, написанного          сторонними            организациями     на       языках,         отличных     от C++.

Операторы  указания     на       члены           ".*"    и          "->*"

Операторы     указания     на       член   позволяют получить    доступ         к члену класса          через указатель на        этот            член.

В   C++     предусмотрена     возможность         сгенерировать       указатель специального        типа,  который "ссылается"   не       на       конкретный экземпляр   члена            в          объекте,       а          на       член   класса           вообще. Указатель    такого           типа   называется указателем           на       член   класса (pointer-to-member).       Это     —        не обычный           С++-указатель.      Этот специальный         указатель    обеспечивает         только          соответствующее смещение    в          объекте,       которое        позволяет    обнаружить            нужный член   класса.          Поскольку указатели     на       члены           —        не       настоящие указатели,   к          ним    нельзя          применять  операторы  "."       и          ">".    Для получения   доступа        к          члену класса           через указатель    на       член необходимо           использовать специальные    операторы  ".*"    и          "->*".

Если        идея, изложенная            в          предыдущем         абзаце,         вам показалась немного       "туманной",         то следующий       пример         поможет      ее прояснить.  При    выполнении           этой   программы отображается сумма     чисел от 1          до       7.        Здесь доступ          к          членам         класса           myclass         (функции sum_it()        и переменной       sum)   реализуется            путем            использования указателей  на       члены.

//  Пример        использования      указателей  на       члены           класса.

#include <iostream>

using       namespace   std;

class        myclass         {  public:

  int  sum;

  void           myclass::sum_it(int           x);

};

void         myclass::sum_it(int           x)         {

 int       i;

 sum    =          0;

 for(i=x;          i;          i--)       sum    +=       i;

}

int    main() {

 int       myclass::*dp;          //         указатель    на       int-член        класса

 void    (myclass::*fp)(int   x);       //         указатель    на       функцию-член

 myclass          с;

 dp       =          &myclass::sum;      //         получаем    адрес            члена            данных

 fp        =          &myclass::sum_it; //         получаем    адрес            функции-члена

 (c.*fp)(7);     //         вычисляем  сумму           чисел от        1          до       7

 cout    <<       "Сумма         чисел от        1          до       7          равна            "          <<     с.*dp;

 return            0;

}

Результат         выполнения           этой   программы таков.

Сумма    чисел от        1          до       7          равна            28

В   функции       main()            создается    два     члена-указателя:  dp       (для   указания на       переменную          sum)   и fp    (для   указания      на       функцию      sum_it()). Обратите     внимание    на       синтаксис    каждого объявления.    Для    уточнения класса           используется         оператор     разрешения            контекста     (оператор разрешения            области        видимости).           Программа также            создает объект          типа   myclass         с          именем        с.

Затем      программа получает      адреса          переменной           sum    и          функции sum_it()        и          присваивает           их указателям       dp       и          fp соответственно.    Как     упоминалось         выше,            эти      адреса          в действительности представляют      собой            лишь смещения    в          объекте типа   myclass,        по       которым      можно          найти переменную         sum    и функцию      sum_it().       Затем            программа использует  указатель    на       функцию fp, чтобы      вызвать        функцию      sum_it()        для     объекта        с.         Наличие дополнительных  круглых        скобок объясняется        необходимостью корректно применить  оператор     ".*".   Наконец,      программа отображает             значение суммы          чисел,           получая        доступ          к          переменной           sum    объекта с          через указатель    dp.

При         доступе        к          члену объекта        с          помощью    объекта        или ссылки          на       него   необходимо использовать      оператор     ".*".   Но       если   для этого  используется         указатель    на       объект,         нужно использовать       оператор "->*", как     показано     в          этой   версии          предыдущей          программы.

#include <iostream> using     namespace   std;

class        myclass         {

 public:

  int  sum;

  void           myclass::sum_it(int           x);

};

void         myclass::sum_it(int           x)         {

 int       i;

 sum    =          0;

 for(i=x;          i;          i--)       sum    +=       i;

}

int    main() {

 int       myclass::*dp;          //         указатель    на       int-член        класса

 void    (myclass::*fp)(int   x);       //         указатель    на       функцию-член

 myclass          *c,      d;        //         член   с          сейчас           --         указатель     на       объект  с  =          &d;     //         присваиваем          указателю   с     адрес            объекта

 dp       =          &myclass::sum;      //         получаем    адрес            члена            данных     sum

 fp        =          &myclass::sum_it; //         получаем    адрес            функции       sum_it()

 (c->*fp)         (7);     //         Теперь          используем            оператор     для     вызова            функции sum_it().

 cout    <<       "Сумма         чисел от        1          до       7          равна            "          <<       c->*dp;      //         ->*

 return            0;

}

В   этой   версии          переменная            с          объявляется           как     указатель    на объект          типа   myclass,        а          для доступа            к          члену данных         sum    и функции-члену     sum_it()        используется         оператор     "->*".

Помните,          что      операторы  указания      на       члены           класса предназначены    для     специальных случаев,   и          их        не       стоит  использовать для     решения      обычных      повседневных       задач программирования.

Создание        функций      преобразования

Функция           преобразования  автоматически  преобразует         тип   класса     в          другой          тип.

Иногда   возникает    необходимость     в          одном           выражении объединить созданный программистом    класс с          данными     других          типов.           Несмотря на       то        что      перегруженные операторные            функции       могут обеспечить использование      смешанных             типов данных,        в некоторых           случаях         все же       можно          обойтись      простым      преобразованием            типов.           И тогда,            чтобы преобразовать     класс в          тип,    совместимый         с          типом остальной   части выражения,            можно использовать     функцию преобразования   типа.  Общий          формат         функции       преобразования   типа имеет            следующий            вид.

operator            type() {return          value;}

Здесь      элемент        type    —        новый           тип,    который       является       целью нашего         преобразования,  а элемент    value—         значение      после преобразования.  Функция       преобразования   должна        быть членом          класса, для     которого      она     определяется.

Чтобы     проиллюстрировать       создание     функции       преобразования, воспользуемся      классом three_d   еще    раз.    Предположим,     что      нам    нужно иметь            средство      преобразования   объекта        типа three_d          в целочисленное     значение,    которое        можно          использовать         в целочисленном выражении.  Более            того,  такое преобразование   должно происходить          с          использованием произведения        значений     трех координат. Для    реализации            этого  мы      будем           использовать функцию преобразования,  которая        выглядит     следующим            образом,

operator            int()    {           return            х          *          у          *          z;         }

Теперь   рассмотрим            программу,            которая        иллюстрирует        работу     функции       преобразования.

#include <iostream> using     namespace   std;

class        three_d         {

  int  x,         y,         z;         //         3-мерные    координаты

 public:

  three_d(int          a,         int       b,        int       с)         {           x          =          a;         у          =     b;        z          =          c;         }

  three_d    operator+(three_d            op2);

  friend        ostream        &operator<<(ostream      &stream,      three_d         &obj);

  operator  int()    {return          x          *          у          *          z;         }

};

/* Отображение         координат   X,        Y,         Z          -           функция       вывода            данных         для класса  three_d.

*/ ostream        &operator<<(ostream      &stream,      three_d     &obj) {

 stream           <<       obj.x   <<       ",         ";

 stream           <<       obj.у  <<       ",         ";

 stream           <<       obj.z   <<       "\n";

 return            stream;

}

three_d  three_d::operator+(three_d      op2) {

 three_d         temp(0,         0,        0);

 temp.x           =          x+op2.x;

 temp.у           =          y+op2.y;

 temp.z           =          z+op2.z;

 return            temp;

}

int            main()

{

 three_d         a(1,     2,        3),       b(2,    3,        4);

 cout    <<       a          <<       b;

 cout    <<       b+100;          /*        Отображает            число 124,   поскольку   здесь выполняется          преобразование   объекта        класса           в          значение      типа   int.            */

 cout    <<       "\n";

 а          =          а          +          b;        //         Сложение    двух   объектов      класса            three_d выполняется     без     преобразования   типа.

 cout    <<       а;         //         Отображает            координаты           3,        5,        7

 return            0;

}

Эта          программа генерирует  такие результаты.

1, 2,        3

2, 3,        4

124

3, 5,        7

Как          подтверждают      результаты выполнения           этой   программы, если   в          таком            выражении целочисленного  типа,  как     cout<<b+100, используется         объект          типа   three_d,        к          этому            объекту применяется функция       преобразования.  В          данном        случае           функция преобразования возвращает значение      24,      которое        затем участвует     в операции     сложения    с          числом         100.   Но когда      в          преобразовании   нет необходимости,   как     при     вычислении            выражения а=а+b, функция преобразования   не       вызывается.

Если        функция       преобразования   создана,       то        она     будет вызываться везде,           где     требуется преобразование,    включая       ситуации,     когда объект передается функции       в          качестве       аргумента. Например,   если   объект класса           three_d         передать      стандартной           функции       abs(), также будет вызвана       функция,      выполняющая       преобразование   объекта        типа three_d         в          значение      типа   int, поскольку        функция       abs()  должна принимать  аргумент      целочисленного   типа.

Узелок   на       память.        Для    различных  ситуаций    можно          создавать различные  функции преобразования.      Например,  можно          определить функции,     которые     преобразуют       объекты типа     three_d         в          значения типа double           или     long,  при     этом созданные  функции      будут применяться автоматически. Функции      преобразования  позволяют интегрировать  новые типы классов,       создаваемые         программистом,            в          С++-среду программирования.

Глава     21:  Введение     в      стандартную       библиотеку шаблонов

Материал         этой   главы составляет   то,       что      многие         считают        самым     важным       добавлением         в

C++     за        последние  годы. Речь   идет   о          стандартной           библиотеке шаблонов    (Standard     Template Library   —        STL).   Именно        включение библиотеки            STL      в          C++     было основным   событием, которое обсуждалось         в          период         стандартизации    C++.   Библиотека             STL предоставляет шаблонные     классы          и          функции       общего         назначения, которые       реализуют   многие         популярные           и часто          используемые алгоритмы  и          структуры    данных.        Например,  она     включает     поддержку векторов,     списков,       очередей     и          стеков,          а          также            определяет различные  функции, обеспечивающие     к          ним    доступ.         Поскольку библиотека             STL      состоит         из        шаблонных классов, алгоритмы        и структуры    данных         могут быть  применены             к          данным        практически любого         типа.

Библиотека    STL      —        это    набор            шаблонных            классов        и     функций      общего         назначения.

Библиотека      STL— это      результат     разработки программного      обеспечения, который       вобрал в      себя   одни  из        самых           сложных      средств         языка C++.   Чтобы           понимать     содержимое          библиотеки STL    и          уметь им пользоваться,        необходимо           освоить        весь   материал,    изложенный          в предыдущих          главах.          Особенно    хорошо         нужно           ориентироваться  в шаблонах.   Откровенно говоря,       шаблонный            синтаксис,   который описывает   STL,    может           поначалу     испугать,      хотя    он выглядит           сложнее, чем    есть    на       самом           деле. Несмотря     на       то        что      материал     этой главы не труднее  остального  в          этой   книге,            не       следует        огорчаться, если   что-то            на       первый         взгляд           вам покажется      непонятным. Немного      терпения      при     рассмотрении        примеров    —        и          вскоре          вы поймете,      что      за        непривычным       синтаксисом           скрывается строгая простота      STL.

STL           —        довольно    большая      библиотека,           и          все      ее        средства невозможно           изложить     в          одной главе.          Полное         описание библиотеки            STL      со        всеми            ее        нюансами   и          методами программирования         заняло          бы      целую           книгу.            Цель представленного здесь обзора          — познакомить    вас      с          основными операциями,          принципами           проектирования   и          основами STLпрограммирования.             Освоив         материал     этой   главы,           вы       сможете легко изучить         остальную   часть библиотеки             STL      самостоятельно.

В   этой   главе также            описан          еще    один  важный        класс C++     — класс string.            Он      предназначен для          определения         строкового  типа данных,        который       позволяет    обрабатывать        символьные строки        во многом        подобно      тому, как     обрабатываются   данные         других          типов: с          помощью операторов.  Класс string тесно связан           с          библиотекой          STL.

Обзор   STL

Несмотря          на       большой      размер         стандартной           библиотеки шаблонов    и          порой            пугающий синтаксис,     в          действительности            ее средства      довольно    легко использовать,        если   понять,         как она построена   и          из        каких элементов   состоит.        Поэтому,      прежде         чем переходить к рассмотрению   примеров,   познакомимся      с          основными составляющими   STL.

Ядро       стандартной           библиотеки            шаблонов    включает     три     основных элемента:    контейнеры, алгоритмы       и          итераторы.         Они    работают совместно   один  с          другим,        предоставляя        тем     самым готовые     решения различных  задач программирования.

Контейнеры   —        это    объекты,    которые     содержат  другие          объекты.

Контейнеры   —        это      объекты,      содержащие          другие          объекты. Существует несколько различных    типов контейнеров.         Например,  класс vector определяет             динамический       массив, класс        queue            создает двустороннюю      очередь,      а          класс list       обеспечивает         работу          с линейным списком.       Эти     контейнеры            называются            последовательными контейнерами     и          являются базовыми       в          STL.    Помимо       базовых, библиотека             STL      определяет             ассоциативные   контейнеры, которые позволяют  эффективно            находить      нужные        значения      на       основе заданных     ключевых значений        (ключей).     Например,  класс map   обеспечивает хранение     пар     "ключ-значение"  и предоставляет   возможность         находить значение      по       заданному  уникальному         ключу.

Каждый контейнерный       класс определяет             набор            функций,     которые можно          применять  к данному   контейнеру.            Например,  контейнер   списка включает     функции,     предназначенные            для выполнения   вставки,        удаления и          объединения         элементов.  А         стек    включает     функции, которые позволяют  помещать    значения      в          стек    и          извлекать    их        из        стека.

Алгоритмы    обрабатывают  содержимое          контейнеров.

Алгоритмы       обрабатывают       содержимое          контейнеров.         Их возможности         включают средства         инициализации,   сортировки,            поиска и          преобразования   содержимого        контейнеров.

Многие         алгоритмы  работают     с          заданным   диапазоном           элементов контейнера.

Итераторы   подобны      указателям.

Итераторы   —        это      объекты,      которые       в          той     или     иной  степени действуют   подобно указателям.     Они    позволяют  циклически             опрашивать содержимое          контейнера             практически так   же,     как     это      делается      с помощью    указателя    при     циклическом         опросе          элементов   массива. Существует  пять   типов итераторов.

В   общем          случае           итератор,     который       имеет            большие возможности         доступа,       можно использовать     вместо          итератора    с меньшими  возможностями.  Например,  однонаправленным итератором      можно заменить     входной       итератор.

Итераторы        обрабатываются   аналогично указателям.            Их       можно инкрементировать          и декрементировать.     К          ним    можно          применять оператор     разыменования    адреса          *.        Итераторы объявляются          с помощью    типа   iterator,        определяемого    различными           контейнерами.

Библиотека      STL      поддерживает       реверсивные          итераторы,         которые являются     либо двунаправленными,       либо  итераторами          произвольного доступа,       позволяя     перемещаться       по последовательности            в          обратном направлении.        Следовательно,    если   реверсивный         итератор указывает        на конец            последовательности,     то        после инкрементирования       он       будет указывать    на элемент,            расположенный   перед            концом последовательности.

При         ссылке          на       различные  типы  итераторов в          описаниях   шаблонов в          этой   книге будут использованы       следующие термины.

STL           опирается    не       только          на       контейнеры,           алгоритмы  и итераторы,  но       и          на       другие стандартные       компоненты.          Основными из        них     являются     распределители   памяти,        предикаты и          функции сравнения.

Распределитель      памяти       управляет выделением           памяти       для     контейнера.

Каждый контейнер   имеет            свой   распределитель памяти       (allocator). Распределители управляют   выделением          памяти          для     контейнера. Стандартный          распределитель   —        это      объект класса        allocator,      но       при необходимости     (в        специализированных     приложениях)       можно определять собственные          распределители.  В          большинстве          случаев        стандартного распределителя    вполне          достаточно.

Предикат        возвращает          в          качестве     результата          значение     ИСТИНА/ЛОЖЬ.

Некоторые       алгоритмы  и          контейнеры            используют            специальный тип     функции, называемый  предикатом          (predicate).  Существует два     варианта предикатов:           унарный      и бинарный.           Унарный      предикат     принимает один  аргумент,    а          бинарный    —        два.    Эти     функции возвращают    значения ИСТИНА/ЛОЖЬ,    но       точные          условия,       которые       заставят        их        вернуть истинное      или     ложное         значение,    определяются       программистом.  В остальной   части этой главы, когда потребуется            унарная        функция-предикат, на       это      будет указано        с          помощью    типа UnPred.          При    необходимости использования      бинарного   предиката   будет применяться          тип BinPred.            В бинарном    предикате   аргументы  всегда           расположены        в          порядке первый,        второй относительно   функции,     которая        вызывает     этот    предикат. Как     для     унарного,    так      и          для бинарного      предикатов             аргументы должны       содержать   значения,    тип     которых       совпадает    с типом        объектов, хранимых    данным        контейнером.

Функции           сравнения   сравнивают          два     элемента     последовательности.

Некоторые       алгоритмы  и          классы          используют            специальный         тип бинарного   предиката, который       сравнивает  два     элемента.    Функции сравнения   возвращают           значение      true,   если   их первый   аргумент      меньше второго.       Функции      сравнения   идентифицируются         с          помощью    типа Comp.

Помимо            заголовков,            требуемых  различными           классами     STL, стандартная           библиотека             C++ включает         заголовки    <utility>        и <functional>,          которые       обеспечивают        поддержку STL. Например,     в заголовке    <utility>        определяется        шаблонный            класс pair,   который может хранить      пару   значений.    Мы     будем           использовать         класс pair ниже  в          этой   главе.

Шаблоны          в          заголовке    <functional>            позволяют  создавать    объекты, которые       определяют функцию   operator().   Эти     объекты       называются объектами-функциями,             и          их        во       многих случаях     можно использовать         вместо          указателей  на       функции.     Существует несколько встроенных объектов-функций,         объявленных         в          заголовке    <functional>. Приведем   здесь некоторые  из        них.

Возможно,       наиболее     широко        используется         объект-функция    less, который       определяет, при  каких условиях      один  объект          меньше        другого. Объекты-функции           можно          использовать вместо     реальных     указателей  на функции       в          алгоритмах STL,    о          которых       пойдет          речь   ниже. Используя   объекты-функции            вместо          указателей  на       функции, библиотека             STL      в          некоторых случаях         генерирует  более эффективный         программный       код.

Материал         этой   главы не       предусматривает использования      объектов-функций,      и          мы      не будем     применять  их        напрямую.  (Подробное описание     объектов-функций           можно          найти             в моей          книге Полный справочник по       C++,   4-е      издание,      М.       :           Издательский        дом "Вильямс".)

Контейнерные        классы

Как          упоминалось         выше,            контейнеры            представляют        собой объекты       STL,    которые предназначены          для     хранения     данных. Контейнеры,          определяемые      в          STL,    представлены       в табл.          21.1.  В ней     также            указаны       заголовки,  которые       необходимо           включать     в программу при использовании         каждого       контейнера.            Но       несмотря     на то,       что      класс string также            является контейнером, позволяющим       хранить и          обрабатывать        символьные           строки,         он       в          эту      таблицу не включен       и          рассматривается  ниже  в          этой   главе.

Поскольку        имена           типов в          объявлениях          шаблонных классов произвольны,        контейнерные классы   объявляют  typedef-версии      этих    типов, что      конкретизирует    имена           типов.           Некоторые из        наиболее популярных           typedef-имен         приведены ниже.

Поскольку        в          одной           главе невозможно           рассмотреть           все контейнеры,           в          следующих разделах     мы      представим            только          три из        них:    vector,           list       и          map.  Если   вы       поймете,      как работают         эти три     контейнера,            у          вас      не       будет проблем      с          использованием остальных.

Векторы

Векторы          представляют    собой            динамические       массивы.

Одним   из        контейнеров          самого          широкого    назначения             является вектор.         Класс vector поддерживает     динамический       массив,         который       при необходимости     может           увеличивать           свой размер.         Как     вы       знаете, в          C++     размер         массива        фиксируется           во       время            компиляции. И         хотя    это самый   эффективный         способ          реализации            массивов,    он в          то        же       время            является       и          самым ограничивающим, поскольку   размер         массива        нельзя          изменять     во       время выполнения программы.        Эта      проблема    решается     с          помощью    вектора, который       по       мере  необходимости обеспечивает           выделение дополнительного            объема         памяти.        Несмотря     на       то        что      вектор —        это динамический           массив,         тем     не       менее,          для     доступа        к его      элементам  можно          использовать стандартное      обозначение индексации            массивов.

Вот          как     выглядит     шаблонная спецификация       для     класса           vector:

template       <class Т,         class   Allocator       =          allocator<T>            >          class   vector

Здесь      T—      тип     сохраняемых         данных,        а          элемент        Allocator означает      распределитель   памяти, который  по       умолчанию использует стандартный          распределитель.  Класс vector            имеет следующие конструкторы.

explicit    vector(const            Allocator       &a       =          Allocator());

                explicit                 vector(size_type              num,        const        T       &val       =         T(),              const

Allocator       &a       =          Allocator());

vector(const     vector<T,      Allocator>    &ob);

template            <class InIter>           vector(InIter            start,  InIter  end,    const Allocator            &a       =          Allocator());

Первая   форма           конструктора         предназначена     для     создания     пустого вектора.       Вторая          создает вектор,    который       содержит     num   элементов   со значением  val,     причем         значение      val      может быть            установлено           по умолчанию.           Третья           форма           позволяет    создать         вектор,         который содержит те           же       элементы,   что      и          заданный    вектор           ob.      Четвертая предназначена     для     создания     вектора, который            содержит     элементы    в диапазоне, заданном    параметрами-итераторами     start   и          end.

Ради       достижения            максимальной      гибкости      и          переносимости любой           объект,         который предназначен для     хранения     в          векторе, должен        определяться        конструктором      по       умолчанию. Кроме         того,  он должен        определять             операции     "<"      и          "=="   Некоторые  компиляторы могут потребовать          определения         и          других          операторов сравнения.  (В        виду  существования      различных реализаций для     получения точной          информации          о          требованиях,          предъявляемых    вашим компилятором,     следует        обратиться  к          прилагаемой         к          нему документации.)    Все     встроенные типы автоматически      удовлетворяют     этим требованиям.

Несмотря          на       то        что      приведенный        выше синтаксис    шаблона выглядит     довольно "массивно",   в          объявлении            вектора        нет      ничего сложного.   Рассмотрим            несколько   примеров.

vector<int>       iv;        /*        Создание     вектора        нулевой       длины           для     хранения

int-значений.          */

vector<char>   cv(5); /*        Создание     5-элементного      вектора для           хранения            char-значений.      */

vector<char>   cv(5,   'х');     /*        Инициализация    5-элементного      char-

вектора.       */

vector<int>       iv2(iv);           /*        Создание     int-вектора  на       основе            intвектора   iv.        */

Для         класса           vector            определены           следующие операторы     сравнения:

==,           <,        <=,      !=,       >          и          >=

Для          вектора        также            определен  оператор     индексации            "[]", который       позволяет    получить доступ   к          элементам  вектора        с          помощью стандартной           записи          с          использованием  индексов. Функции-члены, определенные      в          классе           vector,           перечислены         в          табл.  21.2. Самыми       важными из           них     являются     size(), begin(),         end(), push_back(), insert()          и          erase().         Очень            полезна        функция size(),      которая возвращает             текущий       размер         вектора,       поскольку   она     позволяет определить размер        вектора        во       время            выполнения           программы. Помните,     что      векторы       при     необходимости увеличивают            свой   размер, поэтому       нужно           иметь            возможность         определять             его      величину во время      работы         программы,           а          не       только          во       время компиляции.



Функция            begin()          возвращает             итератор,     который       указывает    на начало          вектора.       Функция end()       возвращает             итератор,     который указывает    на       конец            вектора.       Как     уже    разъяснялось, итераторы подобны      указателям,            и          с          помощью    функций       begin()          и end()  можно          получить итераторы       для     начала          и          конца            вектора соответственно.

Функция            push_back() помещает    заданное     значение      в          конец вектора.       При    необходимости длина  вектора        увеличивается       так,     чтобы он       мог     принять        новый           элемент.      С          помощью функции         insert() можно          добавлять   элементы    в          середину     вектора.       Кроме           того, вектор           можно инициализировать.      В          любом          случае,         если   вектор содержит     элементы,   то        для     доступа        к          ним    и их    модификации можно          использовать         средство      индексации            массивов.    А         с помощью функции         erase()           можно          удалять        элементы    из        вектора.

Рассмотрим    короткий     пример,       который       иллюстрирует        базовое     поведение  вектора.

//  Демонстрация       базового      поведения  вектора.

#include <iostream> #include <vector> using     namespace   std;

int    main() {

 vector<int>  v;         //         создание     вектора        нулевой       длины

 unsigned       int       i;

 //         Отображаем          исходный    размер         вектора        v.

 cout    <<       "Размер       =          "          <<       v.size()           <<       endl;

 /*        Помещаем  значения      в          конец            вектора,       и          размер вектора        будет по      необходимости     увеличиваться.      */  for(i=0;           i<10;  i++) v.push_back(i);

 //         Отображаем          текущий       размер         вектора        v.

 cout    <<       "Текущее     содержимое:\n";

 cout    <<       "Новый         размер         =          "          <<       v.size()           <<       endl;

 //         Отображаем          содержимое          вектора.

 for(i=0;          i<v.size();      i++)     cout    <<       v[i]      <<       "          ";

 cout    <<       endl;

 /*        Помещаем  в          конец            вектора        новые           значения,    и            размер         вектора будет       по       необходимости     увеличиваться.      */

 for(i=0;          i<10;  i++      )           v.push_back(i+10);

 //         Отображаем          текущий       размер         вектора     v.  cout      <<       "Новый         размер         =          "          <<     v.size()           <<       endl;

 //         Отображаем          содержимое          вектора.

 cout    <<       "Текущее     содержимое:\n";

 for(i=0;          i<v.size();      i++)     cout    <<       v[i]      <<       "          ";

cout    <<       endl;

 //         Изменяем   содержимое          вектора.

 for(i=0;          i<v.size();      i++)     v[i]      =          v[i]      +          v[i];

 //         Отображаем          содержимое          вектора.

 cout    <<       "Содержимое        удвоено:\n";

 for(i=0;          i<v.size();      i++)     cout    <<       v[i]      <<       "          ";

 cout    <<       endl;

 return            0;

}

Результаты      выполнения           этой   программы таковы.

Размер  =          0 Текущее     содержимое:

Новый    размер         =          10

0   1          2          3          4          5          6          7          8          9

Новый    размер         =          20 Текущее содержимое: 0     1          2          3     4                      5                      6                                  7                      8                           9                      10       11       12       13       14       15       16       17       18       19 Содержимое   удвоено:

0   2          4          6          8          10       12       14       16       18       20       22       24       26       28     30       32       34       36       38

Рассмотрим     внимательно          код     этой   программы.           В          функции main()            создается    вектор           v          для хранения         int-элементов. Поскольку   при     его      создании     не       было предусмотрено     никакой инициализации,    вектор           v          получился   пустым,        а          его      емкость равна            нулю. Другими      словами, мы          создали        вектор           нулевой длины.         Это     подтверждается   вызовом      функции-члена     size(). Затем, используя   функцию-член       push_back(),           в          конец            этого  вектора        мы помещаем  10 элементов,        что      заставляет   вектор           увеличиться           в размере,      чтобы            разместить  новые           элементы.

Теперь          размер         вектора        стал    равным        10.      Обратите     внимание    на то,       что      для     отображения содержимого    вектора        v          используется стандартная           запись           индексации            массивов.    После            этого в вектор           добавляются          еще    10       элементов,  и          вектор           v автоматически      увеличивается       в          размере, чтобы    и          их        принять        на хранение.    Наконец,      используя   опять-таки   стандартную          запись индексации массивов,    мы      изменяем    значения      элементов   вектора        v.

Обратите           также            внимание    на       то,       что      для     управления циклами,     используемыми   для отображения             содержимого        вектора        v и          его      модификации,      в          качестве       признака     их        завершения применяется          значение      размера       вектора,       получаемое            с          помощью функции       v.size().         Одно из преимуществ   векторов      перед            массивами состоит         в          том,   что      у          нас     есть    возможность         узнать текущий размер         вектора,       что      в          определенных      ситуациях    является       очень полезным   средством. Доступ        к          вектору      с          помощью   итератора

Как          вы       знаете,          массивы       и          указатели    в          C++     тесно связаны между          собой.           К          элементам массива        можно          получить      доступ как     с          помощью    индекса,      так      и          с          помощью    указателя.   В библиотеке             STL      аналогичная           связь существует  между          векторами   и итераторами.         Это     значит, что  к          членам         вектора        можно обращаться            как     с          помощью    индекса,      так      и          с          помощью итератора.   Эта      возможность         демонстрируется в          следующей программе.

//  Доступ          к          вектору        с          помощью    итератора.

#include <iostream> #include <vector> using     namespace   std;

int    main() {

 vector<char>           v;         //         создание     массива        нулевой       длины

 int       i;

 //         Помещаем  значения      в          вектор.

 for(i=0;          i<10;  i++)     v.push_back('A'      +          i);

 /*        Получаем    доступ          к          содержимому       вектора        с          помощью     индекса.      */

 for(i=0;          i<10;  i++)     cout    <<       v[i]      <<       "          ";

cout    <<       endl;

 /*        Получаем    доступ          к          содержимому       вектора        с          помощью     итератора.

*/

 vector<char>::        iterator          р          =          v.begin();

 while(p          !=        v.end())         {

  cout           <<       *p       <<       "          ";

  p++;

 }

 return            0;

}

Вот          как     выглядят     результаты выполнения           этой   программы.

A  B          C          D         E          F          G         H         I           J

A  B          C          D         E          F          G         H         I           J

В   этой   программе сначала        создается    вектор           нулевой       длины. Затем            с          помощью    функции push_back()     в          конец            вектора помещаются          символы,     в          результате  чего   размер         вектора соответствующим            образом       увеличивается.

Обратите           внимание    на       то,       как     объявляется           итератор      р.        Тип этого  итератора    определяется контейнерными          классами.    Поэтому       для получения   итератора    для     конкретного           контейнера используйте объявление,           аналогичное          показанному         в          этом   примере:     просто укажите        для данного           итератора    имя    контейнера.            В          нашей программе итератор      p          инициализируется           таким образом,    чтобы            он указывал     на       начало          вектора        (с         помощью    функции-члена     begin()). Итератор,     который       возвращает             эта      функция,      можно          затем использовать         для     поэлементного доступа           к          вектору,       инкрементируя его      соответствующим            образом.     Этот    процесс        аналогичен тому, как можно          использовать         указатель    для     доступа        к          элементам  массива. Чтобы определить,         когда будет достигнут    конец            вектора,       используется функция-член        end(). Эта функция           возвращает             итератор,     установленный за        последним элементом  вектора.       Поэтому, если       значение      р          равно v.end(),         значит,          конец            вектора        достигнут.

Вставка         и          удаление     элементов             из        вектора

Помимо            занесения    новых            элементов   в          конец            вектора,       у нас     есть    возможность         вставлять элементы        в          середину     вектора, используя   функцию      insert().         Удалять        элементы    можно          с помощью функции       erase().         Использование     функций       insert()          и          erase() демонстрируется в следующей         программе.

// Демонстрация       вставки         элементов   в          вектор           и          удаления     их            из него.

#include <iostream> #include <vector> using     namespace   std;

int    main() {

 vector<char>           v;

 unsigned       int       i;

 for(i=0;          i<10;  i++)     v.push_back('A'      +          i);

 //         Отображаем          исходное     содержимое          вектора.

 cout    <<       "Размер       =          "          <<       v.size()           <<       endl;

 cout    <<       "Исходное   содержимое          вектора:\n";

 for(i=0;          i<v.size();      i++)     cout    <<       v[i]      <<       "          ";

 cout    <<       endl    <<       endl;

 vector<char>::        iterator          p          =          v.begin();

 p          +=       2;        //         указатель    на       3-й      элемент        вектора

 //         Вставляем   10       символов    'X'        в          вектор           v.

 v.insert(p,     10,      'X');

/*   Отображаем          содержимое          вектора        после вставки         символов.   */

 cout    <<       "Размер       вектора        после вставки         =          "          <<       v.size()<<     endl;

 cout    <<       "Содержимое        вектора        после вставки:\n";  for(i=0;   i<v.size();      i++)     cout    <<       v[i]      <<       "          ";

 cout    <<       endl    <<       endl;

 //         Удаление     вставленных           элементов.

 p          =          v.begin();

 p          +=       2;        //         указатель    на       3-й      элемент        вектора

 v.erase(р,     р+10);           //         Удаляем      10       элементов   подряд.

 /*        Отображаем          содержимое          вектора        после удаления     символов.   */

 cout    <<       "Размер       вектора        после удаления     символов    =          "<<     v.size()

<<       endl;

 cout    <<       "Содержимое        вектора        после удаления     символов:\n";

 for(i=0;          i<v.size();      i++)     cout    <<       v[i]      <<       "          ";

 cout    <<       endl;

 return            0;

}

При         выполнении           эта      программа генерирует  следующие результаты.

Размер  =          10 Исходное     содержимое          вектора:

A  B          C          D         E          F          G         H         I           J

Размер  вектора        после вставки         =          20

Содержимое   вектора        после вставки:

A  B          X          X          X          X          X          X          X          X          X          X          C          D         E     F          G         H         I           J

Размер  вектора        после удаления     символов    =          10 Содержимое   вектора        после удаления     символов:

A  B          C          D         E          F          G         H         I           J

Сохранение   в          векторе      объектов   класса

В   предыдущих          примерах    векторы       служили       для     хранения     значений только          встроенных типов,          но       этим   их        возможности         не ограничиваются.  В          вектор           можно          помещать    объекты любого  типа, включая       объекты       классов,       создаваемых         программистом.  Рассмотрим пример,       в котором   вектор           используется         для     хранения     объектов класса           three_d.        Обратите     внимание    на то, что      в          этом   классе определяются       конструктор           по       умолчанию и          перегруженные    версии операторов "<"      и          "==".  Имейте         в          виду, что,    возможно,  вам    придется определить            и          другие операторы           сравнения.  Это     зависит         от того,  как     используемый      вами  компилятор            реализует библиотеку   STL.

//  Хранение     в          векторе        объектов      класса.

#include <iostream> #include <vector> using     namespace   std;

class        three_d     {   int       x,     y,         z;

 public:

  three_d() {           x          =          у          =          z          =          0;        }

  three_d(int          a,         int       b,        int       с)         {           x          =          a;         у          =     b;        z          =          c;         }

  three_d    &operator+(int       a)        {

   x +=       a;

   у +=       a;

   z +=       a;

   return  *this;

  }

  friend        ostream        &operator<<(ostream      &stream,      three_d         obj);

  friend        bool    operator<(three_d            a,         three_d         b);

  friend        bool    operator==(three_d         a,         three_d         b);

};

/* Отображаем          координаты           X,        Y,         Z          с          помощью оператора   вывода         для класса  three_d.        */ ostream   &operator<<(ostream &stream,      three_d         obj) {

 stream           <<       obj.x   <<       ",         ";

 stream           <<       obj.у  <<       ",         ";

 stream           <<       obj.z   <<       "\n";

 return            stream;

}

bool         operator<(three_d            a,     three_d         b) {

 return            (a.x     +          a.у      +          a.z)     <          (b.x     +          b.y      +          b.z);

}

bool         operator==(three_d         a,         three_d     b) {

 return            (a.x     +          a.у      +          a.z)     ==       (b.x     +          b.y      +          b.z);

}

int    main() {

 vector<three_d>    v;

 unsigned       int       i;

 //         Добавляем в          вектор           объекты.

 for(i=0;          i<10;  i++)

  v.push_back(three_d(i,           i+2,     i-3));

 //         Отображаем          содержимое          вектора.

 for(i=0;          i<v.size();      i++)

  cout           <<       v[i];

 cout    <<       endl;

 //         Модифицируем    объекты       в          векторе.

 for(i=0;          i<v.size();      i++)     v          [i]        =          v[i]      +          10;  //     Отображаем          содержимое          модифицированного     вектора.

 for(i=0;          i<v.size();      i++)     cout    <<       v[i];

 return            0;

}

Эта          программа генерирует  такие результаты.

0, 2,        -3

1, 3,        -2

2, 4,        -1

3, 5,        0

5, 7,        2

6, 8,        3

7, 9,        4

8, 10,      5

9, 11,      6

10,           12,      7

11,           13,      8

12,           14,      9

13,           15,      10

14,           16,      11

15,           17,      12

16,           18,      13

17,           19,      14

18,           20,      15

19,           21,      16      

Векторы            обеспечивают        безопасность         хранения     элементов, обнаруживая         при     этом чрезвычайную       мощь и          гибкость       их обработки,  но       уступают      массивам    в          эффективности использования. Поэтому       для     большинства          задач программирования        чаще  отдается предпочтение        обычным     массивам.   Однако         возможны   ситуации,     когда уникальные особенности         векторов      оказываются          важнее         затрат системных   ресурсов,     связанных   с          их использованием.

О            пользе          итераторов

Частично           сила   библиотеки            STL      обусловлена          тем,    что      многие ее        функции       используют итераторы.            Этот    факт   позволяет    выполнять операции     с          двумя           контейнерами       одновременно. Рассмотрим, например,   такой формат         векторной   функции       insert().

template            <class InIter>

void         insert(iterator         i,          InIter  start,  InIter  end);

Эта           функция       вставляет     исходную    последовательность,      определенную параметрами         start и           end,    в          приемную   последовательность,      начиная с          позиции       i.          При    этом   нет      никаких требований,      чтобы            итератор i           относился    к          тому  же       вектору,       с          которым      связаны итераторы start    и          end.    Таким            образом,     используя   эту      версию функции       insert(),         можно          один  вектор вставить    в          другой. Рассмотрим            пример.

//  Вставляем   один  вектор           в          другой.

#include <iostream> #include <vector> using     namespace   std;

int    main() {

 vector<char>           v,         v2;

 unsigned       int       i;

 for(i=0;          i<10;  i++)     v.push_back('A'      +          i);

 //         Отображаем          исходное     содержимое          вектора.

 cout    <<       "Исходное   содержимое          вектора:\n";

 for(i=0;          i<v.size();      i++)

  cout           <<       v[i]      <<       "          ";

 cout    <<       endl    <<       endl;

 //         Инициализируем второй          вектор.

 char    str[]    =          "-STL  —        это      сила!-";

 for(i=0;          str[i];  i++)     v2       .push_back  (str      [i]);

 /*        Получаем    итераторы   для     середины    вектора        v,         а          также            начала          и конца        вектора        v2.      */

 vector<char>::iterator      р          =          v.begin()+5;

 <char>::iterator      p2start          =          v2.begin();  vector<char>::iterator  p2end     =          v2.end();

 //         Вставляем   вектор           v2       в          вектор           v.

 v.insert(p,     p2start,         p2end);

 //         Отображаем          результат     вставки.

 cout    <<       "Содержимое        вектора        v          после вставки:\n";

 for(i=0;          i<v.size();      i++)     cout    <<       v[i]      <<       "          ";

 return            0;

}

При         выполнении           эта      программа генерирует  следующие результаты.

Исходное          содержимое          вектора:

A  B          C          D         E          F          G         H     I           J Содержимое       вектора        v     после вставки:

A  B          C          D         E          -           S          T          L          --         это      cилa! -           F          G     H         I           J

Как          видите,         содержимое          вектора        v2       вставлено    в          середину     вектора        v.

По            мере  дальнейшего         изучения      возможностей,      предоставляемых STL,    вы       узнаете,        что итераторы       являются     связующими          средствами, которые       делают         библиотеку             единым        целым. Они            позволяют работать      с          двумя           (и        больше)       объектами  STL      одновременно,     но особенно полезны          при     использовании     алгоритмов,           описанных  ниже  в этой   главе.

Списки

Список   —        это    контейнер с          двунаправленным          последовательным доступом   к          его элементам.

Класс      list       поддерживает       функционирование         двунаправленного линейного   списка.         В отличие    от        вектора,       в          котором       реализована поддержка произвольного     доступа,       список позволяет            получать      к своим            элементам  только          последовательный          доступ. Двунаправленность списка           означает,     что      доступ          к          его      элементам  возможен    в двух направлениях:        от        начала          к          концу            и          от        конца            к началу.

Шаблонная     спецификация       класса           list       выглядит     следующим     образом.

template            <class Т,         class   Allocator       =          allocator<T>>          class   list

Здесь      T          —        тип     данных,        сохраняемых         в          списке,         а элемент        Allocator       означает распределитель        памяти,        который       по умолчанию использует  стандартный          распределитель.  В классе       list определены           следующие конструкторы.

explicit    list(const       Allocator       &а       =          Allocator()    );

explicit    list(size_type           num,  const  T          &val   =          T(),      const  Allocator

&a       =          Allocator());

list(const            list<T, Allocator>    &ob);

template            <class InIter>list(InIter      start,  InIter  end,    const Allocator       &a       =            Allocator());

Конструктор,   представленный  в          первой          форме,         создает         пустой список.         Вторая          форма предназначена   для     создания     списка,         который содержит     num   элементов   со        значением  val. Третья   создает         список, который       содержит     те        же       элементы,   что      и          объект          ob.      Четвертая создает         список,         который       содержит     элементы    в          диапазоне, заданном параметрами         start   и end.

Для         класса           list       определены           следующие операторы  сравнения:

==,           <,        <=,      !=,       >          и          >=

Функции-члены,        определенные      в          классе           list,      перечислены         в табл.  21.3.  В          конец            списка, как и          в          конец            вектора,       элементы можно          помещать    с          помощью    функции       push_back(),           но       с помощью    функции       push_front()            можно          помещать    элементы    в начало          списка.         Элемент       можно также         вставить       и          в          середину списка,         для     этого  используется         функция       insert().         Один список можно поместить   в          другой,         используя   функцию      splice().         А         с          помощью функции       merge()         два списка  можно          объединить            и          упорядочить результат.



Чтобы     достичь        максимальной      гибкости      и          переносимости     для любого         объекта,       который подлежит         хранению    в          списке,         следует определить            конструктор           по       умолчанию и          оператор     " <"     (и желательно            другие          операторы  сравнения).            Более            точные требования             к          объекту        (как    к потенциальному           элементу     списка) необходимо           согласовывать       в          соответствии          с документацией на используемый      вами  компилятор. Рассмотрим        простой        пример         списка.

//  Базовые       операции,   определенные      для     списка.

#include <iostream> #include <list> using     namespace   std; int            main() {

 list<char>      lst;       //         создание     пустого         списка

 int       i;

 for(i=0;          i<10;  i++)     lst.push_back('A'+i);

 cout    <<       "Размер       =          "          <<       lst.size()         <<       endl;

 cout    <<       "Содержимое        :           ";

 list<char>::iterator             p          =          lst.begin();

 while(p          !=        lst.end())       {

  cout           <<       *p;

  p++;

 }

 return            0;

}

Результаты      выполнения           этой   программы таковы:

Размер  =          10

Содержимое   :           ABCDEFGHIJ

При         выполнении           эта      программа создает         список          символов. Сначала        создается    пустой объект       списка.         Затем            в          него помещается            десять           букв   (от      А         до       J).        Заполнение            списка реализуется            путем            использования      функции       push_back(),           которая помещает    каждое         новое значение    в          конец            существующего    списка. После            этого  отображается        размер         списка           и          его содержимое. Содержимое          списка           выводится   на       экран в          результате  выполнения следующего кода.

list<char>::iterator     р          =          lst.begin();

while(p   !=        lst.end())       {

 cout    <<       *p;

 p++;

}

Здесь      итератор      p          инициализируется           таким            образом,     чтобы он       указывал     на       начало          списка. При            выполнении           очередного прохода       цикла            итератор      р          инкрементируется,          чтобы            указывать на        следующий            элемент        списка.         Этот    цикл  завершается,          когда итератор      р          указывает    на       конец списка.        Применение           подобных циклов          —        обычная       практика      при     использовании     библиотеки STL. Например,  аналогичный         цикл  мы      применили для     отображения содержимого        вектора        в предыдущем     разделе.

Поскольку        списки          являются     двунаправленными,       заполнение            их элементами            можно производить       с          обоих            концов.        Например,  при выполнении           следующей программы создается два       списка,         причем элементы    одного          из        них     расположены        в          порядке,      обратном    по отношению к          другому.

/* Элементы    можно          помещать    в          список          как     с          начала,         так     и          с          конца.

*/

#include <iostream> #include <list> using     namespace   std;

int    main() {

 list<char>      lst;

 list<char>      revlst;

 int       i;

 for(i=0;          i<10;  i++      )           lst.push_back('A'+i);

 cout    <<       "Размер       списка           lst        =          "          <<       lst.size()         <<     endl;

 cout    <<       "Исходное   содержимое          списка:         ";

 list<char>::iterator             p;

 /*        Удаляем      элементы    из        списка           lst        и          помещаем  их        в            список revlst          в          обратном    порядке.      */

 while(!lst.empty())             {

  р     =          lst.begin();

  cout           <<       *р;

  revlst.push_front(*р);

  lst.pop_front();

 }

 cout    <<       endl    <<       endl;

 cout    <<       "Размер       списка           revlst  =          ";

 cout    <<       revlst.size()  <<       endl;

 cout    <<       "Реверсированное           содержимое          списка:         ";

 p          =          revlst.begin();

 while(p          !=        revlst.end()) {

  cout           <<       *p;

  p++;

 }

 return            0;

}

Эта          программа генерирует  такие результаты.

Размер  списка           lst        =          10 Исходное     содержимое          списка:         ABCDEFGHIJ

Размер  списка           revlst  =          10

Реверсированное      содержимое          списка:         JIHGFEDCBA

В   этой   программе список          реверсируется       путем            удаления элементов   с          начала          списка           lst        и занесения            их        в          начало списка           revlst.

Сортировка  списка

Список   можно          отсортировать       с          помощью    функции-члена     sort(). При выполнении следующей          программы создается    список          случайно выбранных целых            чисел,           который       затем упорядочивается            по возрастанию.

//  Сортировка             списка.

#include <iostream>

#include <list> #include     <cstdlib> using     namespace   std;

int    main() {

 list<int>         lst;

 int       i;

 //         Создание     списка           случайно     выбранных целых            чисел.

 for(i=0;          i<10;  i++      )lst.push_back(rand()       );

 cout    <<       "Исходное   содержимое          списка:\n";

 list<int>::iterator    p          =          lst.begin();

 while(p          !=        lst.end())       {

  cout           <<       *p       <<       "          ";

  p++;

 }

 cout    <<       endl    <<       endl;

 //         Сортировка             списка.

 lst.sort();

 cout    <<       "Отсортированное           содержимое          списка:\n";

 p          =          lst.begin();

 while(p          !=        lst.end())       {

  cout           <<       *p       <<       "          ";

  p++;

 }

 return            0;

}

Вот          как     может           выглядеть   один  из        возможных вариантов     выполнения           этой   программы.

Исходное          содержимое          списка: 41   18467            6334  26500     19169            15724            11478            29358            26962            24464 Отсортированное      содержимое          списка:

41 6334  11478            15724            18467            19169            24464            26500     26962            29358

Объединение            одного          списка          с          другим

Один      упорядоченный    список          можно          объединить            с          другим. В          результате  мы      получим упорядоченный         список,         который       включает содержимое          двух   исходных     списков.       Новый список        остается       в вызывающем        списке,         а          второй          список          становится  пустым.        В следующем примере     выполняется          слияние        двух   списков.       Первый список          содержит     буквы            ACEGI,           а второй—  буквы            BDFHJ.           Эти списки          затем объединяются,      в          результате  чего   образуется упорядоченная последовательность       букв   ABCDEFGHIJ.

//  Слияние       двух   списков.

#include <iostream> #include <list> using     namespace   std;

int    main() {

 list<char>      lst1,    lst2;

 int       i;

 for(i=0;          i<10;  i+=2)  lst1.push_back('A'+i);

 for(i=1;          i<11;  i+=2)  lst2.push_back('A'+i);

 cout    <<       "Содержимое        списка           lst1:    ";

 list<char>::iterator             p          =          lst1.begin();

 while(p          !=        lst1.end())    {

  cout           <<       *p;

  p++;

 }

 cout    <<       endl    <<       endl;

 cout    <<       "Содержимое        списка           lst2:    ";

 р          =          lst2.begin();

 while(p          !=        lst2.end())    {

  cout           <<       *p;

  p++;

 }

 cout    <<       endl    <<       endl;

 //         Теперь          сливаем       эти      два     списка.

 lst1.merge(lst2);

 if(lst2.empty())

  cout           <<       "Список        lst2     теперь           пуст.\n";

 cout    <<       "Содержимое        списка           lst1     после объединения:\n";

 р          =          lst1.begin();

 while(p          !=        lst1.end())    {

  cout           <<       *p;

  p++;

 }

 return            0;

}

Результаты      выполнения           этой   программы таковы.

Содержимое   списка           lst1:    ACEGI

Содержимое   списка           lst2:    BDFHJ

Список   lst2     теперь           пуст.

Содержимое   списка           lst1     после объединения:

ABCDEFGHIJ

Хранение        в          списке           объектов   класса

Рассмотрим     пример,       в          котором       список          используется         для хранения     объектов      типа myclass.         Обратите     внимание    на       то,       что      для объектов      типа   myclass         перегружены         операторы  " <",   ">",    "!="    и "==".  (Для   некоторых   компиляторов       может           оказаться     излишним определение          всех    этих    операторов             или     же       придется      добавить некоторые  другие.)       В библиотеке         STL      эти      функции       используются        для определения         упорядочения       и          равенства объектов        в          контейнере. Несмотря     на       то        что      список          не       является       упорядоченным контейнером,        необходимо           иметь            средство      сравнения   элементов, которое        применяется          при их           поиске,         сортировке или     объединении.

//  Хранение     в          списке           объектов      класса.

#include <iostream>

#include <list> #include     <cstring> using     namespace   std;

class        myclass     {   int       a,     b;

  int  sum;

 public:

  myclass()  {           a          =          b          =          0;        }

  myclass(int          i,          int       j)         {

a       =         i;

b       =         j;

   sum       =          a          +          b;

  }

  int  getsum()       {           return            sum;   }

  friend        bool    operator<(const     myclass         &o1,   const  myclass         &o2);   friend      bool    operator>(const     myclass         &o1,   const  myclass         &o2);

  friend        bool    operator==(const  myclass         &o1,   const  myclass &o2);

               friend          bool             operator!=(const               myclass         &o1,         const              myclass

&o2); };

bool         operator<(const     myclass         &o1,   const  myclass         &o2) {

 return            o1.sum          <          o2.sum;

}

bool         operator>(const     myclass         &o1,   const  myclass         &o2) {

 return            o1.sum          >          o2.sum;

}

bool         operator==(const  myclass         &o1,   const  myclass         &o2) {

 return            o1.sum          ==       o2.sum;

}

bool         operator!=(const   myclass         &o1,   const  myclass         &o2) {

 return            o1.sum          !=        o2.sum;

}

int    main() {

 int       i;

 //         Создание     первого        списка.

 list<myclass>           lst1;

 for(i=0;          i           <10;   i++)     lst1.push_back(myclass(i,           i));

 cout    <<       "Первый      список:         ";

 list<myclass>::iterator      p          =          lst1.begin();

  while(p     !=        lst1.end())    {

  cout           <<       p->getsum() <<       "          ";

  p++;

 }

 cout    <<       endl;

 //         Создание     второго        списка.

 list<myclass>           lst2;

 for(i=0;          i<10;  i++)     lst2.push_back(myclass(i*2,      i*3));

 cout    <<       "Второй        список:         ";

 p          =          lst2.begin();

 while(p          !=        lst2.end())    {

  cout           <<       p->getsum() <<       "          ";

  p++;

 }

 cout    <<       endl;

 //         Теперь          объединяем           списки          lst1     и          lst2.

  lst1.merge(lst2);

 //         Отображаем          объединенный      список.

 cout    <<       "Объединенный   список:         ";

 р          =          lst1.begin();

 while(p          !=        lst1.end())    {

  cout           <<       p->getsum() <<       "          ";

  p++;

 }

 return            0;

}

Эта           программа создает         два     списка           объектов      типа   myclass         и отображает             их        содержимое. Затем        выполняется          объединение         этих двух   списков        с          последующим       отображением      нового содержимого результирующего            списка.         Итак,  программа генерирует  такие результаты.

Первый список:         0          2          4          6          8          10       12       14       16       18

Второй   список:         0          5          10       15       20       25       30       35       40       45

Объединенный          список:         0          0          2          4          5          6          8          10       10            12       14       15       16       18       20       25       30 35 40       45

Отображения        

Отображение            —        это    ассоциативный   контейнер.

Класс      map   поддерживает       ассоциативный     контейнер,  в          котором уникальным           ключам соответствуют  определенные      значения.    По       сути, ключ  —        это      просто          имя,   которое        присвоено некоторому значению. После            того    как     значение      сохранено   в          контейнере,            к          нему можно получить  доступ,         используя   его      ключ. Таким            образом,     в самом           широком     смысле отображение     —        это      список          пар     "ключ-значение".   Если   нам    известен       ключ, мы      можем          легко найти            значение. Например,  мы      могли            бы      определить            отображение,        в          котором в          качестве ключа    используется         имя    человека,    а          в          качестве значения      —        его      телефонный           номер. Ассоциативные контейнеры становятся   все      более            популярными        в          программировании.

Как          упоминалось         выше,            отображение         может           хранить только          уникальные            ключи.          Ключидубликаты не       разрешены. Чтобы           создать         отображение,        которое        бы      позволяло   хранить неуникапьные       ключи,          используйте           класс multimap.

Контейнер        map   имеет            следующую            шаблонную            спецификацию.

template            <class Key,    class   T,         class   Comp =          less<Key>,    class Allocator            =allocator<pair<const      Key,    T>       >          >class map

Здесь      Key— тип     данных         ключей,        T—      тип     сохраняемых (отображаемых)   значений,    а          Comp —       функция,      которая        сравнивает  два ключа.          По       умолчанию в          качестве       функции       сравнения используется стандартная           функция-объект    less.    Элемент       Allocator       означает распределитель памяти,          который       по       умолчанию использует  стандартный распределитель   allocator. Класс     map   имеет            следующие конструкторы.

explicit    map(const    Comp &cmpfn         =          Comp(),         const  Allocator       &a       = Allocator());

map(const         map<Key,     T,         Comp,            Allocator>    &ob);

template            <class InIter>           map(InIter    start,  InIter  end,    const  Comp &cmpfn            =          Comp(),         const  Allocator       &a       =          Allocator());

Первая   форма           конструктора         создает         пустое           отображение. Вторая          предназначена     для создания         отображения,        которое        содержит те        же       элементы,   что      и          отображение         ob.      Третья создает отображение,        которое        содержит     элементы    в          диапазоне, заданном итераторами          start и           end.    Функция,     заданная     параметром           cmpfn (если она     задана),       определяет             характер упорядочения            отображения.

В   общем          случае           любой           объект,         используемый      в          качестве ключа,          должен        определять конструктор          по       умолчанию и перегружать           оператор     "<"      (а        также            другие          необходимые операторы  сравнения).            Эти     требования             для     разных          компиляторов различны. Для      класса           map   определены           следующие операторы сравнения:

==,           <,        <=,      !=,       >          и          >=

Функции-члены,        определенные      для     класса           map,  представлены       в табл.  21.4.  В          их описании           под    элементом  key_type      понимается            тип ключа,          а          под    элементом  value_type   — значение            выражения pair<Key, Т>.



Пары      "ключ-значение"  хранятся       в          отображении         как     объекты класса           pair,   который       имеет следующую          шаблонную            спецификацию.

template            <class Ktype,            class   Vtype> struct      pair     {

 typedef          Ktype first_type;    //         тип     ключа

 typedef          Vtype second_type;          //         тип     значения

 Ktype first;   //         содержит     ключ

 Vtype second;         //         содержит     значение

 //         Конструкторы

 pair();

 pair     (const            Ktype &k,     const  Vtype &v);

 template<class        A,        class   B>       pair(const<A,          B>       &ob);

}

Как          отмечено     в          комментариях,      член   first     содержит     ключ, а член   second           —        значение, соответствующее    этому            ключу.

Создать пару   "ключ-значение"  можно          либо  с          помощью    конструкторов класса           pair,   либо путем вызова          функции       make_pair(),           которая создает         парный         объект          на       основе          типов данных, используемых в качестве       параметров.           Функция       make_pair() —        это      обобщенная функция, прототип          которой        имеет            следующий            вид.

template            <class Ktype,            class   Vtype>

pair<Ktype,       Vtype>          make_pair(const    Ktype &k,     const  Vtype &v);

Как          видите,         функция       make_pair() возвращает             парный         объект, состоящий   из        значений, типы    которых       заданы         параметрами         Ktype и Vtype.            Преимущество      использования      функции make_pair()     состоит         в том,   что      типы  объектов,    объединяемых      в          пару, определяются автоматически       компилятором,     а          не       явно   задаются     программистом.

Возможности  использования      отображения         демонстрируется в следующей программе.            В данном     случае           в          отображении сохраняется            10       пар     "ключ-значение".            Ключом        служит          символ, а значением  —        целое            число.           Пары "ключ-значение"  хранятся следующим            образом:

А  0

В   1

С   2

и  т.д.     После            сохранения             пар     в          отображении         пользователю     предлагается         ввести           ключ  (т.е.

букву из        диапазона   А-J),    после чего   выводится   значение,    связанное    с этим   ключом.

//  Демонстрация       использования      простого      отображения.

#include <iostream> #include <map> using     namespace   std;

int    main() {

 map<char,    int>     m;

 int       i;

 //         Помещаем  пары  в          отображение.

 fоr(i    =          0;        i           <10;   i++)     {

  m.insert(pair<char,      int>('A'+i,      i));

 }

 char    ch;

 cout    <<       "Введите      ключ: ";

 cin       >>       ch;

 map<char,    int>::iterator            p;

 //         Находим      значение      по       заданному  ключу.

 р          =          m.find(ch);

 if(р      !=        m.end())

  cout           <<       p->second;

 else     cout    <<       "Такого         ключа           в          отображении         нет.\n";

 return            0;

}

Обратите           внимание    на       использование      шаблонного           класса           pair для     построения пар "ключ-значение".    Типы  данных,        задаваемые           pair-выражением,         должны       соответствовать типам  отображения,        в          которое вставляются           эти      пары.

После     инициализации     отображения         ключами      и          значениями можно          выполнять  поиск значения     по       заданному  ключу,          используя функцию      find(). Эта      функция       возвращает             итератор, который указывает    на       нужный        элемент        или     на       конец            отображения, если   заданный    ключ  не был          найден.        При    обнаружении         совпадения значение,    связанное    с          ключом,       можно          найти             в члене         second парного        объекта        типа   pair.

В   предыдущем         примере      пары  "ключ-значение"  создавались           явным образом       с          помощью шаблона         pair<char,    int>.   И         хотя    в          этом   нет ничего          неправильного,    зачастую      проще использовать      с          этой   целью функцию      make_pair(),           которая        создает         pair-объект на       основе типов данных,        используемых       в          качестве       параметров.           Например,  эта строка           кода  также позволит    вставить       в          отображение         m        пары "ключ-значение"  (при   использовании     предыдущей программы):

m.insert(make_pair((char)  ('А'+i),            i));

Здесь,     как     видите,         выполняется          операция     приведения            к типу   char,  которая        необходима для  переопределения            автоматического преобразования   в          тип     int       результата  сложения значения        i           с символом   'А'.

Хранение        в          отображении      объектов   класса

Подобно           всем  другим         контейнерам,        отображение         можно использовать         для     хранения объектов         создаваемых         вами  типов. Например,  следующая программа создает         простой словарь  на       основе отображения         слов   с          их        значениями.           Но       сначала        она     создает два     класса word           и          meaning.      Поскольку   отображение         поддерживает отсортированный            список          ключей, программа        также            определяет для     объектов      типа   word  оператор     "<".    В          общем          случае оператор   "<" следует        определять             для     любых          классов,       которые       вы собираетесь           использовать в     качестве       ключей.        (Некоторые компиляторы        могут потребовать           определения         и          других операторов сравнения.)

//  Использование     отображения         для     создания     словаря.

#include <iostream>

#include <map> #include     <cstring> using     namespace   std;

class        word  {   char         str[20];

 public:

  word()      {           strcpy(str,     "");     }

  word(char           *s)      {           strcpy(str,     s);        }

  char           *get() {           return            str;      }

};

bool         operator<(word     a,     word  b) {

 return            strcmp(a.get(),        b.get())          <          0;

}

class        meaning     {   char     str[80];

 public:

  meaning()            {           strcmp(str,   "          ");}

  meaning(char     *s)      {           strcpy(str,     s);        }

  char           *get() {           return            str;      }

};

int    main() {

 map<word,  meaning>     dictionary;

 /*        Помещаем  в          отображение         объекты       классов        word  и     meaning.      */

 dictionary.insert(   pair<word,   meaning>     (word("дом"), meaning("Место            проживания.")));

 dictionary.insert(   pair<word,   meaning>     (word("клавиатура"), meaning("Устройство      ввода            данных.")));

 dictionary.insert(  

pair<word,           

meaning>

(word("программирование"),            программы.")));

meaning("Процесс          

создания

 dictionary.insert(   pair<word,   meaning>     (word("STL"), meaning("Standard            Template      Library")));

 //         По       заданному  слову находим      его      значение.

 char    str[80];

 cout    <<       "Введите      слово:           ";

  cin  >>       str;

 map<word,  meaning>::iterator            р;

 р          =          dictionary.find(word(str));

 if(p      !=        dictionary.end())

  cout           <<       "Определение:     "          <<       p->second.get();

 else     cout    <<       "Такого         слова в          словаре        нет.\n";

 return            0;

}

Вот          один  из        возможных вариантов   выполнения           этой   программы.

Введите слово:           дом

Определение:            Место            проживания.

В   этой   программе каждый        элемент        отображения         представляет собой            символьный массив,      который       содержит     строку           с завершающим      нулем.          Ниже в          этой   главе мы      рассмотрим более простой        вариант        построения этой   программы,           в          которой использован          стандартный тип  string.

Алгоритмы

Алгоритмы       обрабатывают       данные,       содержащиеся      в          контейнерах. Несмотря     на       то        что каждый            контейнер   обеспечивает         поддержку собственных           базовых       операций,   стандартные алгоритмы          позволяют выполнять  более            расширенные        или     более            сложные      действия.     Они также позволяют  работать      с          двумя           различными           типами контейнеров          одновременно.     Для получения      доступа        к          алгоритмам библиотеки            STL      необходимо           включить     в          программу заголовок <algorithm>.

В   библиотеке             STL      определено            множество  алгоритмов,           которые описаны       в          табл.  21.5.  Все эти          алгоритмы  представляют        собой шаблонные            функции.     Это     означает,     что      их        можно применять           к контейнеру любого         типа.



Подсчет         элементов

Одна       из        самых           популярных           операций,   которую       можно выполнить  для     любой последовательности    элементов,  —        подсчитать  их количество.            Для    этого  можно использовать     один  из        алгоритмов: count()          или     count_if().    Общий          формат         этих    алгоритмов имеет следующий            вид.

template            <class InIter, class   T> ptrdiff_t  count(InIter start,  InIter  end,     const  T          &val);

template            <class InIter, class   UnPred>

ptrdiff_t count_if(InIter        start,  InIter  end,    UnPred          pfn);

Алгоритм          count()          возвращает             количество элементов,  равных значению    val,     в последовательности,  границы       которой        заданы параметрами         start   и          end.    Алгоритм     count_if(), действуя         в последовательности,     границы       которой        заданы         параметрами         start   и end, возвращает   количество элементов,  для     которых       унарный      предикат     pfn возвращает             значение true.       Тип     ptrdiff_t        определяется        как некоторая   разновидность      целочисленного   типа.

Использование          алгоритмов            count()          и          count_if()     демонстрируется в          следующей программе.

/* Демонстрация       использования      алгоритмов            count и          count_if.

*/

#include <iostream>

#include <vector>

#include <algorithm> #include <cctype> using     namespace   std;

/* Это     унарный      предикат,    который       определяет,           представляет         ли данный        символ         гласный       звук.

*/ bool   isvowel(char     ch) {

 ch        =          tolower(ch);

 if(ch=='a'      ||        ch=='e'          ||        ch=='и'          ||        ch=='o'          ||        ch=='у'            || ch=='ы'    ||        ch=='я'          ||        ch=='ё'          ||        ch=='ю'         ||        ch=='э')

  return       true;

 return            false;

}

int    main() {

 char    str[]    =          "STL-программирование          —        это      сила!";

 vector<char>           v;

 unsigned       int       i;

 for(i=0;          str[i];  i++)     v.push_back(str[i]);

 cout    <<       "Последовательность:   ";

 for(i=0;          i<v.size();      i++)     cout    <<       v[i];

 cout    <<       endl;

 int       n;

 n          =          count (v.begin(),    v.end(),         'м');

 cout    <<       n          <<       "          символа       м\n";

 n          =          count_if(v.begin(),            v.end(),         isvowel);

 cout    <<       n          <<       "          символов    представляют        гласные        звуки.\n";

 return            0;

}

При         выполнении           эта      программа генерирует  такие результаты.

Последовательность:          STL-программирование            --         это      сила!

2   символа       м

11 символов    представляют        гласные        звуки.

Программа      начинается  с          создания     вектора,       который       содержит строку           "STLпрограммирование           -           это    сила!".          Затем используется         алгоритм     count()          для     подсчета количества      букв   'м'       в этом   векторе.       После            этого  вызывается             алгоритм     count_if(),    который подсчитывает        количество символов,   представляющих  гласные        звуки с использованием  в качестве   предиката   функции       isvowel().      Обратите     внимание на       то,       как     закодирован          этот предикат.       Все     унарные       предикаты получают     в          качестве       параметра   объект,         тип     которого совпадает        с типом            элементов,  хранимых    в          контейнере,            для     которого      и создается    этот предикат.       Предикат     должен        возвращать             значение ИСТИНА        или     ЛОЖЬ.

Удаление        и          замена         элементов

Иногда   полезно       сгенерировать       новую           последовательность,      которая будет состоять только    из        определенных      элементов   исходной последовательности.     Одним          из        алгоритмов, который    может справиться  с          этой   задачей,       является       remove_copy().     Его      общий формат выглядит так.

template            <class ForIter,          class   OutIter,         class   T>

OutIter   remove_copy(InIter          start,  InIter  end,    OutIter          result, const            T            &val);

Алгоритм          remove_copy()       копирует      с          извлечением          из заданного   диапазона   элементы, которые        равны           значению    val,     и помещает    результат     в          последовательность,      адресуемую параметром result.            Алгоритм     возвращает             итератор,     указывающий        на       конец результата.

Контейнер-приемник     должен        быть  достаточно большим,    чтобы            принять результат.

Чтобы     в          процессе      копирования          один  элемент        в последовательности       заменить     другим, используйте      алгоритм     replace_copy(). Его      общий           формат         выглядит     так.

template            <class ForIter,          class   OutIter,         class   T>

OutIter   replace_copy(InIter          start,  InIter  end,    OutIter          result, const            T            &old,  Const T          &new);

Алгоритм          replace_copy()       копирует      элементы    из        заданного диапазона   в последовательность,  адресуемую           параметром           result.            В процессе      копирования          происходит замена        элементов,  которые       имеют значение      old,     элементами,          имеющими значение      new. Алгоритм помещает    результат     в          последовательность,      адресуемую           параметром result,            и возвращает         итератор,     указывающий        на       конец            этой последовательности.     Контейнерприемник      должен        быть  достаточно большим, чтобы            принять        результат.

В   следующей программе демонстрируется использование      алгоритмов remove_copy()       и replace_copy().  При    ее        выполнении           создается последовательность       символов,   из        которой удаляются         все      буквы            'т'. Затем            выполняется          замена          всех    букв   'о'        буквами       'Х'.

/* Демонстрация       использования      алгоритмов            remove_copy          и replace_copy.

*/

#include <iostream>

#include <vector> #include     <algorithm> using     namespace   std;

int    main() {

 char    str[]    =          "Это   очень            простой        тест.";

 vector<char>           v,         v2(30);

 unsigned       int       i;

 for(i=0;          str[i];  i++)     v.push_back(str[i]);

 //         ****  демонстрация       алгоритма   remove_copy          ****

 cout    <<       "Входная     последовательность:      ";

 for(i=0;          i<v.size();      i++)

  cout           <<       v[i];

 cout    <<       endl;

 //         Удаляем      все      буквы            'т'.

 remove_copy(v.begin(),  v.end(),         v2.begin(),   'Т');

 cout    <<       "После          удаления     букв   'т'        :           ";

 for(i=0;          v2[i];  i++)

  cout           <<       v2[i];

 cout    <<       endl    <<       endl;

 //         ****  Демонстрация       алгоритма   replace_copy          ****

 cout    <<       "Входная     последовательность:      ";

 for(i=0;          i<v.size();      i++)

  cout           <<       v[i];

 cout    <<       endl;

 //         Заменяем    буквы            'о'        буквами       'Х'.

 replace_copy(v.begin(),   v.end(),         v2.begin(),   'о',      'Х');

 cout    <<       "После          замены         букв   'o'        буквами       'Х':      ";

 for(i=0;          v2[i];  i++)

  cout           <<       v2[i];

 cout    <<       endl    <<       endl;

 return            0;

}

Результаты      выполнения           этой   программы таковы.

Входная            последовательность:      Это     очень            простой        тест.

После     удаления     букв   'т':       Эо       очень            просой          ес.

Входная            последовательность:      Это     очень            простой        тест.

После     замены         букв   'о'        буквами       'Х':      ЭтХ     Хчень            прХстХй     тест.

Реверсирование      последовательности

В   программах           часто используется         алгоритм     reverse(),      который       в диапазоне, заданном параметрами           start   и          end,    меняет          порядок следования            элементов   на       противоположный.         Его общий   формат имеет            следующий            вид. template         <class BiIter>

void         reverse(BiIter          start,  BiIter  end);

В  следующей программе демонстрируется использование      этого  алгоритма.

//  Демонстрация       использования      алгоритма   reverse.

#include <iostream>

#include <vector> #include     <algorithm> using     namespace   std;

int    main() {

 vector<int>  v;

 unsigned       int       i;

 for(i=0;          i<10;  i++)     v.push_back(i);

 cout    <<       "Исходная   последовательность:      ";

 for(i=0;          i<v.size();      i++)

  cout           <<       v[i]      <<       "          ";

 cout    <<       endl;

 reverse          (v.begin(),    v.end());

 cout    <<       "Реверсированная           последовательность:      ";

 for(i=0;          i<v.size();      i++)     cout    <<       v[i]      <<       "          ";

 return            0;

}

Результаты      выполнения           этой   программы таковы.

Исходная          последовательность:      0123456789

Реверсированная      последовательность:      9876543210

Преобразование     последовательности

Одним   из        самых           интересных             алгоритмов            является transform(), поскольку   он       позволяет модифицировать   каждый        элемент        в диапазоне   в          соответствии          с          заданной     функцией. Алгоритм transform()  используется         в          двух   общих           форматах.

template            <class InIter, class   OutIter,         class   Func>

OutIter   transform(InIter     start,  InIter  end,    OutIter          result,            Func unaryfunc);

template            <class InIter1,          class   InIter2,          class   OutIter,         class Func>

OutIter   transform(InIter1  start1,            InIter1           end1, InIter2           start2, OutIter            result,            Func   binaryfunc);

Алгоритм          transform()  применяет  функцию      к          диапазону   элементов   и сохраняет    результат в последовательности,     заданной     параметром           result. В          первой          форме           диапазон     задается параметрами  start   и          end. Применяемая        для     преобразования   функция       задается       параметром unaryfunc.   Она    принимает  значение      элемента     в          качестве       параметра   и должна        возвратить преобразованное             значение.    Во       второй          форме алгоритма   преобразование   выполняется          с использованием           бинарной функции,     которая        принимает  в          качестве       первого        параметра значение предназначенного          для     преобразования   элемента     из        последовательности, а          в качестве   второго        параметра   —        элемент        из        второй последовательности.     Обе    версии возвращают        итератор,     указывающий        на конец            результирующей  последовательности.

В   следующей программе используется         простая        функция преобразования   xform(),        которая возводит            в          квадрат        каждый элемент        списка.         Обратите     внимание    на       то,       что      результирующая последовательность       сохраняется            в          том     же       списке,         который содержал    исходную последовательность.

//  Пример        использования      алгоритма   transform.

#include <iostream>

#include <list> #include     <algorithm> using     namespace   std;

//  Простая        функция       преобразования.

int            xform(int      i)          {

 return            i           *          i;          //         квадрат        исходного   значения

}

int    main() {

 list<int>         x1;

 int       i;

 //         Помещаем  значения      в          список.

 for(i=0;          i<10;  i++)     x1.push_back(i);

 cout    <<       "Исходный  список          x1:      ";

 list<int>::       iterator          p          =          x1.begin();

 while(p          !=        x1.end())       {

  cout           <<       *p       <<       "          ";

  p++;

 }

 cout    <<       endl;

 //         Преобразование  списка           x1.

 p          =          transform(x1.begin(),       x1.end(),       x1.begin(),    xform);

 cout    <<       "Преобразованный         список          x1:      ";

 p          =          x1.begin();

 while(p          !=        x1.end())       {

  cout           <<       *p       <<       "          ";

  p++;

 }

 return            0;

}

При         выполнении           эта      программа генерирует  такие результаты.

Исходный         список          x1:      0          1          2          3          4          5          6          7          8     9

Преобразованный    список          x1:      0          1          4          9          16       25       36       49     64       81

Как          видите,         каждый        элемент        в          списке           x1        теперь     возведен     в          квадрат.

Исследование           алгоритмов

Описанные      выше алгоритмы  представляют        собой            только          малую часть  всего содержимого библиотеки       STL.    И         конечно       же,     вам    стоит самим           исследовать           другие          алгоритмы. Заслуживают         внимания многие,        например    set_union()  и          set_difference().    Они    предназначены для обработки   содержимого        такого           контейнера,            как     множество. Интересны  также алгоритмы next_permutation()          и          prev_permutation().        Они создают       следующую            и          предыдущую перестановки    элементов   заданной последовательности.     Время,          затраченное           на       изучение алгоритмов библиотеки            STL,    —        это      время,          потраченное          не       зря!

Класс    string

Класс     string обеспечивает       альтернативу     для     строк           с     завершающим     нулем.

Как          вы       знаете,          C++     не       поддерживает       встроенный строковый   тип.    Однако         он предоставляет            два     способа        обработки строк.            Во-первых,  для     представления      строк можно использовать традиционный      символьный           массив          с          завершающим      нулем. Строки, создаваемые    таким            способом    (он      вам    уже    знаком),       иногда называют    С-строками.          Вовторых,    можно          использовать         объекты класса           string,            и          именно         этот    способ          рассматривается  в данном разделе.

В   действительности            класс string представляет         собой            специализацию более            общего шаблонного       класса           basic_string.            Существует две специализации     типа   basic_string:            тип     string, который     поддерживает       8-битовые       символьные           строки,         и          тип     wstring,        который поддерживает строки,  образованные       двухбайтовыми    символами.            Чаще всего в          обычном     программировании используются  строковые   объекты типа   string.            Для    использования      строковых   классов        C++ необходимо включить     в          программу заголовок    <string>.

Прежде чем    рассматривать       класс string,            важно           понять,         почему он       является       частью          С++библиотеки.    Стандартные          классы          не сразу были добавлены в          определение         языка            C++.   На самом    деле каждому      нововведению      предшествовали  серьезные   дискуссии   и          жаркие споры. При том,   что      C++     уже    содержит     поддержку строк в          виде  массивов с          завершающим      нулем, включение           класса           string в          C++,   на первый         взгляд,         может           показаться  исключением        из        этого правила. Но       это      далеко          не       так.     И         вот      почему:        строки           с завершающим      нулем           нельзя обрабатывать     стандартными       С++-операторами,        и          их        нельзя          использовать         в          обычных С++выражениях.  Рассмотрим,          например,   следующий            фрагмент     кода.

char         s1        [80],   s2[80],           s3[80]; s1 =          "один";         //         так      делать     нельзя s2     =          "два";            //     так      делать          нельзя

s3 =          s1        +          s2;       //         ошибка

Как          отмечено     в          комментариях,      в          C++     невозможно использовать         оператор     присваивания для          придания    символьному массиву        нового          значения      (за      исключением        инструкции инициализации),  а          также            нельзя          применять  оператор     "+"      для конкатенации        двух   строк.            Эти операции        можно          выполнить  с помощью    библиотечных       функций.

strcpy(s1,          "one"); strcpy(s2,     "two"); strcpy(s3,  s1);

strcat(s3,           s2);

Поскольку        символьный           массив          с          завершающим      нулем формально не       является самостоятельным     типом            данных,        к          нему нельзя          применить  С++-операторы.    Это     лишает "изящества"     даже  самые элементарные       операции     со        строками.    И         именно         неспособность обрабатывать        строки           с          завершающим      нулем           с          помощью стандартных           С++-операторов привела         к          разработке стандартного строкового  класса.          Вспомните: создавая      класс в          C++,   мы определяем новый           тип     данных,        который       можно          полностью  интегрировать       в С++-среду.  Это, конечно          же,     означает,     что      для     нового          класса можно          перегружать           операторы. Следовательно, вводя  в          язык стандартный          строковый   класс,            мы      создаем       возможность         для обработки   строк так      же,     как     и          данных         любого         другого        типа,  а именно         посредством          операторов.

Однако  существует  еще    одна  причина,      оправдывающая  создание стандартного         класса string:         безопасность.        Руками         неопытного            или неосторожного     программиста       очень            легко обеспечить выход           за границы       массива,      который       содержит     строку           с          завершающим нулем. Рассмотрим,       например,   стандартную          функцию      копирования strcpy().        Эта      функция       не предусматривает       механизм    проверки     факта нарушения  границ          массива-приемника.      Если исходный      массив          содержит больше         символов,   чем    может           принять        массив-приемник,           то        в результате   этой   ошибки        очень            вероятен      полный        отказ системы.      Как будет показано     ниже, стандартный         класс string не       допускает    возникновения подобных    ошибок.

Итак,       существует  три     причины      для     включения  в          C++     стандартного класса           string: непротиворечивость     данных         (строка         теперь определяется        самостоятельным            типом            данных), удобство (программист        может           использовать         стандартные          С++-операторы)    и безопасность (границы массивов     отныне         не       будут нарушаться).          Следует иметь            в          виду, что      все      выше перечисленное     не       означает,     что      вы должны       отказываться         от        использования      обычных      строк с завершающим       нулем.          Они    по-прежнему         остаются      самым эффективным        средством реализации  строк.            Но       если   скорость      не является       для     вас      определяющим    фактором, использование       нового класса           string даст   вам    доступ          к          безопасному          и          полностью интегрированному          способу        обработки   строк.

И  хотя    класс string традиционно         не       воспринимается   как     часть библиотеки            STL,    он,      тем не           менее,          представляет         собой            еще один  контейнерный       класс,            определенный      в          C++.   Это означает,         что он       поддерживает       алгоритмы, описанные  в          предыдущем         разделе.      При этом строки            имеют           дополнительные  возможности.        Для    получения доступа        к          классу           string необходимо           включить     в          программу заголовок    <string>.

Класс      string очень            большой,     он       содержит     множество  конструкторов и          функций-членов. Кроме           того,  многие         функции-члены    имеют несколько   перегруженных    форм.            Поскольку   в одной        главе невозможно рассмотреть           все      содержимое          класса           string,            мы      обратим ваше внимание    только          на       самые           популярные           его      средства. Получив       общее           представление      о          работе класса        string,            вы сможете       легко разобраться           в          остальных   его      возможностях самостоятельно.

Прототипы       трех    самых           распространенных          конструкторов       класса string имеют следующий          вид.

string(); string(const  char     *str);

string      (const            string &str);

Первая   форма           конструктора         создает         пустой           объект          класса string.            Вторая          форма           создает string-объект     из        строки           с завершающим      нулем,          адресуемой            параметром           str.      Эта      форма конструктора         обеспечивает         преобразование   из        строки           с завершающим      нулем           в          объект          типа string. Третья           создает string-объект          из        другого        string-объекта.

Для         объектов      класса           string определены           следующие операторы.

Эти           операторы  позволяют  использовать         объекты       типа   string в обычных      выражениях           и избавляют           программиста       от        необходимости вызывать     такие функции,     как     strcpy()         или strcat().            В          общем случае           в          выражениях           можно          смешивать  string-объекты       и строки           с завершающим   нулем.          Например,  string-объект          можно присвоить   строке           с          завершающим нулем.

Оператор          "+"      можно          использовать         для     конкатенации        одного string-объекта        с          другим         или string-объекта           со        строкой, созданной   в          С-стиле       (С-строкой).            Другими      словами, поддерживаются следующие операции. string-объект            +          string-объект string-объект      +          С-строка

С-строка            +          string-объект

Оператор         "+"      позволяет    также            добавлять   символ         в          конец     строки.

В   классе           string определена            константа    npos,  которая        равна            -1. Она    представляет         размер строки      максимально         возможной длины.

Строковый       класс C++     существенно          облегчает    обработку   строк.     Например,  используя string-объекты,       можно          применять  оператор     присваивания        для     назначения             string-объекту строки    в          кавычках,     оператор     "+"      —        для     конкатенации        строк и          операторы     сравнения   —        для сравнения       строк.            Выполнение           этих    операций     демонстрируется в          следующей программе.

//  Программа демонстрации       обработки   строк.

#include <iostream> #include <string> using     namespace   std;

int    main() {

 string  str1("Класс  string позволяет    эффективно            ");

 string  str2("обрабатывать         строки.");

 string  str3;

 //         Присваивание       string-объекта.

 str3     =          str1;

 cout    <<       str1     <<       "\n"    <<       str3     <<       "\n";

 //         Конкатенация        двух   string-объектов.

 str3     =          str1     +          str2;

 cout    <<       str3     <<       "\n";

 //         Сравнение   string-объектов.

 if(str3 >          str1)   cout    <<       "str3   >          str1\n";

 if(str3 ==       str1     +          str2)

  cout           <<       "str3   ==       str1+str2\n";

 /*        Объекту       класса           string можно          также            присвоить   обычную     строку.

*/

 str1     =          "Это   строка           с          завершающим      нулем.\n";

 cout    <<       str1;

 /*        Создание     string-объекта        с          помощью    другого        string-объекта.

*/

 string  str4     (str1);

 cout    <<       str4;

 //         Ввод  строки.

 cout    <<       "Введите      строку:          ";

  cin  >>       str4;

 cout    <<       str4;

 return            0;

}

При         выполнении           эта      программа генерирует  такие результаты.

Класс      string позволяет    эффективно

Класс      string позволяет    эффективно Класс           string позволяет     эффективно            обрабатывать        строки. str3 >          str1 str3     ==       str1+str2 Это           строка           с          завершающим      нулем.

Это          строка           с          завершающим      нулем.

Введите строку:          Привет

Привет

Обратите           внимание    на       то,       как     легко теперь           выполняется обработка   строк.            Например, оператор      "+"      используется         для конкатенации        строк,            а          оператор     ">"      —        для     сравнения   двух строк.            Для    выполнения           этих    операций     с          использованием  С-стиля обработки   строк,            т.е. использования          строк с          завершающим      нулем, пришлось    бы      применять  менее            удобные средства,          а          именно вызывать     функции       strcat()          и          strcmp().       Поскольку   С++-объекты типа   string можно          свободно     смешивать  с          С-строками,          их        (string-объекты)      можно          использовать         в          любой программе           не       только безо   всякого         ущерба         для     эффективности,     но       даже  с          заметным выигрышем.

Важно    также            отметить,     что      в          предыдущей          программе размер строк не       задается.     Объекты типа        string автоматически      получают     размер, нужный        для     хранения     заданной     строки.         Таким образом,   при выполнении           операций     присваивания        или     конкатенации        строк строкаприемник   увеличится  по       длине            настолько,  насколько   это необходимо           для     хранения     нового содержимого     строки.         При обработке   string-объектов     невозможно           выйти            за        границы       строки. Именно        этот    динамический       аспект           string-объектов     выгодно       отличает их        от        строк с завершающим   нулем           (которые      часто страдают     от нарушения  границ).

Обзор   функций-членов  класса          string

Если        самые           простые       операции     со        строками     можно реализовать           с          помощью    операторов, то     при     выполнении           более сложных      не       обойтись      без     функций-членов   класса           string.            Класс string содержит     слишком      много            функций-членов,  мы      же       рассмотрим здесь только          самые употребительные          из        них.

Важно!  Поскольку   класс string—         контейнер,            он       поддерживает такие           обычные контейнерные         функции,     как     begin(),         end()  и size().

Основные       манипуляции        над     строками

Чтобы     присвоить   одну  строку           другой,         используйте           функцию assign().        Вот     как     выглядят два         возможных формата       ее        реализации.

string      &assign(const          string &strob,          size_type      start,  size_type num);

string      &assign(const          char    *str,   size_type      num);

Первый  формат         позволяет    присвоить   вызывающему      объекту        num символов    из        строки, заданной параметром           strob, начиная        с          индекса start.  При    использовании     второго        формата вызывающему           объекту присваиваются      первые         num   символов    строки           с          завершающим нулем, заданной  параметром           str.      В          каждом        случае           возвращается ссылка          на       вызывающий         объект. Конечно, гораздо        проще           для присвоения            одной           полной         строки           другой          использовать оператор      "=".    О         функции-члене     assign()         вспоминают,          в основном,   тогда,            когда нужно присвоить только          часть  строки.

С   помощью    функции-члена     append()      можно          часть  одной           строки присоединить        в          конец другой.       Два     возможных формата       ее реализации            имеют           следующий            вид.

string      &append(const       string &strob,          size_type      start,  size_type num);

string      &append(const       char    *str,   size_type      num);

Здесь      при     использовании     первого        формата       num   символов    из строки,         заданной параметром  strob, начиная        с          индекса        start,  будет присоединено       в          конец            вызывающего объекта.             Второй          формат позволяет    присоединить        в          конец            вызывающего       объекта        первые num символов      строки           с          завершающим      нулем,          заданной параметром           str.      В          каждом        случае возвращается      ссылка          на вызывающий         объект.         Конечно,      гораздо        проще           для     присоединения одной            полной         строки           в          конец            другой          использовать оператор     Функция       же       append() применяется   тогда,            когда необходимо присоединить        в          конец            вызывающего       объекта        только часть строки.

Вставку  или     замену          символов    в          строке           можно          выполнять  с помощью    функций-членов insert()           и          replace().      Вот     как     выглядят прототипы  их        наиболее     употребительных             форматов.

string      &insert(size_type   start,  const  string &strob);

string      &insert(size_type   start,  const  string &strob,          size_type

insStart,        size_type      num);

string      &replace(size_type           start,  size_type      num,  const  string

&strob);

string      &replace(size_type           start,  size_type      orgNum,       const  string

&strob,          size_type      replaceStart,            size_type      replaceNum);

Первый  формат         функции       insert()          позволяет    вставить       строку, заданную    параметром           strob, в         позицию      вызывающей         строки, заданную    параметром           start.  Второй          формат         функции insert() предназначен        для     вставки         num   символов    из        строки,         заданной параметром           strob, начиная       с          индекса        insStart,        в          позицию вызывающей         строки,         заданную    параметром           start.

Первый  формат         функции       replace()       служит          для     замены         num символов    в          вызывающей строке,     начиная        с          индекса        start,  строкой, заданной     параметром           strob. Второй          формат позволяет           заменить orgNum        символов    в          вызывающей         строке,          начиная        с          индекса start, replaceNum символами строки,         заданной     параметром           strob, начиная с          индекса        replaceStart. В       каждом        случае           возвращается        ссылка на       вызывающий         объект.

Удалить символы      из        строки           можно          с          помощью    функции erase().         Один из        ее        форматов выглядит        так:

string      &erase(size_type   start   =          0,        size_type      num   =          npos);

Эта           функция       удаляет        num   символов    из        вызывающей         строки, начиная        с          индекса        start. Функция       возвращает             ссылку          на вызывающий         объект.

Использование          функций       insert(),         erase()           и          replace() демонстрируется в          следующей программе.

// Демонстрация       использования      функций       insert(),         erase()           и replace().

#include <iostream> #include <string> using     namespace   std;

int    main() {

 string  str1("Это      простой        тест.");

 string  str2("ABCDEFG");

 cout    <<       "Исходные  строки:\n";

 cout    <<       "str1: "          <<       str1     <<       endl;

 cout    <<       "str2: "          <<       str2     <<       "\n\n";

 //         Демонстрируем    использование      функции       insert().

 cout    <<       "Вставляем строку           str2     в          строку           str1:\n";

 str1.insert(5,            str2);

 cout    <<       str1     <<       "\n\n";

 //         Демонстрируем    использование      функции       erase().  cout         <<       "Удаляем    7          символов    из        строки     str1:\n";

 str1.erase(5,            7);

 cout    <<       str1     <<"\n\n";

 //         Демонстрируем    использование      функции       replace().

 cout    <<       "Заменяем  2          символа       в          str1     строкой        str2:\n";

 str1.replace(5,        2,        str2);

 cout    <<       str1     <<       endl;

 return            0;

}

Результаты      выполнения           этой   программы таковы.

Исходные         строки: str1:        Это     простой     тест. str2:     ABCDEFG

Вставляем        строку           str2     в          строку           str1:

Это          пABCDEFGростой  тест.

Удаляем           7          символов    из        строки           str1:

Это          простой        тест.

Заменяем         2          символа       в          str1     строкой        str2:

Это          пABCDEFGстой       тест.

Поиск   в          строке

В   классе           string предусмотрено     несколько   функций-членов,  которые осуществляют        поиск. Это,  например,   такие функции,     как     find()  и          rfind(). Рассмотрим            прототипы  самых употребительных           версий          этих    функций.

size_type           find(const     string &strob,          size_type      start=0)         const;

               size_type               rfind(const               string             &strob,             size_type     start=npos)

const;

Функция            find(), начиная        с          позиции       start,  просматривает вызывающую        строку           на       предмет поиска   первого        вхождения  строки, заданной     параметром           strob. Если   поиск успешен, функция           find() возвращает             индекс,         по       которому     в          вызывающей         строке было обнаружено совпадение.        Если   совпадения            не       обнаружено, возвращается        значение      npos.  Функция       rfind() выполняет то        же       действие, но       с          конца.           Начиная       с          позиции       start,  она     просматривает вызывающую        строку           в          обратном    направлении          на       предмет поиска          первого        вхождения  строки, заданной параметром           strob  (т.е.    она находит        в          вызывающей         строке           последнее   вхождение строки, заданной     параметром           strob).           Если   поиск прошел        удачно,        функция rfind()            возвращает индекс,       по       которому     в          вызывающей         строке было обнаружено           совпадение.           Если   совпадения не      обнаружено, возвращается        значение      npos.

Рассмотрим    короткий     пример         использования      функции       find().

#include <iostream> #include <string> using     namespace   std;

int    main() {

 int       i;

 string  s1        ="Класс        string облегчает    обработку   строк.";

 string  s2;

 i           =          s1.find("string");

 if(i       !=        string::npos)            {

  cout           <<       "Совпадение          обнаружено           в          позиции       "          <<     i<<      endl;

  cout           <<       "Остаток      строки           таков:            ";

  s2.assign  (s1,     i,          s1.size());

  cout           <<       s2;

 }

 return            0;

}

Программа      генерирует  такие результаты.

Совпадение     обнаружено           в          позиции       6

Остаток строки           таков:            string облегчает    обработку   строк.

Сравнение      строк

Чтобы     сравнить      полное         содержимое          одного          string-объекта        с другим,        обычно используются   описанные  выше перегруженные    операторы отношений.            Но       если   нужно сравнить    часть  одной           строки           с другой,         вам    придется      использовать         функцию-член       compare().

int            compare(size_type            start,  size_type      num,  const  string &strob)

const;

Функция            compare()    сравнивает  с          вызывающей         строкой        num символов    строки,         заданной параметром  strob, начиная        с          индекса start.  Если   вызывающая         строка           меньше        строки           strob, функция compare()    возвратит    отрицательное      значение.    Если   вызывающая         строка больше строки      strob, она     возвратит    положительное    значение.    Если   строка strob  равна            вызывающей строке,     функция       compare()    возвратит    нуль.

Получение     строки         с          завершающим     нулем

Несмотря          на       неоспоримую        полезность объектов      типа   string, возможны   ситуации,     когда вам    придется      получать      из        такого           объекта символьный           массив          с          завершающим      нулем,          т.е.      его версию С-строки.          Например,  вы       могли            бы      использовать         string-объект          для создания     имени файла.        Но,     открывая     файл, вам    нужно           задать указатель    на       стандартную          строку           с завершающим   нулем.          Для решения      этой   проблемы   и          используется         функция-член        c_str().          Вот как      выглядит     ее        прототип:

const       char    *c_str()         const;

Эта           функция       возвращает             указатель    на       С-версию     строки           (т.е. на       строку           с          завершающим нулевым          символом), которая содержится            в          вызывающем        объекте        типа   string.            Полученная строка           с          завершающим      нулем           изменению             не       подлежит. Кроме           того,  после выполнения других        операций     над     этим   string-объектом допустимость        применения           полученной            С-строки не           гарантируется.

Хранение        строк           в          других          контейнерах

Поскольку        класс string определяет             тип     данных,        можно          создать контейнеры,           которые будут      содержать   объекты       типа   string. Рассмотрим,          например,   более            удачный      вариант программы-словаря, которая        была  показана      выше.

/* Использование     отображения         string-объектов     для     создания словаря.

*/

#include <iostream>

#include <map> #include     <string> using     namespace   std;

int    main() {

 map<string, string>           dictionary;

 dictionary.insert(   pair<string,  string>           ("дом",         "Место проживания."));

 dictionary.insert(   pair<string,  string>           ("клавиатура", "Устройство     ввода            данных."));

 dictionary.insert(   pair<string,  string>           ("программирование", "Процесс            создания     программы."));

 dictionary.insert(   pair<string,  string>           ("STL",           "Standard Template            Library"));

 string  s;

 cout    <<       "Введите      слово:           ";

  cin  >>       s;

 map<string, string>::iterator      p;

 p          =          dictionary.find(s);

 if(p      !=        dictionary.end())

  cout           <<       "Определение:     "          <<       p->second;

 else     cout    <<       "Такого         слова в          словаре        нет.\n";

 return            0;

}

И еще    об       STL

Библиотека      STL      —        важная         составляющая       языка            C++.   Многие задачи          программирования можно     описать,       используя   терминологию      STL. Эта      библиотека             великолепно          сочетает       силу своих  средств         с гибкостью   их        применения.          Несмотря     на       то        что      ее        синтаксис немного сложноват,       он       быстро          осваивается.           Ни       один  уважающий себя   С++-программист            не       может пренебречь          возможностями библиотеки            STL,    поскольку   она     —        не       только          настоящее,  но       и будущее       С++-программирования.

Глава     22:  Препроцессор    C++

Заключительная        глава книги посвящена  описанию    препроцессора     C++. Препроцессор C++          —        это      часть  компилятора,        которая        подвергает вашу  программу различным текстовым преобразованиям             до       реальной трансляции исходного   кода  в          объектный. Программист         может давать препроцессору     команды,    называемые           директивами       препроцессора (preprocessor directives),           которые,      не       являясь        формальной          частью языка            C++,   способны    расширить  область действия             его      среды программирования.

Препроцессор            C++     включает     следующие директивы.

Как          видите,         все      директивы  препроцессора     начинаются            с символа       '#'.Теперь    рассмотрим каждую      из        них     в          отдельности.

На            заметку.      Препроцессор       C++     —        прямой         потомок препроцессора     С,        и          некоторые его средства        оказались   избыточными после введения     в          C++     новых            элементов.            Однако         он попрежнему          является     важной        частью        С++-среды   программирования.

Директива    #define        

Директива        #define          определяет             имя    макроса.

Директива        #define          используется         для     определения идентификатора   и          символьной последовательности,   которая        будет подставлена           вместо          идентификатора   везде,           где     он встречается      в исходном    коде  программы.           Этот    идентификатор     называется макроименем, а процесс    замены         —        макроподстановкой     (реализацией макрорасширения).        Общий          формат использования этой   директивы  имеет следующий            вид.

#define   макроимя   последовательность_символов

Обратите           внимание    на       то,       что      здесь нет      точки с          запятой. Заданная последовательность_символов        завершается           только символом   конца            строки.         Между элементами        макроимя   (имя_макроса) и          последовательность_символов     может           быть  любое количество пробелов.

Итак,       после включения  этой   директивы  каждое         вхождение  текстового фрагмента, определенное       как     макроимя,  заменяется заданным   элементом последовательность_символов.    Например,  если   вы       хотите           использовать слово UP       в          качестве значения           1          и          слово DOWN           в          качестве значения      0,        объявите      такие директивы  #define.

#define   UP       1

#define   DOWN           0

Данные  директивы  вынудят       компилятор            подставлять           1          или     0 каждый        раз,    когда в          файле исходного кода  встретится   слово UP       или DOWN           соответственно.    Например,  при выполнении  инструкции:

cout         <<       UP       <<       '           '           <<       DOWN           <<       '           '           <<       UP     +          UP;

На            экран будет выведено    следующее:

1   0          2

После     определения         имени           макроса       его      можно          использовать как     часть  определения других      макроимен.            Например,  следующий            код определяет             имена           ONE,   TWO   и          THREE            и соответствующие         им значения.

#define   ONE    1

#define   TWO  ONE+ONE

#define   THREE            ONE+TWO

Важно    понимать,   что      макроподстановка       —        это      просто          замена идентификатора соответствующей  строкой.       Следовательно,    если   вам    нужно определить            стандартное сообщение,          используйте           код,   подобный этому.

#define   GETFILE         "Введите      имя    файла"

     //         ...

Препроцессор            заменит        строкой        "Введите    имя    файла"         каждое вхождение идентификатора   GETFILE.        Для    компилятора         эта      cout-инструкция

cout        <<       GETFILE; в     действительности     выглядит     так.

cout         <<       "Введите      имя    файла";

Никакой            текстовой    замены         не       произойдет,           если   идентификатор находится    в          строке, заключенной     в          кавычки.      Например,  при выполнении           следующего           кода #define           GETFILE         "Введите      имя файла"  //            ...

cout        <<       "GETFILE       -           это     макроимя\n"; на  экране          будет     отображена            эта      информация

GETFILE  -           это     макроимя, а          не     эта:

Введите имя    файла            -           это      макроимя

Если        текстовая     последовательность       не       помещается            на       строке, ее        можно          продолжить           на следующей,     поставив      обратную    косую черту в          конце            строки,         как     показано     в          этом   примере.

#define   LONG_STRING        "Это   очень            длинная       последовательность,\

 которая        используется         в          качестве       примера."

Среди     С++-программистов        принято        использовать         для     макроимен прописные  буквы.          Это соглашение    позволяет    с          первого        взгляда понять,         что      здесь используется         макроподстановка. Кроме      того,  лучше всего поместить   все      директивы  #define          в          начало          файла            или включить     в отдельный          файл, чтобы            не       искать           их        потом            по всей   программе.

Макроподстановки  часто используются        для     определения         "магических чисел"           программы. Например,            у          вас      есть    программа,            которая определяет             некоторый  массив,         и          ряд     функций, которые           получают доступ          к          нему. Вместо          "жесткого"           кодирования         размера массива        с помощью константы   лучше           определить            имя,   которое        бы представляло        размер,        а          затем использовать        это      имя    везде,           где должен        стоять            размер         массива.      Тогда,           если   этот    размер придется      изменить,    вам    достаточно будет внести           только          одно изменение,             а          затем перекомпилировать      программу.            Рассмотрим пример. #define   MAX_SIZE    100

 //         ...

float        balance[MAX_SIZE]; double     index[MAX_SIZE];

int            num_emp[MAX_SIZE];

Важно!  Важно          помнить,    что    в          C++     предусмотрен     еще    один способ          определения констант,         который     заключается        в использовании     спецификатора   const. Однако         многие программисты "пришли"    в          C++     из        С-среды,      где     для     этих  целей            обычно использовалась директива   #define.        Поэтому     вам    еще    часто           придется с          ней     сталкиваться      в          С++-коде.

Макроопределения,         действующие      как     функции

Директива        #define          имеет            еще    одно  назначение:            макроимя может           использоваться     с аргументами.     При    каждом        вхождении макроимени          связанные   с          ним    аргументы  заменяются реальными аргументами,        указанными           в          коде  программы.           Такие макроопределения действуют          подобно      функциям.  Рассмотрим            пример.

/* Использование     "функциональных"         макроопределений.     */ #include   <iostream> using   namespace   std; #define MIN(a,     b)        (((a)<(b))      ?          a          :           b)

int            main()

{

 int       x,         y;

x        =         10;

y       =         20;

 cout    <<       "Минимум  равен:           "          <<       MIN(x,           у);

 return            0;

}

При         компиляции           этой   программы выражение,            определенное идентификатором           MIN    (а, b), будет заменено,   но       x          и          y          будут рассматриваться  как     операнды.  Это     значит,          что      coutинструкция     после компиляции           будет выглядеть   так.

cout         <<       "Минимум  равен:           "          <<       (((х)<(у))       ?          х          :           у);

По            сути,  такое макроопределение         представляет         собой            способ определить            функцию, которая           вместо          вызова          позволяет    раскрыть свой   код     в          строке.

Макроопределения,          действующие       как     функции,    —        это макроопределения,       которые принимают  аргументы.

Кажущиеся       избыточными        круглые       скобки,         в          которые заключено  макроопределение         MIN, необходимы,          чтобы            гарантировать правильное            восприятие компилятором      заменяемого выражения.       На самом           деле  дополнительные  круглые       скобки          должны       применяться практически           ко       всем  макроопределениям,    действующим        подобно функциям.  Нужно           всегда очень          внимательно          относиться  к определению        таких макросов;   в          противном  случае возможно получение неожиданных        результатов.           Рассмотрим,          например,   эту      короткую программу, которая        использует  макрос         для     определения         четности значения.

//  Эта      программа дает   неверный    ответ. #include <iostream> using   namespace   std;

#define   EVEN(a)         a%2==0        ?          1     :           0 int    main()

{

 if(EVEN(9+1))          cout    <<       "четное         число";

 else     cout    <<       "нечетное    число ";

 return            0;

}

Эта           программа не       будет работать      корректно,  поскольку   не обеспечена             правильная подстановка         значений.    При    компиляции выражение EVEN(9+1)   будет заменено     следующим образом.

9+1%2==0        ?          1          :           0

Напомню,         что      оператор     "%"     имеет            более            высокий приоритет,  чем    оператор     "+".    Это значит, что      сначала        выполнится операция     деления       по       модулю       (%)      для     числа 1,        а          затем ее результат     будет сложен         с          числом         9,        что      (конечно      же)     не равно            0.        Чтобы           исправить    ошибку, достаточно       заключить   в круглые       скобки          аргумент      a          в          макроопределении        EVEN, как показано      в          следующей (исправленной)    версии          той     же       программы.

//  Эта      программа работает      корректно. #include <iostream> using   namespace   std; #define   EVEN(a)         (a)%2==0     ?          1     :           0

int    main() {

 if(EVEN(9+1))          cout    <<       "четное         число";

 else     cout    <<       "нечетное    число";

 return            0;

}

Теперь   сумма           9+1     вычисляется           до       выполнения           операции деления       по       модулю.      В          общем случае       лучше           всегда заключать   параметры  макроопределения         в          круглые       скобки,         чтобы избежать      непредвиденных результатов,           подобных    описанному           выше.

Использование          макроопределений        вместо          настоящих   функций имеет            одно  существенное достоинство:    поскольку   код     макроопределения расширяется          в          строке,          и          нет      никаких        затрат системных ресурсов      на       вызов            функции,     скорость      работы         вашей программы будет выше по сравнению        с          применением        обычной      функции. Но       повышение             скорости      является       платой          за увеличение       размера программы (из-за дублирования       кода  функции).

Важно!  Несмотря   на       то      что    макроопределения         все      еще встречаются       в          C++-коде, макросы,        действующие       подобно функциям,  можно          заменить   спецификатором           inline, который справляется         с          той   же      ролью           лучше           и          безопаснее. (Вспомните:          спецификатор inline     обеспечивает       вместо        вызова функции      расширение           ее        тела в          строке.)      Кроме           того, inline-функции       не       требуют    дополнительных            круглых       скобок,         без которых     не       могут обойтись макроопределения.       Однако         макросы, действующие       подобно       функциям,  все      еще остаются    частью        С++-программ,  поскольку   многие         С/С++-программисты  продолжают использовать       их        по       привычке.

Директива    #еrror           

Директива     #error            отображает        сообщение об       ошибке.

Директива        #error            дает   указание      компилятору         остановить компиляцию.         Она    используется в     основном    для     отладки.      Общий формат         ее        записи          таков.

#error     сообщение

Обратите           внимание    на       то,       что      элемент        сообщение не       заключен в          двойные      кавычки.      При встрече            с          директивой            #error отображается        заданное     сообщение и          другая          информация          (она зависит         от        конкретной             реализации            рабочей       среды),         после чего компиляция           прекращается. Чтобы    узнать,          какую            информацию отображает             в          этом   случае           компилятор,          достаточно провести эксперимент.

Директива    #include

Директива        #include        включает     заголовок    или     другой          исходный     файл.

Директива        препроцессора     #include        обязывает   компилятор            включить либо  стандартный заголовок,           либо  другой          исходный    файл, имя    которого указано        в          директиве   #include.       Имя стандартных заголовков заключается в          угловые       скобки,         как     показано     в          примерах, приведенных          в этой   книге.            Например,  эта      директива

#include <vector>

Включает          стандартный          заголовок    для     векторов.

При         включении  другого        исходного   файла            его      имя    может быть  указано        в          двойных кавычках          или     угловых        скобках. Например,  следующие две     директивы  обязывают  C++ прочитать       и скомпилировать   файл  с          именем        sample.h:

#include <sample.h>

#include "sample.h"

Если        имя    файла            заключено  в          угловые       скобки,         то        поиск файла            будет осуществляться     в одном       или     нескольких специальных каталогах,   определенных      конкретной             реализацией.

Если        же       имя    файла            заключено  в          кавычки,      поиск файла     выполняется,         как     правило,      в текущем   каталоге       (что    также     определено            конкретной             реализацией).       Во       многих          случаях     это означает           поиск текущего      рабочего      каталога.     Если   заданный     файл  не       найден,        поиск повторяется           с          использованием  первого     способа        (как    если   бы      имя    файла            было заключено  в угловые     скобки).       Чтобы           ознакомиться        с          подробностями,   связанными     с          различной обработкой директивы  #include        в          случае     использования      угловых        скобок          и          двойных      кавычек, обратитесь     к          руководству           пользователя,       прилагаемому      к          вашему     компилятору.        Инструкции #include      могут быть  вложенными         в     другие          включаемые          файлы.

Директивы   условной     компиляции

Существуют     директивы, которые       позволяют  избирательно        компилировать части исходного   кода. Этот    процесс,       именуемый            условной     компиляцией, широко        используется коммерческими           фирмами     по       разработке программного      обеспечения,         которые       создают       и поддерживают множество  различных  версий          одной           программы.

Директивы   #if,      #else, #elif   и          #endif

Директивы     #if,      #ifdef,           #ifndef,         #else, #elif   и         #endif           —        это            директивы условной компиляции.

Главная идея  состоит         в          том,   что      если   выражение,            стоящее после директивы  #if оказывается     истинным,   то        будет скомпилирован    код, расположенный   между          нею    и директивой         #endif            в          противном случае           данный        код     будет опущен.       Директива   #endif используется        для обозначения          конца            блока            #if.

Общая   форма           записи          директивы  #if       выглядит     так.

#if константное_выражение

 последовательность       инструкций

#endif

Если        константное_выражение     является       истинным,   код, расположенный непосредственно  за        этой   директивой,           будет скомпилирован.   Рассмотрим            пример.

//  Простой       пример         использования      директивы     #if. #include             <iostream> using   namespace   std;

#define   MAX     100 int     main() {

 #if       MAX>10

  cout           <<       "Требуется  дополнительная   память\n";

 #endif

 //         ...

 return            0;

}

При         выполнении           эта      программа отобразит    сообщение  Требуется дополнительная память       на       экране,         поскольку,  как     определено            в программе,            значение      константы   МАХ больше          10.      Этот    пример иллюстрирует        важный        момент:       Выражение,            которое        стоит  после директивы  #if,      вычисляется           во       время            компиляции.          Следовательно, оно     должно        содержать только           идентификаторы,             которые       были предварительно   определены,          или     константы. Использование      же переменных           здесь исключено.

Поведение       директивы  #else  во       многом        подобно      поведению инструкции else,    которая является частью          языка            C++:   она     определяет альтернативу         на       случай          невыполнения директивы      #if.      Чтобы показать,     как     работает      директива   #else, воспользуемся      предыдущим примером,  немного       его      расширив.

//  Пример        использования      директив     #if/#else. #include <iostream> using   namespace   std;

#define     MAX   6 int     main() {

 #if       MAX>10

  cout           <<       "Требуется  дополнительная   память.\n");

 #else

  cout           <<       "Достаточно           имеющейся            памяти.\n";

 #endif

 //         .           .           .

 return            0;

}

В   этой   программе для     имени           МАХ   определено            значение,    которое меньше        10,      поэтому       #ifветвь        кода  не       скомпилируется,  но       зато скомпилируется   альтернативная    #else-ветвь. В результате           отобразится сообщение  Достаточно         имеющейся            памяти..

Обратите           внимание    на       то,       что      директива   #else  используется         для индикации одновременно      как     конца            #if-блока,    так      и          начала #еlse-блока.            В          этом   есть    логическая необходимость,    поскольку   только одна  директива   #endif            может           быть  связана         с          директивой

#if.

Директива        #elif    эквивалентна         связке           инструкций else-if и используется         для формирования         многозвенной       схемы           if-else-if, представляющей несколько   вариантов компиляции.           После            директивы  #elif должно        стоять            константное           выражение.            Если   это выражение истинно,       следующий            блок  кода  скомпилируется,  и          никакие        другие #elifвыражения     не       будут тестироваться        или     компилироваться.           В противном  случае           будет проверено  следующее по       очереди       #elif-выражение.            Вот     как     выглядит     общий           формат использования директивы  #elif.

#if выражение

 последовательность       инструкций

#elif         выражение 1

 последовательность       инструкций

#elif         выражение 2

 последовательность       инструкций

#еlif         выражение 3

 последовательность       инструкций

//  .           .           .

#elif         выражение N

 последовательность       инструкций

#endif

Например,       в          этом   фрагменте   кода  используется         идентификатор COMPILED_BY,       который позволяет         определить,           кем    компилируется программа.

#define   JOHN 0

#define   BOB    1

#define   TOM   2

#define   COMPILED_BY        JOHN

#if COMPILED_BY        ==       JOHN

 char    who[] =          "John";

#elif         COMPILED_BY        ==       BOB

 char    who[] =          "Bob";

#else

 char    who[] =          "Tom";

#endif

Директивы       #if       и          #elif    могут быть  вложенными.        В          этом   случае директива   #endif,           #else  или #elif       связывается            с          ближайшей директивой            #if       или     #elif.   Например,  следующий            фрагмент кода совершенно           допустим.

#if COMPILED_BY        ==       BOB

 #if       DEBUG          ==       FULL

  int  port    =          198;

 #elif    DEBUG          ==       PARTIAL

  int  port    =          200;

 #endif

#else

 cout    <<       "Боб   должен        скомпилировать   код"   <<       "для   отладки            вывода данных.\n";

#endif

Директивы   #ifdef и          #ifndef

Директивы       #ifdef и          #ifndef          предлагают            еще    два     варианта     условной     компиляции,          которые можно    выразить     как     "если     определено"          и          "если не       определено"          соответственно.

Общий   формат         использования      директивы  #ifdef таков.

#ifdef      макроимя

 последовательность       инструкций

#endif

Если        макроимя   предварительно   определено            с          помощью    какой-нибудь          директивы  #define, то   последовательность   инструкций, расположенная    между          директивами         #ifdef и          #endif, будет скомпилирована.

Общий   формат         использования      директивы  #ifndef          таков.

#ifndef   макроимя

 последовательность       инструкций

#endif

Если        макроимя   не       определено            с          помощью    какой-нибудь директивы  #define,        то последовательность         инструкций,          расположенная между          директивами         #ifdef и          #endif,           будет скомпилирована.

Как          директива   #ifdef,            так      и          директива   #ifndef          может иметь            директиву   #else  или     #elif. Рассмотрим пример. #include  <iostream> using  namespace   std; #define TOM

int    main() {

 #ifdef TOM

  cout           <<       "Программист       Том.\n";

 #else

  cout           <<       "Программист       неизвестен.\n";

 #endif

 #ifndef           RALPH

  cout           <<       "Имя  RALPH           не       определено.\n";

 #endif

 return            0;

}

При         выполнении           эта      программа отображает             следующее.

Программист  Том.

Имя         RALPH           не       определено.

Но            если   бы      идентификатор     ТОМ   был    не       определен, то        результат выполнения           этой программы  выглядел     бы      так.

Программист  неизвестен.

Имя         RALPH           не       определено.

И  еще.   Директивы  #ifdef и          #ifndef          можно          вкладывать            точно так     же,     как     и          директивы  #if.

Директива    #undef

Директива        #undef          используется         для     удаления     предыдущего определения         некоторого макроимени.         Ее        общий           формат         таков.

#undef   макроимя Рассмотрим    пример.

#define   TIMEOUT      100

#define   WAIT     0  //            .     .           .

#undef   TIMEOUT

#undef   WAIT

Здесь      имена           TIMEOUT      и          WAIT  определены           до       тех      пор, пока   не       выполнится            директива #undef.

Основное          назначение             директивы  #undef          —        разрешить локализацию         макроимен для     тех частей    кода, в          которых       они     нужны.

Использование        оператора             defined

Помимо            директивы  #ifdef существует  еще    один  способ          выяснить, определено            ли       в программе          некоторое   макроимя.  Для    этого  можно использовать         директиву   #if       в          сочетании    с оператором        времени компиляции           defined.        Например,  чтобы            узнать,          определено            ли макроимя MYFILE,           можно          использовать         одну  из        следующих команд препроцессорной            обработки.

#if defined          MYFILE или

#ifdef      MYFILE

При         необходимости,   чтобы            реверсировать      условие        проверки, можно          предварить оператор    defined          символом   "!".     Например, следующий            фрагмент     кода  скомпилируется только            в          том     случае, если   макроимя   DEBUG          не       определено.

#if !defined        DEBUG

 cout    <<       "Окончательная   версия!\n";

#endif

О            роли  препроцессора

Препроцессор            C++     —        прямой         потомок       препроцессора     языка С,        причем         без     каких-либо усовершенствований.    Однако         его      роль  в C++     намного       меньше        роли, которую       играет препроцессор     в          С. Дело  в          том,   что      многие         задачи,         выполняемые       препроцессором  в С, реализованы    в          C++     в          виде  элементов   языка.           Страуструп  тем самым          выразил       свое   намерение сделать        функции       препроцессора ненужными,           чтобы            в          конце            концов          от        него   можно было бы совсем   освободить             язык.

На            данном        этапе препроцессор        уже    частично      избыточен. Например,  два     наиболее употребительных    свойства       директивы  #define были заменены    инструкциями       C++.   В частности,            ее        способность создавать    константное           значение      и          определять             макроопределение, действующее         подобно      функциям,  сейчас           совершенно           избыточна. В C++     есть    более эффективные        средства      для     выполнения           этих    задач. Для    создания     константы   достаточно определить            const-переменную.         А с          созданием  встраиваемой        (подставляемой)  функции вполне   справляется спецификатор        inline. Оба    эти      средства      лучше           работают,    чем соответствующие механизмы директивы  #define.

Приведем         еще    один  пример         замены         элементов   препроцессора элементами            языка.           Он связан    с          использованием  однострочного комментария.        Одна из        причин         его      создания     — разрешить "превращение"     кода  в          комментарий.       Как     вы       знаете,          комментарий, использующий /*...*/-стиль,  не       может           быть  вложенным.           Это     означает, что      фрагменты  кода, содержащие /*...*/-комментарии,  одним           махом "превратить            в          комментарий"       нельзя.         Но       это      можно сделать     с //-комментариями,         окружив       их        /*...*/-символами           комментария. Возможность "превращения"            кода  в          комментарий         делает использование      таких директив     условной компиляции, как     #ifdef,            частично избыточным.

Директива    #line

Директива     #line   изменяет   содержимое          псевдопеременных         _     _LINE_           _          и          _          _FILE_           _.

Директива        #line   используется         для     изменения  содержимого псевдопеременных         _          _LINE_ _       и          _          _FILE_           _,        которые являются     зарезервированными    идентификаторами         (макроименами). Псевдопеременная         _          _LINE_           _          содержит     номер скомпилированной         строки,         а псевдопеременная      _          _FILE_           _—     имя компилируемого файла.          Базовая        форма           записи          этой команды имеет            следующий            вид.

#line        номер           "имя_файла"

Здесь      номер           —        это      любое           положительное    целое            число, а          имя_файла —        любой допустимый        идентификатор     файла.          Значение элемента     номер           становится  номером     текущей исходной          строки,         а значение      элемента     имя_файла—        именем        исходного   файла. Имя_файла —      элемент        необязательный.  Директива   #line   используется, главным      образом,     в          целях отладки       и          в          специальных приложениях.

Например,       следующая программа обязывает   начинать      счет    строк с числа 200. Инструкция   cout    отображает             номер           202,   поскольку   это      — третья           строка           в          программе после директивной         инструкции #line   200. #include        <iostream> using   namespace   std;

#line        200     //         Устанавливаем      счетчик         строк равным     200. int         main()            //         Эта      строка           сейчас     имеет            номер           200. {//         Номер           этой   строки     равен            201.

 cout    <<       _          _LINE_           _;//     Здесь выводится   номер           202.

 return            0;

}

Директива    #pragma

Директива     #pragma      зависит       от      конкретной           реализации     компилятора.

Работа    директивы  #pragma      зависит         от        конкретной             реализации компилятора.        Она позволяет      выдавать     компилятору         различные инструкции,            предусмотренные           создателем компилятора.       Общий формат         его      использования      таков.

#pragma            имя

Здесь     элемент        имя    представляет         имя    желаемой   #pragma-инструкции.     Если   указанное имя      не       распознается         компилятором,     директива     #pragma      попросту      игнорируется         без сообщения     об       ошибке.

Важно!  Для    получения   подробной  информации          о          возможных вариантах использования директивы    #pragma      стоит          обратиться          к системной документации     по       используемому    вами компилятору.      Вы можете      найти          для     себя   очень полезную    информацию.       Обычно #pragmaинструкции     позволяют определить,         какие предупреждающие сообщения выдает компилятор,   как     генерируется       код     и          какие библиотеки          компонуются       с          вашими программами.

Операторы  препроцессора     "#"      и          "##"  

В   C++     предусмотрена     поддержка двух   операторов             препроцессора:    "#" и          "##".  Эти операторы      используются        совместно   с          директивой #define.        Оператор     "#"      преобразует следующий         за        ним    аргумент      в строку,          заключенную        в          кавычки.      Рассмотрим,          например, следующую            программу. #include       <iostream> using   namespace   std;

#define   mkstr(s)     #          s int    main() {

 cout    <<       mkstr(Я         в          восторге       от        C++);

 return            0;

}

Препроцессор            C++     преобразует           строку

cout         <<       mkstr(Я         в          восторге       от        C++);

в   строку

cout         <<       "Я       в          восторге       от        C++";

Оператор         используется         для     конкатенации        двух   лексем.     Рассмотрим            пример.

#include <iostream> using     namespace   std; #define     concat(a,      b)        a     ##       b int   main() {

 int       xy        =          10;

 cout    <<       concat(x,       y);

 return            0;

}

Препроцессор            преобразует           строку

cout         <<       concat     (x,       y); в    строку

cout         <<       xy;

Если        эти      операторы  вам    кажутся        странными,            помните,      что      они не       являются     операторами "повседневного           спроса"        и          редко используются        в          программах.          Их       основное     назначение             — позволить    препроцессору     обрабатывать        некоторые  специальные         ситуации.

Зарезервированные     макроимена В      языке            C++     определено     шесть            встроенных             макроимен.

_   _LINE_           _

_   _FILE_           _

_   _DATE_         _

_   _TIME_         _

_   _STDC_         _

_   _cplusplus

Рассмотрим    каждое         из        них     в          отдельности.

Макросы           _          _LINE_           _          и          _          _FILE_           _          описаны       при рассмотрении        директивы  #line   выше в этой            главе.            Они    содержат номер           текущей       строки           и          имя    файла            компилируемой программы.

Макрос  _          _DATE_         _          представляет         собой            строку           в формате       месяц/день/год,   которая означает дату   трансляции исходного   файла в          объектный  код.

Время     трансляции исходного   файла            в          объектный  код     содержится в          виде  строки           в макросе    _          _TIME_         _.        Формат         этой   строки следующий:           часы.минуты.секунды.

Точное   назначение             макроса       _          _STDC_         _          зависит         от конкретной             реализации            компилятора. Как            правило,      если   макрос _          _STDC_         _          определен, то        компилятор            примет         только стандартный          С/С++-код,  который       не       содержит     никаких        нестандартных расширений.

Компилятор,   соответствующий             ANSI/ISO-стандарту         C++,   определяет макрос         _          _cplusplus как        значение,    содержащее           по       крайней мере  шесть            цифр.            "Нестандартные" компиляторы должны  использовать значение,    содержащее           пять   (или   даже  меньше)       цифр.

Мысли "под   занавес"

Мы          преодолели           немалый      путь:  длиной         в          целую           книгу. Если   вы       внимательно          изучили        все приведенные             здесь примеры,    то можете         смело            назвать         себя   программистом    на       C++. Подобно многим        другим         наукам,        программирование        лучше           всего осваивать на       практике, поэтому          теперь           вам    нужно           писать           побольше программ.  Полезно       также            разобраться           в          С++программах, написанных            другими       (причем       разными)    профессиональными программистами. При  этом   важно           обращать     внимание    на       то,       как программа оформлена и          реализована. Постарайтесь    найти             в          них     как достоинства,          так      и          недостатки.            Это     расширит     диапазон     ваших представлений      о          программировании.       Подумайте  также            над     тем,    как можно          улучшить существующий         код,   применив    контейнеры            и алгоритмы  библиотеки            STL.    Эти     средства,     как правило,          позволяют улучшить     читабельность       и          поддержку больших      программ.  Наконец, просто           больше         экспериментируйте!       Дайте            волю своей фантазии     и вскоре          вы       почувствуете себя           настоящим  С++-программистом!

Для          продолжения        теоретического     освоения     C++     предлагаю обратиться  к          моей  книге Полный       справочник по       C++,   М.       : Издательский        дом    "Вильямс".  Она    содержит     подробное описание элементов   языка            C++     и          библиотек.

Приложение       А:    С-ориентированная система ввода-вывода

Это           приложение           содержит     краткое        описание     С-системы   ввода-вывода.        Несмотря     на       то что            вы       предполагаете      использовать         С++-систему        ввода-вывода,      есть    ряд     причин,        по       которым вам         все-таки следует        понимать     основы         С-ориентированной        системы       ввода-вывода. Вопервых,   если   вам    придется      работать      с          С-кодом       (особенно,  если возникнет    необходимость его        перевода     в          С++-код),     то        вам    нужно знать,            как     работает      С-система    ввода-вывода.      Вовторых,    часто в одной           и          той     же       программе используются        как     С-,       так      и          С++-операции     вводавывода.        Это     особенно     характерно для     очень            больших программ,  отдельные  части которых писались           разными      программистами в течение        довольно    длительного          периода       времени.     Втретьих,     большое количество существующих      С-программ            продолжают          находиться в эксплуатации         и          нуждаются  в          поддержке.            Наконец,      многие книги и          периодические     издания содержат           программы,           написанные на       С.        Чтобы           понимать     эти      С-программы,       необходимо понимать основы         функционирования         С-системы   ввода-вывода.

Узелок   на       память.        Для    С++-программ      необходимо           использовать объектноориентированную            С++-систему         ввода-вывода.

В   этом   приложении           описаны       наиболее     употребительные             С-ориентированные            функции ввода-вывода.           Однако         стандартная           С-библиотека             содержит     такое огромное    количество функций      ввода-вывода, что      мы      не       в          силах рассмотреть           их        здесь в          полном        объеме. Если   же вам          придется      серьезно      погрузиться            в          С-программирование,       то        рекомендую          обратиться  к справочной литературе.

Система ввода-вывода       языка            С          требует         включать     в программы заголовочный       файл  stdio.h (ему соответствует         заголовок    <cstdio>, отвечающий           новому         стилю).         Каждая         С-программа должна использовать         заголовочный       файл  stdio.h,          поскольку   язык  С          не поддерживает       С++стиль      включения  заголовков.            С++-программа    может работать      с          использованием  любого         из этих          двух   вариантов. Заголовок   <cstdio>        помещает    свое   содержимое          в          пространство имен std,     а          заголовочный       файл  stdio.h—       в          глобальное пространство имен,            что      соответствует         Сориентации.         В          этом   приложении           в качестве       примеров    приведены С-программы,       поэтому они          используют С-стиль         включения  заголовочного      файла            stdio.h           и          не       требуют установки пространства            имен.

И  еще.   Как     отмечалось             в          главе 1,        стандарт      языка            С          был обновлен     в          1999  году   и          получил название           стандарта    С99.   В          то время            в          С-систему    ввода-вывода       было внесено        несколько усовершенствований.     Но       поскольку   C++     опирается    на       стандарт      С89,   то он       не       поддерживает средств,            которые       были добавлены в          стандарт С99.   (Более           того,  на       момент         написания   этой книги  ни       один  из широко        доступных   компиляторов       C++     не       поддерживал        стандарт      С99. Да       и ни   одна  из        широко        распространяемых          программ    не использовала        средства      стандарта    С99.) Поэтому        здесь не       описываются средства,     внесенные  в          С-систему    ввода-вывода       стандартом С99.  Если   же вас      интересует  язык  С,        включая       полное         описание     его      системы ввода-вывода       и средств,    добавленных         стандартом             С99,   я рекомендую          обратиться  к          моей  книге Полный справочник       по       С,        М.: Издательский        дом    "Вильямс".

Использование        потоков      в          С-системе  ввода-вывода

Подобно           С++-системе           ввода-вывода,      С-ориентированная         система ввода-вывода опирается         на       понятие        потока.         В          начале          работы программы автоматически      открываются          три заранее            определенных текстовых    потока:         stdinstdout            и          stderr.           Их       называют стандартными потоками       ввода            данных         (входной      поток),          вывода данных         (выходной  поток)           и          ошибок соответственно.           (Некоторые компиляторы        открывают  также            и          другие          потоки,         которые зависят         от        конкретной             реализации            системы.)    Эти     потоки представляют        собой            С-версии потоков cin,     cout    и          cerr соответственно.    По       умолчанию они     связаны        с          соответствующим системным  устройством.

Помните,          что      большинство         операционных      систем,         включая Windows,     позволяют перенаправлять    потоки          ввода-вывода,      поэтому функции       чтения           и          записи          данных         можно перенаправлять            на другие          устройства. Никогда       не       пытайтесь    явно   открывать   или закрывать эти        потоки.

Каждый поток,           связанный   с          файлом,       имеет            структуру управления            файлом        типа   FILE.   Эта структура         определена            в заголовочном       файле            stdio.h.          Вы      не       должны       модифицировать содержимое           этого  блока            управления            файлом.

Функции         printf()          и          scanf()

Двумя    самыми        популярными        С-функциями         ввода-вывода       являются printf()          и          scanf(). Функция   printf()          записывает данные         в стандартное           устройство  вывода         (консоль),   а          функция scanf(),   ее дополнение,          считывает    данные         с          клавиатуры.           Поскольку   язык  С не       поддерживает перегрузку      операторов             и          не       использует операторы  "<<"   и          ">>"   в          качестве       операторов ввода-вывода,     то        для консольного          ввода-вывода       используются        именно         функции       printf() и scanf().      Обе    они     могут обрабатывать        данные         любых          встроенных типов,           включая       символы, строки  и          числа.           Но       поскольку   эти функции       не       являются     объектно-ориентированными,          их нельзя использовать         непосредственно             для     ввода-вывода       объектов      классов, создаваемых программистом.

Функция     printf() Функция   printf()          имеет            такой     прототип:

int            printf(const  char    *fmt_string,            ...);

Первый  аргумент,    fmt_string,   определяет             способ          отображения         всех последующих аргументов.      Этот    аргумент      часто называют    строкой форматирования.          Она    состоит         из элементов         двух   типов:           текста и          спецификаторов   формата.     К          элементам  первого        типа относятся символы      (текст),          которые       выводятся   на       экран.           Элементы    второго типа (спецификаторы    формата)     содержат     команды     форматирования, которые       определяют           способ отображения      аргументов.            Команда форматирования  начинается  с          символа       процента,    за которым следует        код формата.     Спецификаторы    формата       перечислены         в          табл.  А.1. Количество аргументов должно        в          точности      совпадать    с          количеством          команд форматирования,             причем         совпадение            обязательно           и          в порядке       их        следования.           Например,  при вызове следующей функции printf().

printf      ("Привет      %с       %d      %s",    'с',       10,      "всем!");

На            экране          будет отображено:          "Привет      с          10       всем! ".

Функция            printf()          возвращает             число реально       выведенных символов.   Отрицательное значение         возврата      свидетельствует   об       ошибке.

Команды          формата       могут иметь            модификаторы,    которые       задают ширину        поля, точность (количество     десятичных             разрядов)    и          признак выравнивания       по       левому         краю. Целое значение,  расположенное    между знаком         %         и          командой    форматирования,          выполняет  роль спецификатора   минимальной       ширины       поля. Наличие       этого  спецификатора приведет     к          тому, что     результат     будет заполнен     пробелами или     нулями, чтобы            гарантированно    обеспечить для выводимого  значения      заданную минимальную       длину.          Если   выводимое значение      (строка         или число) больше         этого  минимума, оно     будет выведено    полностью, несмотря     на превышение минимума.          По       умолчанию в          качестве       заполнителя используется         пробел.        Для    заполнения нулями       нужно           поместить   0 перед            спецификатором  ширины       поля. Например,  строка форматирования %05d дополнит     выводимое число нулями         (их      будет меньше        пяти), чтобы общая           длина            была  равной          пяти   символам.

Точное   значение      модификатора    точности  зависит         от        кода  формата, к          которому     он применяется.   Чтобы           добавить     модификатор        точности, поставьте     за        спецификатором ширины       поля  десятичную            точку,            а после нее     —        значение      спецификации       точности.     Для форматов       а,        А, е,         Е,         f           и          F          модификатор        точности      определяет             число выводимых            десятичных знаков.        Например,  строка           форматирования  %10.4f  обеспечит    вывод           числа,           ширина        которого составит            не меньше        десяти           символов,   с          четырьмя    десятичными         знаками. Применительно    к целым       или     строкам,      число,           следующее за        точкой, задает           максимальную     длину            поля. Например,  строка форматирования  %5.7s отобразит    строку           длиной         не       менее            пяти,  но не более      семи  символов.   Если   выводимая строка           окажется      длиннее максимальной      длины           поля, конечные    символы      будут отсечены.

По            умолчанию все      выводимые            значения      выравниваются     по правому      краю:            если   ширина поля         больше         выводимого           значения, оно     будет выровнено  по       правому       краю  поля. Чтобы установить выравнивание       по       левому         краю, поставьте     знак   "минус"        сразу после знака %. Например,        строка           форматирования  %-10.2f         обеспечит выравнивание       вещественного      числа (с двумя       десятичными         знаками       в 10-символьном    поле) по       левому         краю. Рассмотрим программу,          в которой        демонстрируется использование      спецификаторов   ширины       поля  и выравнивания       по       левому         краю. #include       <stdio.h>

int    main() {

 printf("|%11.6f|\n",        123.23);

 printf  ("|%-11.6f|\n",     123.23);

 printf("|%11.6s      |\n",  "Привет        всем");

 printf("|%-11.6s    |\n",  "Привет        всем");

 return            0;

}

При         выполнении           эта      программа отображает             такие результаты.

|   123.230000 |

|   123.230000 |

|      Привет|

|   Привет          |

Существуют     два     модификатора      команд         форматирования,            которые позволяют  функции printf()   отображать             короткие      (short)           и          длинные (long) целые.          Эти     модификаторы     могут применяться         к спецификаторам  типа   d,        i,          о,        и,        х          и          X.        Модификатор        l уведомляет            функцию printf()   о          длинном      формате       значения. Например,  строка           %ld     означает,     что      должно        быть выведено     длинное целое.           Модификатор        h          указывает    на       применение           короткого формата. Следовательно,        строка           %hu    означает,     что      выводимое целочисленное     значение      имеет            тип short      unsigned.

Чтобы     обозначить,            что      соответствующий             аргумент      указывает    на длинное       целое,           к спецификатору  n          можно          применить  модификатор l.          Для    указания      на       короткое      целое примените             к          спецификатору n          модификатор        h.

Если        вы       используете           современный        компилятор,          который поддерживает       добавленные         в 1995           году   средства      работы         с символами широкого    формата       (двухбайтовыми   символами),          то можете задействовать       модификатор        l           применительно    к          спецификатору     с, чтобы            уведомить об        использовании     двухбайтовых        символов.   Кроме того,  модификатор        l           можно          использовать с     командой    формата       s для     вывода         строки           двухбайтовых        символов.

Модификатор l           можно          также            поставить    перед            командами форматирования вещественных       чисел е,         Е,         f,         F,         g          и          G.        В этом   случае           он       уведомит     о          выводе         значения      типа   long double.

Функция         scanf()

Функция            scanf()           —        это      С-функция   общего         назначения ввода            данных         с          консольного устройства.          Она    может           считывать данные         всех    встроенных             типов и          автоматически преобразует   числа в соответствующий             внутренний             формат.        Ее        поведение  во       многом обратно поведению       функции       printf().         Общий          формат         функции scanf()           таков.

int            scanf  (const            char    *fmt_string,            ...);

Управляющая строка,          задаваемая            параметром           fmt_string,   состоит из        символов    трех категорий:

■  спецификаторов   формата;

■"пробельных"         символов    (пробелы,   символы      табуляции   и     пустой           строки); ■   символов,   отличных     от     "пробельных".

Функция            scanf()           возвращает             количество введенных  полей,           а при     возникновении     ошибки —   значение      EOF     (оно   определено            в заголовке    stdio.h).

Спецификаторы         формата       —        им      предшествует        знак   процента     (%) —        сообщают,  какого типа            данное         будет считано        следующим. Например,  спецификатор        %s       прочитает    строку,          а %d   —        целое значение.    Эти     коды приведены в          табл.  А.2.

Пробельные    символы      в          строке           форматирования  заставляют  функцию scanf()           пропустить один  или     несколько   пробельных           символов    во входном      потоке.         Под    пробельным          символом подразумевается   пробел, символ         табуляции   или     символ         новой            строки.         По       сути,  один пробельный           символ         в          управляющей        строке           заставит       функцию scanf()           считывать,   но       не сохранять          любое           количество (пусть даже  нулевое)      пробельных           символов    до       первого непробельного.

"Непробельный"       символ         в          строке           форматирования  заставляет функцию      scanf()           прочитать и            отбросить    соответствующий             символ. Например,  при     использовании     строки форматирования           %d,     %d      функция scanf()           сначала        прочитает    целое            значение,    затем прочитает   и отбросит      запятую        и          наконец       прочитает    еще    одно  целое.           Если заданный    символ не   обнаружится,         работа          функции       scanf()           будет завершена.

Все           переменные,         используемые       для     приема         значений     с помощью    функции       scanf(), должны    передаваться         посредством          их адресов.      Это     значит,          что      все      аргументы  должны       быть указателями на       переменные.         (С        не       поддерживает       ссылки          или     ссылочные параметры.) Передача  указателей  позволяет    функции       scanf()           изменять содержимое          любого         аргумента. Например,   если   нужно           считать целочисленное     значение      в          переменную          count,            используйте следующий вызов            функции       scanf().

scanf("%d",      &count);

Строки   обычно         считываются          в          символьные           массивы,     а          имя массива        (без    индекса) является           адресом       первого        элемента     в этом   массиве.      Поэтому,      чтобы            считать         строку           в символьный массив          address,        используйте           такой код.

char         address[80];

scanf("%s",       address);

В   этом   случае           параметр     address         уже    является       указателем,            и поэтому       его      не       нужно предварять           оператором            "&".

Элементы         входного     потока,         считываемые         функцией    scanf(), должны       быть  разделены пробелами,            символами табуляции   или     новой строки.         Такие символы,     как     запятая,        точка с запятой     и          тому подобное,   не       распознаются        в          качестве       разделителей.       Это     означает, что инструкция:

scanf("%d%d", &r,      &с);

Примет  значения,    введенные  как     10       20,      но       наотрез        откажется    от "блюда",     поданного  в виде           10,20.

Подобно           printf(),         в          функции       scanf()           спецификаторы    формата по       порядку       сопоставляются с переменными,      перечисленными в          списке аргументов.

Символ  стоящий       "*"      после знака "%"     и          перед            кодом           формата, прочитает    данные заданного          типа,  но       запретит       их        присваивание переменной.          Следовательно,    инструкция

scanf("%d%*c%d",    &х,      &у);

                                        при                                             вводе                                 данных             в       виде                            10/20                                        поместит                            значение          10          в                     переменную                                         х,                                    отбросит        знак

деления       и          присвоит      значение      20       переменной           у.

Команды          форматирования  могут содержать   модификатор        максимальной длины           поля. Он представляет  собой            целое            число,           располагаемое между          знаком         "%"     и          кодом           формата, которое            ограничивает количество символов,   считываемых         для     любого         поля. Например,  если вы хотите           прочитать    в          переменную          str       не       более            20 символов,   используйте           следующую инструкцию.

scanf       ("%20s",       str);

Если        входной       поток содержит     более            20       символов,   то        при последующем       выполнении операции  ввода            считывание             начнется      с того    места,           в          котором       "остановился"       предыдущий вызов        функции scanf().          Например,  если   (при   использовании     данного       примера)     вводится такая  строка           символов

ABCDEFGHIJKLMNOPQRSTUVWXYZ,

то в          переменную          str       будут приняты       только          первые         20     символов    (до     буквы            'Т'),     поскольку

команда       форматирования  здесь содержит     модификатор        максимальной длины           поля.

Это           означает,     что      остальные   символы,     "UVWXYZ",  не       будут использованы       вообще.       В случае       другого        вызова          функции       scanf().

scanf("%s",       str);

символы           "UVWXYZ"   поместились          бы      в          переменной           str.      При     обнаружении         "пробельного" символа           ввод  данных         для     поля     может           завершиться           до       достижения            максимальной      длины поля.      В          этом   случае           функция       scanf()           переходит   к     считыванию           следующего           поля.

Несмотря          на       то        что      пробелы,     символы      табуляции   и          новой строки           используются        в качестве   разделителей        полей,           при считывании            одиночного            символа       они     читаются      подобно любому другому       символу.      Например,  если   входной       поток состоит         из        символов х          у,         то инструкция

scanf       ("%с%с%с", &а,     &b,     &с);

          поместит   символ  х   в   переменную  апробел  —   в   переменную  b   и   символ                у                —        в

переменную           с.

Функцию           scanf()           можно          также            использовать         в          качестве набора          сканируемых         символов (scanset).        В          этом   случае определяется        набор            символов,   которые       могут быть  считаны функцией scanf()           и          присвоены  соответствующему          массиву        символов.   Функция scanf() продолжает         считывать    символы      и          помещать    их        в соответствующий             символьный           массив          до тех            пор,   пока   не встретится   символ,        отсутствующий      в          заданном    наборе.        После этого  она переходит       к          следующему          полю (если такое имеется).

Для          определения         такого           набора          необходимо           заключить символы,     подлежащие сканированию, в          квадратные            скобки. Открывающая       квадратная скобка          должна        следовать сразу   за        знаком процента.    Например,  следующий            набор            сканируемых         символов говорит        о том,            что      необходимо           прочитать    только          символы      X, Y          и          Z.

%[XYZ]

Соответствующая      набору          переменная            должна        быть  указателем на массив          символов.   При возврате         из        функции       scanf()           этот    массив будет содержать   строку           с          завершающим      нулем, состоящую           из считанных   символов.   Например,  следующая программа использует  набор сканируемых         символов    для     считывания             цифр  в          массив          s1. Если   будет введен          символ, отличный           от        цифры,         массив          s1 завершится нулевым      символом,  а          остальные   символы      будут считываться в          массив          s2        до       тех      пор,   пока   не       будет введен          следующий "пробельный"       символ.

/* Простой       пример         использования      набора          сканируемых     символов.

*/ #include     <stdio.h>

int    main() {

 char    s1        [80],   s2        [80];

 printf("Введите      числа,           а          затем несколько   букв:\n");  scanf("%[0123456789]%s",     s1,       s2);

 printf("%s     %s",    s1,       s2);

 return            0;

}

Многие  компиляторы        позволяют  с          помощью    дефиса         задать           в наборе          сканируемых символов            диапазон.    Например,  при     выполнении следующей инструкции функция       scanf() будет          принимать  символы      от        А до       Z.

%  [А-Z]

При         этом   в          наборе          сканируемых         символов    можно          задать даже  несколько   диапазонов. Например,           эта      программа считывает    сначала цифры,         а          затем буквы.

/* Пример        использования      в          наборе          сканируемых         символов            нескольких диапазонов.

*/ #include     <stdio.h>

int    main() {

 char    s1[80],           s2        [80];

 printf("Введите      числа,           а          затем несколько   букв:\n");

 scanf("%[0-9]%[a-zA-Z]",            s1,       s2);

 printf  ("%s   %s",    s1,       s2);

 return            0;

}

Если        первый         символ         в          наборе          сканируемых         символов является       знаком         вставки         (^),     то получаем           обратный    эффект: вводимые   данные         будут считываться            до       первого        символа       из заданного   набора          символов,   т.е.      знак   вставки         заставляет   функцию scanf()           принимать  любые символы, которые       не       определены           в наборе.        В          следующей модификации        предыдущей программы        знак вставки         используется         для     запрещения            считывания             символов,   тип которых указан    в          наборе          сканируемых         символов:

/* Пример        использования      набора          сканируемых         символов    для запрещения            считывания             указанных   в          нем    символов.

*/ #include     <stdio.h>

int    main() {

 char    s1[80],           s2[80];

 printf("Введите      не       цифры,         а          затем не       буквы:\n");

 scanf("%[^0-9]%[^a-zA-Z]",       s1,       s2);

 printf  ("%s   %s",    s1,       s2);

 return            0;

}

Важно    помнить,      что      набор            сканируемых         символов    различает прописные  и          строчные буквы.  Следовательно,    если   вы       хотите сканировать           как     прописные,            так      и          строчные     буквы, задайте      их отдельно.

Некоторые       спецификаторы    формата       могут использовать         такие модификаторы,    которые точно      указывают  тип     переменной,          принимающей данные.       Чтобы           прочитать    длинное       целое, поставьте  перед спецификатором  формата       модификатор        l,          а          чтобы            прочитать короткое целое    —        модификатор        h.        Эти     модификаторы     можно использовать         с          кодами         формата       d,        i,          o, u,    x          и          n.

По            умолчанию спецификаторы    f,         е          и          g          заставляют  функцию scanf()           присваивать           данные переменным     типа   float.  Если   поставить перед            одним           из        этих    спецификаторов   формата модификатор l, функция       scanf()           присвоит      прочитанное          данное         переменной типа   double. Использование же       модификатора      L          означает,     что переменная,          принимающая       значение, имеет   тип     long    double.

Модификатор l           можно          применить  и          к          спецификатору     с, чтобы            обозначить указатель    на двухбайтовый символ         с          типом данных         wchar_t        (если ваш    компилятор            соответствует стандарту C++).  Модификатор        l           можно          также            использовать         с          кодом формата       s,         чтобы обозначить            указатель    на       строку           двухбайтовых символов.   Кроме           того,  модификатор        l           можно использовать     для модификации        набора          сканируемых         двухбайтовых        символов.

С-система     обработки             файлов

Несмотря          на       то        что      файловая     система        в          С          отличается  от той,    что      используется         в          C++, между ними есть    много            общего.        С-система        обработки   файлов         состоит         из        нескольких взаимосвязанных функций.     Наиболее    популярные           из        них     перечислены         в          табл.  А.З.

Общий   поток,           который       "цементирует"      С-систему    ввода-вывода, представляет         собой файловый указатель  (file     pointer).       Файловый   указатель —        это      указатель    на       информацию         о файле,       которая        включает     его имя,   статус            и          текущую      позицию.     По       сути,  файловый   указатель идентифицирует   конкретный            дисковый    файл  и          используется         потоком, чтобы            сообщить     всем С-функциям            ввода-вывода,      где     они     должны выполнять  операции.   Файловый   указатель    —        это переменная-указатель типа   FILE,   который       определен  в          заголовке    stdio.h.

Функция         fopen()

Функция           fopen()          выполняет  три     задачи.

1.  Открывает   поток.

2.  Связывает   файл  с          потоком.

3.  Возвращает            указатель    типа   FILE    на       этот    поток.

Чаще       всего под    файлом        подразумевается дисковый    файл. Функция fopen()          имеет            такой прототип.

FILE         *fopen(const           char    *filename,    const  char    *mode);

Здесь      параметр     filename       указывает    на       имя    открываемого       файла, а          параметр     mode—         на строку,    содержащую         нужный        статус (режим)       открытия     файла.          Возможные            значения параметра      mode показаны     в          приведенной         табл.  А.4.    Параметр    filename       должен представлять строку      символов,   составляющих       имя    файла,          которое допустимо  в          данной         операционной системе.           Эта      строка           может включать     спецификацию      пути,  если   действующая         среда поддерживает такую            возможность.

Если        функция       fopen()          успешно       открыла       заданный    файл, она возвращает             указатель    FILE. Этот     указатель    идентифицирует   файл  и используется         большинством      других          файловых системных     функций.     Он не       должен        подвергаться         модификации        кодом           программы. Если файл   не       удается         открыть,       возвращается        нулевой       указатель.

Как          видно            из        табл.  А.4,    файл  можно          открывать   либо  в текстовом,  либо  в          двоичном режиме.         При    открытии     в          текстовом режиме        выполняются         преобразования   некоторых последовательностей символов.   Например,  символы      новой            строки           преобразуются     в последовательности       символов    "возврат    каретки"/"перевод        строки".     В двоичном    режиме подобные          преобразования   не       выполняются.

Если        вам    нужно           открыть        файл  test     для     записи,         используйте     следующую            инструкцию.

fp =          fopen("test",           "w");

Здесь      переменная            fp        имеет            тип     FILE*. Однако         зачастую      для открытия     файла используется        такой код.

if((fp        =          fopen ("test",          "w"))==NULL)         {

 printf  ("Не   удается         открыть        файл.");

 exit(1);

}

При         таком            методе         выявляется любая           ошибка,       связанная    с открытием  файла            (например, при    использовании     защищенного        от записи          или     заполненного        диска),          и          только          после этого можно предпринимать    попытку       записи          в          заданный    файл. NULL  —        это макроимя, определенное       в          заголовке    stdio.h.

Если        вы       используете           функцию      fopen(),         чтобы            открыть файл  исключительно     для выполнения   операций     вывода         (записи),      любой уже    существующий      файл  с          заданным   именем будет       стерт, и          вместо него   будет создан          новый.          Если   файл  с          таким            именем        не существует,             он будет      создан.         Если   вам    нужно           добавлять   данные в          конец            файла,          используйте           режим          "a". Если      окажется,    что указанный  файл  не       существует,             он       будет создан.         Чтобы           открыть файл для     выполнения           операций     чтения,         необходимо           наличие этого  файла.          В          противном  случае функция    возвратит    значение      ошибки. Наконец,      если   файл  открывается           для     выполнения операций  чтения-записи, то        в          случае           его      существования      он       не       будет удален;        но если   его      нет, он          будет создан.

Функция         fputc()

Функция            fputc()           используется         для     вывода         символов    в поток,           предварительно   открытый для       записи          с          помощью    функции fopen().         Ее        прототип      имеет            следующий            вид.

int            fputc(int        ch,      FILE    *fp);

Здесь      параметр     fp        —        файловый   указатель,   возвращаемый     функцией fopen(),         а          параметр ch           —        выводимый            символ.        Файловый указатель    сообщает     функции       fputc(),          в          какой дисковый файл необходимо           записать       символ.        Несмотря     на       то        что      параметр     ch имеет            тип     int,      в          нем используется            только          младший     байт.

При         успешном   выполнении           операции     вывода         функция       fputc() возвращает             записанный            в файл          символ,        в          противном  случае —        значение      EOF.

Функция         fgetc()

Функция            fgetc()           используется         для     считывания             символов    из потока,         открытого   в          режиме чтения     с          помощью    функции       fopen(). Ее        прототип      имеет            следующий            вид.

int            fgetc(FILE     *fp);

Здесь      параметр     fp        —        файловый   указатель,   возвращаемый     функцией fopen().         Несмотря     на то  что      функция       fgetс()           возвращает             значение типа   int,      его      старший       байт   равен            нулю.

При         возникновении     ошибки        или     достижении            конца            файла функция       fgetc()           возвращает значение    EOF.   Следовательно,    для     того, чтобы            считать         все      содержимое          текстового   файла (до    самого конца),         можно          использовать         следующий            код.

ch =          fgetc(fp); while(ch!=EOF)     {  ch            =          fgetc(fp);

}

Функция         feof()

Файловая          система        в          С          может           также            обрабатывать двоичные    данные.       Если   файл  открыт          в режиме     ввода            двоичных данных,        то        не       исключено, что      может           быть  считано        целое число, равное        значению    EOF.   В          этом   случае           при     использовании такого           кода  проверки     достижения конца          файла,          как     ch        != EOF,   будет создана        ситуация,     эквивалентная       получению сигнала        о достижении            конца            файла,          хотя    в          действительности физический            конец            файла            может           быть еще     не       достигнут. Чтобы           решить         эту      проблему,   в          языке            С          предусмотрена функция       feof(), которая       используется         для     определения         факта достижения            конца            файла            при     считывании двоичных   данных.        Ее прототип      имеет            такой вид.

int            feof(FILE       *fp);

Здесь      параметр     fp        идентифицирует   файл. Функция       feof() возвращает ненулевое   значение, если     конец            файла            был-таки      достигнут;   в противном  случае           —        нуль. Таким            образом,     при выполнении следующей инструкции будет считано        все      содержимое          двоичного   файла.

while(!feof(fp))            ch        =          fgetc(fp);

Безусловно,    этот    метод            применим   и          к          текстовым   файлам.

Функция         fclose()

Функция            fclose()          закрывает   поток,           который       был    открыт          в результате  обращения к функции   fopen().         Она    записывает в          файл  все данные,       еще    оставшиеся в          дисковом    буфере,        и закрывает            файл  на уровне          операционной       системы.      При    вызове          функции       fclose() освобождается      блок  управления            файлом,       связанный   с          потоком,      что делает          его      доступным для     повторного использования.    Вероятно,    вам известно      о          существовании      ограничения операционной   системы       на количество файлов,        которые       можно          держать       открытыми в          любой момент         времени,     поэтому,      прежде         чем    открывать   следующий файл, рекомендуется     закрыть все            файлы,         уже    ненужные   для     работы.

Функция           fclose()          имеет            следующий            прототип,

int            fclose(FILE    *fp);

Здесь      параметр     fp        —        файловый   указатель,   возвращаемый     функцией fopen().         При успешном      выполнении           функция       fclose()          возвращает нуль; в          противном  случае           возвращается значение            EOF.   Попытка закрыть        уже    закрытый    файл  расценивается       как     ошибка.       При удалении носителя      данных         до       закрытия     файла            будет сгенерирована      ошибка, как     и          в          случае недостатка           свободного            пространства         на диске.

Использование        функций      fopen(),         fgetc(),          fputc()           и   fclose()

Функции            fopen(),         fgetc(),          fputc()           и          fclose()          составляют минимальный       набор            операций     с файлами. Их       использование демонстрируется в          следующей программе,            которая        выполняет копирование          файла.          Обратите     внимание    на       то,       что      файлы открываются          в          двоичном    режиме и    что      для     проверки     достижения конца            файла            используется         функция       feof().

/* Эта      программа копирует      содержимое          одного          файла            в     другой.

*/ #include     <stdio.h>

int            main(int        argc,   char     *argv[]) {

 FILE     *in,     *out;

 char    ch;

 if(argc!=3)    {

  printf("Вы           забыли         ввести           имя    файла.\n");

  return       1;

 }

 if((in=fopen(argv[1],         "rb")) ==       NULL) {

  printf("He            удается         открыть        исходный    файл.\n");

  return       1;

 }

 if((out=fopen(argv[2],      "wb"))           ==       NULL) {

  printf("He            удается         открыть        файл-приемник.\n");

  return       1;

 }

 /*        Код    копирования          содержимого        файла.          */

 while(!feof   (in))    {

  ch   =          fgetc(in);

  if(!feof(in))          fputc  (ch,     out);

 }

 fclose(in);

 fclose(out);

 return            0;

}

Функции         ferror()          и          rewind()

Функция            ferror()          используется         для     определения         факта возникновения     ошибки        при выполнении  операции     с          файлом.       Ее прототип      имеет            такой вид.

int            ferror(FILE   *fp);

Здесь      параметр     fp        —        действительный   файловый   указатель.   Функция ferror()          возвращает значение    true,   если   при     выполнении           последней файловой    операции     произошла ошибка;       в противном          случае           — значение      false.  Поскольку   возникновение     ошибки        возможно   при выполнении           любой           операции     с          файлом,       функцию      ferror() необходимо           вызывать     сразу после каждой        функции       обработки   файлов; в          противном  случае           информацию         об       ошибке         можно попросту потерять.

Функция            rewind()        перемещает           индикатор   позиции       файла            в начало          файла,          заданного   в качестве   аргумента.  Ее        прототип      выглядит так.

void         rewind(FILE *fp);

Здесь     параметр     fp        —        действительный   файловый   указатель.

Функции         fread()           и          fwrite()

В   файловой    системе        языка            С          предусмотрено     две     функции, fread()           и          fwrite(),        которые позволяют        считывать    и          записывать блоки            данных.        Эти     функции       подобны      С++-функциям read()     и write().          Их       прототипы  имеют           следующий            вид.

size_t      fread(void    *buffer,        size_t num_bytes, size_t count,            FILE

*fp);

              size_t              fwrite(const             void           *buffer,             size_t            num_bytes,          size_t

count,            FILE    *fp);

При         вызове          функции       fread()           параметр     buffer представляет собой            указатель    на       область памяти,   которая        предназначена     для приема         данных,        считываемых         из        файла.          Функция считывает count объектов      длиной         num_bytes   из        потока,         адресуемого файловым   указателем fp.      Функция       fread()           возвращает             количество считанных   объектов,    которое        может           оказаться меньше           заданного значения      count,            если   при     выполнении           этой   операции     возникла ошибка или            был    достигнут    конец            файла.

При         вызове          функции       fwrite()          параметр     buffer представляет собой            указатель    на информацию,  которая        подлежит    записи          в файл. Эта      функция       записывает count объектов длиной num_bytes   в          поток, адресуемый           файловым   указателем fp.       Функция       fwrite() возвращает количество записанных            объектов,    которое        будет равно            значению count,            если   при выполнении  этой   операции     не       было ошибки.

Если        при     вызове          функций       fread()           и          fwrite()          файл  был открыт          для     выполнения           двоичной операции,      то        они     могут считывать или     записывать данные         любого         типа.  Например, следующая программа записывает в          дисковый    файл  значение      типа   float.

/* Запись          в          дисковый    файл  значения      с          плавающей             точкой.

*/ #include     <stdio.h> int     main() {

 FILE     *fp;

 float    f           =          12.23F;

 if((fp=fopen("test",           "wb"))==NULL)       {

  printf("He            удается         открыть        файл.\n");

  return       1;

 }

 fwrite(&f,     sizeof(float),            1,        fp);

 fclose(fp);

 return            0;

}

Как          показано     в          этой   программе,            роль  буфера         может выполнять  (и        при     том     довольно часто)   одна  переменная.

С  помощью    функций       fread()           и          fwrite()          часто выполняется     считывание             и          запись содержимого     массивов     или     структур.     Например,  следующая программа,            используя   одну только           функцию     fwrite(),        записывает содержимое          массива        значений     с     плавающей             точкой balance      в          файл  с          именем        balance.     Затем            с          помощью    одной           лишь функции       fread()     программа считывает    элементы    этого  массива        и          отображает             их     на       экране.

#include <stdio.h>

int    main() {

 register          int       i;

 FILE     *fp;

 float    balance[100];

 /*        Открываем файл  для     записи.         */

 if((fp=fopen("balance",    "w"))==NULL)         {

  printf("He            удается         открыть        файл.\n");

  return       1;

 }

 for(i=0;          i<100;            i++)     balance[i]     =          (float)            i;

 /*        Одним          "махом"       сохраняем   весь   массив          balance.        */

 fwrite(balance,       sizeof balance,        1,        fp);

 fclose(fp);

 /*        Обнуляем   массив.         */

 for(i=0;          i<100;            i++)     balance[i]     =          0.0;

 /*        Открываем файл  для     чтения.         */

 if((fp=fopen("balance",    "r"))==NULL)           {

  printf("He            удается         открыть        файл.\n");

  return       1;

 }

 /*        Одним          "махом"       считываем  весь   массив          balance.        */

 fread(balance,         sizeof balance,        1,        fp);

 /*        Отображаем          содержимое          массива.      */

 for(i=0;          i<100;            i++)     printf("%f     ",         balance[i]);

 fclose(fp);

 return            0;

}

Использовать  функции       fread()           и          fwrite()          для     считывания             и записи          блоков          данных         более эффективно,         чем    многократно вызывать     функции       fgetc()           и          fputc().

Функция         fseek()           и          выполнение           ввода-вывода       с   произвольным     доступом

С-система         ввода-вывода       позволяет    выполнять  операции     считывания и          записи          данных         с произвольным  доступом.   Для    этого  служит функция       fseek(),          которая        устанавливает       нужным образом индикатор   позиции       файла.          Ее        прототип      таков.

int            fseek(FILE     *fp,    long    numbytes,    int       origin);

Здесь      параметр     fp        —        файловый   указатель,   возвращаемый     функцией fopen(),         параметр numbytes        —        количество байтов          относительно исходного   положения,            заданного   параметром origin.         Параметр    origin может           принимать  одно  из        следующих макроимен (определенных     в заголовке    stdio.h).

Следовательно,         чтобы            переместить           индикатор   позиции       в файле            на       numbytes     байтов относительно     его      начала,         в          качестве параметра   origin необходимо           использовать         значение SEEK_SET.        Для перемещения        относительно         текущей       позиции       используйте           значение SEEK_CUR,   а          для     смещения    с          конца            файла            —        значение SEEK_END.

Нулевое            значение      результата  функции       свидетельствует   об успешном   выполнении функции    fseek(),          а          ненулевое—           о возникновении     сбоя.  Как     правило,      функцию      fseek()           не рекомендуется использовать         для     файлов,        открытых     в          текстовом    режиме, поскольку преобразование     символов    может           привести      к          ошибочным перемещениям     индикатора позиции      в          файле.          Поэтому       лучше использовать         эту      функцию      для     файлов,        открытых     в двоичном режиме.       Например,  если   вам    нужно           считать         234-й байт   в          файле test,    выполните следующий код. int         func1() {

 FILE     *fp;

 if((fp=fopen("test",           "rb")) ==       NULL) {

  printf("He            удается         открыть        файл.\n");

  exit (1);

 }

 fseek(fp,        234L, SEEK_SET);

 return            getc(fp);        /*        Считывание            одного          символа,            расположенного на        234-й позиции.      */

}

Функции         fprintf()        и          fscanf()

Помимо            рассмотренных     выше основных     функций       ввода-вывода,      С-система        ввода-вывода включает           функции       fprintf()         и          fscanf(). Поведение  этих    функций       аналогично поведению функций      printf()          и scanf(),          за        исключением        того,  что      они     работают     с          файлами. Именно поэтому  эти      функции       обычно         используются        в          С-программах. Прототипы  функций       fprintf()         и fscanf()      выглядят     так.

int            fprintf(FILE   *          fp,       const  char    *fmt_string,            ...);

int            fscanf(FILE   *          fp,       const  char    *fmt_string,            ...);

Здесь      параметр     fp        —        файловый   указатель,   возвращаемый     функцией fopen().         Функции fprintf()  и          fscanf()         работают     подобно      функциям printf()          и          scanf()           соответственно,    за исключением   того,  что      их действие      направлено            на       файл, определенный      параметром           fp.

Удаление        файлов

Функция           remove()      удаляет        заданный    файл. Ее        прототип      выглядит     так.

int            remove(const          char    *filename);

Она         возвращает             нуль   при     успешном   удалении     файла            и ненулевое   значение      в          противном случае.

Приложение       Б:    Использование  устаревшего       С++-компилятора

Программы,    приведенные         в          этой   книге,            полностью  соответствуют стандарту    ANSI/ISO для          C++     и          могут быть  скомпилированы практически любым         современным        С++компилятором,         включая       Visual C++ (Microsoft)   и          C++     Builder           (Borland).     Следовательно,    при использовании современного       компилятора         у          вас      не       должно        быть  проблем      с компиляцией программ          из        этой   книги.           (В        этом   случае           вам вообще         не       понадобится          информация, представленная           в          этом приложении.)

Но            если   вы       используете           компилятор,          созданный  несколько   лет назад,           то        при     попытке скомпилировать        наши примеры     он       может выдать          целый           список          ошибок,       не       распознав   ряд новых   С++-средств.        И         в          этом   случае           не       стоит  волноваться.          Для    того чтобы            эти      программы заработали            со        старыми       компиляторами, нужно           внести           в          них     небольшие изменения.             Чаще всего старые и          новые           С++-программы   отличаются использованием  двух   средств: заголовков и         пространств            имен.            Вот     об       этом   и          пойдет речь   в          этом   приложении.

Как          упоминалось         в          главе 2,        инструкция #include        включает     в программу заданный заголовок.     Для    более            ранних          версий          C++     под заголовками          понимались           файлы           с расширением     .h.       Например,  в старой           С++-программе    для     включения  заголовка    iostream       была бы использована        следующая инструкция.

#include <iostream.h>

В   этом   случае           в          программу был    бы      включен       заголовочный файл  iostream.h.  Таким образом,   включая       в          старую          С++-программу заголовок,  необходимо           задавать      имя    файла            с расширением     .h.

В   новых            С++-программах  в          соответствии          со        стандартом ANSI/ISO      для     C++     используются заголовки          другого        типа.  Современные заголовки    определяют           не       имена           файлов,        а стандартные идентификаторы,             которые       могут совпадать    с          таковыми,   но       не всегда.

Современные        С++-заголовки       представляют        собой            абстракцию, которая        попросту гарантирует    включение  в          программу требуемой информации.

Поскольку        современные         заголовки    необязательно      являются     именами файлов,        они     не должны иметь            расширение           .h.       Они    определяют имя    заголовка,   заключенного       в          угловые скобки.   Вот,    например,   как выглядят     два     современных         заголовка,   подлерживаемых стандартом           C++.

<iostream>

<fstream>

Чтобы     преобразовать      эти      "новые"       заголовки    в          "старые" заголовочные        файлы, достаточно         добавить     расширение           .h.

Включая            современный        заголовок    в          программу,            необходимо помнить,      что      его      содержимое относится к          пространству         имен  std.     Как упоминалось         выше,            пространство         имен  —        это просто   декларативная область.       Ее        назначение             —        локализовать         имена идентификаторов            во избежание        коллизий     с          именами.    Старые версии          C++     помещают   имена           библиотечных функций            в глобальное пространство         имен,            а          не       в          пространство         имен  std, используемое современными           компиляторами.  Таким            образом,     работая со        старым         компилятором,     не       нужно использовать       эту      инструкцию:

using       namespace   std;

В   действительности            большинство         старых          компиляторов       вообще не       воспримут инструкцию using  namespace.

Два       простых     изменения

Если        ваш    компилятор            не       поддерживает       пространства         имен  и новые           заголовки,  он       выдаст одно          или     несколько   сообщений об ошибках       при     попытке       скомпилировать   первые         несколько строк программ,  приведенных         в          этой   книге.            В          этом   случае           в          эти программы необходимо внести        только          два     простых        изменения: использовать         заголовок    старого         типа   и          удалить namespace-инструкцию. Например,  замените     эти      инструкции

#include <iostream>

using       namespace   std; такой.

#include <iostream.h>

Это           изменение  преобразует           "новую"       программу в          "старую". Поскольку   при использовании         "старого"  заголовка    в          глобальное пространство         имен  считывается            все содержимое   заголовочного      файла, необходимость     в          использовании     namespaee-инструкции отпадает.    После внесения      этих    изменений,             программу можно          скомпилировать   с помощью старого            компилятора.

Иногда  приходится вносить        и          другие          изменения.             C++     наследует    ряд     заголовков из        языка

С.         Язык  С          не       поддерживает       современный        стиль использования      С++-заголовков, используя вместо            них     заголовочные        .h-файлы.    Для разрешения            обратной     совместимости      стандарт      C++ по-прежнему поддерживает       заголовочные        С-файлы.     Однако         стандарт      C++     также определяет современные       заголовки,  которые       можно          использовать вместо          заголовочных        С-файлов.    В          С ++-версиях          стандартных           С-заголовков  к          имени           С-файла        просто          добавляется           префикс       'c' и опускается          расширение           .h.       Например,  С++-заголовком   для     файла math.h          служит          заголовок <cmath>,        а          для     файла            string.h— заголовок    <cstring>.     Несмотря     на       то        что      в          С++-программу разрешено включать     заголовочный       С-файл,         против          такого           подхода       у разработчиков стандарта        есть    существенные       возражения            (другими словами,      это      не       рекомендовано). Поэтому      в          настоящей   книге используются        современные         С++-заголовки       во       всех    инструкциях #include. Если   ваш    компилятор            не       поддерживает       С++-заголовки       для     С-заголовков, просто замените  "старые"      заголовочные        файлы.

Приложение       В:    *.NET-расширения    для C++

Разработанная           компанией  Microsoft      интегрированная оболочка     .NET Framework определяет среду,           которая        предназначена     для     поддержки разработки и          выполнения           сильно распределенных          приложений, основанных            на       использовании     компонентных      объектов.    Она позволяет "мирно         сосуществовать"   различным языкам         программирования        и обеспечивает безопасность,  переносимость     программ    и          общую          модель программирования        для     платформы Windows.    Несмотря     на       относительную новизну        оболочки     .NET   Framework, по       всей вероятности,            в ближайшем            будущем     в          этой   среде            будут работать      многие         С++-программисты.

Интегрированная      оболочка     .NET   Framework  предоставляет       управляемую среду,           которая следит     за        выполнением        программы.           Программа, предназначенная             для     помещения             в оболочку .NET   Framework, не компилируется     с          целью           получения   объектного кода. Вместо этого          она переводится           на       промежуточный   язык  MSIL   (Microsoft    Intermediate Language),   а затем         выполняется          под    управлением         универсального средства      CLR     (Common     Language Runtime).          Управляемое         выполнение— это      механизм,   который       поддерживает       ключевые преимущества, предлагаемые      оболочкой  .NET   Framework.

Чтобы     воспользоваться  преимуществами управляемого       выполнения, необходимо применять           для     С++-программ       специальный         набор нестандартных      ключевых    слов   и          директив препроцессора,        которые были определены           разработчиками   компании    Microsoft.    Важно понимать, что этот    дополнительный набор            не       включен       в          стандарт      C++     (ANSI/ISO Standard C++).       Поэтому       код,   в          котором       используются        эти      ключевые слова,           нельзя          переносить в другие       среды           выполнения.

Описание          оболочки     .NET   Framework  и          методов       С++-программирования,       необходимых для           ее        использования,    выходит       за рамки           этой   книги.           Однако         здесь приводится             краткий обзор .NET-расширения             языка            C++     ради  тех      программистов,   которые работают     в          .NET-среде.

Ключевые      слова            .NET-среды

Для          поддержки .NET-среды управляемого       выполнения           С++-программ Microsoft      вводит          в язык           C++     следующие ключевые    слова.

Краткое описание     каждого       из        этих    ключевых    слов   приведено  в     следующих разделах.

_ _abstract

Ключевое         слово _          _abstract      используется         в          сочетании    со словом         _          _gc     при     определении абстрактного     управляемого       класса. Объект          _          _abstract-класса   создать         нельзя.         Для    класса, определенного     с          использованием  ключевого  слова _          _abstract, необязательно      включение  в него            чисто виртуальной          функции.

_ _box

Ключевое         слов   _          _box   заключает   в          специальную         оболочку значение      внутри          объекта. Такая      "упаковка"  позволяет    использовать         тип этого  значения      в          коде, который       требует,        чтобы данный       объект          был выведен       из        класса           System::Object,      базового      класса           для     всех .NETобъектов.

_ _delegate

Ключевое         слово _          _delegate     определяет             объект-делегат,    который инкапсулирует указатель         на       функцию      внутри          управляемого       класса (т.е.    класса,          модифицированного ключевым      словом         _          _gc).

_ _event

Ключевое         слово _          _event           определяет             функцию,    которая     представляет         некоторое   событие.

Для     такой функции       задается       только          прототип.

_ _finally

Ключевое         слово _          _finally          —        это      дополнение           к стандартному        С++-механизму     обработки исключительных   ситуаций.     Оно используется         для     определения         блока            кода, который       должен выполняться          после выхода         из        блоков          try/catch.     При    этом   не имеет            значения,    какие условия приводят            к          завершению           try/catch-блока.           Блок  _          _finally          должен        быть  выполнен    в          любом случае.

_ _gc

Ключевое         слово _          _gc     определяет             управляемый        класс. Обозначение         "gc"    представляет собой       сокращение            от        словосочетания "garbage      collection"    (т.е.    "сборка        мусора")      и          означает, что         объекты этого  класса           автоматически      подвергаются        процессу      утилизации памяти, освобождаемой   во       время            работы         программы,           когда они     больше не       нужны.         В          объекте отпадает            необходимость     в          случае, когда на       него   не       существует  ни       одной           ссылки.        Объекты      _ _gc-класса           должны       создаваться            с          помощью    оператора   new.   Массивы, указатели    и интерфейсы        также            можно          определять             с использованием  ключевого  слова _          _gc.

_ _identifier

Ключевое         слово _          _identifier    позволяет    любому        другому ключевому слову языка            C++ использоваться        в          качестве идентификатора.  Эта      возможность         не       предназначена     для широкого применения           и          введена        для     решения      специальных          задач.

_ _interface

Ключевое         слово _          _interface    определяет             класс,            который должен        действовать            как интерфейс.      В          любом          интерфейсе             ни одна  из        функций       не       должна        включать     тело,  т.е.      все функции интерфейса             являются     неявно          заданными чисто виртуальными функциями.            Таким образом,   интерфейс   представляет         собой абстрактный           класс,            в          котором       не       реализована          ни одна        из его      функций.

_ _nogc

Ключевое         слово _          _nogc определяет             неуправляемый    класс. Поскольку   такой (неуправляемый)            тип     класса           создается    по умолчанию,           ключевое    слово _          _nogc используется редко.

_ _pin

Ключевое         слово _          _pin    используется         для     определения указателя,   который       фиксирует местоположение   в          памяти          объекта,       на который       он       указывает.  Таким            образом, "закрепленный"       объект          не будет перемещаться       в          памяти          в          процессе      сборки          мусора.        Как следствие,   сборщик      мусора         не       в          состоянии    сделать недействительным          указатель, модифицированный        с          помощью ключевого  слова _          _pin.

_ _property

Ключевое         слово _          _property     определяет             свойство,     являющееся функцией-членом,          которая позволяет          установить  или     получить      значение некоторой   переменной           (члена           данных         класса). Свойства предоставляют удобное       средство      управления            доступом     к          закрытым    (private)        или защищенным         (protected)  данным.

_ _sealed

Ключевое         слово _          _sealed         предохраняет        модифицируемый           им класс от        наследования другими классами.    Это     ключевое    слово можно также            использовать         для     информирования             о том,            что виртуальная           функция       не       может           быть  переопределена.

_ _try_cast

С   помощью    ключевого  слова _          _try_cast      можно          попытаться преобразовать      тип выражения.    Если   предпринятая        попытка       окажется неудачной, будет сгенерировано исключение   типа   System::InvalidCastException.

_ _typeof

Ключевое         слово _          _typeof         позволяет    получить      объект,         который инкапсулирует информацию о          данном        типе.  Этот    объект          представляет собой            экземпляр   класса

System::Туре.

_ _value

Ключевое         слово _          _value           определяет             класс,            который представляет         собой            обозначение типа.          Любое          обозначение типа   содержит     собственные          значения.    И         этим   тип     _          _value отличается  от        типа   _          _gc,    который       должен        выделять     память          для объекта        с          помощью оператора      new.   Обозначения         типа,  не представляют        интерес        для     "сборщика  мусора".

Расширения  препроцессора

Для          поддержки .NET-среды компания    Microsoft      определяет директиву   препроцессора #using, которая        используется         для импортирования  метаданных           в          программу.            Метаданные содержат информацию         о          типе   и          членах          класса           в          форме,         которая не       зависит         от        конкретного языка         программирования.       Таким образом,     метаданные           обеспечивают        поддержку смешанного использования      языков          программирования.       Все     управляемые         С++-программы должны       импортировать     библиотеку             <mscorlib.dll>,        которая содержит     необходимые метаданные     для     оболочки     .NET   Framework.

Компания         Microsoft      определяет             две     pragma-инструкции (используемые     с          директивой препроцессора    #pragma),    которые       имеют отношение  к          оболочке     .NET   Framework. Первая (managed)           определяет управляемый        код.   Вторая          (unmanaged)          определяет неуправляемый (собственный,         т.е.      присущий    данной         среде)           код.   Эти инструкции могут быть  использованы внутри    программы для     селективного создания     управляемого       и          неуправляемого   кода.

Атрибут       attribute

Компания         Microsoft      определяет             атрибут         attribute,      который используется         для     объявления другого      атрибута.

Компиляция управляемых        С++-программ

На            момент         написания   этой   книги единственный       доступный компилятор,          который       мог обрабатывать            программы, ориентированные            на       работу          в          среде            .NET   Framework, поставлялся компанией           Microsoft      (Visual           Studio            .NET). Чтобы скомпилировать   управляемую        программу, необходимо          использовать команду      /сlr,    которая        передаст      вашу  программу "в        руки" универсального    средства      Common       Language     Runtime.

Предметный       указатель

-Символы-

#define, директива,  570

#elif,       директива,  576

#endif,   директива,  575

#error,   директива,  574

#if,           директива,  575

#ifdef,    директива,  577

#ifndef,  директива,  577

#include,           директива,  574;      602

#pragma,          директива,  580

#undef,  директива,  578

#using,   609

.NET        Framework, 606

_  _abstract,     606

_  _box, 607

_  _cplusplus,   макрос,        582

_  _DATE_         _,        макрос,        582

_  _delegate,    607

_  _event,          607

_  _FILE_           _,        макрос,        580;      582

_  _finally,         607

_  _gc,    607

_  _identifier,   607

_  _interface,   608

_  _LINE_           _,        макрос,        580;      582

_  _nogc,           608

_  _pin,  608

_  _property,   608

_  _sealed,        608

_  _STDC_         _,        макрос,        582

_  _TIME_         _,        макрос,        582

_  _try_cast,     608

_  _typeof,        609

_  _value,          609

-A-

abort(),     417,419 abs(),     167;      191 Allocator,     524

American           National     Standards     Institute,       18 ANSI,      18 asm,           514 assign(),     563 atof(),      164 attribute,     609 auto,        спецификатор,     149;      206

-B-

bad(),     471 bad_cast,     исключение,     484 BASIC,      24 basic       ios,     класс,            440 basic_iostream,     класс,            440 basic_istream,     класс,            440 basic       ostream,     класс,            440 basic_streambuf,     класс,            440 BCPL,      23

before(),     475 begin(),  529

Binding

 early, 393  late,         393 bool,       56 boolalpha,     флаг, 448 break,     95

-C-

C#,           29

C++         Builder,         27;       33

C89,        23

C99,        23

Call-by-reference,       178

Call-by-value,     178 Cast,     75 catch,     416

cerr,    440 char,    56;     61 cin,    440 class,    266 clear(),    471 clock(),    213 clog,    440 close(),    458

CLR,         606

Common           Language     Runtime,       606;      609 compare(),        566 const,     спецификатор        типа,     202;      508 const_cast,     оператор,    488 continue,           94 count(),     алгоритм,    554 count_if(),         алгоритм,     554 cout,         440

-D-

Daylight Saving     Time,  251 dec,     флаг, 448 delete,     230 double,    40;     56 do-while,  93 dynamic_cast,     оператор,    483

-E-

Early   binding,     393 end(),     529 enum,     214 eof(),     463 erase(),     529 exit(),     418;     419

EXIT_FAILURE, константа,   419 EXIT_SUCCESS,            константа,   419

explicit,     510 extern,     206;     516

-F-

fabs(),     191 fail(),     471 false,     константа,   57 fclose(),  595 feof(),     595 ferror(),   597 fgetc(),   595 fill(),     451 find(),       565 fixed,      флаг, 448 flags(),    449 Flat     model,           141 float,       56 flush(),     467 fmtflags,     перечисление,     447 fopen(),   593 for,          цикл, 49;     82

FORTRAN,         24

fprintf(), 600 fputc(),   594 fread(),  597 free(),     233 friend,    294 fscanf(), 600 fseek(),   599 Function     overloading,     190 fwrite(),     597

-G-

gcount(),     463 Generated     function,     398 get(),     460;      465 getline(),     466 gets(),     107 good(),     471 goto,     97

GUI,        18;       34

-H-

Heap,     229 hex, флаг,     448

-I-

IDE          (Integrated   Development          Environment),         33

if, 48;       78 if-else-if,     81 Inline     function,       283 inline,     модификатор,     284;      574 insert(),     529;      537 Instantiating,    398 int,     38;       56;       61

Integral  promotion,  74

Integrated         Development     Environment,          33 internal,    флаг,     448

International    Standards     Organization,          18 ios,     класс,            447 ios_base,     класс,            440 iostate,     перечисление,       470 isalpha(),           114

ISO,         18

-J-

Java,       29

-K-

kbhit(),   139

-L-

labs(),     191 Late   binding,     393 left,     флаг,     448 list,     класс,     536 long    double,     61;     62 long     int,     61;     62

long,       модификатор,       60

-M-

main(),   162 make_pair(),     546 malloc(),     233 managed,     609 Manipulator,     447 map,     класс,     545

merge(), 537

MFC,       387

Microsoft          Foundation  Classes,          387

Microsoft          Intermediate           Language,     606

Modula-2,         23;       41

MSIL,      606 Multiple     indirection,     141 mutable,     509

-N-

name(),  475 namespace,     494 Namespace,     35 new,     оператор,     230;      430

nothrow,     431 npos,     561

NULL,      594

-O-

Object    Oriented     Programming,         264 oct,          флаг, 448 OOP,     264 open(),    456 openmode,     перечисление,       457 operator,           320 Operator,          68 overload,           193

-P-

pair,     класс,     546 Pascal,     23;       41 peek(),     467 Plain     Old     Data,     281 POD-struct,     281 Pointer-to-member,     517 precision(),     451 Predicate,     524 Preprocessor,     570 printf(),     585 private,     281;     355 protected,     357 public,     267;     355

push_back(),     529;     537 put(),     460 putback(),     467

-Q-

qsort(),   503

Quicksort,         алгоритм,    503

-R-

rand(),    138;      478 rdstate(),           470 read(),    461 Reference     parameter,   181 register,     спецификатор,     211 reinterpret_cast,     оператор,    490 remove(),          600 return,   инструкция,     166 rewind(), 597 rfind(),   565 right,     флаг, 448

RTTI,       474

-S-

scanf(),   588 Scanset, 590 scientific,     флаг, 448 seekg(),     468;     470 seekp(),     468;     470 setf(),      448 short     int,      61

short,    модификатор,     60 showbase,     флаг, 448 showflags(),     450 showpoint,     флаг, 448 showpos,     флаг, 448 signed     char,  61 signed     int,      61 signed     long     int,      61;       62 signed     short     int,      61 signed,    модификатор,     60 Simula67, 26

sizeof,     227;     263 skipws,     флаг, 448 splice(),  537

Standard           C++,   27

Standard           Template     Library,         26;       54;     522 static,     модификатор,       208;     210;      506 static_cast,     оператор,    489 std,     пространство         имен,     35;       438 stderr,      поток,     585 stdin,        поток,     585 stdout,     поток,     585 STL,           26;       54;     522 strcat(),   109 strcmp(),     110

strcpy(), 109;      171 Stream,     439 streamsize,         тип,     451 string,      класс,     559 strlen(),   111;      161 struct,     238 switch,     87

-T-

tellg(),     470 tellp(),     470 template,     396;     405 template<>,     401;     413 terminate(),     417 this,317;     508 throw,     416 throw-выражение,     427 time_t,     тип     даты,     251 tm, структура,     251 tolower(),     113 toupper(),     135 true, константа,     57 try,     416

Type  promotion,     74 type_info,     класс,     474 typeid,    474 typename,     396

-U-

unexpected(),  427 union,     258 unitbuf,  флаг, 448 unmanaged,     609 unsetf(), 448;      449 unsigned     char,  61 unsigned            int,     61 unsigned   long     int,      61;       62 unsigned     short  int,      61 unsigned,     модификатор,     60 uppercase,     флаг, 448 using,     35;       497 virtual,     375;      381 Visual     Basic, 23 Visual     C++,   27;       33 void,       43;       47;     56

void-функции,            169 volatile,  спецификатор     типа,  204

-W-

wcerr,     440 wchar_t,     56 wcin,     440 wclog,     440 wcout,     440 while,     91 width(),     451;     452 write(),     461

-А-

Абстрактный   класс,            393

Алгоритм

 adjacent_find(),     551

 binary_search(),     551

 copy(),           551

 copy_backward(), 551

 count(),         551

 count_if(),    551

 equal(),         551

 equal_range(),        551

 fill(),   551

 fill_n(),          551

 find(), 551;      552

 find_end(),   551

 find_first_of(),        552

 for_each(),   552

 generate(),   552

 generate_n(),          552

 includes(),    552

 inplace_merge(),    552

 iter_swap(), 552

 lexicographical_compare(),        552

 lower_bound(),      552

 make_heap(),         552

 max(),            552

 max_element(),      552

 merge(),        552

 min(), 552

 min_element(),       552

 mismatch(), 552

 next_permutation(),         552

 nth_element(),       552

 partial_sort(),          552

 partial_sort_copy(),          552

 partition(),   552

 pop_heap(), 553

 prev_permutation(),         553

 push_heap(),           553

 Quicksort,    105

 random_shuffle(), 553

 remove(),     553

 remove_copy(),     553;      555

 remove_copy_if(), 553

 remove_if(),            553

 replace(),      553

 replace_copy(),      553;      555

 replace_copy_if(), 553

 replace_if(), 553

 reverse(),      553;      557

 rotate(),        553

 search(),       553

 search_n(),  553

 set_difference(),    553

 set_intersection(), 553

 set_symmetric_difference(),      553

 set_union(), 553

 sort(), 553

 sort_heap(),            553

 stable_partition(),  553

 stable_sort(),           553

 swap(),          553

 swap_ranges(),       553

 transform(), 553;      557

 unique(),       553

 upper_bond(),        553

Алгоритмы,     523;      551

 командной строки,         162

 по       умолчанию,           193

 функции       main(),45;      162

Ассемблер,      23;       514

Атрибут

 attribute,      609

-Б-

Библиотека

 <mscorlib.dll>,        609

 STL,    522

Битовое             множество,             525

Битовые            поля, 256

Блок       кода, 24;       51;       148

-В-

Вектор,  527

Виртуальное   наследование,      375

Виртуальные  функции,     381

Выражение,    73

 условное,    79

Глобальные переменные,         59

-Д-

Дек,        525

Декремент,      69

Деструктор,     273

Динамическая            идентификация     типов,           474

Динамическая            инициализация,    300

Динамический           массив,         526

Директива        препроцессора,    570

 #define,        570

 #elif,   576

 #endif,           575

 #error,          574

 #if,      575

 #ifdef,            577

 #ifhdef,         577

 #include,       574;      602

 #line,  580

 #pragma,      580

 #undef,         578

 #using,          609

Дополнительный      код,   62

-З-

Заголовки,       172

Заголовок

 <algorithm>,           551

 <bitset>,       525

 <cctype>,     113

 <cstdio>,       584

 <cstdlib>,      44;       419;      504

 <cstring>,     109

 <ctime>,       213;      251;      298

 <deque>,      525

 <fstream>,   456

 <functional>,           525

 <iomanip>,  453

 <iostream>, 35;       438;      440;      466

 <list>, 525

 <map>,         525

 <new>,          430;      436

 <queue>,      526

 <set>, 526

 <stack>,        526

 <string>,       559

 <typeinfo>,  474

 <utility>,       525

 <vector>,      526

 stdio.h,          588

Заголовочный            файл

 <iostream.h>,         438

 stdio.h,          584

-И-

Идентификатор,        53

Индекс, 102

Инициализация

 динамическая,      300

 массивов,    115

 переменных,          66

Инкапсуляция,           27

Инкремент,     69

Инструкция

 continue,      94

 do-while,      93

 for,     49

 goto,  97

 if,        48;       78

 return,           45;       166

 switch,           87

 while, 91

Исключение,   230;      416

 bad_alloc,    430

 bad_cast,      484

 bad_typeid, 477

 System::InvalidCastException,   608

Исключительная       ситуация,     416

Итераторы,      523

 входные,     523

 выходные,  523

 двунаправленные,          523

 однонаправленные,       523

 произвольного     доступа,       523

 реверсивные,        524

-К-

Класс,    266

 allocator,      524

 basic_ios,      440

 basic_iostream,      440

 basic_istream,         440

 basic_ostream,       440

 basic_streambuf,   440

 fstream,        456

 ifstream,       456

 ios,      447;      457

 ios_base,      440

 list,      536

 map,  545

 ofstream,      456

 pair,   546

 string,            559

type_info,    474 vector,           527 абстрактный,            393 базовый,      352 полиморфный,            381;      475

производный,       352

 шаблонный

 pair,   525

Классы

 контейнерные,      525

  bitset,       525

  deque,      525

  list, 525

  map,         525

  multimap,            526

  multiset,   526

  priority_queue, 526

  queue,      526

  set, 526

  stack,        526

  vector,      526

 обобщенные,         404

Ключевые        слова C++,   53

Комментарий,            34 Компилятор

 C++     Builder,         33

 Visual C++,   33

Константа,        63

 CLOCKS_PER_SEC, 298

 EXIT_FAILURE,        419

 EXIT_SUCCESS,       419

 npos,  561

Конструктор,   272;      511

 копии,           305;      311

 параметризованный,     275

Контейнерные            классы,         525

Контейнеры,   523

 ассоциативные,    523;      545

 векторы,      526

 последовательные,         523 Куча,        229;      552;      553

Кэш,        212

-Л-

Лексема,          132

Линейный        список,         525

Литерал,           63

 восьмеричный,     64

 строковый, 65;       106

 шестнадцатеричный,      64

Локальные      переменные,         57

-М-

Макроимя,      570;      582

Макроподстановка,             570

Макрос

 _          _cplusplus,   582

 _          _DATE_         _,        582

 _          _FILE_           _,        582

 _          _LINE_           _,        582

 _          _STDC_         _,        582

 _          _TIME_         _,     582  SEEK_CUR,     599  SEEK_END,     599

 SEEK_SET,    599

Манипулятор,            452

 boolalpha,    452

 dec,    452

 endl,   452

 ends,  452

 fixed,  452

 flush,  452

 hex,    452

 internal,        452

 left,     452

 noboolalpha,     452  noshowbase,     452

 noshowpoint,          452

 noshowpos, 452

noskipws,            452 nounitbuf,            453 nouppercase,            453 oct,     453 resetiosflags(),            453 right,  453

 scientific,     453  setbase(),     453

 setfill(),          453

 setiosflags(),            453;      454

 setprecision(),         453

 setw(),           453

 showbase,    453

 showpoint,   453

 showpos,      453

 skipws,          453

 unitbuf,         453

 uppercase,   453

 ws,      453;      454

Манипуляторные     функции,     454

Массив, 102;      131

 двумерный,           114

 инициализация,    115

 многомерный,      115

 одномерный,        102

 объектов,    286

 строк,            119

 указателей,             137

Метаданные,  609

Метка,   98

Многоуровневая       непрямая    адресация,  141

Множество,     526

 битовое,       525

Модели            памяти,        140

Модификатор

 const, 488;      508

 inline,             284

 long,   60

 mutable,       509

 short, 60

signed,           60 static,        208;         210

 unsigned,      60

 volatile,         488

 максимальной      длины           поля, 590

 точности,     587

Модификаторы         типов,           60

Мультиотображение,          526

-Н-

Набор    сканируемых         символов,   590

Наследование,           29;       351

 виртуальное,         375

-О-

Обобщенные

 классы,         404

 функции,     396

Объединения,            258

 анонимные,           262

Объект, 28

Объект-функция,      525

 less,    525

Объявление

 доступа,       370

 класса,          360

 опережающее,      297

 переменных,          57

ООП,      25;       264

Оператор

 !=,       475

 &,        125

 *,         125

 ==,      475

 const_cast,   488

 defined,        579

 delete,           230

 dynamic_cast,         483

 new,   230;      430

reinterpret_cast,            490 sizeof,            227;            263 static_cast,            489 typeid,            474;            480;            486 XOR,   279;            221 ввода,            441

 вывода,        441

 декремента,           50

 деления       по       модулю,      68

 дополнения           до       1,        221

 И,        поразрядный,       219

 ИЛИ,  поразрядный,       220

 индексации,           340

 инкремента,           50;       323

 исключающее       ИЛИ,  219;      221

 НЕ,      221

 присваивания,       38;       336

 разрешения            контекста,   297;      374

 разрешения            области        видимости, 268;      297;374

 разыменования    адреса          *,        523

Операторы,     68

 арифметические, 68

 декремента,           69

 инкремента,           69

 логические,     71  отношений,     71

 поразрядные,        218

 приведения            типов,           483

 присваивания,

 составные,  225

 сдвига,         222

Операция

 приведения            типов,           75

Опережающее           объявление,           297

Отображение,            525;      545

Очередь,          526

 приоритетная,       526

-П-

Параметры,     44

ссылочные,            181 формальные,         154

Перегрузка

 конструкторов,     298

 операторов,           319

 ввода-вывода,      441

 шаблона      функции,     401

 функций,     190

Переменные,  38

 глобальные,           59;       154

 инициализация,    66

 локальные, 57

Перечисление,           214

 fmtflags,       447

 iostate,          470

 openmode,  457

Позднее            связывание,           393

Поле

 сборное

 ios::basefield,          448

 ios::adjustfield,       448

 ios::floatfield,          448

Полиморфизм,          28;       377

Полиморфный           класс,            381;      475

Порожденная             функция,      398

Поток,     439  cerr,     440

 cin,     440

 clog,     440  cout,     440

 stderr,           585

 stdin,  585

 stdout,     585  wcerr,     440

 wcin,  440

 wclog,     440  wcout,    440

 двоичный,  439

 стандартный

  ввода,      585

  вывода,   585

  ошибок,  585

 текстовый,  439

Предикат,        524

Приоритетная             очередь,      526

Пространство имен,            494

 std,     35;       438;      500

 неименованное,   499

Прототип          функции,     43;       171

Псевдопеременные

 _          _FILE_           _,        580

 _          _LINE_           _,        580

-Р-

Раннее   связывание,           393

Распределитель        памяти,        524

Расширение    типа,  74

Реализация,    398

Рекурсия,         173

Ритчи,    Дэнис,           23

-С-

Связывание

 позднее,      393

 раннее,         393

Специализация

 класса

  явная,      413

 функции,     398   явная,    399

Спецификатор

 explicit,          510

 inline,             574

 private,         355

 protected,    357

 public,           355

 компоновки,          515

 минимальной        ширины       поля, 586

Спецификатор            класса           памяти

 auto,  206

 extern,           206

 register,        211

Спецификатор            типа

 const, 202

 volatile,         204

Список, 536

 сортировка,     541 Ссылки

 на       объекты,      291

 на       производные         типы, 381

 независимые,        188

Стандарт

 С89,   584

 С99,   584

Стандарт           С,        23

Стандартная    библиотека             C++,   54

Стандартная    библиотека             шаблонов,  54

Стек,       152;      526

Страуструп,     Бьерн,           25;       70

Строка,  36;       106

Строковый       литерал,       106

Структура,        238

-Т-

Таблица            строк,            136

Тег,         255

Тип

 basic_string,            559

 BinPred,        524

 bool,  57;       74

 char,  56

 clock_t,         298

 double,          56

 float,  56

 int,      56

 iterator,        523

 nothrow_t,  436

 off_type,      468

 pos_type,     470

 ptrdiff_t,       554

 size_t,            432;      504

 streamsize,   451

 string,            559

 UnPred,        524

 void,   57

 wchar_t,       56

 wstring,         559

-У-

Указатели,       123

 на       объекты,      289

 на       производные         типы, 378

 на       функции,     502

 на       член   класса,          517

Управляющие            последовательности,     65

Условное          выражение,            79

-Ф-

Фабрика           объектов,    478

Файл,     439

Файловый        указатель,   592

Факториал       числа,           174

Флаг

 boolalpha,    448

 dec,    448

 fixed,  448

 hex,    448

 internal,        448

 left,     448

 oct,     448

 right,  448

 scientific,      448

 showbase,    448

 showpoint,   448

 showpos,      448

 skipws,          448

 unitbuf,         448

 uppercase,   448

Флаг       знака,            62

Формальные  параметры,            58

Функции,          24;       147;      294

 виртуальные,         381

 встраиваемые,      283

 манипуляторные,            454

 обобщенные,         396

 перегрузка,            190

 сравнения,  524

Функция,          36;       41

 abort(),         417;      419

 abs(), 43;       167;      191;      403

 asctime(),     252

 assign(),        563

 atof(),     164  bad(),     471

 before(),       475

 begin(),         529

 clear(),           471

 clock(),     213  close(),   458

 compare(),   566

 end(), 529

 eof(),  463

 erase(),          529;      532

 exit(), 418;      419

 fabs(),            191

 fail(),  471

 fclose(),         595

 feof(),            595

 ferror(),        597

 fgetc(),          595

 fill(),   451

 find(), 565

 flags(),           449

 flush(),           467

 fopen(),         593

 fprintf),         600

 fputc(),          594

 fread(),          597

 free(),            233

 fscanf(),        600

 fseek(),          599

 fwrite(),        597

 gcount(),       463

 get(),  460;     465  getline(),     466

 gets(),            107

 good(),          471

 insert(),         529;      532;      537

 isalpha(),       114

 kbhit(),          139

 labs(),            191

 localtime(),   251;      252

 main(),          46;       162

 make_pair(),           546

 malloc(),       233;      431

 merge(),        537

 name(),         475

 open(),          456

 operator,      320

 operator(),   525

 peek(),           467

 precision(),   451

 printf(),         585

 push_back(),           529;      537

 push_front(),          537

 put(), 460

 putback(),    467

 qsort(),          503

 rand(),           138;      478

 rdstate(),      470

 read(),           461

 remove(),     600

 rewind(),      597

 rfind(),           565

 scanf(),          588

 seekg(),     468;      470  seekp(),  468;      470

 setf(), 448

 showflags(), 450  splice(),           537

 strcat(),         109

 strcmp(),       110

 strcpy(),        109;      171

 strlen(),         111;      161

 tellg(),            470

 tellp(),            470

 terminate(), 417

 time(),           251

 tolower(),     113

 toupper(),    113;      135

 unexpected(),          427

 unsetf(),        448

 width(),         451;      452

 write(),          461

 операторная,         320

 параметризованная,       45

 порожденная,       398

 преобразования,  519

 шаблонная,            398

-Ц-

Цикл

 do-while,      93

 for,     49;       82

 while, 91

 бесконечный,        87

 вложенный,           97

-Ш-

Шаблон,           396

Шаблонная     функция,      398

pdf
Пов’язані теми
Інформатика, Контрольні роботи
Інкл
Додано
29 квітня 2020
Переглядів
10060
Оцінка розробки
Відгуки відсутні
Безкоштовний сертифікат
про публікацію авторської розробки
Щоб отримати, додайте розробку

Додати розробку