Б. Страуструп - Дизайн и Эволюция C++. 2006 (1160775), страница 16
Текст из файла (страница 16)
2.11. Гарантии времени исполнения Описанные выше механизмы контроля доступа просто предотвращают неразрешенный доступ. Другой вид гарантий предоставляется такими «специальными функциями-членами», как конструкторы, которые компилятор распознает и неявно вызывает. Идея заключалась в том, чтобы дать программисту средства формулировать гарантированные условия (их еще называют инвариантами), на выполнение которых другие функции-члены могли бы рассчитывать (см. также раздел 16.10).
2. 11.1. Конструкторы и деструкторы В то время я часто объяснял эту концепцию так: функция печч (конструктор) создает среду, в которой работают другие функции-члены, а функция с1е1есе использовалось бы глобальное а). Я больше не считаю этот аргумент решающим (если и считал раньше), но такой ход оказался полезным, поскольку позволяет программисту добавлять и удаля~ь во время отладки роЬ11с и ргйуасе, не меняя смысла программы. Любопытно, была ли эта с~арона определения С++ осознанным решением. Может быть, это просто побочный результат препроцессорной технологии, использовавшейся во времена С ту111т С!аззсз, который не пересматривался при реализации настоящего компилятора во время перехода к С++ (см. раздел 3.3). Другой аспект механизма зашиты в С++, свидетельствующий о влиянии операционных систем, — зто отношение к нарупгению правил. Я думаю, что любой компетентный программист может обойти любое правило, которое не поддержано аппаратно, так что не стоит даже пытаться защититься от мошенничества 1АЯМ1: «Механизмы контроля доступа в С++ обеспечивают защиту от случайности, а не от преднамеренного обмана.
Любой язык программирования, который поддерживает прямой доступ к памяти, обязателычо оставляет любой элемент данных открытым дпя сознательного «жульничества», нарушающего явные правила типов, сформулированные дпя этого элемента». Задача механизма защиты — гарантировать, что любое такое действие в обход системы типов выражено явно, и свести к минимуму необходимость в таких нарушениях. Пришедшая из операционных систем концепция защиты от чтения/записи превратилась в С++ в понятие о сопя с (см. раздел 3.8). За прошедшие годы было много предложений о том, как предоставить доступ к единице, меньшей, чем целый класс.
Например: ЕИИИИИИ6." Язык С )))г!1!т С!аввев (деструктор) эту среду уничтожает и освобождает выделенные лля пее ресурсы. Например: с1азз щоо1сог: о)з1есс ( l* *l рл)з11с: пеи() ( Г* создать защелку для монитора */ ) бе1есе() ( /* освободить и удалить защелку *Г ) l* ... *l ): См. также разделы 3.9 и 13.2А.
Откуда возникла концепция конструктора? Подозреваю, что ее изобрел я сам. Я был знаком с механизмом инициализации объекта класса в 3(шц!а. Однако я смотрел на объявление класса прежде всего как па определение интерфейса, так что хотел избежать помещения в него кода. Поскольку в С ~у!г!) С!аззеэ вслед за С было три класса хранения, то какой-то вид функций инициализации почти неизбежно должен был распознаваться компилятором (см. раздел 2.11.2).
Скоро выяснилось, что было бы полезно иметь несколько конструкторов. Это наблюдение послужило причиной появления механизма перегрузки в С++ (см. раздел 3.6). 2. 11.2. Распределение памяти и конструкторы Как и в С, память для объектов можно выделять тремя способами: из стека (автоматическая память), по фиксированному адресу (статическая память) и нз свободной памяти (куча или динамическая память).
В любом случае для созданного объекта должен быть вызван конструктор. В С размещение объекта в свободной памяти требует только вызова функпии распределения. Например: щолгсог* р = (щол1сог*)ща11ос(всзеог(щолггог)); Очевидно, что для С щчг!) С)аззеэ этого недостаточно, так как нельзя гарантировать вызов конструктора. Поэтому я ввел оператор, отвечающий одновременно за выделение памяти и инициализацию: щолвсог* р = пеи щолссог; Я назвал этот оператор пеуг, поскольку так назывался аналогичный оператор в 3)шп!а.
Оператор пен вызывает некую функцию для выделения памяти, а затем конструктор для инициализации этой памяти. Такая комбинированная операция называется порождением или просто созданием объекта; она создает объект в неинициализированной области памяти. Нотация, вводимая оператором пех, очень удобна. Однако объединение выделения и инициализации памяти в одной операции без явного механизма извещения об ошибках на практике привело к некоторым сложностям. Обработка ошибок, происшедших в конструкторе, редко бывает конструктивно важной, однако с введением исключений (см, раздел 16.5) было найдено общее решение и этой проблемы. Чтобы свести к минимуму необходимость повторной компиляции, в С(гонг оператор пеы для классов, имеющих конструктор, был реализован просто как Менее существенные средства 11ИИИИИЫ вызов этого конструктора.
Стало быть, конструктор должен выполнить и выделение памяти, и ее инициализацию. При таком подходе, если единица трансляции выделяет память для всех объектов класса Х с помощью пе»/ и не вызывает встраиваемых функций из Х, то ее не пало перекомпилировать в случае изменения размера или представления Х. Единица трансляции — это принятый в Аг)81 С термин для обозначения исходного файла после обработки препроцсссором. Иными словами, это то, что передается компилятору. Такой подход показался очень удобным для уменьшения числа компиляций моих моделирующих программ.
Однако в сообществе пользователей С туйЬ С!аээсз и С++ важность данной оптимизации была осознана много позже (см. раздел !3,2). Оператор с)е1есе был введен как парный к пе»/, точно так же как функция атее)) является парной по отношению к юа11ос ! ) — см. разделы 3.9 и 10.7. 2. 11З.
Функции са11 и ге1игп Интересно, что в первой реализации С ту)тЬ С!аьвез была возможность, которая в С++ исчезла, хотя меня часто просят ее вернуть. Можно было определить функцию, которая неявно вызывалась при каждом вызове любой функции-члена (кроме конструктора), и другую функцию, которая вызывалась перед каждым возвратом из любой функции-члена (кроме деструктора). Эти функции назывались соответственно са11 и сего сп.
Я использовал их для синхронизации в классе монитора, входившего в первоначальный вариант библиотеки для поддержки многозадачности !8тгоиэтгир, 1980Ь1: с1азз щолссот : о)з)ест ! /* ... */ са11)); ! /* установить защелку */ ) теситл!)/ ! /* снять защелку */ ) /* ... */ ): Данные функции аналогичны методам: )зевоте и: айсег в языке С).08. Они были исключены из языка, поскольку никто (кроме меня) ими не пользовался и мне так и не удалось убедить публику, что у них имеются важные применения. В 1987 г. Майк Тиман (М!)се Т!ещапп) предложил альтернативное решение 1Т!ешапп, !987], которое назвал «обертками» (щ гаррегэ), но на семинаре разработчиков ВВЕХ1Х в Эстес Парк было решено, что с этой идеей связано слишком много проблем, и включать ее в С++ не стали. 2.12. Менее существенные средства В С щчтЬ С!аэзез были включены две не очень важные особенности: перегрузка оператора присваивания и аргументы по умолчанию.
Они были предтечами механизма перегрузки в С++ (см. раздел 3.6). 2. 12.1. Перегрузка оператора присваивания Вскоре было отмечено, что классы с нетривиальным представлением, такие как эсг1 пд и уесс от, нельзя нормально копировать, поскольку принятая в С семантика Язык С ~а(тг) С!аваев ЕИИИИИИ1~ копирования (побитовое) для таких типов не годилась. Для них копирование по умолчанию сводилось к разделяемому несколькими объектами представлению, а не к созданию настояших копий.
В качестве решения я позволил программисту самому определять семантику копирования [81гоцзтгцр, 1980[: кК сожолению, стандартное почленное [как для всгцсс) присвоивание не всегда идеально. Типичный объект классо — это тол~ко корень информационного дерево, а копирование одного корня беэ учета ветвей нежелательно. Точно так же простоя перезапись объекта клоссо может привести к хаосу. Решение этих проблем — в изменении семантики присвоивония для объектов класса.
Это можно сделоть, объявив функцию-член клоссо орегагог>ч Нопример: с1авв х ( рцо11сг 1пг а; с1авв у * Р' чо1г) орегасог= (с1авв х *); )г чо(г( х.орегасог= (с1авв х * ггояг) ( а = Ггоэг->аг г)е1есе рг р = Гголг->рг Ггоэг->р = Ог ) Токим образом, для объектов кпоссо х мы определили разрушающее чтение, а не опероцию копирования, подразумеваемую стондортной семантикой». В работе [8ггоцзггцр,1982[ приведен вариант примера, в котором проверяется условие с)тфз==1голг, чтобы корректно обработать присваивание самому себе.
Как видите, я учился этой технике на собственных ошибках. Если в классе был определен оператор присваивания, то он использовался для всех операций копирования. Во время инициализации объект сначала инициировался значением по умолчанию с помощью функции пен (конструктора) без аргументов, а потом выполнялось присваивание. Это было сочтено неэффективным и привело к появлению в Сч-ь копируюших конструкторов (см. раздел 11А.1). 2. (2.2. Аргументы по умолчанию Интенсивное использование конструкторов цо умолчанию — результат наличия операторов присваивания, определенных пользователем, — привело к появлению аргументов по умолчанию [8ггоцзггцр, 1980[: кСписок аргументов по умолчанию добавлен к механизму классов уже на очень поздней стадии, чтобы противостоять распространению практики передочи идентичных ксписков стандартных аргументов» для объектов кпоссо через оргументы функции, аргументов для объектов класса, являющихся членоми другого клоссо, о токже аргументов для базового клоссо.