Этот текст является конспектом глав "классы", "производные классы", "иерархии классов" книги Страуструпа "язык программирования С++" и предназначен для упрощения работы по повторному прочтению этих глав. Чтобы быстрее вспомнить, про что там говорилось. Поэтому этот материал возможно будет полезен только тем, кто уже хорошо знаком с понятием класса С++ и уже с ним работал, но не помнит всех подробностей и тонкостей (что в общем то, наверно, и невозможно), а внимательно читать весь текст упомянутых глав накладно с точки зрения времени. В то же время я постарался выдернуть максимум информации, чтобы материал был полным. Изложение носит чисто качественный характер, без кусков кода и примеров, иначе это изложение было бы просто переписанными главами из книги, что противоречило бы поставленной цели. Буду благодарен за любые замечания и комментарии.
Целью введения классов является предоставление средств создания новых типов, которые настолько же удобны в использовании, как и встроенные. Тщательно подобранный набор типов, определяемых пользователем, делает программу более краткой и выразительной.
"Конкретные" типы, определяемые пользователем, с логической точки зрения незначительно отличаются от встроенных типов. В идеале, такие типы должны отличаться от встроенных не по методам их использования, а только способом их создания.
Класс - это определяемый пользователем тип.
Конструкция
class X {...};
называется определением класса, потому что она определяет новый тип. По историческим причинам определение класса часто называют объявлением класса. Также как и объявления, не являющиеся определениями, определение класса может быть повторено в другом исходном классе при помощи #include, не нарушая правила одного определения ODR.
Во время проектирования класса возникает искушение добавить средства просто потому, что они кому-нибудь могут понадобиться. Требуется основательно подумать, какие средства нужны на самом деле, и включать только их. Эти усилия приводят к большей понимаемости программы и уменьшают её размер. Одним из способов уменьшения количества похожих друг на друга функций является использование аргументов по умолчанию. Когда значение аргумента используется для указания "вставить значение по умолчанию", оно должно находиться вне пределов допустимых "обычных" значений этого аргумента.
Переменная, которая является частью класса, но не является частью объекта этого класса, называется статическим членом. Существует ровно одна копия статического члена. Аналогично, функция, которой требуется доступ к членам класса, но не требуется, чтобы она вызывалась для конкретного объекта класса, называется статической функцией-членом.
К статическому объекту можно можно обращаться без указания имени объекта. Вместо этого в качестве квалификатора его имени используется имя самого класса.
Статические члены - и функции и данные - должны быть где-то определены.
По умолчанию, объекты класса можно копировать. Объект некоторого класса можно проинициализировать при помощи копирования объекта того же класса. Это можно сделать даже там, где объявлен конструктор. По умолчанию, копия объекта класса содержит копию каждого члена. Если это не то что требуется, можно реализовать более подходящее поведение, определив копирующий конструктор
X::X(const X&)
Аналогично, объекты класса могут по умолчанию копироваться при помощи операции присваивания. Семантика по умолчанию - почленное копирование. Пользователь может определить свой оператор присваивания.
Суффикс const является частью типа константных функций-членов. То есть, когда константная функция-член определяется вне класса, суффикс const обязателен.
В нестатической функции-члене ключевое слово this является указателем на объект, для которого вызвана функция. В нестатической функции-члене класса Х this имеет тип X*. Это не обычная переменная. Невозможно получить её адрес или присвоить ей что-нибудь. В константной функции члене класса X this имеет тип const X* для предотвращения модификации самого объекта.
Иногда функцию-член необходимо сделать константной, но для необходимой функциональности все же требуется менять части объекта (не заметно для пользователя). В таком случае говорят о логическом и физическом постоянствах. Логически функция-член константна, физически - нет.
Если есть необходимость написать константную функцию-член класса Х, но которой все же необходимо изменить некоторые логически "скрытые" члены класса, можно использовать конструкцию const_cast<X*>(this). Однако поведение будет неопределённым, если объект объявлен константой. Чтобы избежать явного преобразования типа "снятие const путем приведения" можно при объявлении соответствующих членов класса использовать квалификатор хранения mutable. mutable означает: "ни при каких условиях не является const".
По определению структура есть класс, все члены которого по умолчанию являются открытыми. Страуструп предпочитает использовать структуры для классов, у которых все данные открыты. Он думает о таких классах как о "не совсем типах, являющихся просто структурами данных". Конструкторы и функции доступа к ним могут быть полезны для таких структур, но скорее для удобства, а не как гаранты свойств типа.
Имеет смысл помещать данные класса в конце, чтобы сделать акцент на функциях открытого интерфейса.
Модификаторы доступа (private, public) можно использовать несколько раз в одном и том же объявлении. Это может запутать программиста , но полезно при автоматической генерации кода.
Функция, определённая в пределах определения класса, является встроенной функцией членом. Функция, объявленная в классе, может обращаться к любому члену класса так, как будто весь класс был полностью определен до рассмотрения тела функции. Однако это может быть неочевидно для читающего программу. Поэтому можно определять встраиваемые (inline) функции после класса.
Набор операций, характерный для типа, определяемого пользователем:
Инициализация является относительно сложной операцией, так как включает в себя проверку корректности данных. Это позволяет пользоваться и копировать созданный класс без дополнительных проверок. Конструктор устанавливает инвариант класса, а другие функции-члены могут полагаться на этот инвариант и должны сохранять его.
Как правило, у класса есть набор функций, связанных с ним, но не требующих определения их в классе, так как они не нуждаются в непосредственном доступе к представлению. Такие функции-помощники объявляются в заголовочном файле класса либо помещаются в одно пространство имён с классом или классами. Использование пространства имён для одного класса обычно является избыточным.
Пользователю должен быть предоставлен механизм определения небольших конкретных типов. Слово "конкретный" нужно для того, чтобы отличать такие типы или классы от абстрактных классов и иерархий классов, а также, чтобы подчеркнуть их сходство со встроенными типами, такими как int или char. Конкретные типы называют ещё типами значений, а их использование - программированием, ориентированным на значения. Модель использования и проектирование конкретных типов сильно отличается от того, называется ООП. В частности, конкретные типы не демонстрируют полиморфное поведение. Хороший набор конкретных типов может создать фундамент приложения.
Конструкторы инициализируют объект. Они создают среду. в которой работают функции-члены. Требуется функция, которая будет гарантировано вызвана при уничтожения объекта. Такая функция называется деструктором. Деструктор вызывается неявно, когда автоматическая переменная выходит из области видимости, когда удаляется объект, находящийся в свободной памяти и т.д. Только в очень необычных ситуациях пользователю требуется явно вызывать деструктор.
Комплиментарные пары конструктор/деструктор являются типичным механизмом в С++ для выражения понятия объекта переменного размера. К типам без деструктора можно относится как типы, деструкторы которых ничего не делают.
Аналогично можно полагать, что многие типы имеют конструктор по умолчанию. Конструктор по умолчанию есть конструктор, вызываемый без аргумента. Если пользователь объявил конструктор по умолчанию, то он и будет задействован. В противном случае (и если пользователь не объявил другие конструкторы) компилятор попытается при необходимости сгенерировать его. Конструктор по умолчанию, сгенерированный компилятором, неявно вызывает конструкторы по умолчанию для членов класса и конструкторы базовых классов.
Если тип члена класса не является классом - он не инициализируется.
Константы и ссылки должны быть проинициализированы, а потому класс, содержащий члены, являющиеся константами или ссылками, не может быть сконструирован по умолчанию, если программист не предоставил явно конструктор.
Конструкторы по умолчанию можно вызывать явно.
Встроенные типы имеют конструкторы по умолчанию.
Классификация объектов способам создания и уничтожения:
Конструктор локальной переменной вызывается когда управление передается инструкции, содержащей объявление этой локальной переменной. Деструктор - когда происходит выход из блока, содержащего это объявление. Деструкторы локальных переменных вызываются в порядке обратном выполнению конструкторов.
Если у класса есть члены, которые являются указателями, то при копировании объектов этого класса из-за "почленности" копирования может быть не тот результат, что ожидается. Поэтому для таких классов надо обязательно определять, что понимается под копированием, то есть задать копирующий конструктор и копирующее присваивание. Копирующий конструктор и копирующее присваивание значительно отличаются. Основная причина - это то, что копирующий конструктор инициализирует "чистую" память, а копирующий оператор присваивания доложен правильно работать с уже созданным объектом.
Основная стратегия при реализации оператора присваивания:
Для объекта, создаваемого в свободной памяти, вызывается конструктор класса, указанный в операторе new. Вызов оператора delete для одного и того же объекта является ошибкой. Если вызова для объекта в свободной памяти оператора delete не будет, то возможна утечка памяти. Некоторые реализации имеют сборщики мусора, но их поведение не стандартизовано. Если известно, что имеется сборщик мы можем во избежание двойного удаления не использовать оператор delete, но тогда не будет переносимости кода. После применения к объекту delete обращение к нему - это ошибка. Реализации надёжно не отслеживают такие ошибки.
Пользователь сам может определить, каким образом new выделяет память и как delete её освобождает. Кроме того можно указать способы взаимодействия выделения памяти, инициализации(конструирования) и исключений.
Аргументы конструкторов-членов указываются в списке инициализации членов в определении конструктора объемлющего класса. Конструкторы членов класса вызываются до вызова конструктора самого класса. Конструкторы вызываются в том порядке, в котором они объявлены в классе, а не в том, в котором они указаны в списке инициализации. Поэтому во избежание путаницы порядок объявлений в классе и порядок в списке инициализации должны соответствовать (за этим должен следить программист). Деструкторы членов вызываются в порядке обратном порядку конструкторов и после вызова деструктора самого класса.Если конструктор члена не нуждается в аргументах, то его можно не указывать в списке инициализации.
Члены объекта, у которых присваивание и инициализация отличны (объекты-члены без конструктора по умолчанию, константные члены, члены ссылки) должны быть проинициализированы конструктором объемлющего класса (в списке инициализации). Отсутствие инициализации объектов такого типа является ошибкой.
Для большинства типов мы имеем выбор между использованием инициализатора и присваиванием. Желательно пользоваться синтаксисом инициализации, делая явным факт инициализации.
Проинициализировав статический константный член класса внутри класса(определение статического члена делается вне класса) необходимо помнить, что при определении его нельзя инициализировать ещё раз.
Копирующие конструкторы по умолчанию и копирующие операторы присваивания по умолчанию копируют все элементы класса. Если такое копирование не может быть выполнено, то попытка копирования объекта класса будет ошибкой. Присваивание по умолчанию не может быть сгенерировано, если нестатический член класса является ссылкой, константой или типом без копирующего оператора присваивания.
При добавлении нового члена класса всегда проверяйте, не надо ли модифицировать определяемые пользователем конструкторы с учётом инициализации и копирования нового члена.
Не существует способа явного указания аргументов конструктора (за исключением списка инициализации) при объявлении массива.
Деструктор для каждого элемента массива вызывается при уничтожении массива. Это происходит неявно для массивов, память для которых не выделялась с помощью new. И в си и в с++ указатели на отдельный объект и на первый элемент массива не различаются. Поэтому программист должен указать, удаляется ли массив или отдельный объект (delete, delete[]). Как выделяется память, зависит от реализации. Поэтому реакция на неверное использование delete или delete[] будет различной. Компилятор подобную ошибку в простейшем случае находить самостоятельно, но, как правило, неприятности из-за такой путаницы происходят во время исполнения.
Вместо массивов в стиле си желательно пользоваться классом, подобным vector.
Конструктор локального статического объекта вызывается один раз при первом выполнении инструкции, содержащей определение объекта. Деструкторы локальных статических объектов выполняются в порядке, обратном созданию, при завершении программы. В какой момент точно - не определено.
Переменная, определенная вне любой функции(глобальная, объявленная в пространстве имен или статически в классе) инициализируется (конструируется) до вызова main(). После выхода из main() будет вызван деструктор для каждой такой переменной. Динамическая компоновка усложняет ситуацию, откладывая инициализацию до того момента, когда код будет скомпонован в исполняемый код. Конструкторы нелокальных объектов в единице трансляции выполняются в процессе их определений. Объявления типов не влияют на порядок создания объектов. Деструкторы вызываются в порядке, обратном конструкторам.
Не дается никаких гарантий по поводу порядка конструирования (а также вызова деструкторов) нелокальных объектов из разных единиц компиляции. Это будет зависеть от реализации. Причём не гарантируется один и тот же порядок в одной и той же реализации.
Если временный объект не связан со ссылкой или не используется для инициализации именованного объекта, он уничтожается по достижении конца полного выражения, в котором был создан. Полное выражение - это выражение, которое не является частью другого выражения.
Временная переменная может использоваться в качестве инициализатора константной ссылки или именованного объекта.
Надо помнить, что возврат из функции ссылки на локальную переменную является ошибкой, и что неконстантная ссылка не может ссылаться на временную переменную.
Мы можем поместить объекты куда угодно, написав функцию выделения памяти, имеющую дополнительные аргументы, и затем задавая эти аргументы при использовании оператора new:
void* operator new(size_t, void* p){return p;}
void* buf = reinterpret_cast<void*>(0xFFFF);
X* p2 = new(buf)X;
Из-за своего использования синтаксис new(buf)X называется синтаксисом размещения.
Размер размещаемого объекта задаётся неявно. Выбор функции operator new() для вызова оператора new подчиняется обычным правилам соответсятвия аргументов. Каждый operator new() имеет в качестве первого аргумента size_t.
В большинстве случаев в результате применения reinterpret_cast получается значение нужного типа с той же последовательностью битов, что и его аргумент. Этим преобразованием пользуются для опасных, зависящих от реализации, но необходимых преобразований целых значений в указатели и наоборот.
Базовый класс иногда называют суперклассом, а производный подклассом. Однако лучше такой терминологией не пользоваться, так как данные в объекте производного класса являются надмножеством данных базового класса.
Если производный класс Derived имеет базовым открытым классом Base, то Derived* может быть присвоен переменной типа Base* без явного преобразования типа. Обратное преобразование должно быть явным (с помощью static_cast и dynamic_cast). То есть с объектом производного класса можно обращаться как с объектом базового класса при обращении к нему при помощи указателей и ссылок. Обратное не верно.
Член производного класса может пользоваться открытыми (и защищёнными) членами базового класса так, как будто они объявлены в самом производном классе.. Производный класс не может использовать закрытые имена базового класса. Защищённый член ведёт себя как открытый по отношению к члену производного класса и как закрытый по отношению к другим функциям.
Некоторый производные классы нуждаются в конструкторах. Если базовый класс имеет конструктор, он должен быть вызван. Конструкторы по умолчанию могут быть вызваны неявно. Однако, если все конструкторы базового класса требуют указания аргументов, то конструктор этого базового класса должен быть вызван явным образом. Аргументы конструктора базового класса указываются в определении конструктора производного класса. В этом отношении базовый класс ведет себя как член производного класса.
Объекты класса создаются снизу вверх: сначала базовый класс, потом члены и затем сам производный класс. Они уничтожаются в противоположном порядке: сначала производный класс, члены, а затем базовый класс. Члены и подобъекты базовых классов конструируются в порядке их объявления в классе и уничтожаются в обратном порядке.
Копирование объектов класса определяется копирующими конструктором и операторами присваивания.
Копирующая функция базового класса ничего не знает о производном классе. Поэтому при копировании объекта производного класса в объект базового класса происходит так называемая "срезка". Одной из причин передачи указателей и ссылок на объекты в иерархии является желание избежать срезки. Другой причиной является обеспечение полиморфного поведения.
Если не определить оператор копирующего присваивания, он будет сгенерирован компилятором. Отсюда - операторы присваивания не наследуются. Конструкторы никогда не наследуются.
Пусть имеется указатель на некий класс. Этот указатель не обязательно будет указывать на объект именно этого класса, но может и на объект любого класса потомка этого некоего класса в иерархии. Как определить, какому производному типу принадлежит объект на самом деле? Существует четыре фундаментальных решения этой проблемы:
Виртуальные функции решают проблему, связанную с полем типа, предоставляя возможность программисту объявлять в базовом классе функции, которые можно заместить в каждом производном классе.
Для того чтобы объявление виртуальной функции работало в качестве интерфейса к функциям, определённым в производных классах, типы аргументов функции в производном классе не должны отличаться от типов аргументов, объявленных в базовом классе, и только очень (!) небольшие изменения допускаются для типа возвращаемого значения.
Виртуальную функцию иногда называют методом.
Виртуальная функция должна быть определена для класса, в котором она впервые объявлена.
Виртуальную функцию можно использовать, даже если у её класса нет производных классов.
Производный класс, который не нуждается в собственной версии виртуальной функции, не обязан её реализовывать.
Говорят, что функция из производного класса с тем же именем и тем же набором типов аргументов, что и виртуальная функция в базовом классе, замещает (override) виртуальную функцию из базового класса.
За исключением случаев, когда мы явно указываем, какая версия виртуальной функции должна вызываться, активизируется наиболее подходящий для вызывающего объекта вариант замещённой функции.
Тип, имеющий виртуальные функции, называется полиморфным типом.
Для достижения полиморфного поведения в С++ вызываемые функции-члены должны быть виртуальными, и доступ к объектам должен осуществляться через ссылки и указатели. При непосредственном манипулировании объектом (без указателя или ссылки на него) его точный тип известен компилятору, и потому полиморфизм времени выполнения не требуется.
Некоторые классы представляют абстрактную концепцию, для которой объекты не существуют. В таких классах нет данных. Также нету конструктора, так как нет того, что надо инициализировать. Но нужен виртуальный деструктор, чтобы гарантировать правильную очистку данных, которая будет определена в производных классах.
Виртуальные функции можно объявлять в виде чисто виртуальных функций. Виртуальная функция делается чистой при помощи инициализатора =0. Класс с одной или несколькими чисто виртуальными функциями называется абстрактным классом.
Чисто виртуальная функция, которая не определена в производном классе, остается чисто виртуальной, а потому такой класс также является абстрактным.
Оператор delete явным образом уничтожает объект, но нет способа точно определить, к какому классу принадлежит объект. Однако благодаря вируальному деструктору будет вызван нужный деструктор.
Абстрактный класс с набором операций конструирования иногда называют фабрикой, а его функции иногда называют виртуальными конструкторами.
В общем случае, класс создаётся из решётки базовых классов. Исторически большинство таких решёток были деревьями, поэтому решётку классов часто называют иерархией классов. Использование более одного непосредственного базового класса обычно называется множественным наследованием. Наличие одного базового класса называется одиночным наследованием.
Два базовых класса могут иметь функции-члены с одинаковым именем. Неоднозначности для этих функций в производных классах можно устранить с помощью ::.
Разрешение перегрузки не пресекает границ областей видимости классов. В частности, неоднозначности между функциями из различных базовых классов не разрешаются на основе типов аргументов. Объявления using позволяют создавать набор перегруженных функций из базовых и производных классов.
using-объявление в определении класса должно относиться к членам базового класса. using-объявление нельзя использовать для члена класса вне этого класса, его производных классов или их функций членов. using-директиву нельзя поместить в определение класса, и она не может использоваться для класса. using-объявление не может использоваться для доступа к дополнительной информации. Оно просто является механизмом предоставления более удобного доступа к информации, доступ к которой разрешён в принципе.
Виртуальная функция повторяющегося базового класса может быть замещена (единственной) функцией в производном классе.
В решётке наследования все виртуальные базовые классы с одним именем будут представлены одним единственным объектом этого класса. Невиртуальный базовый класс в этой же решётке будет иметь отдельный подобъект, представляющий его.
Конструктор виртуального базового класса вызывается только один раз. Он вызывается (явно или неявно) из конструктора объекта (конструктора самого "нижнего" производного класса). Конструктор виртуального базового класса вызывается до конструкторов производных классов.
Если различные производные классы замещают одну и ту же функцию, то если у этих классов имеются свои производные классы, то в них мы должны заместить эту функцию.
Член класса может быть закрытым, защищённым, открытым (1)private, 2)protected, 3)public):
1)Его имя может использоваться только в функциях-членах и друзьях класса, в котором он объявлен;
2)Его имя может использоваться только в функциях-членах и друзьях класса, в котором он объявлен, и классов, производных от него;
3)Его именем может пользоваться любая функция;
Объявление данных защищёнными обычно свидетельствует об ошибке на этапе проектирования. Защищённые данные приводят к проблемам сопровождения. Члены класса по умолчанию закрыты, что, как правило, является наилучшим вариантом. Эти возражения против защищённости не имеют значения для защищённых функций. Защищённость - это прекрасный способ задания операций для использования в производных классах.
class D:pivate B{/* ... */};
Спецификатор доступа может быть опущен. Тогда private будет по умолчанию.
Спецификатор доступа к базовому классу управляет доступом к членам базового класса и преобразованием указателей и ссылок из типа производного класса в тип базового класса.
Если доступ к имени или базовому классу может быть осуществлён при помощи нескольких путей в решётке классов, то доступ разрешён только в том случае, если он разрешён по каждому из возможных путей.
dynamic_cast - это операция преобразования типа, которая возвращает корректный указатель, если объект имеет ожидаемый тип, и нулевой указатель - в противном случае.
Использование информации о типе во время выполнения обычно называют RTTI (Run-Time Type Information).
Приведение из базового класса в производный часто называют понижающим приведением, а из производного класса в базовый - повышающим приведением (downcast и upcast), так как принято изображать дерево наследования растущим вниз из корня наверху. Приведение между производными классами одного базового класса называют перекрёстным приведением (crosscast).
dynamic_cast не допускает случайных нарушений правил доступа к закрытым и защищённым базовым классам.
dynamic_cast используется тогда, когда правильность преобразования не может быть определена компилятором.
Для выполнения понижающего или перекрёстного приведения требуется, чтобы аргумент dynamic_cast ,был ссылкой или указателем на полиморфный тип.
Если у объекта нет виртуальных функций, им нельзя безопасно манипулировать, не зная его конкретный тип. Если его тип неизвестен, то нет необходимости в использовании dynamic_cast.
Результирующий тип dynamic_cast не обязан быть полиморфным. Это позволяет "заворачивать" конкретный тип в полиморфный, а затем извлекать объект этого конкретного типа обратно.
Приведение к void* с помощью dynamic_cast можно использовать для определения адреса начала объекта полиморфного типа:
void* p1=dynamic_cast<void*>(p2);
где p1 - адрес размещения объекта.
Это полезно при взаимодействии с низкоуровневыми функциями.
В круглых скобках в dynamic_cast должен быть указатель на объект только полиморфного типа.
Результат применения dynamic_cast к указателю нужно всегда явно проверять. Для указателя приведение dynamic_cast можно интерпретировать как вопрос "объект, на который указывает этот указатель, имеет этот тип?". Если dynamic_cast применяется к ссылке, то приведение dynamic_cast является не вопросом, а утверждением "объект, на который указывает этот указатель, имеет этот тип.", а потому при приведении dynamic_cast в случае, если приводимая ссылка является ссылкой на объект неожидаемого типа, то генерируется стандартное исключение bad_cast. Если надо защититься от неудачных приведений к ссылке, необходимо предоставить подходящий обработчик.
Статическое приведение static_cast не анализирует объект, который оно приводит.
Объект типа с ограничениями на способ размещения в памяти (задаваемый, например, языками си или фортран) может использоваться в качестве виртуального базового класса. Для таких объектов доступна только статическая информация о типе.
Существует миллионы строк кода. написанные до появления dynamic_cast.
При использовании dynamic_cast накладные расходы невелики.
Динамическое приведение более безопасно.
Компилятор не может делать предположений о памяти, на которую показывает void*. оэтому динамическое приведение не может осуществить приведение из *void. В этом случае требуется статическое приведение.
Приведения dynamic_cast и static_cast учитывают модификатор const и управление доступом.
Невозможно осуществить приведение в закрытый базовый класс, а "снятие const" (или volatile) требует const_cast. И даже в этом случае использование результата безопасно только в том случае, если объект не был изначально объявлен константным (или volatile).
Создание объекта идёт "снизу вверх", уничтожение "сверху вниз". При этом объект класса является объектом в той мере, в какой он был создан или уничтожен. Неразумно полагаться на порядок создания и уничтожения, но тем не менее этот порядок можно наблюдать при помощи вызовов виртуальных функций, динамического приведения или использования typeid в момент, когда объект ещё не завершён. Лучше не вызывать виртуальные функции на этапе создания или уничтожения.
dynamic_cast удовлетворяет большинство потребностей в информации о типе объекта на этапе выполнения. dynamic_cast сохраняет гибкость и способность к расширению, присущую виртуальным функциям. Но иногда надо знать не "принадлежит ли объект данному типу", а "каков точный тип объекта". Для этой цели служит оператор typeid. Он выдает объект, представляющий тип его операнда. typeid() возвращает ссылку на тип стандартной библиотеки, называемый type_info, определённый в <typeinfo>.
Оператор typeid() наиболее часто используется для нахождения типа объекта, на который указывает указатель или ссылка.
Если значение указателя или ссылки полиморфного типа равно 0, type_info() генерирует исключение bad_typeid. Если операнд type_info() имеет неполиморфный тип или не является lvalue, результат определяется на этапе компиляции без вычисления выражения операнда.
Не гарантируется, что только один объект type_info() существует для каждого типа в системе. Поэтому необходимо использовать == с объектами type_info для проверки равенства, а нес указателями на такие объекты.
Пример использования имени класса:
cout<<typeid(*p).name();
Символьное представление имени класса зависит от реализации Это С-строка находящаяся в системной области памяти, а потому нельзя пытаться её уничтожить при помощи delete[].
Как правило, следует избегать использование switch-инструкции.
С++ предлагает средство косвенной ссылки на член класса. Указатель на член является значением, идентифицирующим член класса. Его можно рассматривать как позицию члена в объекте класса (но компилятор принимает в расчёт различия между данными, виртуальными функциями, невиртуальными функциями и т.д.).
typedef void (ourClass::* memberPointerType)();//тип указателя на функцию-член
memberPointerType memberPointer = &ourClass::memberFunction;
objectPointer -> memberFunction();//прямой вызов
(objectPointer ->* memberPointer)();//вызов через указатель на член
Производный класс по крайней мере содержит члены, которые он унаследовал от базовых классов. Также он может содержать дополнительные члены. Из этого следует, что можно безопасно присвоить указатель на член базового класса указателю на член производного класса, но не наоборот. Это свойство называют контр вариацией.
Создаётся впечатление, что это правило противоречит правилу, что можно присваивать указатель на производный класс указателю на его базовый класс. В действительности оба правила нужны для фундаментальной гарантии того, что указатель ни при каких условиях не укажет на объект, который не реализует (по крайней мере) свойств, подразумеваемых типом этого указателя.
Можно самостоятельно управлять памятью для класса, определив собственные operator new() и operator delete() для индивидуальных объектов. operator new[]() и operator delete[]() играют ту же роль для массивов. Возможна также замена глобальных operator new() и operator delete(). Более надёжный подход состоит в реализации этих операций для конкретного класса. Эта реализация будет справедлива также и для производных классов.
Операторы new() и delete() неявно являются статическими членами. Следовательно у них нет указателя this и они не изменяют объект.
Добавление виртуального деструктора к базовому классу облегчает задачу определения истинного размера уничтожаемого объекта оператором operator delete(). Иначе при удалении объекта с потерянным типом (например, в результате приведения) будет нужна помощь программиста (как и в случае с удалением массива).
Если необходимо иметь пару для выделения/освобождения памяти, которая корректно работает с производными классами, то приходится предоставить виртуальный деструктор в базовом классе, либо воздержаться от использования аргумента типа size_t при освобождении памяти.
Может ли конструктор быть виртуальным? Нет. Но желаемое поведение можно получить. Чтобы создать объект конструктор должен знать тип создаваемого объекта. Поэтому он не может быть виртуальным. Конструктор является не совсем обычной функцией. Конструктор работает с памятью способом недоступным обычным функциям-членам. Нельзя получить указатель на конструктор. Можно создать функцию, которая вызывает конструктор и возвращает созданный объект.
Целью введения классов является предоставление средств создания новых типов, которые настолько же удобны в использовании, как и встроенные. Тщательно подобранный набор типов, определяемых пользователем, делает программу более краткой и выразительной.
"Конкретные" типы, определяемые пользователем, с логической точки зрения незначительно отличаются от встроенных типов. В идеале, такие типы должны отличаться от встроенных не по методам их использования, а только способом их создания.
Класс - это определяемый пользователем тип.
Конструкция
class X {...};
называется определением класса, потому что она определяет новый тип. По историческим причинам определение класса часто называют объявлением класса. Также как и объявления, не являющиеся определениями, определение класса может быть повторено в другом исходном классе при помощи #include, не нарушая правила одного определения ODR.
Во время проектирования класса возникает искушение добавить средства просто потому, что они кому-нибудь могут понадобиться. Требуется основательно подумать, какие средства нужны на самом деле, и включать только их. Эти усилия приводят к большей понимаемости программы и уменьшают её размер. Одним из способов уменьшения количества похожих друг на друга функций является использование аргументов по умолчанию. Когда значение аргумента используется для указания "вставить значение по умолчанию", оно должно находиться вне пределов допустимых "обычных" значений этого аргумента.
Переменная, которая является частью класса, но не является частью объекта этого класса, называется статическим членом. Существует ровно одна копия статического члена. Аналогично, функция, которой требуется доступ к членам класса, но не требуется, чтобы она вызывалась для конкретного объекта класса, называется статической функцией-членом.
К статическому объекту можно можно обращаться без указания имени объекта. Вместо этого в качестве квалификатора его имени используется имя самого класса.
Статические члены - и функции и данные - должны быть где-то определены.
По умолчанию, объекты класса можно копировать. Объект некоторого класса можно проинициализировать при помощи копирования объекта того же класса. Это можно сделать даже там, где объявлен конструктор. По умолчанию, копия объекта класса содержит копию каждого члена. Если это не то что требуется, можно реализовать более подходящее поведение, определив копирующий конструктор
X::X(const X&)
Аналогично, объекты класса могут по умолчанию копироваться при помощи операции присваивания. Семантика по умолчанию - почленное копирование. Пользователь может определить свой оператор присваивания.
Суффикс const является частью типа константных функций-членов. То есть, когда константная функция-член определяется вне класса, суффикс const обязателен.
В нестатической функции-члене ключевое слово this является указателем на объект, для которого вызвана функция. В нестатической функции-члене класса Х this имеет тип X*. Это не обычная переменная. Невозможно получить её адрес или присвоить ей что-нибудь. В константной функции члене класса X this имеет тип const X* для предотвращения модификации самого объекта.
Иногда функцию-член необходимо сделать константной, но для необходимой функциональности все же требуется менять части объекта (не заметно для пользователя). В таком случае говорят о логическом и физическом постоянствах. Логически функция-член константна, физически - нет.
Если есть необходимость написать константную функцию-член класса Х, но которой все же необходимо изменить некоторые логически "скрытые" члены класса, можно использовать конструкцию const_cast<X*>(this). Однако поведение будет неопределённым, если объект объявлен константой. Чтобы избежать явного преобразования типа "снятие const путем приведения" можно при объявлении соответствующих членов класса использовать квалификатор хранения mutable. mutable означает: "ни при каких условиях не является const".
По определению структура есть класс, все члены которого по умолчанию являются открытыми. Страуструп предпочитает использовать структуры для классов, у которых все данные открыты. Он думает о таких классах как о "не совсем типах, являющихся просто структурами данных". Конструкторы и функции доступа к ним могут быть полезны для таких структур, но скорее для удобства, а не как гаранты свойств типа.
Имеет смысл помещать данные класса в конце, чтобы сделать акцент на функциях открытого интерфейса.
Модификаторы доступа (private, public) можно использовать несколько раз в одном и том же объявлении. Это может запутать программиста , но полезно при автоматической генерации кода.
Функция, определённая в пределах определения класса, является встроенной функцией членом. Функция, объявленная в классе, может обращаться к любому члену класса так, как будто весь класс был полностью определен до рассмотрения тела функции. Однако это может быть неочевидно для читающего программу. Поэтому можно определять встраиваемые (inline) функции после класса.
Набор операций, характерный для типа, определяемого пользователем:
- Конструктор, определяющий как должны быть проинициализированы переменные данного типа
- Набор функций доступа (функций-селекторов). Эти функции имеют модификатор const
- Набор функций-модификаторов. При их использовании не должно возникать необходимости разбираться в деталях представления
- Набор неявно определённых операций, позволяющих свободно копировать объекты
- Класс, используемый для сообщений об ошибках путём возбуждения исключений
Инициализация является относительно сложной операцией, так как включает в себя проверку корректности данных. Это позволяет пользоваться и копировать созданный класс без дополнительных проверок. Конструктор устанавливает инвариант класса, а другие функции-члены могут полагаться на этот инвариант и должны сохранять его.
Как правило, у класса есть набор функций, связанных с ним, но не требующих определения их в классе, так как они не нуждаются в непосредственном доступе к представлению. Такие функции-помощники объявляются в заголовочном файле класса либо помещаются в одно пространство имён с классом или классами. Использование пространства имён для одного класса обычно является избыточным.
Пользователю должен быть предоставлен механизм определения небольших конкретных типов. Слово "конкретный" нужно для того, чтобы отличать такие типы или классы от абстрактных классов и иерархий классов, а также, чтобы подчеркнуть их сходство со встроенными типами, такими как int или char. Конкретные типы называют ещё типами значений, а их использование - программированием, ориентированным на значения. Модель использования и проектирование конкретных типов сильно отличается от того, называется ООП. В частности, конкретные типы не демонстрируют полиморфное поведение. Хороший набор конкретных типов может создать фундамент приложения.
Конструкторы инициализируют объект. Они создают среду. в которой работают функции-члены. Требуется функция, которая будет гарантировано вызвана при уничтожения объекта. Такая функция называется деструктором. Деструктор вызывается неявно, когда автоматическая переменная выходит из области видимости, когда удаляется объект, находящийся в свободной памяти и т.д. Только в очень необычных ситуациях пользователю требуется явно вызывать деструктор.
Комплиментарные пары конструктор/деструктор являются типичным механизмом в С++ для выражения понятия объекта переменного размера. К типам без деструктора можно относится как типы, деструкторы которых ничего не делают.
Аналогично можно полагать, что многие типы имеют конструктор по умолчанию. Конструктор по умолчанию есть конструктор, вызываемый без аргумента. Если пользователь объявил конструктор по умолчанию, то он и будет задействован. В противном случае (и если пользователь не объявил другие конструкторы) компилятор попытается при необходимости сгенерировать его. Конструктор по умолчанию, сгенерированный компилятором, неявно вызывает конструкторы по умолчанию для членов класса и конструкторы базовых классов.
Если тип члена класса не является классом - он не инициализируется.
Константы и ссылки должны быть проинициализированы, а потому класс, содержащий члены, являющиеся константами или ссылками, не может быть сконструирован по умолчанию, если программист не предоставил явно конструктор.
Конструкторы по умолчанию можно вызывать явно.
Встроенные типы имеют конструкторы по умолчанию.
Классификация объектов способам создания и уничтожения:
- Именованный автоматический объект
- Объект в свободной памяти
- Нестатический член-объект
- Элемент массива
- Локальный статический объект
- Глобальный объект, объект в пространстве имен, статический объект класса
- Временный объект-часть вычисления выражения
- Объект, помещённого в память, выделенную функцией пользователя
- Член объединения union, который не может иметь ни конструктора, ни деструктора
Конструктор локальной переменной вызывается когда управление передается инструкции, содержащей объявление этой локальной переменной. Деструктор - когда происходит выход из блока, содержащего это объявление. Деструкторы локальных переменных вызываются в порядке обратном выполнению конструкторов.
Если у класса есть члены, которые являются указателями, то при копировании объектов этого класса из-за "почленности" копирования может быть не тот результат, что ожидается. Поэтому для таких классов надо обязательно определять, что понимается под копированием, то есть задать копирующий конструктор и копирующее присваивание. Копирующий конструктор и копирующее присваивание значительно отличаются. Основная причина - это то, что копирующий конструктор инициализирует "чистую" память, а копирующий оператор присваивания доложен правильно работать с уже созданным объектом.
Основная стратегия при реализации оператора присваивания:
- защита от присваивания самому себе
- удаление старых элементов
- инициализация и копирование новых элементов
Для объекта, создаваемого в свободной памяти, вызывается конструктор класса, указанный в операторе new. Вызов оператора delete для одного и того же объекта является ошибкой. Если вызова для объекта в свободной памяти оператора delete не будет, то возможна утечка памяти. Некоторые реализации имеют сборщики мусора, но их поведение не стандартизовано. Если известно, что имеется сборщик мы можем во избежание двойного удаления не использовать оператор delete, но тогда не будет переносимости кода. После применения к объекту delete обращение к нему - это ошибка. Реализации надёжно не отслеживают такие ошибки.
Пользователь сам может определить, каким образом new выделяет память и как delete её освобождает. Кроме того можно указать способы взаимодействия выделения памяти, инициализации(конструирования) и исключений.
Аргументы конструкторов-членов указываются в списке инициализации членов в определении конструктора объемлющего класса. Конструкторы членов класса вызываются до вызова конструктора самого класса. Конструкторы вызываются в том порядке, в котором они объявлены в классе, а не в том, в котором они указаны в списке инициализации. Поэтому во избежание путаницы порядок объявлений в классе и порядок в списке инициализации должны соответствовать (за этим должен следить программист). Деструкторы членов вызываются в порядке обратном порядку конструкторов и после вызова деструктора самого класса.Если конструктор члена не нуждается в аргументах, то его можно не указывать в списке инициализации.
Члены объекта, у которых присваивание и инициализация отличны (объекты-члены без конструктора по умолчанию, константные члены, члены ссылки) должны быть проинициализированы конструктором объемлющего класса (в списке инициализации). Отсутствие инициализации объектов такого типа является ошибкой.
Для большинства типов мы имеем выбор между использованием инициализатора и присваиванием. Желательно пользоваться синтаксисом инициализации, делая явным факт инициализации.
Проинициализировав статический константный член класса внутри класса(определение статического члена делается вне класса) необходимо помнить, что при определении его нельзя инициализировать ещё раз.
Копирующие конструкторы по умолчанию и копирующие операторы присваивания по умолчанию копируют все элементы класса. Если такое копирование не может быть выполнено, то попытка копирования объекта класса будет ошибкой. Присваивание по умолчанию не может быть сгенерировано, если нестатический член класса является ссылкой, константой или типом без копирующего оператора присваивания.
При добавлении нового члена класса всегда проверяйте, не надо ли модифицировать определяемые пользователем конструкторы с учётом инициализации и копирования нового члена.
Не существует способа явного указания аргументов конструктора (за исключением списка инициализации) при объявлении массива.
Деструктор для каждого элемента массива вызывается при уничтожении массива. Это происходит неявно для массивов, память для которых не выделялась с помощью new. И в си и в с++ указатели на отдельный объект и на первый элемент массива не различаются. Поэтому программист должен указать, удаляется ли массив или отдельный объект (delete, delete[]). Как выделяется память, зависит от реализации. Поэтому реакция на неверное использование delete или delete[] будет различной. Компилятор подобную ошибку в простейшем случае находить самостоятельно, но, как правило, неприятности из-за такой путаницы происходят во время исполнения.
Вместо массивов в стиле си желательно пользоваться классом, подобным vector.
Конструктор локального статического объекта вызывается один раз при первом выполнении инструкции, содержащей определение объекта. Деструкторы локальных статических объектов выполняются в порядке, обратном созданию, при завершении программы. В какой момент точно - не определено.
Переменная, определенная вне любой функции(глобальная, объявленная в пространстве имен или статически в классе) инициализируется (конструируется) до вызова main(). После выхода из main() будет вызван деструктор для каждой такой переменной. Динамическая компоновка усложняет ситуацию, откладывая инициализацию до того момента, когда код будет скомпонован в исполняемый код. Конструкторы нелокальных объектов в единице трансляции выполняются в процессе их определений. Объявления типов не влияют на порядок создания объектов. Деструкторы вызываются в порядке, обратном конструкторам.
Не дается никаких гарантий по поводу порядка конструирования (а также вызова деструкторов) нелокальных объектов из разных единиц компиляции. Это будет зависеть от реализации. Причём не гарантируется один и тот же порядок в одной и той же реализации.
Если временный объект не связан со ссылкой или не используется для инициализации именованного объекта, он уничтожается по достижении конца полного выражения, в котором был создан. Полное выражение - это выражение, которое не является частью другого выражения.
Временная переменная может использоваться в качестве инициализатора константной ссылки или именованного объекта.
Надо помнить, что возврат из функции ссылки на локальную переменную является ошибкой, и что неконстантная ссылка не может ссылаться на временную переменную.
Мы можем поместить объекты куда угодно, написав функцию выделения памяти, имеющую дополнительные аргументы, и затем задавая эти аргументы при использовании оператора new:
void* operator new(size_t, void* p){return p;}
void* buf = reinterpret_cast<void*>(0xFFFF);
X* p2 = new(buf)X;
Из-за своего использования синтаксис new(buf)X называется синтаксисом размещения.
Размер размещаемого объекта задаётся неявно. Выбор функции operator new() для вызова оператора new подчиняется обычным правилам соответсятвия аргументов. Каждый operator new() имеет в качестве первого аргумента size_t.
В большинстве случаев в результате применения reinterpret_cast получается значение нужного типа с той же последовательностью битов, что и его аргумент. Этим преобразованием пользуются для опасных, зависящих от реализации, но необходимых преобразований целых значений в указатели и наоборот.
Базовый класс иногда называют суперклассом, а производный подклассом. Однако лучше такой терминологией не пользоваться, так как данные в объекте производного класса являются надмножеством данных базового класса.
Если производный класс Derived имеет базовым открытым классом Base, то Derived* может быть присвоен переменной типа Base* без явного преобразования типа. Обратное преобразование должно быть явным (с помощью static_cast и dynamic_cast). То есть с объектом производного класса можно обращаться как с объектом базового класса при обращении к нему при помощи указателей и ссылок. Обратное не верно.
Член производного класса может пользоваться открытыми (и защищёнными) членами базового класса так, как будто они объявлены в самом производном классе.. Производный класс не может использовать закрытые имена базового класса. Защищённый член ведёт себя как открытый по отношению к члену производного класса и как закрытый по отношению к другим функциям.
Некоторый производные классы нуждаются в конструкторах. Если базовый класс имеет конструктор, он должен быть вызван. Конструкторы по умолчанию могут быть вызваны неявно. Однако, если все конструкторы базового класса требуют указания аргументов, то конструктор этого базового класса должен быть вызван явным образом. Аргументы конструктора базового класса указываются в определении конструктора производного класса. В этом отношении базовый класс ведет себя как член производного класса.
Объекты класса создаются снизу вверх: сначала базовый класс, потом члены и затем сам производный класс. Они уничтожаются в противоположном порядке: сначала производный класс, члены, а затем базовый класс. Члены и подобъекты базовых классов конструируются в порядке их объявления в классе и уничтожаются в обратном порядке.
Копирование объектов класса определяется копирующими конструктором и операторами присваивания.
Копирующая функция базового класса ничего не знает о производном классе. Поэтому при копировании объекта производного класса в объект базового класса происходит так называемая "срезка". Одной из причин передачи указателей и ссылок на объекты в иерархии является желание избежать срезки. Другой причиной является обеспечение полиморфного поведения.
Если не определить оператор копирующего присваивания, он будет сгенерирован компилятором. Отсюда - операторы присваивания не наследуются. Конструкторы никогда не наследуются.
Пусть имеется указатель на некий класс. Этот указатель не обязательно будет указывать на объект именно этого класса, но может и на объект любого класса потомка этого некоего класса в иерархии. Как определить, какому производному типу принадлежит объект на самом деле? Существует четыре фундаментальных решения этой проблемы:
- Гарантировать, что ссылки осуществляются только на объекты одного типа
- Поместить поле типа в базовый класс
- Использовать динамическое преобразование типа dynamic_cast
- Использовать виртуальные функции
Виртуальные функции решают проблему, связанную с полем типа, предоставляя возможность программисту объявлять в базовом классе функции, которые можно заместить в каждом производном классе.
Для того чтобы объявление виртуальной функции работало в качестве интерфейса к функциям, определённым в производных классах, типы аргументов функции в производном классе не должны отличаться от типов аргументов, объявленных в базовом классе, и только очень (!) небольшие изменения допускаются для типа возвращаемого значения.
Виртуальную функцию иногда называют методом.
Виртуальная функция должна быть определена для класса, в котором она впервые объявлена.
Виртуальную функцию можно использовать, даже если у её класса нет производных классов.
Производный класс, который не нуждается в собственной версии виртуальной функции, не обязан её реализовывать.
Говорят, что функция из производного класса с тем же именем и тем же набором типов аргументов, что и виртуальная функция в базовом классе, замещает (override) виртуальную функцию из базового класса.
За исключением случаев, когда мы явно указываем, какая версия виртуальной функции должна вызываться, активизируется наиболее подходящий для вызывающего объекта вариант замещённой функции.
Тип, имеющий виртуальные функции, называется полиморфным типом.
Для достижения полиморфного поведения в С++ вызываемые функции-члены должны быть виртуальными, и доступ к объектам должен осуществляться через ссылки и указатели. При непосредственном манипулировании объектом (без указателя или ссылки на него) его точный тип известен компилятору, и потому полиморфизм времени выполнения не требуется.
Некоторые классы представляют абстрактную концепцию, для которой объекты не существуют. В таких классах нет данных. Также нету конструктора, так как нет того, что надо инициализировать. Но нужен виртуальный деструктор, чтобы гарантировать правильную очистку данных, которая будет определена в производных классах.
Виртуальные функции можно объявлять в виде чисто виртуальных функций. Виртуальная функция делается чистой при помощи инициализатора =0. Класс с одной или несколькими чисто виртуальными функциями называется абстрактным классом.
Чисто виртуальная функция, которая не определена в производном классе, остается чисто виртуальной, а потому такой класс также является абстрактным.
Оператор delete явным образом уничтожает объект, но нет способа точно определить, к какому классу принадлежит объект. Однако благодаря вируальному деструктору будет вызван нужный деструктор.
Абстрактный класс с набором операций конструирования иногда называют фабрикой, а его функции иногда называют виртуальными конструкторами.
В общем случае, класс создаётся из решётки базовых классов. Исторически большинство таких решёток были деревьями, поэтому решётку классов часто называют иерархией классов. Использование более одного непосредственного базового класса обычно называется множественным наследованием. Наличие одного базового класса называется одиночным наследованием.
Два базовых класса могут иметь функции-члены с одинаковым именем. Неоднозначности для этих функций в производных классах можно устранить с помощью ::.
Разрешение перегрузки не пресекает границ областей видимости классов. В частности, неоднозначности между функциями из различных базовых классов не разрешаются на основе типов аргументов. Объявления using позволяют создавать набор перегруженных функций из базовых и производных классов.
using-объявление в определении класса должно относиться к членам базового класса. using-объявление нельзя использовать для члена класса вне этого класса, его производных классов или их функций членов. using-директиву нельзя поместить в определение класса, и она не может использоваться для класса. using-объявление не может использоваться для доступа к дополнительной информации. Оно просто является механизмом предоставления более удобного доступа к информации, доступ к которой разрешён в принципе.
Виртуальная функция повторяющегося базового класса может быть замещена (единственной) функцией в производном классе.
В решётке наследования все виртуальные базовые классы с одним именем будут представлены одним единственным объектом этого класса. Невиртуальный базовый класс в этой же решётке будет иметь отдельный подобъект, представляющий его.
Конструктор виртуального базового класса вызывается только один раз. Он вызывается (явно или неявно) из конструктора объекта (конструктора самого "нижнего" производного класса). Конструктор виртуального базового класса вызывается до конструкторов производных классов.
Если различные производные классы замещают одну и ту же функцию, то если у этих классов имеются свои производные классы, то в них мы должны заместить эту функцию.
Член класса может быть закрытым, защищённым, открытым (1)private, 2)protected, 3)public):
1)Его имя может использоваться только в функциях-членах и друзьях класса, в котором он объявлен;
2)Его имя может использоваться только в функциях-членах и друзьях класса, в котором он объявлен, и классов, производных от него;
3)Его именем может пользоваться любая функция;
Объявление данных защищёнными обычно свидетельствует об ошибке на этапе проектирования. Защищённые данные приводят к проблемам сопровождения. Члены класса по умолчанию закрыты, что, как правило, является наилучшим вариантом. Эти возражения против защищённости не имеют значения для защищённых функций. Защищённость - это прекрасный способ задания операций для использования в производных классах.
class D:pivate B{/* ... */};
Спецификатор доступа может быть опущен. Тогда private будет по умолчанию.
Спецификатор доступа к базовому классу управляет доступом к членам базового класса и преобразованием указателей и ссылок из типа производного класса в тип базового класса.
- Если базовый класс является закрытым, то его открытые и защищённые члены могут быть использованы только функциями-членами и друзьями производного класса. Только друзья и члены производного класса могут преобразовывать указатель на объект производного класса в указатель на объект базового класса
- Если базовый класс защищённый, то его открытые и защищённые члены могут быть использованы только функциями-членами и друзьями производного класса и его производных классов. Только друзья и члены производного класса и его производных классов могут преобразовывать указатель на объект производного класса в указатель на объект базового класса
- Если базовый класс открытый, то его открытые члены могут быть использованы любой функцией. Его защищённые члены могут быть использованы членами и друзьями производного класса и его производных. Преобразовывать указатель на объект производного класса в указатель на объект базового класса может любая функция
Если доступ к имени или базовому классу может быть осуществлён при помощи нескольких путей в решётке классов, то доступ разрешён только в том случае, если он разрешён по каждому из возможных путей.
dynamic_cast - это операция преобразования типа, которая возвращает корректный указатель, если объект имеет ожидаемый тип, и нулевой указатель - в противном случае.
Использование информации о типе во время выполнения обычно называют RTTI (Run-Time Type Information).
Приведение из базового класса в производный часто называют понижающим приведением, а из производного класса в базовый - повышающим приведением (downcast и upcast), так как принято изображать дерево наследования растущим вниз из корня наверху. Приведение между производными классами одного базового класса называют перекрёстным приведением (crosscast).
dynamic_cast не допускает случайных нарушений правил доступа к закрытым и защищённым базовым классам.
dynamic_cast используется тогда, когда правильность преобразования не может быть определена компилятором.
Для выполнения понижающего или перекрёстного приведения требуется, чтобы аргумент dynamic_cast ,был ссылкой или указателем на полиморфный тип.
Если у объекта нет виртуальных функций, им нельзя безопасно манипулировать, не зная его конкретный тип. Если его тип неизвестен, то нет необходимости в использовании dynamic_cast.
Результирующий тип dynamic_cast не обязан быть полиморфным. Это позволяет "заворачивать" конкретный тип в полиморфный, а затем извлекать объект этого конкретного типа обратно.
Приведение к void* с помощью dynamic_cast можно использовать для определения адреса начала объекта полиморфного типа:
void* p1=dynamic_cast<void*>(p2);
где p1 - адрес размещения объекта.
Это полезно при взаимодействии с низкоуровневыми функциями.
В круглых скобках в dynamic_cast должен быть указатель на объект только полиморфного типа.
Результат применения dynamic_cast к указателю нужно всегда явно проверять. Для указателя приведение dynamic_cast можно интерпретировать как вопрос "объект, на который указывает этот указатель, имеет этот тип?". Если dynamic_cast применяется к ссылке, то приведение dynamic_cast является не вопросом, а утверждением "объект, на который указывает этот указатель, имеет этот тип.", а потому при приведении dynamic_cast в случае, если приводимая ссылка является ссылкой на объект неожидаемого типа, то генерируется стандартное исключение bad_cast. Если надо защититься от неудачных приведений к ссылке, необходимо предоставить подходящий обработчик.
Статическое приведение static_cast не анализирует объект, который оно приводит.
Объект типа с ограничениями на способ размещения в памяти (задаваемый, например, языками си или фортран) может использоваться в качестве виртуального базового класса. Для таких объектов доступна только статическая информация о типе.
Существует миллионы строк кода. написанные до появления dynamic_cast.
При использовании dynamic_cast накладные расходы невелики.
Динамическое приведение более безопасно.
Компилятор не может делать предположений о памяти, на которую показывает void*. оэтому динамическое приведение не может осуществить приведение из *void. В этом случае требуется статическое приведение.
Приведения dynamic_cast и static_cast учитывают модификатор const и управление доступом.
Невозможно осуществить приведение в закрытый базовый класс, а "снятие const" (или volatile) требует const_cast. И даже в этом случае использование результата безопасно только в том случае, если объект не был изначально объявлен константным (или volatile).
Создание объекта идёт "снизу вверх", уничтожение "сверху вниз". При этом объект класса является объектом в той мере, в какой он был создан или уничтожен. Неразумно полагаться на порядок создания и уничтожения, но тем не менее этот порядок можно наблюдать при помощи вызовов виртуальных функций, динамического приведения или использования typeid в момент, когда объект ещё не завершён. Лучше не вызывать виртуальные функции на этапе создания или уничтожения.
dynamic_cast удовлетворяет большинство потребностей в информации о типе объекта на этапе выполнения. dynamic_cast сохраняет гибкость и способность к расширению, присущую виртуальным функциям. Но иногда надо знать не "принадлежит ли объект данному типу", а "каков точный тип объекта". Для этой цели служит оператор typeid. Он выдает объект, представляющий тип его операнда. typeid() возвращает ссылку на тип стандартной библиотеки, называемый type_info, определённый в <typeinfo>.
Оператор typeid() наиболее часто используется для нахождения типа объекта, на который указывает указатель или ссылка.
Если значение указателя или ссылки полиморфного типа равно 0, type_info() генерирует исключение bad_typeid. Если операнд type_info() имеет неполиморфный тип или не является lvalue, результат определяется на этапе компиляции без вычисления выражения операнда.
Не гарантируется, что только один объект type_info() существует для каждого типа в системе. Поэтому необходимо использовать == с объектами type_info для проверки равенства, а нес указателями на такие объекты.
Пример использования имени класса:
cout<<typeid(*p).name();
Символьное представление имени класса зависит от реализации Это С-строка находящаяся в системной области памяти, а потому нельзя пытаться её уничтожить при помощи delete[].
Как правило, следует избегать использование switch-инструкции.
С++ предлагает средство косвенной ссылки на член класса. Указатель на член является значением, идентифицирующим член класса. Его можно рассматривать как позицию члена в объекте класса (но компилятор принимает в расчёт различия между данными, виртуальными функциями, невиртуальными функциями и т.д.).
typedef void (ourClass::* memberPointerType)();//тип указателя на функцию-член
memberPointerType memberPointer = &ourClass::memberFunction;
objectPointer -> memberFunction();//прямой вызов
(objectPointer ->* memberPointer)();//вызов через указатель на член
Производный класс по крайней мере содержит члены, которые он унаследовал от базовых классов. Также он может содержать дополнительные члены. Из этого следует, что можно безопасно присвоить указатель на член базового класса указателю на член производного класса, но не наоборот. Это свойство называют контр вариацией.
Создаётся впечатление, что это правило противоречит правилу, что можно присваивать указатель на производный класс указателю на его базовый класс. В действительности оба правила нужны для фундаментальной гарантии того, что указатель ни при каких условиях не укажет на объект, который не реализует (по крайней мере) свойств, подразумеваемых типом этого указателя.
Можно самостоятельно управлять памятью для класса, определив собственные operator new() и operator delete() для индивидуальных объектов. operator new[]() и operator delete[]() играют ту же роль для массивов. Возможна также замена глобальных operator new() и operator delete(). Более надёжный подход состоит в реализации этих операций для конкретного класса. Эта реализация будет справедлива также и для производных классов.
Операторы new() и delete() неявно являются статическими членами. Следовательно у них нет указателя this и они не изменяют объект.
Добавление виртуального деструктора к базовому классу облегчает задачу определения истинного размера уничтожаемого объекта оператором operator delete(). Иначе при удалении объекта с потерянным типом (например, в результате приведения) будет нужна помощь программиста (как и в случае с удалением массива).
Если необходимо иметь пару для выделения/освобождения памяти, которая корректно работает с производными классами, то приходится предоставить виртуальный деструктор в базовом классе, либо воздержаться от использования аргумента типа size_t при освобождении памяти.
Может ли конструктор быть виртуальным? Нет. Но желаемое поведение можно получить. Чтобы создать объект конструктор должен знать тип создаваемого объекта. Поэтому он не может быть виртуальным. Конструктор является не совсем обычной функцией. Конструктор работает с памятью способом недоступным обычным функциям-членам. Нельзя получить указатель на конструктор. Можно создать функцию, которая вызывает конструктор и возвращает созданный объект.