About Global Documents
Global Documents provides you with documents from around the globe on a variety of topics for your enjoyment.
Global Documents utilizes edocr for all its document needs due to edocr's wonderful content features. Thousands of professionals and businesses around the globe publish marketing, sales, operations, customer service and financial documents making it easier for prospects and customers to find content.
Джефф Элджер
C++
БИБЛИОТЕКА ПРОГРАММИСТА
Содержание
БЛАГОДАРНОСТИ ......................................................................................................................................... 9
ИЗВИНЕНИЯ… ИЛИ ВРОДЕ ТОГО ..................................................................................................................... 9
ЧАСТЬ 1. ВВЕДЕНИЕ И КРАТКИЙ ОБЗОР............................................................................................ 11
ГЛАВА 1. ЗАЧЕМ НУЖНА ЕЩЕ ОДНА КНИГА О С++? .................................................................... 13
ДАО С++......................................................................................................................................................... 13
ТРИ ВЕЛИКИЕ ИДЕИ С++................................................................................................................................ 15
КАК ЧИТАТЬ ЭТУ КНИГУ ................................................................................................................................ 16
НЕСКОЛЬКО СЛОВ О СТИЛЕ ПРОГРАММИРОВАНИЯ ...................................................................................... 17
ГЛАВА 2. СИНТАКСИС С++ ...................................................................................................................... 19
ПЕРЕМЕННЫЕ И КОНСТАНТЫ ........................................................................................................................ 19
const............................................................................................................................................................ 19
Стековые и динамические объекты ....................................................................................................... 23
ОБЛАСТИ ДЕЙСТВИЯ И ФУНКЦИИ.................................................................................................................. 25
Области действия .................................................................................................................................... 25
Перегрузка ................................................................................................................................................. 28
Видимость................................................................................................................................................. 29
ТИПЫ И ОПЕРАТОРЫ ...................................................................................................................................... 33
Конструкторы.......................................................................................................................................... 33
Деструкторы ............................................................................................................................................ 40
Присваивание ............................................................................................................................................ 41
Перегрузка операторов............................................................................................................................ 46
ГЛАВА 3. ШАБЛОНЫ И БЕЗОПАСНОСТЬ ТИПОВ ............................................................................ 55
ЧТО ТАКОЕ ШАБЛОНЫ И ЗАЧЕМ ОНИ НУЖНЫ? ............................................................................................. 55
Проблемы................................................................................................................................................... 55
Обходные решения.................................................................................................................................... 56
Шаблоны — усовершенствованные макросы ........................................................................................ 56
СИНТАКСИС ШАБЛОНОВ ................................................................................................................................ 57
Параметризованные типы ...................................................................................................................... 57
Параметризованные функции ................................................................................................................. 57
Параметризованные функции классов ................................................................................................... 58
Передача параметра................................................................................................................................ 58
Шаблоны с несколькими параметрами.................................................................................................. 59
Долой вложенные параметризованные типы!...................................................................................... 59
Наследование............................................................................................................................................. 59
КОМБИНАЦИИ ПРОСТЫХ И ПАРАМЕТРИЗОВАННЫХ ТИПОВ ......................................................................... 59
Небезопасные типы в открытых базовых классах .............................................................................. 60
Небезопасные типы в закрытых базовых классах................................................................................ 60
Небезопасные типы в переменных класса ............................................................................................. 60
ГЛАВА 4. ИСКЛЮЧЕНИЯ .......................................................................................................................... 63
4
ОБРАБОТКА ИСКЛЮЧЕНИЙ В СТАНДАРТЕ ANSI ...........................................................................................63
Синтаксис инициирования исключений ..................................................................................................63
Синтаксис перехвата исключений ..........................................................................................................66
Конструкторы и деструкторы...............................................................................................................67
НЕСТАНДАРТНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ ...............................................................................................69
УСЛОВНЫЕ ОБОЗНАЧЕНИЯ .............................................................................................................................69
ЧАСТЬ 2. КОСВЕННЫЕ ОБРАЩЕНИЯ ..................................................................................................71
ГЛАВА 5. УМНЫЕ УКАЗАТЕЛИ ...............................................................................................................73
ГЛУПЫЕ УКАЗАТЕЛИ ......................................................................................................................................73
УМНЫЕ УКАЗАТЕЛИ КАК ИДИОМА.................................................................................................................75
Оператор -> .............................................................................................................................................75
Параметризованные умные указатели...................................................................................................75
Иерархия умных указателей ....................................................................................................................76
Арифметические операции с указателями.............................................................................................77
Во что обходится умный указатель?.....................................................................................................78
ПРИМЕНЕНИЯ .................................................................................................................................................78
Разыменование значения NULL ...............................................................................................................78
Отладка и трассировка ...........................................................................................................................80
Кэширование..............................................................................................................................................82
ГЛАВА 6. ВЕДУЩИЕ УКАЗАТЕЛИ И ДЕСКРИПТОРЫ .....................................................................85
СЕМАНТИКА ВЕДУЩИХ УКАЗАТЕЛЕЙ............................................................................................................85
Конструирование ......................................................................................................................................86
Уничтожение ............................................................................................................................................87
Копирование...............................................................................................................................................87
Присваивание.............................................................................................................................................88
Прототип шаблона ведущего указателя ...............................................................................................89
ДЕСКРИПТОРЫ В C++.....................................................................................................................................90
ЧТО ЖЕ ПОЛУЧАЕТСЯ? ...................................................................................................................................90
Подсчет объектов ....................................................................................................................................90
Указатели только для чтения .................................................................................................................92
Указатели для чтения/записи..................................................................................................................92
ГЛАВА 7. ГРАНИ И ДРУГИЕ МУДРЫЕ УКАЗАТЕЛИ.........................................................................93
ИНТЕРФЕЙСНЫЕ УКАЗАТЕЛИ .........................................................................................................................93
Дублирование интерфейса .......................................................................................................................93
Маскировка указываемого объекта ........................................................................................................94
Изменение интерфейса ............................................................................................................................96
ГРАНИ .............................................................................................................................................................96
Преобразование указываемого объекта в грань ....................................................................................97
Кристаллы .................................................................................................................................................98
Вариации на тему граней .........................................................................................................................99
Инкапсуляция указываемого объекта ...................................................................................................102
Проверка граней ......................................................................................................................................103
Обеспечение согласованности ...............................................................................................................103
Грани и ведущие указатели....................................................................................................................105
ПЕРЕХОДНЫЕ ТИПЫ .....................................................................................................................................106
Полиморфные указываемые объекты...................................................................................................106
Выбор типа указываемого объекта во время конструирования .......................................................107
Изменение указываемого объекта во время выполнения программы................................................107
ПОСРЕДНИКИ ................................................................................................................................................107
ФУНКТОРЫ....................................................................................................................................................108
ГЛАВА 8. КОЛЛЕКЦИИ, КУРСОРЫ И ИТЕРАТОРЫ .......................................................................111
МАССИВЫ И ОПЕРАТОР [] ..........................................................................................................................111
5
Проверка границ и присваивание........................................................................................................... 111
Оператор [] с нецелыми аргументами............................................................................................... 112
Имитация многомерных массивов........................................................................................................ 112
Множественные перегрузки оператора [] ........................................................................................ 113
Виртуальный оператор [].................................................................................................................... 113
КУРСОРЫ ...................................................................................................................................................... 114
Простой класс разреженного массива................................................................................................. 114
Курсоры и разреженные массивы......................................................................................................... 115
Операторы преобразования и оператор ->........................................................................................ 116
Что-то знакомое… ................................................................................................................................ 117
ИТЕРАТОРЫ .................................................................................................................................................. 117
Активные итераторы............................................................................................................................ 118
Пассивные итераторы .......................................................................................................................... 118
Что лучше? ............................................................................................................................................. 119
Убогие, но распространенные варианты ............................................................................................ 119
Лучшие варианты................................................................................................................................... 120
Итератор абстрактного массива ....................................................................................................... 121
ОПЕРАТОРЫ КОЛЛЕКЦИЙ............................................................................................................................. 123
МУДРЫЕ КУРСОРЫ И НАДЕЖНОСТЬ ИТЕРАТОРОВ....................................................................................... 124
Частные копии коллекций...................................................................................................................... 126
Внутренние и внешние итераторы ...................................................................................................... 127
Временная пометка ................................................................................................................................ 129
Пример ..................................................................................................................................................... 131
ГЛАВА 9. ТРАНЗАКЦИИ И ГЕНИАЛЬНЫЕ УКАЗАТЕЛИ .............................................................. 137
ТЕРНИСТЫЕ ПУТИ ДИЗАЙНА ........................................................................................................................ 137
Транзакции............................................................................................................................................... 137
Отмена .................................................................................................................................................... 138
Хватит? .................................................................................................................................................. 138
ОБРАЗЫ И УКАЗАТЕЛИ ................................................................................................................................. 138
Простой указатель образов .................................................................................................................. 139
Стеки образов......................................................................................................................................... 140
Образы автоматических объектов...................................................................................................... 141
Образы указателей................................................................................................................................. 144
Комбинации и вариации ......................................................................................................................... 145
ТРАНЗАКЦИИ И ОТМЕНА .............................................................................................................................. 145
Транзакции и блокировки ....................................................................................................................... 146
Класс ConstPtr......................................................................................................................................... 147
Класс LockPtr .......................................................................................................................................... 149
Создание и уничтожение объектов ..................................................................................................... 150
Упрощенное создание объектов............................................................................................................ 151
Отмена .................................................................................................................................................... 152
ВАРИАНТЫ ................................................................................................................................................... 152
Вложенные блокировки .......................................................................................................................... 152
Взаимные блокировки и очереди............................................................................................................ 153
Многоуровневая отмена ........................................................................................................................ 154
Оптимизация объема ............................................................................................................................. 154
НЕСКОЛЬКО ПРОЩАЛЬНЫХ СЛОВ................................................................................................................ 155
ЧАСТЬ 3. СНОВА О ТИПАХ ..................................................................................................................... 157
ГЛАВА 10. МНОЖЕСТВЕННАЯ ПЕРЕДАЧА....................................................................................... 159
ГОМОМОРФНЫЕ ИЕРАРХИИ КЛАССОВ ......................................................................................................... 159
Взаимозаменяемость производных классов......................................................................................... 160
Нормальное наследование...................................................................................................................... 160
Инкапсуляция производных классов ...................................................................................................... 161
МНОЖЕСТВЕННАЯ ПЕРЕДАЧА ..................................................................................................................... 162
6
Двойная передача ....................................................................................................................................163
Гетероморфная двойная передача........................................................................................................164
Передача более высокого порядка.........................................................................................................165
Группировка передач и преобразования................................................................................................166
ЭТО ЕЩЕ НЕ ВСЕ ...........................................................................................................................................167
ГЛАВА 11. ПРОИЗВОДЯЩИЕ ФУНКЦИИ И ОБЪЕКТЫ КЛАССОВ ............................................169
ПРОИЗВОДЯЩИЕ ФУНКЦИИ..........................................................................................................................169
make-функции ..........................................................................................................................................170
Символические классы и перегруженные make-функции ....................................................................170
Оптимизация с применением производящих функций.........................................................................170
Локализованное использование производящих функций......................................................................171
Уничтожающие функции ......................................................................................................................172
Снова о двойной передаче: промежуточные базовые классы............................................................172
Нет — конструкторам копий и оператору =!....................................................................................173
ОБЪЕКТЫ КЛАССОВ ......................................................................................................................................173
Информация о классе..............................................................................................................................174
Еще несколько слов об уничтожающих функциях...............................................................................175
Определение класса по объекту ............................................................................................................176
ПРЕДСТАВИТЕЛИ ..........................................................................................................................................177
ГЛАВА 12. НЕВИДИМЫЕ УКАЗАТЕЛИ ................................................................................................179
ОСНОВНЫЕ КОНЦЕПЦИИ ..............................................................................................................................179
Инкапсуляция указателей и указываемых объектов ...........................................................................180
Производящие функции ..........................................................................................................................180
Ссылки на указатели ..............................................................................................................................181
Неведущие указатели .............................................................................................................................181
Ведущие указатели .................................................................................................................................183
СНОВА О ДВОЙНОЙ ПЕРЕДАЧЕ .....................................................................................................................184
Удвоенная двойная передача..................................................................................................................185
Самомодификация и переходимость ....................................................................................................187
Множественная двойная передача .......................................................................................................189
ПРИМЕНЕНИЕ НЕВИДИМЫХ УКАЗАТЕЛЕЙ ...................................................................................................189
Кэширование............................................................................................................................................189
Распределенные объекты и посредники ...............................................................................................189
Нетривиальные распределенные архитектуры...................................................................................189
ЧАСТЬ 4. УПРАВЛЕНИЕ ПАМЯТЬЮ....................................................................................................191
ГЛАВА 13. ПЕРЕГРУЗКА ОПЕРАТОРОВ УПРАВЛЕНИЯ ПАМЯТЬЮ.........................................193
ПЕРЕГРУЗКА ОПЕРАТОРОВ NEW И DELETE...................................................................................................193
Простой список свободной памяти ......................................................................................................193
Наследование операторов new и delete.................................................................................................196
Аргументы оператора new....................................................................................................................197
Конструирование с разделением фаз ....................................................................................................197
Уничтожение с разделением фаз..........................................................................................................198
КТО УПРАВЛЯЕТ ВЫДЕЛЕНИЕМ ПАМЯТИ?...................................................................................................199
Глобальное управление............................................................................................................................199
Выделение и освобождение памяти в классах .....................................................................................200
Управление памятью под руководством клиента...............................................................................200
Объекты классов и производящие функции .........................................................................................200
Управление памятью с применением ведущих указателей.................................................................200
Перспективы ...........................................................................................................................................204
ГЛАВА 14. ОСНОВЫ УПРАВЛЕНИЯ ПАМЯТЬЮ ..............................................................................205
СТРОИТЕЛЬНЫЕ БЛОКИ ................................................................................................................................205
Поблочное освобождение памяти ........................................................................................................205
7
Скрытая информация ............................................................................................................................ 208
Списки свободных блоков....................................................................................................................... 208
ПОДСЧЕТ ССЫЛОК........................................................................................................................................ 210
Базовый класс с подсчетом ссылок ...................................................................................................... 210
Укзатели с подсчетом ссылок .............................................................................................................. 211
Ведущие указатели с подсчетом ссылок ............................................................................................. 211
Дескрипторы с подсчетом ссылок ....................................................................................................... 212
Трудности подсчета ссылок ................................................................................................................. 213
Подсчет ссылок и ведущие указатели.................................................................................................. 213
ПРОСТРАНТСВА ПАМЯТИ............................................................................................................................. 214
Деление по классам ................................................................................................................................. 214
Деление по размеру ................................................................................................................................. 215
Деление по способу использования ........................................................................................................ 215
Деление по средствам доступа............................................................................................................. 215
Пространства стека и кучи.................................................................................................................. 216
ГЛАВА 15. УПЛОТНЕНИЕ ПАМЯТИ..................................................................................................... 217
ПОИСК УКАЗАТЕЛЕЙ .................................................................................................................................... 217
Мама, откуда берутся указатели? ...................................................................................................... 217
Поиск указателей ................................................................................................................................... 220
ДЕСКРИПТОРЫ, ПОВСЮДУ ДЕСКРИПТОРЫ.................................................................................................. 223
Общее описание архитектуры.............................................................................................................. 223
Ведущие указатели ................................................................................................................................. 223
Вариации.................................................................................................................................................. 227
Оптимизация в особых ситуациях........................................................................................................ 229
АЛГОРИТМ БЕЙКЕРА .................................................................................................................................... 229
Пространства объектов ....................................................................................................................... 229
Последовательное копирование ............................................................................................................ 232
Внешние объекты ................................................................................................................................... 233
Алгоритм Бейкера: уход и кормление в C++ ....................................................................................... 234
УПЛОТНЕНИЕ НА МЕСТЕ .............................................................................................................................. 236
Базовый класс VoidPtr ............................................................................................................................ 236
Пул ведущих указателей ........................................................................................................................ 237
Итератор ведущих указателей ............................................................................................................ 238
Алгоритм уплотнения ............................................................................................................................ 238
Оптимизация........................................................................................................................................... 239
Последовательное уплотнение на месте ............................................................................................. 239
ПЕРСПЕКТИВЫ ............................................................................................................................................. 239
ГЛАВА 16. СБОРКА МУСОРА ................................................................................................................. 241
ДОСТУПНОСТЬ.............................................................................................................................................. 241
Периметр ................................................................................................................................................ 241
Внутри периметра ................................................................................................................................. 242
Анализ экземпляров................................................................................................................................. 243
Перебор графа объектов ....................................................................................................................... 244
СБОРКА МУСОРА ПО АЛГОРИТМУ БЕЙКЕРА ................................................................................................ 245
Шаблон слабого дескриптора ............................................................................................................... 245
Шаблон сильного дескриптора ............................................................................................................. 245
Итераторы ведущих указателей.......................................................................................................... 246
Перебор указателей ............................................................................................................................... 248
Оптимизация........................................................................................................................................... 251
Внешние объекты ................................................................................................................................... 251
Множественные пространства ........................................................................................................... 251
Сборка мусора и уплотнение на месте ................................................................................................ 251
Нужно ли вызывать деструкторы? .................................................................................................... 252
ТОЛЬКО ДЛЯ ПРОФЕССИОНАЛЬНЫХ КАСКАДЕРОВ ..................................................................................... 252
Концепции «матери всех объектов» .................................................................................................... 252
Организация памяти .............................................................................................................................. 253
8
Поиск периметра ....................................................................................................................................254
Перебор внутри периметра ...................................................................................................................254
Сборка мусора .........................................................................................................................................255
Последовательная сборка мусора .........................................................................................................255
ИТОГОВЫЕ ПЕРСПЕКТИВЫ ...........................................................................................................................255
ПРИЛОЖЕНИЕ. JAVA ПРОТИВ C++ .....................................................................................................257
Благодарности
Эта книга, как и любая другая, обязана своим существованием слишком многим, чтобы их можно было
перечислить в одном списке. Книга — это нечто большее, чем просто страницы, покрытые забавными
черными значками. Это смесь альтруизма, авторского ego и первобытного крика души. Кроме того, она
сыграла заметную роль в жизни автора и его семьи. Я глубоко благодарен своей жене Синди и
сыновьям Нику, Джей-Джею и Бобби за их терпение, поддержку и прощение, когда папа не мог
уделять им достаточно времени для игр.
Если считать терпение добродетелью, то самые добродетельные люди, о которых мне известно,
работают в издательстве AP Professional. Я в долгу перед всеми, кто с самого начала поддержал мою
идею этой книги и продолжал нажимать на меня, чтобы работа не стояла на месте. Иногда мне кажется,
что на мою долю выпала самая легкая часть — теперь я отдыхаю, а вы пытаетесь продать!
Я особенно благодарен Джону Трудо (John Trudeau) из компании Apple Computer, который впервые
предложил мне изложить на бумаге свои разрозненные мысли и переживания в виде семинара для
опытных программистов С++. Даже не знаю, что я должен выразить многим слушателям этих
семинаров, которые пережили ранние варианты этого курса, прежде чем он начал принимать
законченные формы, — то ли благодарность, то ли свои искренние извинения.
За эти годы многие люди повлияли на мое отношение к С++ и объектно-ориентированному
программированию. В голову сразу же приходит несколько имен — Нил Голдстейн (Neal Goldstein),
Ларри Розенштейн (Larry Rosenstein), Эрик Бердал (Eric Berdahl), Джон Брюгге (John Brugge), Дэйв
Симмонс (Dave Simmons) и Дэйв Бьюэлл (Dave Buell). Никто из них не несет ответственности за то, с
чем вы не согласитесь в этой книге, но именно они заронили в мою душу первые идеи.
Моя благодарность распространяется и на новых коллег из Microsoft Corporation, куда я был принят,
когда книга была «почти готова» — то есть были готовы первые 90 процентов и оставалось сделать
еще 90 процентов. Эта книга не была написана «под знаменем Microsoft», поэтому, пожалуйста, не
обвиняйте их во всем, что в ней написано. Книга была начата и почти завершена до того, как я начал
работать на Microsoft, и никто из работников Microsoft не помогал мне, не просматривал книгу и не
одобрял ее.
Джеймс Коплин (James Coplien), мы никогда не встречались, но твоя книга «Advanced C++
Programming Styles and Idioms» оказала самое большое влияние на мое мировоззрение. Книга
великолепно раскрывает тему нетривиального использования С++. Надеюсь, по твоим следам пойдут и
другие авторы.
Наконец, хочу поблагодарить Бьярна Страуструпа (Bjarne Stroustrup) за то, что он изобрел такой
странный язык. О простых, последовательных языках типа SmallTalk неинтересно не то что писать, но
даже думать. Если бы в С++ не было всех этих тихих омутов и загадочных правил, пропала бы
благодатная почва для авторов, консультантов и прочих личностей вроде вашего покорного слуги.
Бьярн, я люблю твой язык… Честное слово, люблю — как Черчилль любил демократию. С++ —
худший объектно-ориентированный язык… но остальные еще хуже.
Извинения… или вроде того
Заодно хочу воспользоваться случаем и извиниться перед всеми, кого я обидел в своей книге. Понятия
не имею, кто вы такие, но на своем горьком опыте (по двум статьям, опубликованным в журнале IEEE
Computer) я узнал, как много людей обижается на несерьезный подход к серьезной теме — такой как
10
С++. Если вы принадлежите к их числу, я сожалею, что задел ваши чувства. Пусть не так сильно,
чтобы лишиться сна, но все же сожалею.
Я не претендую на авторство изложенных в книге идей. Если вы увидите в ней что-то, придуманное
вами или кем-то другим, — смело заявляйте, что это ваших рук дело, спорить я не стану. Мастерство
нетривиального использования С++ растет от свободного обмена идеями, а не от формального
изучения, так что в действительности очень трудно однозначно определить, кто, что и когда сказал. Я
написал эту книгу, чтобы как можно больше людей смогли быстро и безболезненно повысить свою
квалификацию, поэтому вопросам авторства идей уделялось второстепенное внимание. Если вам это не
нравится, примите мои искренние извинения и напишите свою собственную книгу.
С другой стороны, я взял на себя смелость использовать новые имена для старых концепций,
вызывающих недоразумения, и нисколько об этом не жалею. Такова уж великая традиция сообщества
С++, которое переименовало почти все объектно-ориентированные концепции: субкласс (производный
класс), суперкласс (базовый класс), метод (функция класса) и т.д. Сторонники переименования не
обошли вниманием даже такие традиционные концепции С, как поразрядный сдвиг (<< и >>). Вам не
нравится, что для старых идей используются новые имена, — пусть даже в интересах ясности?
Приятель, вы ошиблись с выбором языка.
Я сделал все возможное, чтобы все фрагменты кода в этой книге работали как положено, но без
ошибок дело наверняка не обошлось. Действуйте так, словно ваша программу уже горит синим
пламенем, — проверяйте, проверяйте и еще раз проверяйте мой код, прежде чем использовать его в
своих программах. Помните: в этой книге я демонстрирую различные идиомы и концепции, а не
создаю библиотеку классов. Все идиомы вполне работоспособны, но дальше вам придется действовать
самостоятельно.
Джефф Элджер
Январь 1998 г.
Введение
и краткий обзор
В этой части я отвечаю на главный вопрос: «Зачем было писать еще одну
книгу о С++»? Далее в головокружительном темпе рассматриваются
некоторые нетривиальные возможности языка. Все это делается
исключительно для подготовки к следующим главам, поэтому материал
можно читать или пропускать в зависимости от того, насколько уверенно
вы владеете теми или иными тонкостями синтаксиса С++.
1
Часть
Зачем нужна
еще одна книга о
С++?
По последним данным, на рынке продается по крайней мере 2 768 942 книги о С++, не говоря уже о
всевозможных курсах, обучающих программах, журналах и семинарах с коктейлями. И все же в этом
изобилии наблюдается удручающее однообразие. Просматривать полку книг о С++ в книжном
магазине ничуть не интереснее, чем литературу по бухгалтерии. В сущности, все книги пересказывают
одно и то же и отличаются разве что по весу и количеству цветов в диаграммах и таблицах. По моим
подсчетам, 2 768 940 из них предназначены для новичков, ориентированы на конкретный компилятор
или представляют собой справочники по синтаксису С++. Для тех, кто уже знает язык и желает
подняться на
следующий уровень,
существующая
ситуация оборачивается
сплошными
разочарованиями и расходами. Чтобы узнать что-то новое, приходится дергать главу отсюда и раздел
оттуда. Для знатока С++ такая трата времени непозволительна.
Эта книга — совсем другое дело. Прежде всего, она предполагает, что вы уже владеете С++. Вероятно,
вы программировали на С++ в течение года-двух или более. Став настоящим асом, на вопрос о
должности вы перестали скромно отвечать «Программист»; теперь ваш титул складывается из слов
«Старший», «Специалист», «Ведущий», «Разработчик», «Проектировщик» (расставьте в нужном
порядке). Вы уже знаете, что «перегрузка оператора» не имеет никакого отношения к телефонной
компании, а «класс-коллекция» — вовсе не сборище филателистов. На вашей полке стоит книга
Страуструпа «Annotated C++ Reference Manual», которую в профессиональных разговорах вы часто
сокращенно именуете ARM и даже не считаете нужным расшифровывать.
Если вы узнали себя, добро пожаловать — эта книга для вас. Ее можно было бы еще назвать «С++:
путь гуру». С++ в ней описывается совсем не так, как в книгах для начинающих. На этом уровне С++
— не столько язык, сколько целая субкультура со своими идиомами, приемами и стандартными
архитектурными решениями, которые не следуют очевидным образом из формального описания языка.
Об этом «языке внутри языка» редко упоминается с книгах и журналах. Одни программисты
самостоятельно обнаруживают все эти возможности и с гордостью считают, что изобрели нечто
потрясающее, пока не выяснится, что «нет ничего нового под солнцем». Другим везет, и они
становятся учениками подлинных мастеров С++ — к сожалению, такие мастера встречаются слишком
редко. В этой книге я попытался проложить третий путь истинного просветления — самостоятельное
изучение. Кроме того, книга предназначена для тех, кто уже достиг заветной цели, но хочет
пообщаться, поболтать в дружеской компании и пошевелить мозгами над очередной головоломкой.
Дао С++
С++ — язык, который изучается постепенно. Лишь после того, как будет сделан последний шаг,
разрозненные приемы и фрагменты синтаксиса начинают складываться в общую картину. По-моему,
изучение С++ чем-то напоминает подъем на лифте. Дзынь! Второй этаж. С++ — это
усовершенствованный вариант С, с сильной типизацией (которую, впрочем, при желании можно
обойти) и удобными комментариями //. Любой программист на С, если он не хочет подаваться в
менеджеры, должен двигаться дальше… а Бьярн Страуструп (Господи, благослови его) придумал для
этого отличную возможность.
1
14
Дзынь! Третий этаж. С++ — хороший, хотя и не потрясающий объектно-ориентированный язык
программирования. Не Smalltalk, конечно, но чего ожидать от языка, работающего с такой
головокружительной скоростью? С++ — это Cobol 90-х, политически выдержанный язык, которые
гарантирует финансирование вашего проекта высшим руководством. А уж если С++ достаточно часто
упоминается в плане, можно надеяться на удвоение бюджета. Это тоже хорошо, потому что никто
толком не умеет оценивать проекты на С++ и управлять ими. А что касается инструментария — глаза
разбегаются, не правда ли?
Дзынь! Последний этаж, все выходят. Но позвольте, где же «все»? Лифт почти пуст. С++ — это на
самом деле не столько язык, сколько инструмент для создания ваших собственных языков. Его
элегантность заключается отнюдь не в простоте (слова С++ и простота режут слух своим явным
противоречием), а в его потенциальных возможностях. За каждой уродливой проблемой прячется
какая-нибудь умная идиома, изящный языковой финт, благодаря которому проблема тает прямо на
глазах. Проблема решается так же элегантно, как это сделал бы настоящий язык типа Smalltalk или
Lisp, но при этом ваш процессор не дымится от напряжения, а на Уолл-Стрит не растут акции
производителей чипов памяти. С++ — вообще не язык. Это мировоззрение или наркотик, меняющий
способ мышления.
Но вернемся к слову «элегантный». В программировании на С++ действует перефразированный
принцип Дао: «Чтобы достичь истинной элегантности, нужно отказаться от стремления к
элегантности». С++ во многом представляет собой С следующего поколения. Написанные на нем
программы эффективно компилируются и быстро работают. Он обладает очень традиционной блочной
структурой и сокращенной записью для многих распространенных операций (например, i++). В нем
есть свои существительные, глаголы, прилагательные и свой жаргон:
cout << 17 << endl << flush;
Ревнители частоты языка часто нападают на С++. Они полагают, что высшее достижение современной
цивилизации — язык, построенный исключительно из атомов и скобок. По мнению этих террористов
от синтаксиса, если простую переменную с первого взгляда невозможно отличить от вызова функции
или макроса — это вообще не язык, а шарлатанство для развлечения праздной толпы. К сожалению,
теория расходится с практикой. В реальной жизни толпа платит лишь за то, чтобы видеть языки, в
которых разные идеи выглядят по-разному. «Простые и последовательные» языки никогда не
пользовались особым успехом за стенками академий, а языки с блочной структурой овладели массами.
Стоит ли этому удивляться? Ведь компьютерные языки приходится изучать и запоминать, а для этого
используется то же серое вещество, с помощью которого мы изучаем и запоминаем естественные
языки. Попробуйте-ка назвать хотя бы один естественный язык без существительных, глаголов и
скобок! Я бы не рискнул. Все наши познания в лингвистике говорят о том, что эти «плохие»
особенности только ускоряют изучение компьютерного языка и делают его более понятным. i++ во
всех отношениях действительно понятнее, чем i:=i+1, а x=17+29 читается лучше, нежели (setq
x(+17, 29)). Речь идет не о строении компьютерного языка, а скорее о нашем собственном
строении. Все уродства С++ — это в основном наши уродства. Когда вы научитесь понимать и любить
его странности, когда перестанете беспокоиться о математической стройности, будет сделан ваш
первый шаг к достижению элегантности в С++.
С++ наряду с Lisp, Smalltalk и другими динамическими языками (в отличие от С) обладает средствами
для низкоуровневых манипуляций с компьютером. Вы можете создать свой собственный тип данных и
подсунуть его компилятору так, чтобы он принял этот тип за встроенный. Вы можете управлять
вызовами своих функций, обращениями к переменным классов, выделением и освобождением памяти,
инициализацией и удалением объектов — и все это (в основном) происходит без потери
эффективности или безопасности типов. Но в отличие от других языков, если эта сила будет применена
неправильно, программа на С++ «грохнется». А если устоит программа, грохнутся ваши коллеги-
программисты — если вы не придумаете, как пояснить свои намерения и использовать правильную
идиому для особенно сложных моментов. Как известно, Дедал со своим сыном Икаром бежал и
заточения на Крите с помощью крыльев, сделанных из перьев и воска. Дедал, главный архитектор и
изобретатель, спокойно порхал где-то внизу. Его безрассудный сын поднялся слишком высоко к
солнцу и упал в море. Хммм… Нет, пожалуй, аналогия получилась неудачная. Ведь именно Дедал
построил Лабиринт — такой сложный, что в попытках выбраться из него люди либо умирали, либо
попадали на обед к Минотавру. Может, попробуем более современную аналогию? Используя
15
низкоуровневые возможности С++, вы действуете, как суровый детектив с его сакраментальной
фразой: «Доверься мне — я знаю, что делаю». Компилятор закатывает глаза и безмолвно подчиняется.
С++ интригует своими явными противоречиями. Его гибкость легко превращается в главный источник
ошибок. За возможности его расширения не приходится расплачиваться скоростью или объемом кода.
Он элегантен в одних руках и опасен в других, прост и сложен одновременно. После нескольких лет
работы вы так и не можете решить, восхищаться им или проклинать. Да, настоящий знаток понимает
все концепции, лежащие в основе языка и склоняющие чашу весов в его пользу. Эти концепции не
видны с первого взгляда; чтобы понять их, необходимо в течение нескольких лет пытаться решать
совершенно разные задачи. Некоторые архитектурные парадигмы лучше всего соответствуют
конкретным языковым решениям. Их неправильное сочетание обернется хаосом, а правильное —
элегантностью.
Три великие идеи С++
О нетривиальном использовании C++ написано так много, что я даже не знаю, с чего начать. Вам
когда-нибудь приходилось видеть стереограммы? С первого взгляда они похожи на случайный узор, но
после медленного и внимательного разглядывания на них проявляется слон, спираль или что-нибудь
еще. Чтобы увидеть смысл в точках и пятнах, нужно рассматривать их в контексте объединяющей
темы. Именно здесь кроется одно из самых больших разочарований при изучении архитектуры и идиом
C++. Сначала кажется, что перед вами — огромная куча разрозненных приемов и ни одного правила
того, как ими пользоваться. Эта книга научит вас «видеть слона». Существует множество
классификаций нетривиальных аспектов C++, но я разделил их на несколько простых тем:
• Косвенные обращения.
• Гомоморфные иерархии классов.
• Пространства памяти.
В основе каждой темы лежит конкретный синтаксис и средства C++, и их совместное применение
позволяет решать самые разные задачи. Существует много других приемов и принципов, которые тоже
стоило бы включить в эту книгу, но эти три категории помогают организовать очень большое
количество тем в логически стройную структуру.
В первой части приведен обзор многих важных аттракционов синтаксического цирка C++. Например,
многие программисты C++ не имеют большого опыта работы с перегруженными операторами и лишь
теоретически знают, как они применяются. Оказывается, большинство программистов никогда не
использует шаблоны или обработку исключений и лишь немногие умеют пользоваться потоками
ввода/вывода за рамками простейших обращений к объектам cout и cin. В части 1 стараюсь
выровнять уровни подготовки читателей, заполнить пробелы в ваших знаниях C++ и подготовиться к
игре. Часть 1 можно читать от корки до, бегло просматривать или пропускать целые разделы в
зависимости от того, насколько хорошо вы знакомы с нюансами C++.
Термин косвенное обращение (indirection) относится к разным конкретным темам, однако везде
используется одна и та же концепция: клиентский объект обращается с запросом к другому объекту,
который, в свою очередь, поручает работу третьему объекту. Косвенность связана со средним объектом
в цепочке. Иногда годится слышать, что это определение почти совпадает с определением
делегирования
(delegation), одного из
краеугольных
камней объектно-ориентированного
программирования. Тем не менее, в C++ идиомы, используемые с этой концепцией, и ее языковая
поддержка выходят далеко за рамки того, что считается делегированием в других языках. В этой книге
часто используется термин указатель (pointer); вы встретите его в каждой главе. Указатели C++
способны на многое. Они могут определить, где в памяти, на диске или в сети находится объект, на
который они ссылаются; когда он уничтожается; изменяется ли он или доступен только для чтения; и
даже то, существует ли объект или просто представляет собой некую область в абстрактном
пространстве памяти — и все это происходит без активного участия самого объекта, который может
ничего не знать об этих низкоуровневых операциях. Возможности, что и говорить, впечатляющие,
однако они основаны на нескольких очень простых идиомах.
О проектировании иерархии классов говорили все кому не лень — одни по делу, другие болтали об
«имитации объектов реального мира». Большинство аргументов в равной степени относится к любому
объектно-ориентированному языку, и я вовсе не намерен захламлять книгу по C++ своими личными
16
взглядами на объектно-ориентированный дизайн. Тем не менее, один конкретный тип наследования —
гомоморфное наследование (homomorphic derivation) — оказывается исключительно полезным в
сочетании со специфическими средствами C++. В гомоморфной иерархии все производные классы
получают свой открытый интерфейс от некоторого базового класса-предка. Как правило, «мать всех
базовых классов» девственно чиста — она не содержит ни одной переменной, а все ее функции
являются чисто виртуальными. В C++ с этой концепцией ассоциируются многие полезные идиомы
проектирования и программирования.
За концепцией пространства памяти (memory space) кроется нечто большее, чем обычное управление
памятью. Перегружая в C++ операторы new и delete, вы определяете, где создаются объекты и как
они уничтожаются. Кроме того, можно создавать абстрактные коллекции, в которых не всегда понятно,
с чем вы имеете дело — с настоящим объектом или с абстракцией. На горизонте уже видны контуры
новых распределенных объектно-ориентированных структур, разработанных такими фирмами, как
Microsoft, Apple и Taligent. Правда, вам придется пересмотреть некоторые базовые представления о
том, где находятся объекты и как они перемещаются в другое место — все эти темы я выделил в
категорию пространств памяти. Пространства памяти позволяют определить тип объекта во время
выполнения программы — возможность, которой до обидного не хватает в C++. Конечно, мы
поговорим и об управлении памятью, но этим дело не ограничится.
Как читать эту книгу
Перед вами — не руководство с готовыми рецептами для конкретных ситуаций. Скорее это сборник
творческих идей и головоломок. Если к концу книги вы почувствуете, что ваш арсенал приемов
программирования на C++ расширился, значит, я достиг своей цели, а учить вас, когда и как
пользоваться этими приемами, я не стану.
Материал каждой отдельной главы невозможно в полной мере понять без предварительного знакомства
со всеми остальными главами. И все же я приложил максимум усилий, чтобы материал любой главы
был полезен немедленно после знакомства с ней и чтобы главы логически следовали друг за другом, а
наш воображаемый слон вырисовывался постепенно — бивни, уши, хобот и т. д. После прочтения
книга может пригодиться в качестве справочника — что-то вроде личной и очень краткой
энциклопедии приемов программирования и идиом C++.
За многие годы изучения и использования C++ я узнал, что даже у опытных программистов в
познаниях встречаются пробелы; в оставшемся материале части я постараюсь выровнять уровень
подготовки всех читателей. Это не вступительное описание языка, а скорее краткая сводка тем,
которые будут использованы в последующих главах. В главе 2 мы стремительно пробежимся по
некоторым особенностям языка. Глава 3 посвящена шаблонам — постепенно эта тема становится все
более важной, поскольку шаблоны поддерживаются во все большем числе компиляторов. В главе 4
рассматривается обработка исключений на основе рекомендованного стандарта ANSI и приводится
пара замечаний о нестандартных исключениях, встречающихся в реальном мире.
Часть 2 посвящена разным типам указателей — от глупых до гениальных. На этом фундаменте
построена вся книга, и я уверен, что эти сведения будут полезны любому читателю.
В части 3 рассматриваются структура и реализация типов и иерархий классов в C++. Основное
внимание уделено одному из частных случаев — гомоморфным иерархиям классов. Заодно мы
поговорим об объектах классов, представителях и других любопытных темах. Большинству читателей
стоит прочитать третью часть от начала до конца, но никто не запрещает вам просмотреть ее и
отобрать темы по своему вкусу. И хотя вы будете полагать, что знаете об указателях все на свете, они
совершенно неожиданно снова возникнут в контексте гомоморфных иерархий.
В части 4 нас поджидает самая ужасная тема C++ — управление памятью. Уровень изложения
меняется от примитивного до нормального и сверхсложного, но основное внимание уделяется тем
проблемам, которые могут возникнуть при программировании на C++, и их возможным решениям на
базе разных языковых средств. Лично я считаю, что начальные главы этой части абсолютно
необходимы для счастливой и полноценной жизни в C++, но если вас, допустим, совершенно не
интересует процесс сборки мусора — оставьте последнюю пару глав и займитесь чем-нибудь более
полезным для общества.
17
Несколько слов о стиле программирования
Вот эти несколько слов: стиль программирования меня не волнует. Я достаточно краток? Если хотя бы
половина времени, израсходованного на правильную расстановку фигурных скобок, тратилась на
обдумывание программы или еще лучше — на общение с пользователями, то вся отрасль работала бы
намного эффективнее. Конечно, единство стиля — вещь хорошая, но я еще не видел книги или
руководства по стилю, которые бы стоили даже часового собрания группы в начале проекта. К тому же
ни одна книга или руководство по стилю не превратят код неаккуратного программиста в нечто
осмысленное. В сущности, стиль часто используется как оправдание недостатка внимания к самой
программе. Наконец, я еще не видел, чтобы в спорах о стиле один программист в чем-то убедил
другого, поэтому любые дискуссии на эту тему считаю бесполезной тратой времени.
У меня есть свои собственные принципы и свой стиль, но в основном я собираюсь отказаться от своего
пути и понемногу пользоваться всеми стилями, с которыми мне приходилось встречаться. Книга
посвящена языковым идиомам, а не расположению фигурных скобок или регистру символов. Надеюсь,
мое решение будет раздражать всех читателей в равной мере.
Я также весьма вольно обошелся с подставляемыми (inline) функциями классов, особенно с
виртуальными. В каноническом варианте подставляемые функции должны выглядеть следующим
образом:
class Foo {
public:
void MemberFn();
};
inline void Foo::MemberFn()
{
...
}
В моей книге этот фрагмент будет выглядеть иначе:
class Foo {
public:
void MemberFnO {...};
};
Я оформлял как подставляемые даже виртуальные функции классов, хотя одни компиляторы
отвергают такой синтаксис, а другие обрабатывают его неправильно. Делалось это для экономии места.
Если бы тексты всех подставляемых функций приводились отдельно, книга заметно выросла бы в
размерах, а разрывы страниц чаще приходились на середину листинга. Так что не относитесь к
подставляемым функциям слишком серьезно.
Садитесь в любимое кресло, заводите хорошую музыку, ставьте под руку чашку чая и попытайтесь
получить удовольствие!
Синтаксис С++
За годы преподавания C++ я узнал, что подавляющее большинство программистов C++ (включая
самых опытных) редко пользуется некоторыми возможностями языка. Конечно, дело это сугубо
индивидуальное, но при всей сложности и глубине C++ небольшой обзор не повредит никому. В этой и
двух следующих главах я постараюсь выровнять уровень подготовки читателей перед тем, как пе-
реходить к действительно интересным темам. Эта глава не заменит Annotated Reference Manual или
другое справочное руководство — вы не найдете в ней полной спецификации языка. Я лишь
рассмотрю некоторые языковые средства, которые часто понимаются неверно или не понимаются
вовсе. Придержите шляпу и приготовьтесь к стремительному облету синтаксиса C++!
Переменные и константы
Болтовню о том, что такое переменные и для чего они нужны, пропускаем. Нашего внимания
заслуживают две темы: константность и сравнение динамических объектов со стековыми.
const
Ключевое слово const, которое в разных контекстах принимает разные значения, одно из самых
информативных в C++. Да, между этими значениями есть кое-что общее, но вам все равно придется
запомнить все конкретные случаи.
Константные переменные
Если переменная объявлена с ключевым словом const, значит, она не должна меняться. После
определения константной переменной вы уже не сможете изменить ее значение или передать ее в
качестве аргумента функции, которая не гарантирует ее неизменности. Рассмотрим простой пример с
константной целой переменной.
const int j = 17;
// Целая константа
j = 29;
// Нельзя, значение не должно меняться
const int i;
// Нельзя, отсутствует начальное значение
Третья строка неверна, поскольку в ней компилятору предлагается определить случайную переменную,
которую никогда не удастся изменить, — этакий странный генератор случайных целых констант.
Вообще говоря, вы сообщаете компилятору, какой конструктор он должен использовать в конкретном
случае. Если бы переменная i относилась к нетривиальному классу, то при объявлении константного
экземпляра пришлось бы явно указать конструктор и его аргументы. int — вырожденный случай,
поскольку на самом деле const int j=17; — то же, что и int j(17).
Но вот компилятор узнал, что нечто должно быть константным. Он просыпается и начинает искать
ошибки — не только фактические, но и потенциальные. Компилятор не разрешит использовать ваше
константное нечто в любом неконстантном контексте, даже если шестилетний ребенок разберется в
программе и докажет, что в ней нет ни одной ошибки.
const i = 17;
int& j = 1;
// Нельзя, потому что позднее j может измениться
2
20
Не важно, будете ли вы изменять величину, на которую ссылается j. Компилятор предполагает, что
вам захочется это сделать, и на всякий случай устраняет искушение. Иначе говоря, константность —
свойство переменной, а не данных, поэтому неконстантная переменная не может ссылаться на
константную величину.
const и #define
Две следующие строки не эквивалентны:
const int i = 17;
#define i 17;
В первой строке определяется переменная, занимающая некоторую область памяти, а во второй —
макрос. Обычно отличия несущественны, если не считать одного-двух лишних тактов, затраченных на
каждое обращение к константной переменной. Однако если переменная является глобальной и
принадлежит нетривиальному классу со своим конструктором, ситуация резко меняется.
Дополнительные сведения приведены в разделе «Инициализация глобальных объектов» этой главы.
Константы в перечислениях
Перечисления (епит) не очень широко использовались в языке С по одной простой причине:
символические имена констант имеют глобальную область действия и быстро захламляют
пространство имен. В C++ эта проблема исчезла, поскольку область действия символических имен
ограничивается классом или структурой.
class Foo {
public:
enum Status { kOpen = 1, kClosed };
};
// Где-то в программе
Foo::Status s = Foo::kOpen;
Обратите внимание — область действия должна быть явно указана как в имени типа, так и в
символическом имени. Следовательно, символические имена kOpen и kClosed можно использовать в
программе и для других целей. Компилятор рассматривает символические имена перечислений как
макросы, а не как константные переменные. Это обстоятельство может оказаться важным при
инициализации глобальных переменных (см, далее в этой главе).
Указатель на константу
С указателями дело обстоит несколько сложнее, поскольку приходится учитывать два значения: адрес
и содержимое памяти по этому адресу. В следующем примере р — это указатель на константу;
находящийся в указателе адрес может измениться, но содержимое памяти по этому адресу — нет.
const int* p;
int i = 17;
p = &i;
// Можно
*p = 29;
// Нельзя
Сказанное также относится к структурам и объектам.
class foo {
public:
int x;
};
const foo* f = new foo;
f->x = 17;
// Нельзя, присвоение членам класса не допускается
21
Константный указатель
С константными указателями все наоборот: адрес изменять нельзя, но зато можно изменять
содержимое памяти по этому адресу.
int i = 17;
int j = 29;
int* const p;
// Нельзя! Должно быть задано начальное значение
int* const p1 = &i;
// Порядок
*p1 = 29;
// Можно; величина, на которую ссылается указатель,
// может изменяться
p1 = &j;
// Нельзя
Константный указатель на константу
Константный указатель на константу (попробуйте-ка трижды быстро произнести это вслух!) изменить
вообще нельзя. Это неизменяемый адрес неизменяемой величины.
int i = 17;
int j = 29;
const int* const p;
// Нельзя. Должен быть задан начальный адрес
const int* const p1 = &i; // Можно
*p1 = 29;
// Нельзя
p1 = &j;
// Нельзя
Константные аргументы функций
Константный аргумент функции должен подчиняться тем же правилам, что и любая другая
константная переменная.
void f(const int* p)
{
*p = 17;
// Нельзя
int i = 29;
p = &i;
// Можно, но зачем?
}
// Где-то в программе
int i = 17;
f(&i);
// Порядок, фактический аргумент не обязан быть константой
Обратите внимание — аргумент, указанный при вызове функции, не обязан быть константным. Этот
вопрос целиком остается на усмотрение стороны-получателя. Передача по ссылке осуществляется по
тем же правилам, что и передача по адресу.
void f(const int& p)
{
p = 17;
// Нельзя
int i = 29;
p = i;
// Можно (на грани фола)
}
// Где-то глубоко в программе
int i = 17;
f(i);
// Порядок
22
Неконстантные аргументы функций
Если формальный аргумент функции объявлен неконстантным, то и фактический аргумент,
используемый при вызове, тоже должен быть неконстантным.
void f(int*);
int i = 17;
const int* p = &i;
const int j = 29;
f(&i);
// Можно, потому что i – не константа
f(p);
// Нельзя
f(&j);
// Тоже нельзя, потому что j - константа
Это еще одно средство, с помощью которого компилятор соблюдает принцип «единожды константный
всегда остается константным». Даже если функция f на самом деле не изменяет значения своего
формального параметра, это ни на что не влияет.
Константные функции классов
В константных функциях классов переменная this интерпретируется как указатель на константу.
Компилятор даст вам по рукам, если вы попробуете воспользоваться переменной this для изменения
переменной класса или найти для нее иное, неконстантное применение. Смысл ключевого слова const
зависит от его места в объявлении функции; для константных функций оно, словно бородавка, торчит
после сигнатуры функции.
class foo {
private:
int x;
public:
void f() const;
void g();
};
void h(int*);
void m(foo*);
void foo::f();
{
x = 17;
// Нельзя: изменяется переменная класса
this->g();
// Нельзя: g – некоторая функция
h(&x);
// Нельзя: h может изменить x
m(this);
// Нельзя: неконстантный аргумент в m()
}
Первая ошибка — попытка изменить переменную класса через this. В константных функциях класса
foo переменная this фактически объявляется как const foo* this;. Вторая ошибка сложнее. Из
приведенного фрагмента неизвестно, изменяет ли функция g какие-либо переменные класса foo, но это
и не важно; одной возможности достаточно, чтобы ваш компилятор разразился негодующими воплями.
Из константной функции класса нельзя вызывать неконстантные функции через this. Похожая
ситуация возникает с третьей и четвертой ошибкой — компилятор попытается спасти вас от самого
себя и не допустит потенциально опасные строки.
Один из верных признаков профессионала C++ — ключевые слова const, обильно разбросанные по
функциям классов. Любая функция класса, которая гарантированно не изменяет this, должна без
малейших размышлений объявляться константной. Впрочем, как видно из приведенного выше
фрагмента, эта стратегия работает лишь в том случае, если все участники команды следуют вашему
примеру и объявят константными свои функции, В противном случае возникают каскадные ошибки.
Часто выясняется, что недавно купленная библиотека классов не использует константные функции и
23
нарушает ваш пуританский стиль кодирования. Мораль: константные функции классов нужно
использовать либо с полным фанатизмом (желательно), либо не использовать вовсе.
Стековые и динамические объекты
Иногда мне кажется, что C++ лучше изучать без предварительного знакомства с C. В C++ часто
используются те же термины, что и в С, но за ними кроются совершенно иной смысл и правила
применения. Например, возьмем примитивный целый тип.
int x = 17;
В C++ это будет экземпляр встроенного «класса» int. В С это будет... просто int. Встроенные классы
имеют свои конструкторы. У класса int есть конструктор с одним аргументом, который
инициализирует объект передаваемым значением. Теоретически существует и деструктор, хотя он
ничего не делает и ликвидируется всеми нормальными разработчиками компиляторов в процессе
оптимизации. Важно осознать, что встроенные типы за очень редкими исключениями подчиняются тем
же базовым правилам, что и ваши расширенные типы.
Вы должны понимать эту теоретическую особенность C++, чтобы правильно относиться к стековым и
динамическим объектам и связанным с ними переменным.
Размещение в стеке
Чтобы выделить память для стековой переменной в области действия блока, достаточно просто
объявить ее обычным образом.
{
int i;
foo f(constructor_args);
// Перед выходом из блока вызываются деструкторы i и f
}
Стековые объекты существуют лишь в границах содержащего их блока. При выходе за его пределы
автоматически вызывается деструктор. Разумеется, получение адреса стекового объекта — дело
рискованное, если только вы абсолютно, стопроцентно не уверены, что этот указатель не будет
использован после выхода за пределы области действия объекта. Все фрагменты наподобие
приведенного ниже всегда считаются потенциально опасными:
{
int i;
foo f;
SomeFunction(&f);
}
Без изучения функции SomeFunction невозможно сказать, безопасен ли этот фрагмент.
SomeFunction может передать адрес дальше или сохранить его в какой-нибудь переменной, а по
закону Мэрфи этот адрес наверняка будет использован уже после уничтожения объекта f. Даже если
сверхтщательный анализ SomeFunction покажет, что адрес не сохраняется после вызова, через пару
лет какой-нибудь новый программист модифицирует SomeFunction, продлит существование адреса
на пару машинных команд и — БУМ!!! Лучше полностью исключить такую возможность и не
передавать адреса стековых объектов.
Динамическое размещение
Чтобы выделить память для объекта в куче (heap), воспользуйтесь оператором new.
foo* f = new foo(constructor_args);
Вроде бы все просто. Оператор new выделяет память и вызывает соответствующий конструктор на
основании переданных аргументов. Но когда этот объект уничтожается? Подробный ответ на этот
вопрос займет примерно треть книги, но я не буду вдаваться в технические детали и отвечу так: «Когда
24
кто-нибудь вызовет оператор delete для его адреса». Сам по себе объект из памяти не удалится; вы
должны явно сообщить своей программе, когда его следует уничтожить.
Указатели и ссылки
Попытки связать указатели с динамическими объектами часто приводят к недоразумениям. В
сущности, они не имеют друг с другом ничего общего. Вы можете получить адрес стекового объекта и
выполнить обратное преобразование, то есть разыменование (dereferencing) адреса динамического
объекта. И на то, и на другое можно создать ссылку.
{
foo f;
foo* p = &f;
f.MemberFn();
// Использует сам объект
p->MemberFn();
// Использует его адрес
p = new foo;
foo& r = *p;
// Ссылка на объект
r.MemberFn();
// То же, что и p->MemberFn()
}
Как видите, выбор оператора . или -> зависит от типа переменной и не имеет отношения к атрибутам
самого объекта. Раз уж мы заговорили об этом, правильные названия этих операторов (. и ->) —
селекторы членов класса (member selectors). Если вы назовете их «точкой» или «стрелкой» на семинаре
с коктейлями, наступит гробовая тишина, все повернутся и презрительно посмотрят на вас, а в дальнем
углу кто-нибудь выронит свой бокал.
Недостатки стековых объектов
Если использовать оператор delete для стекового объекта, то при большом везении ваша программа
просто грохнется. А если вам (как и большинству из нас) не повезет, то программа начнет вести себя,
как ревнивая любовница — она будет вытворять, всякие гадости в разных местах памяти, но не скажет,
на что же она разозлилась. Дело в том, что в большинстве реализаций C++ оператор new записывает
пару скрытых байтов перед возвращаемым адресом. В этих байтах указывается размер выделенного
блока. По ним оператор delete определяет, сколько памяти за указанным адресом следует освободить.
При выделении памяти под стековые объекты оператор new не вызывается, поэтому эти
дополнительные данные отсутствуют. Если вызвать оператор delete для стекового объекта, он
возьмет содержимое стека над вашей переменной и интерпретирует его как размер освобождаемого
блока.
Итак, мы знаем по крайней мере две причины, по которым следует избегать стековых объектов — если
у вас нет действительно веских доводов в их пользу:
1. Адрес стекового объекта может быть сохранен и использован после выхода за границы области
действия объекта.
2. Адрес стекового объекта может быть передан оператору delete.
Следовательно, для стековых объектов действует хорошее правило: Никогда не получайте их адреса
или адреса их членов.
Достоинства стековых объектов
С другой стороны, память в стеке выделяется с головокружительной быстротой — так же быстро, как
компилятор выделяет память под другие автоматические переменные (скажем, целые). Оператор new
(по крайней мере, его стандартная версия) тратит несколько тактов на то, чтобы решить, откуда взять
блок памяти и где оставить данные для его последующего освобождения. Быстродействие — одна из
веских причин в пользу выделения памяти из стека. Как вы вскоре убедитесь, существует немало
способов ускорить работу оператора new, так что эта причина менее важна, чем может показаться с
первого взгляда.
25
Автоматическое удаление — второе большое преимущество стековых объектов, поэтому
программисты часто создают маленькие вспомогательные стековые классы, которые играют роль
«обертки» для динамических объектов. В следующем забавном примере динамический класс Foo
«упаковывается» в стековый класс PFoo. Конструктор выделяет память для Foo; деструктор
освобождает ее. Если вы незнакомы с операторами преобразования, обратитесь к соответствующему
разделу этой главы. В двух словах, функция operator Foo*() позволяет использовать класс PFoo везде,
где должен использоваться Foo* — например, при вызове функции g().
class PFoo {
private:
Foo* f;
public:
PFoo() : f(new Foo) {}
~PFoo() { delete f; }
operator Foo*() { return f; }
}
void g(Foo*);
{
PFoo p;
g(p);
// Вызывает функцию operator Foo*() для преобразования
// Уничтожается p, а за ним – Foo
}
Обратите внимание, что этот класс не совсем безопасен, поскольку адрес, возвращаемый функцией
operator Foo*(), становится недействительным после удаления вмещающего PFoo. Мы разберемся
с этим чуть позже.
Мы еще не раз встретимся с подобными фокусами. Вся соль заключается в том, что стековые объекты
могут пригодиться просто из-за того, что их не приходится удалять вручную. Вскоре я покажу вам, как
организовать автоматическое удаление динамических объектов, но эта методика очень сложна и вряд
ли пригодна для повседневного применения.
У стековых объектов есть еще одно преимущество — если ваш компилятор поддерживает ANSI-
совместимую обработку исключений (exception). Когда во время раскрутки стека происходит
исключение, деструкторы стековых объектов вызываются автоматически. Для динамических объектов
этого не случается, и ваша куча может превратиться в настоящий хаос. Рискуя повториться, я скажу,
что мы вернемся к этой теме позднее.
Области действия и функции
Одно из значительных преимуществ C++ над С — возможность ограничения области действия
символических имен. Впрочем, это палка о двух концах, поскольку правила определения области
действия иногда довольно запутанны. Кроме того, в C++ появилась перегрузка функций и — как ее
расширение — перегрузка операторов. Предполагается, что вы уже знакомы с азами, поэтому в своем
кратком обзоре я ограничусь лишь некоторыми нетривиальными особенностями функций и областей
действия.
Области действия
Область действия создается следующими конструкциями:
• класс;
• структура;
• объединение;
• блок;
•
глобальное пространство имен.
26
Символические имена, объявленные в области действия, относятся только к данной области. Они не
ограничиваются перечислениями и простыми переменными. Структуры, классы и функции также
могут определяться в конкретной области действия.
Классы
Класс в C++ — нечто большее, чем простая структура данных. Это аналог модуля из других языков
программирования, средство упорядочения символьных имен.
class Foo {
public:
static int y;
// Глобальная переменная
static void GFn();
// Глобальная функция
int x;
// Переменная класса
Foo();
// Конструктор
void Fn();
// Функция класса
typedef int (*IntFn)();
// Тип
enum Status { kOpen = 0, kClosed }; // Другой тип
struct Bar {
// Вложенная структура
int a;
int b;
static void BarFn();
}
private:
void Hn();
};
В этом фрагменте приведены некоторые вариации на тему классов. Переменная у — глобальная
переменная, a GFn() — глобальная функция, хотя область действия их имен ограничивается классом
Foo. Во всех функциях класса Foo к ним можно обращаться просто по имени, но за его пределами
необходимо использовать оператор области действия :::
Foo::Foo()
{
GFn();
// Мы уже находимся в области действия Foo
}
void f()
{
Foo::GFn(); // Необходимо задать область действия
}
Аналогично, определение типа IntFn, перечисление Status и даже вложенную структуру Bar также
можно использовать без указания области действия в функциях класса Foo, но в любом другом месте
эту область необходимо задать. Для вложенных типов с открытой видимостью синтаксис указания
области действия может принять несколько устрашающий вид, как видно из следующего примера для
структуры Ваr:
Foo::Bar b;
Foo::Bar::BarFn();
По этой причине вложенные структуры либо делаются тривиальными, либо доступ к ним
ограничивается.
Члены класса х, Foo и Fn(), имеют смысл лишь в контексте конкретного экземпляра (instance) этого
класса. Для обращения к ним используются операторы-селекторы членов класса, . и ->. Широкие
массы (и, как я выяснил на собственном горьком опыте, даже разработчики компиляторов C++) почти
не знают о том, что с помощью селекторов можно вызывать статические функции класса и обращаться
27
к статическим переменным класса. Следующий фрагмент верен, хотя бедные читатели вашей
программы придут в такое замешательство, что подобное можно проделывать только в последний день
перед увольнением:
Foo f;
f.Gfn();
// То же, что и Foo::GFn();
Структуры
Структура в C++ — почти что полноценный класс. Со структурой можно делать все, что можно делать
с классом. Например, структуры могут участвовать в наследовании; в них можно объявлять секции
public, private, protected и даже виртуальные функции. Тем не менее, для структур действуют
несколько иные правила: по умолчанию все члены считаются открытыми (public), чтобы готовые
программы на С не приходилось переписывать заново под каноны C++.
Теория — вещь хорошая, но давайте вернемся на землю. Стоит ли демонстрировать свою «крутизну» и
объявлять структуру с множественным наследованием и виртуальными функциями? На практике
структуры используются вместо классов лишь при соблюдении следующих условий:
• Структура не содержит виртуальных функций.
• Структура не является производной от чего-либо, кроме разве что другой структуры.
• Структура не является базовой для чего-либо, кроме разве что другой структуры.
Нормальные программисты C++ обычно используют структуры лишь для маленьких удобных наборов
данных с тривиальными функциями. В частности, структуры часто используются в ситуациях, когда
объект C++ должен быть совместим на битовом уровне с внешней структурой данных (особенно со
структурами С). При этом можно запросто объявлять конструкторы и невиртуальные функции (осо-
бенно тривиальные встроенные), поскольку для них не создается v-таблица, которая могла бы
нарушить битовую совместимость.
Объединения
Объединения C++ почти не отличаются от объединений С. Они позволяют сэкономить несколько байт
за счет наложения различных структур данных поверх друг друга. Объединения могут содержать
невиртуальные функции, в том числе конструкторы и деструкторы, но при этом они должны
подчиняться довольно жестким ограничениям:
• Члены объединения не могут иметь конструкторов (хотя само объединение — может).
• Объединение не может быть производным от чего-либо.
• Ничто не может быть производным от объединения.
• Деструкторы членов не вызываются, хотя деструктор самого объединения, если он есть,
вызывается.
Поскольку объединения не участвуют в иерархии наследования, нет смысла объявлять в них
виртуальные функции или защищенные члены. Члены объединений разрешается объявлять закрытыми
(private) или открытыми (public). Объединения пригодятся лишь тогда, когда вам действительно
нужно сэкономить память, когда вы не собираетесь делать объединение производным или базовым, а
также включать в него виртуальные функции или конструкторы. Иначе говоря, пользы от них не так
уж много.
Блоки
Все, что стоило бы сказать о блоках, уже известно вам из С или из предыдущего описания стековых
объектов.
Глобальные пространства имен
Глобальные пространства имен C++ настолько сложны, что в моем представлении процесс компиляции
глобальных конструкций напоминает магический ритуал с дымом благовоний и пением мантр. Я
постараюсь изложить эти правила как можно проще. Область действия глобальных типов
28
ограничивается файлом, в котором они объявляются. Глобальные переменные и функции к тому же
подчиняются правилам компоновки для нескольких исходных файлов. Рассмотрим следующую
ситуацию:
// В файле Foo.cpp
typedef int Symbol;
// В файле Bar.cpp
typedef void (*Symbol)();
Никакого конфликта не возникнет, если только по мазохистским соображениям вы не включите один
файл с расширением .срр в другой директивой #include. Символическое имя Symbol известно
компилятору лишь в тех исходных файлах, в которых оно встречается, поэтому в разных исходных
файлах его можно использовать по-разному. Следующий фрагмент неверен, поскольку на этот раз
символическое имя соответствует переменной, а не типу. Имя переменной должно быть уникальным
для всех файлов, передаваемых компоновщику.
// В файле Foo.cpp
int Symbol;
// В файле Bar.cpp
void (*Symbol)();
Единственное исключение из этого правила относится к перегрузке функций, о которой будет
рассказано в следующем разделе. Конечно, конфликты имен часто возникают в любом достаточно
большом проекте, в котором несколько программистов работают над разными исходными файлами.
Одно из возможных решений — использование статических членов; другое — объявление глобальных
переменных и функций статическими. Если переменная или функция объявляется статической, она
определена лишь в границах исходного файла.
// В файле Foo.cpp
static int Symbol;
// В файле Bar.cpp
static void (*Symbol)();
Увидев ключевое слово static, компилятор проследит за тем, чтобы компоновщик не перепутал две
разные версии одного символического имени при условии что исходные файлы не компилируются
вместе; будут сгенерированы две разные переменные.
К любому символическому имени, объявленному в глобальном пространстве имен, можно обратиться с
помощью оператора :: без указания области действия:
::Fn();
// Вызвать глобальную функцию с заданным именем
int x = ::i;
// Присвоить x значение глобальной переменной
::SomeType y;
// Использовать глобально объявленный тип
Явно заданная область действия всегда отменяет все символические имена, определенные локально —
например, внутри блока или класса.
Перегрузка
В C++ существует несколько способов многократного использования имен функций. В частности,
пространства имен функций формируются на основе классов. Одноименные функции в классах, не
связанных друг с другом, выполняют совершенно разные задачи. Перегрузка функций развивает
великую традицию разделения пространств имен функций и позволяет многократно использовать
имена функций в границах одной области действия.
Аргументы
Две функции с одинаковыми именами считаются разными, если они отличаются по количеству,
порядку или типу аргументов.
void Fn();
29
void Fn(int);
void Fn(long);
// Можно, если типы long и int отличаются размером
int Fn(int);
// Нельзя – отличается только тип возвращаемого значения
int Fn(char*);
// Можно, отличаются аргументы
void Fn(int, char*);
void Fn(char*, int);
// Можно, аргументы следуют в другом порядке
void Fn(char* s, int x, int y = 17); // Можно – три аргумента вместо двух
Fn(“hello”, 17); // Ошибка – совпадают две сигнатуры
Пока аргументы отличаются, компилятор не жалуется на изменение возвращаемого типа.
Инициализация по умолчанию (такая как у=17) может присутствовать при объявлении функции, хотя
позднее она может стать причиной неоднозначности при вызове функции (как в последней строке
примера).
Константные функции
Константная функция, аргументы которой совпадают с аргументами неконстантной функции, тем не
менее считается другой функцией. Компилятор вызывает константную или неконстантную версию в
зависимости от типа переменной, указывающей или ссылающейся на объект.
class Foo {
public:
void Fn();
void Fn() const; // Другая функция!
};
Foo* f = new Foo;
f->Fn();
// Вызывается неконстантная версия
const Foo* f1 = f;
f1->Fn();
// Вызывается константная версия
Видимость
В C++ существует подробная (а по мнению некоторых, даже слишком подробная) система правил, по
которым можно узнать, что вы видите прямо перед собой, а что вышло из вашего поля зрения. Базовые
правила для открытых защищенных и закрытых символических имен в классах и структурах настолько
просты, что я не стану их пересказывать. Ниже приведена краткая сводка наиболее каверзных
вопросов, относящихся к понятию видимости (visibility) в C++.
Закрытое наследование
При закрытом наследовании от базового класса все его защищенные и открытые члены становятся
закрытыми в производном классе; члены закрытого базового класса недоступны для пользователей
производного класса. Доступ к ним возможен лишь из функций базового и производного класса, а
также из друзей производного класса.
Кроме того, производный класс нельзя преобразовать к одному из его закрытых базовых классов или
надеяться, что это сделает компилятор.
class Mixin {
private:
int x;
protected:
int y;
public:
Mixin();
Void a();
};
30
class Foo : private Mixin {...};
class Bar : public Foo {...};
Переменная х видна лишь в функциях класса Mixin — в конструкторе и А(). Переменная у видна
лишь в функциях класса Foo, как и функция Mixin::A(). Все члены Mixin не видны в классах,
производных от Foo (таких как Ваr в этом фрагменте). Все друзья Foo видят х и А(), а друзья Bar —
нет.
Переобъявление членов
Хотя описанная ситуация возникает довольно редко, допускается переобъявление виртуальных
функций с целью изменения их атрибутов видимости по отношению к базовому классу.
class Foo {
protected:
virtual void Fn();
};
class Bar : public Foo {
public:
virtual void Fn();
};
В классе Foo функция Fn() была защищенной, но в новом варианте она объявлена открытой. Для
переменных класса или невиртуальных функции это сделать нельзя. Переобъявление переменной или
невиртуальной функции скрывает прототип из базового класса.
class Foo {
private:
int x;
public:
void Fn();
};
class Bar : public Foo {
private:
int x;
// Вторая переменная с тем же именем
public:
void Fn();
// Вторая функция
};
// В клиентской программе
Bar *b = new Bar;
b->Fn();
// Вызывает Bar::Fn()
Foo* f = b; // Можно, потому что Foo – открытый базовый класс
f->Fn();
// Вызывает Foo::Fn()
Существуют две разные переменные с одним локальным именем х. В области действия Foo
символическое имя х означает Foo::х. В области действия Bar символическое имя х означает Ваr::х.
Конечно, для открытой или защищенной переменной х это вызовет невероятную путаницу, но для
закрытой переменной подобной двусмысленности не будет. Пример Fn() показывает, какой хаос
возникает при скрытии открытой или защищенной функции класса. При попытке скрыть открытую или
защищенную функцию хороший компилятор C++ выдает предупреждение.
Видимость перегруженных и виртуальных функций класса
Если в базовом классе функция объявлена невиртуальной, превращать ее в виртуальную в производном
классе не рекомендуется. Она поведет себя не так, как виртуальная функция, и безнадежно запутает
читателей вашей программы. Но на ситуацию можно взглянуть и под другим углом. Удивительно, но
факт — ключевое слово virtual обязано присутствовать только в базовом классе. Если оно
31
пропущено в производном классе, компилятор должен интерпретировать версию функции в
производном классе так, словно она и там была объявлена виртуальной. Я люблю называть подобную
логику работы компилятора DWIMNIS: «Do what I mean, not what I say» («Делай то, что я
подразумеваю, а не то, что я говорю»). Как правило, в C++ эта логика начисто отсутствует, поэтому ее
редкие проявления (как в данном случае) смотрятся неестественно. В следующем примере для обоих
указателей будет вызвана функция Bar::Fn():
class Foo {
public:
virtual void Fn();
};
class Bar {
public:
void Fn();
// Все равно считается виртуальной
};
Bar* b = new Bar;
b->Fn();
// Вызывает Bar::Fn()
Foo* f = b;
f->Fn();
// Также вызывает Bar::Fn()
Подобные ситуации нежелательны по двум причинам. Во-первых, компилятор может неправильно
интерпретировать их, и тогда в последней строке будет вызвана функция Foo::Fn(). Во-вторых, ваши
коллеги ни за что не разберутся, почему в одном месте функция Fn() виртуальная, а в другом — нет.
После бессонной ночи они могут устроить погром в конторе.
Если в производном классе создается функция с тем же именем, но с другой сигнатурой, она скрывает
все сигнатуры базового класса для данной функции, но только в области действия производного класса.
Понятно? Нет? Что ж, вы не одиноки.
class Foo {
public:
virtual void Fn();
virtual void Fn(char*);
};
class Bar {
public:
virtual void Fn(int); // Можно, но не желательно
};
Вероятно, многие новички-программисты допоздна засиживались на работе, пытаясь разобраться в
происходящем. А происходит следующее:
• При попытке вызвать Fn() через Ваr* доступной будет лишь одна сигнатура, void Fn(int).
Обе версии базового класса скрыты и недоступны через Bar*.
• При преобразовании Bar* в Foo* становятся доступными обе сигнатуры, объявленные в Foo,
но не сигнатура void Fn(int). Более того, это не переопределение, поскольку сигнатура
Bar::Fn() отличается от версии базового класса. Другими словами, ключевое слово virtual
никак не влияет на работу этого фрагмента.
Если вам когда-нибудь захочется сделать нечто похожее, встаньте с кресла, медленно прогуляйтесь
вокруг дома, сделайте глубокий вдох, сядьте за компьютер и придумайте что-нибудь другое. Если уж
перегружать, то перегружайте все сигнатуры функции. Никогда не перегружайте часть сигнатур и
никогда не добавляйте новые сигнатуры в производный класс без переопределения всех сигнатур
функции базового класса. Если это покажется слишком сложным, запомните хорошее правило: когда
при чтении программы возникают вопросы, вероятно, ваше решение неудачное.
32
Друзья
Любой класс может объявить что-нибудь своим другом (friend). Друзья компилируются обычным
образом, за исключением того, что все защищенные и закрытые члены дружественного класса видны
так, словно друг является функцией этого класса. Друзьями можно объявлять функции — как
глобальные, так и члены классов. Классы тоже могут объявляться друзьями других классов; в этом
случае во всех функциях класса-друга «видны» все члены того класса, другом которого он является.
class Foo;
class BarBar {
public:
int Fn(Foo*);
};
class Foo {
friend void GlobalFn();
// Дружественные глобальные функции
friend class Bar;
// Дружественный класс
friend int BarBar::Fn(Foo*); // Дружественная функция класса
friend class DoesNotExist; // См. Ниже
private:
int x;
struct ListNode {
ListNode* next;
void* datum;
ListNode() : next(NULL), datum(NULL) {}
} head;
protected:
int y;
public:
void G();
};
void GlobalFn()
{
Foo* f = new Foo;
f->x = 17;
// Разрешается из-за дружеских отношений
}
class Bar {
private:
Foo* f;
public:
Bar() : f(new Foo) {}
void WalkList();
};
void Bar::WalkList()
{
Foo::ListNode* n = f->head.next;
for (; n != NULL; n = n->next)
cout << n->datum << endl;
}
int BarBar::Fn(Foo* f)
{
return f->x;
}
33
Друзей принято объявлять сначала, перед членами класса и перед ключевыми словами public,
protected и private. Это объясняется тем, что на друзей не действуют обычные атрибуты
видимости; нечто либо является другом, либо не является. Весь фрагмент программы после
определения класса Foo вполне допустим. Друзья имеют доступ ко всем членам Foo, включая
закрытые. В этом примере есть одна действительно интересная строка — та, в которой другом объяв-
ляется несуществующий класс DoesNotExist. Как ни странно, она не вызовет ни предупреждения, ни
ошибки компилятора. Объявления друзей игнорируются на момент компиляции Foo. Они
используются лишь тогда, когда будет компилироваться друг. Даже когда друга не существует,
компилятор остается в счастливом неведении.
Типы и операторы
Темы, рассматриваемые в этом разделе, на первый взгляд не кажутся близкими, однако все они
вращаются вокруг общей концепции — абстрактных типов данных.
Конструкторы
Конструктор можно рассматривать двояко — как функцию, инициализирующую объект, или, с
позиций математики, как отображение аргументов конструктора на домен класса. Я предпочитаю
второй подход, поскольку он помогает разобраться с некоторыми языковыми средствами (например,
операторами преобразования).
С конструкторами связаны очень сложные правила, но каждый программист C++ должен досконально
знать их, иначе минимум три ночи в году ему придется проводить за отладкой.
Конструкторы без аргументов
Если в вашем классе имеется конструктор, который вызывается без аргументов, он используется по
умолчанию в трех следующих случаях.
class Foo {
public:
Foo();
};
class Bar : public Foo {
// 1. Базовый класс
public:
Bar();
};
class BarBar {
private:
Foo f;
// 2. Переменная класса
};
Foo f;
// 3. Созданный экземпляр Foo
Foo* f1 = new Foo;
// 3. То же, что и предыдущая строка
Если в списке инициализации членов (см. следующий раздел) конструктора Bar не указан какой-
нибудь другой конструктор Foo, то при каждом создании экземпляра Ваг будет вызываться
конструктор Foo без аргументов. Аналогично, если f отсутствует в списке инициализации членов
конструктора BarBar, будет использован конструктор Foo без аргументов. Наконец, при каждом
создании экземпляра Foo без указания конструктора по умолчанию используется конструктор без
аргументов.
Конструкторы с аргументами
Конструкторы, как и все остальные функции, можно перегружать. Вы можете объявить столько
сигнатур конструкторов, сколько вам потребуется. Единственное настоящее отличие между
сигнатурами конструкторов и обычных функций заключается в том, что конструкторы не имеют
возвращаемого значения и не могут объявляться константными. Если вы объявите какие-либо
34
конструкторы с аргументами, но не объявите конструктора без аргументов, то компилятор не позволит
конструировать объекты этого класса, даже в качестве базового для другого класса, с использованием
конструктора без аргументов.
class Foo {
public:
Foo(char*);
};
Foo f;
// Нельзя – нет конструктора без аргументов!
class Bar : public Foo {
public:
Bar();
};
Bar::Bar()
{
// Ошибка! Нет конструктора Foo без аргументов
}
Списки инициализации членов
Чтобы избавиться от этой проблемы, в C++ находится очередное применение символу : — для
создания списков инициализации членов. Так называется список спецификаций конструкторов,
разделенных занятыми и расположенных между сигнатурой конструктора и его телом.
class Foo {
public:
Foo(char*);
};
class Bar : public Foo {
public:
Bar(char*);
};
class BarBar {
private:
Foo f;
int x;
public:
BarBar();
};
Bar::Bar(char* s) : Foo(s) {...}
BarBar::BarBar : f(“Hello”), x(17) {...}
В конструкторе Bar список инициализации членов используется для инициализации базового класса
Foo. Компилятор выбирает используемый конструктор на основании сигнатуры, определяемой по
фактическим аргументам. При отсутствии списка инициализации членов сконструировать Bar было бы
невозможно, поскольку компилятор не мог бы определить, какое значение должно передаваться
конструктору базового класса Foo. В конструкторе BarBar список инициализации членов
использовался для инициализации (то есть вызова конструкторов) переменных f и х. В следующем
варианте конструктор работает не столь эффективно (если только компилятор не отличается
сверхъестественным интеллектом):
BarBar::BarBar() : f(“Hello”)
{
x = 17;
}