Б. Страуструп - Язык программирования С++ (1119446), страница 49
Текст из файла (страница 49)
Это - размеросвобождаемого объекта. При удалении простого служащего этот параметр получает значениеsizeof(employee), а при удалении управляющего - sizeof(manager). Поэтому собственные функцииклассы для размещения могут не хранить размер каждого размещаемого объекта.
Конечно, они могутхранить эти размеры (подобно функциям размещения общего назначения) и игнорировать параметрsize_t в вызове operator delete(), но тогда вряд ли они будут лучше, чем функции размещения иосвобождения общего назначения.Как транслятор определяет нужный размер, который надо передать функции operator delete()? Пока тип,указанный в operator delete(), соответствует истинному типу объекта, все просто; но рассмотрим такойпример:class manager : public employee {int level;// ...};void f(){employee* p = new manager; // проблемаdelete p;}В этом случае транслятор не сможет правильно определить размер.
Как и в случае удаления массива,нужна помощь программиста. Он должен определить виртуальный деструктор в базовом классеemployee:class employee {// ...public:// ...void* operator new(size_t);174Бьерн Страуструп.Язык программирования С++void operator delete(void*, size_t);virtual ~employee();};Даже пустой деструктор решит нашу проблему:employee::~employee() { }Теперь освобождение памяти будет происходить в деструкторе (а в нем размер известен), а любойпроизводный от employee класс также будет вынужден определять свой деструктор (тем самым будетустановлен нужный размер), если только пользователь сам не определит его.
Теперь следующийпример пройдет правильно:void f(){employee* p = new manager; // теперь без проблемdelete p;}Размещение происходит с помощью (созданного транслятором) вызоваemployee::operator new(sizeof(manager))а освобождение с помощью вызоваemployee::operator delete(p,sizeof(manager))Иными словами, если нужно иметь корректные функции размещения и освобождения для производныхклассов, надо либо определить виртуальный деструктор в базовом классе, либо не использовать вфункции освобождения параметр size_t.
Конечно, можно было при проектировании языкапредусмотреть средства, освобождающие пользователя от этой проблемы. Но тогда пользователь"освободился" бы и от определенных преимуществ более оптимальной, хотя и менее надежнойсистемы.В общем случае, всегда есть смысл определять виртуальный деструктор для всех классов, которыедействительно используются как базовые, т.е. с объектами производных классов работают и, возможно,удаляют их, через указатель на базовый класс:class X {// ...public:// ...virtual void f();// в X есть виртуальная функция, поэтому// определяем виртуальный деструкторvirtual ~X();};6.7.1 Виртуальные конструкторыУзнав о виртуальных деструкторах, естественно спросить: "Могут ли конструкторы то же бытьвиртуальными?" Если ответить коротко - нет.
Можно дать более длинный ответ: "Нет, но можно легкополучить требуемый эффект".Конструктор не может быть виртуальным, поскольку для правильного построения объекта он должензнать его истинный тип. Более того, конструктор - не совсем обычная функция. Он можетвзаимодействовать с функциями управления памятью, что невозможно для обычных функций. Отобычных функций-членов он отличается еще тем, что не вызывается для существующих объектов.Следовательно нельзя получить указатель на конструктор.Но эти ограничения можно обойти, если определить функцию, содержащую вызов конструктора ивозвращающую построенный объект. Это удачно, поскольку нередко бывает нужно создать новыйобъект, не зная его истинного типа.
Например, при трансляции иногда возникает необходимостьсделать копию дерева, представляющего разбираемое выражение. В дереве могут быть узлывыражений разных видов. Допустим, что узлы, которые содержат повторяющиеся в выражении175Бьерн Страуструп.Язык программирования С++операции, нужно копировать только один раз. Тогда нам потребуется виртуальная функцияразмножения для узла выражения.Как правило "виртуальные конструкторы" являются стандартными конструкторами без параметров иликонструкторами копирования, параметром которых служит тип результата:class expr {// ...public:expr();// стандартный конструкторvirtual expr* new_expr() { return new expr(); }};Виртуальная функция new_expr() просто возвращает стандартно инициализированный объект типаexpr, размещенный в свободной памяти. В производном классе можно переопределить функциюnew_expr() так, чтобы она возвращала объект этого класса:class conditional : public expr {// ...public:conditional(); // стандартный конструкторexpr* new_expr() { return new conditional(); }};Это означает, что, имея объект класса expr, пользователь может создать объект в "точности такого жетипа":void user(expr* p1, expr* p2){expr* p3 = p1->new_expr();expr* p4 = p2->new_expr();// ...}Переменным p3 и p4 присваиваются указатели неизвестного, но подходящего типа.Тем же способом можно определить виртуальный конструктор копирования, называемый операциейразмножения, но надо подойти более тщательно к специфике операции копирования:class expr {// ...expr* left;expr* right;public:// ...// копировать `s' в `this'inline void copy(expr* s);// создать копию объекта, на который смотрит thisvirtual expr* clone(int deep = 0);};Параметр deep показывает различие между копированием собственно объекта (поверхностноекопирование) и копированием всего поддерева, корнем которого служит объект (глубокое копирование).Стандартное значение 0 означает поверхностное копирование.Функцию clone() можно использовать, например, так:void fct(expr* root){expr* c1 = root->clone(1);expr* c2 = root->clone();// ...}// глубокое копирование// поверхностное копированиеЯвляясь виртуальной, функция clone() способна размножать объекты любого производного от expr176Бьерн Страуструп.класса.Язык программирования С++Настоящее копирование можно определить так:void expr::copy(expression* s, int deep){if (deep == 0) { // копируем только члены*this = *s;}else { // пройдемся по указателям:left = s->clone(1);right = s->clone(1);// ...}}Функция expr::clone() будет вызываться только для объектов типа expr (но не для производных от exprклассов), поэтому можно просто разместить в ней и возвратить из нее объект типа expr, являющийсясобственной копией:expr* expr::clone(int deep){expr* r = new expr(); // строим стандартное выражениеr->copy(this,deep);// копируем `*this' в `r'return r;}Такую функцию clone() можно использовать для производных от expr классов, если в них не появляютсячлены-данные (а это как раз типичный случай):class arithmetic : public expr {// ...// новых членов-данных нет =>// можно использовать уже определенную функцию clone};С другой стороны, если добавлены члены-данные, то нужно определять собственную функцию clone():class conditional : public expression {expr* cond;public:inline void copy(cond* s, int deep = 0);expr* clone(int deep = 0);// ...};Функции copy() и clone() определяются подобно своим двойникам из expression:expr* conditional::clone(int deep){conditional* r = new conditional();r->copy(this,deep);return r;}void conditional::copy(expr* s, int deep){if (deep == 0) {*this = *s;}else {expr::copy(s,1); // копируем часть exprcond = s->cond->clone(1);}}177Бьерн Страуструп.Язык программирования С++Определение последней функции показывает отличие настоящего копирования в expr::copy() отполного размножения в expr::clone() (т.е.
создания нового объекта и копирования в него). Простоекопирование оказывается полезным для определения более сложных операций копирования иразмножения. Различие между copy() и clone() эквивалентно различию между операцией присваиванияи конструктором копирования ($$1.4.2) и эквивалентно различию между функциями _draw() и draw()($$6.5.3). Отметим, что функция copy() не является виртуальной. Ей и не надо быть таковой, посколькувиртуальна вызывающая ее функция clone(). Очевидно, что простые операции копирования можнотакже определять как функции-подстановки.6.7.2 Указание размещенияПо умолчанию операция new создает указанный ей объект в свободной памяти.
Как быть, если надоразместить объект в определенном месте? Этого можно добиться переопределением операцииразмещения. Рассмотрим простой класс:class X {// ...public:X(int);// ...};Объект можно разместить в любом месте, если ввести в функцию размещения дополнительныепараметры:// операция размещения в указанном месте:void* operator new(size_t, void* p) { return p; }и задав эти параметры для операции new следующим образом:charvoid{X*//}buffer[sizeof(X)];f(int i)p = new(buffer) X(i); // разместить X в buffer...Функция operator new(), используемая операцией new, выбирается согласно правилам сопоставленияпараметров ($$R.13.2).