Перегрузка операторов new и delete
В С++ имеются две возможности перегрузки операторов new и delete - локально (в пределах класса) и глобально (в пределах программы). Эти операторы имеют правила переопределения, отличные от рассмотренных выше правил переопределения других операторов. Одна из причин перегрузки операторов new и delete состоит в том, чтобы придать им новые свойства, например, выдачи диагностики или более высокой защищенности от ошибок. Кроме того, может быть реализована более эффективная схема распределения памяти по сравнению со схемой, обеспечиваемой системой.
Оператор new можно задать в следующих формах:
<::> new <аргументы> имя_типа <инициализирующее_выражение>;
<::> new <аргументы> имя_типа [ ];
Параметр ”аргументы” можно использовать либо для того, чтобы различить разные версии глобальных операторов new, либо для использования их в теле функции operator. Доопределенную функцию operator new можно объявить:
void *operator new(size_t t<список _аргументов>);
void *operator new[](size_t t<список _аргументов>);
Вторая форма используется для выделения памяти для массивов. Возвращаемое значение всегда должно иметь тип void *. Единственный обязательный аргумент функции operator всегда должен иметь тип size_t. При этом в функцию operator автоматически подставляется аргумент sizeof(t).
Ниже приведен пример программы, в которой использованы две глобальные перегруженные и одна локальная функции operator.
#include "iostream.h"
#include "string.h"
void *operator new(size_t tip,int kol) //глобальная ф-ция operator new
{ cout << "глобальная функция 1" <<endl; // с одним параметром
return new char[tip*kol];
}
void *operator new(size_t tip,int n1,int n2) //глобальная ф-ция operator new
{ cout << "глобальная функция 2" <<endl; // с двумя параметрами
void *p=new char[tip*n1*n2];
return p;
}
Class cls
{ char a[40];
public:
cls(char *aa){ strcpy(a,aa); }
~cls(){}
void *operator new(size_t,int);
};
void *cls::operator new(size_t tp,int n) // локальная функция operator
{ cout << "локальная функция " <<endl;
return new char[tp*n]; }
void main()
{ cls obj("перегрузка оператора new");
float *ptr1;
ptr1=new (5) float; // вызов 1 глобальной функции operator new
ptr1=new (2,3) float; // вызов 2 глобальной функции operator new
ptr1=new float; // вызов сиcтемной глобальной функции
cls *ptr2=new (3) cls("aa"); // вызов локальной функции operator new
} // используя cls::cls("aa")
Результаты работы программы:
глобальная функция 1
глобальная функция 2
локальная функция
Первое обращение ptr1=new (5) float приводит к вызову глобальной функции operator с одним параметром, в результате выделяется память 5*sizeof(float) байт (это соответствует массиву из пяти элементов типа float) и адрес заносится в указатель ptr1. Второе обращение приводит к вызову функции operator с двумя параметрами. Следующая инструкция new float приводит к вызову системной функции new. Инструкция new (3) cls("aa") соответствует вызову функции operator, описанной в классе cls. В функцию в качестве имени типа передается тип созданного объекта класса cls. Таким образом, ptr2 получает адрес массива из трех объектов класса cls.
Оператор delete разрешается доопределять только по отношению к классу. В то же время можно заменить системную версию реализации оператора delete на свою.
Доопределенную функцию operator delete можно объявить:
void operator delete(void *p<,size_t t>);
void operator delete[](void *p<,size_t t>);
Функция operator должна возвращать значение void и имеет один обязательный аргумент типа void * - указатель на область памяти, которая должна быть освобождена. Ниже приведен пример программы с доопределением оператора delete.
#include "iostream.h"
#include "string.h"
#include "stdlib.h"
void *operator new(size_t tip,int kol) // глобальная функция operator
{ cout << "глобальная функция NEW" <<endl;
return new char[tip*kol]; // вызов системной функции new
}
Class cls
{ char a[40];
public:
cls(char *aa)
{ cout<<”конструктор класса cls”<<endl;
strcpy(a,aa);
}
~cls(){}
void *operator new(size_t,int);
void operator delete(void *);
};
void *cls::operator new(size_t tip,int n) // локальная функция operator
{ cout << "локальная функция " <<endl;
return new char[tip*n]; // вызов системной функции new
}
void cls::operator delete(void *p) // локальная функция operator
{ cout << "локальная функция DELETE" <<endl;
delete p; // вызов системной функции delete
}
void operator delete[](void *p) // глобальная функция operator
{ cout << "глобальная функция DELETE" <<endl;
delete p; // вызов системной функции delete
}
void main()
{ cls obj("перегрузка операторов NEW и DELETE");
float *ptr1;
ptr1=new (5) float; // вызов глобальной ф-ции доопр. оператора new
delete [] ptr1; // вызов глобальной ф-ции доопр.оператора delete
cls *ptr2=new (10) cls("aa"); // вызов локальной функции доопределения
// оператора new (из класса cls)
delete ptr2; // вызов локальной функции доопределения
// оператора delete (из класса cls)
}
Результаты работы программы:
глобальная функция NEW
глобальная функция DELETE
локальная функция NEW
конструктор класса cls
локальная функция DELETE
Инструкция cls *ptr2=new (10) cls("aa") выполняется следующим образом: вначале вызывается локальная функция operator для выделения памяти, равной 10*sizeof(cls), затем вызывается конструктор класса cls.
Необходимо отметить тот факт, что при реализации переопределения глобальной функции в ней не должен использоваться оператор delete [], так как это приведет к бесконечной рекурсии. При выполнении инструкции системный оператор delete ptr2 сначала вызывается локальная функция доопределения оператора delete для класса cls, а затем из нее глобальная функция переопределения delete.
Далее рассмотрим пример доопределения функций new и delete в одном из классов, содержащемся в некоторой иерархии классов.
#include "iostream.h"
Class A
{ public:
A(){cout<<"конструктор A"<<endl;}
virtual ~A(){cout<<"деструктор A"<<endl;} //виртуальный деструктор
void *operator new(size_t,int);
void operator delete(void *,size_t);
};
Class B : public A
{ public:
B(){cout<<"конструктор В"<<endl;}
~B(){cout<<"деструктор В"<<endl;}
};
void *A::operator new(size_t tip,int n)
{ cout << "перегрузка operator NEW" <<endl;
return new char[tip*n];
}
void A::operator delete(void *p,size_t t) //глобальная функция operator
{ cout << " перегрузка operator DELETE" <<endl;
delete p; // вызов глобальной (системной функции)
}
void main()
{ A *ptr1=new(2) A; // вызов локальной функции, используя A::A()
delete ptr1;
A *ptr2=new(3) B; // вызов локальной функции, используя B::B()
delete ptr2;
}
Результаты работы программы:
перегрузка operator NEW
конструктор A
деструктор A
перегрузка operator DELETE
перегрузка operator NEW
конструктор A
конструктор В
деструктор В
деструктор A
перегрузка operator DELETE
При public наследовании класса А классом В public функции new и delete наследуются как public функции класса В. Отметим, что необходимость использования в данном примере виртуального деструктора рассмотрена ранее.
Преобразование типа
Выражения, содержащиеся в функциях и используемые для вычисления некоторого значения, записываются в большинстве случаев с учетом корректности по отношению к объектам этого выражения. В то же время, если используемая операция для типов величин, участвующих в выражении, явно не определена, то компилятор пытается выполнить такое преобразование типов в два этапа.
На первом этапе выполняется попытка использовать стандартные преобразования типов. Если это невозможно, то компилятор использует преобразования, определенные пользователем.
Явные преобразования типов
Явное приведение типа выполненное в стиле языка С имеет вид:
(тип) выражение.
То есть если перед выражением указать имя типа в круглых скобках, то значение выражения будет преобразовано к данному типу. Например:
int a = (int) b;
char *s=(char *) addr;
Недостатком их является полное отсутствие контроля, что приводит к ошибкам и путанице. Существует большая разница между приведением указателя на const объект к указателю на не const объект (изменяется только атрибут объекта), и приведением указателя на объект базового класса к указателю на объект производного класса (изменяется полностью тип указателя). Было бы неплохо более точно определять цель каждого приведения. Чтобы преодолеть недостатки приведения типов выполняемых в стиле языка С, в язык С++ введены четыре оператора приведения типа: static_cast, const_cast, dynamic_cast и reinterpret_cast. Спецификация преобразования имеет следующий вид:
оператор_приведения _типа<тип> (выражение).
Для преобразования с минимальным контролем можно использовать оператор staic_cast. Он позволяет выполнять преобразования, не проверяя типы выражений во время выполнения, а основываясь на сведениях, полученных при компиляции. Оператор static_cast позволяет выполнять преобразования не только указателя на базовый класс к указателю на производный, но и наоборот.
int i,j;
double d=static_cast<double>(i)*j;
В то же время, например, преобразование int к int* (и обратно) приведет к ошибке компиляции. Рассматриваемые далее операторы приведения используются для более узкого круга задач. Для преобразований не связанных между собой типов используется reinterpret_cast.
int i;
void *adr= reinterpret_cast<void *> (i);
Необходимо отметить, что reinterpret_cast не выполнит преобразование таких типов как float, double, объектов к типу указателя.
Если же нужно выполнить преобразование выражения имеющего тип с атрибутами char и volatile к типу без них, то можно использовать оператор const_cast:
const char *s;
char *ss=const_cast<char*> (s);
Следующий оператор dynamic_cast предназначен для приведения указателя или ссылки на объект базового класса к указателям или ссылкам на объекты производных классов. При этом можно определить, была ли попытка приведения типа успешной. В результате неудачной попытки преобразования возвращается либо нулевой указатель, либо возникает исключение (при преобразовании ссылок).
class A { . . . };
class B : public A { . . .};
class C : public B { . . .};
main()
{ A *pa;
B *pb;
C *pc;
. . .
pa= dynamic_cast<A*>(pc);
pb= dynamic_cast<B*>(pc);
}
dynamic_cast Operator
The expression dynamic_cast<type-id>( expression ) converts the operand expression to an object of type type-id. The type-id must be a pointer or a reference to a previously defined class type or a “pointer to void”. The type of expression must be a pointer if type-id is a pointer, or an l-value if type-id is a reference.
Syntax
dynamic_cast < type-id > ( expression )
If type-id is a
pointer to an unambiguous accessible direct or indirect base class of expression, a pointer to the unique subobject of type type-id is the result. For example:
class B { ... };class C : public B { ... };class D : public C { ... }; void f(D* pd){ C* pc = dynamic_cast<C*>(pd); // ok: C is a direct base class // pc points to C subobject of pd B* pb = dynamic_cast<B*>(pd); // ok: B is an indirect base class // pb points to B subobject of pd ...}This type of conversion is called an “upcast” because it moves a pointer up a class hierarchy, from a derived class to a class it is derived from. An upcast is an implicit conversion.
If type-id is void*, a run-time check is made to determine the actual type of expression. The result is a pointer to the complete object pointed to by expression. For example:
class A { ... }; class B { ... }; void f(){ A* pa = new A; B* pb = new B; void* pv = dynamic_cast<void*>(pa); // pv now points to an object of type A ... pv = dynamic_cast<void*>(pb); // pv now points to an object of type B}If type-id is not void*, a run-time check is made to see if the object pointed to by expression can be converted to the type pointed to by type-id.
If the type of expression is a base class of the type of type-id, a run-time check is made to see if expression actually points to a complete object of the type of type-id. If this is true, the result is a pointer to a complete object of the type of type-id. For example:
class B { ... };class D : public B { ... }; void f(){ B* pb = new D; // unclear but ok B* pb2 = new B; D* pd = dynamic_cast<D*>(pb); // ok: pb actually points to a D ... D* pd2 = dynamic_cast<D*>(pb2); //error: pb2 points to a B, not a D // pd2 == NULL ...}This type of conversion is called a “downcast” because it moves a pointer down a class hierarchy, from a given class to a class derived from it.
In cases of multiple inheritance, possibilities for ambiguity are introduced. Consider the class hierarchy shown in Figure 4.5: