<< Пред. стр. 70 (из 121) След. >>
};Когда с помощью new создается массив объектов типа класса, компилятор проверяет, определен ли в классе оператор new[](). Если да, то для выделения памяти под массив вызывается именно он, в противном случае – глобальный new[](). В следующей инструкции в хипе создается массив из десяти объектов Screen:
Screen *ps = new Screen[10];
В этом классе есть оператор new[](), поэтому он и вызывается для выделения памяти. Его параметр size_t автоматически инициализируется значением, равным объему памяти в байтах, необходимому для размещения десяти объектов Screen.
Даже если в классе имеется оператор-член new[](), программист может вызвать для создания массива глобальный new[](), воспользовавшись оператором разрешения глобальной области видимости:
Screen *ps = ::new Screen[10];
Оператор delete(), являющийся членом класса, должен иметь тип void, а в качестве первого параметра принимать void*. Вот как выглядит его объявление для Screen:
class Screen {
public:
void operator delete[]( void * );
};
Чтобы удалить массив объектов класса, delete должен вызываться следующим образом:
delete[] ps;
Когда операндом delete является указатель на объект типа класса, компилятор проверяет, определен ли в этом классе оператор delete[](). Если да, то для освобождения памяти вызывается именно он, в противном случае – его глобальная версия. Параметр типа void* автоматически инициализируется значением адреса начала области памяти, в которой размещен массив.
Даже если в классе имеется оператор-член delete[](), программист может вызвать глобальный delete[](), воспользовавшись оператором разрешения глобальной области видимости:
::delete[] ps;
Добавление операторов new[]() или delete[]() в класс или удаление их оттуда не отражаются на пользовательском коде: вызовы как глобальных операторов, так и операторов-членов выглядят одинаково.
При создании массива сначала вызывается new[]() для выделения необходимой памяти, а затем каждый элемент инициализируется с помощью конструктора по умолчанию. Если у класса есть хотя бы один конструктор, но нет конструктора по умолчанию, то вызов оператора new[]() считается ошибкой. Не существует синтаксической конструкции для задания инициализаторов элементов массива или аргументов конструктора класса при создании массива подобным образом.
При уничтожении массива сначала вызывается деструктор класса для уничтожения элементов, а затем оператор delete[]() – для освобождения всей памяти. При этом важно использовать правильный синтаксис. Если в инструкции
delete ps;
ps указывает на массив объектов класса, то отсутствие квадратных скобок приведет к вызову деструктора лишь для первого элемента, хотя память будет освобождена полностью.
У оператора-члена delete[]() может быть не один, а два параметра, при этом второй должен иметь тип size_t:
class Screen {
public:
// заменяет
// void operator delete[]( void* );
void operator delete[]( void*, size_t );
};
Если второй параметр присутствует, то компилятор автоматически инициализирует его значением, равным объему отведенной под массив памяти в байтах.
15.8.2. Оператор размещения new() и оператор delete()
Оператор-член new() может быть перегружен при условии, что все объявления имеют разные списки параметров. Первый параметр должен иметь тип size_t:
class Screen {
public:
void *operator new( size_t );
void *operator new( size_t, Screen * );
// ...
};
Остальные параметры инициализируются аргументами размещения, заданными при вызове new:
void func( Screen *start ) {
Screen *ps = new (start) Screen;
// ...
}
Та часть выражения, которая находится после ключевого слова new и заключена в круглые скобки, представляет аргументы размещения. В примере выше вызывается оператор new(), принимающий два параметра. Первый автоматически инициализируется значением, равным размеру класса Screen в байтах, а второй – значением аргумента размещения start.
Можно также перегружать и оператор-член delete(). Однако такой оператор никогда не вызывается из выражения delete. Перегруженный delete() неявно вызывается компилятором, если конструктор, вызванный при выполнении оператора new (это не опечатка, мы действительно имеем в виду new), возбуждает исключение. Рассмотрим использование delete() более внимательно.
Последовательность действий при вычислении выражения
Screen *ps = new ( start ) Screen;
такова:
Вызывается определенный в классе оператор new(size_t, Screen*).
Вызывается конструктор по умолчанию класса Screen для инициализации созданного объекта.
Переменная ps инициализируется адресом нового объекта Screen.
Предположим, что оператор класса new(size_t, Screen*) выделяет память с помощью глобального new(). Как разработчик может гарантировать, что память будет освобождена, если вызванный на шаге 2 конструктор возбуждает исключение? Чтобы защитить пользовательский код от утечки памяти, следует предоставить перегруженный оператор delete(), который вызывается только в подобной ситуации.
Если в классе имеется перегруженный оператор с параметрами, типы которых соответствуют типам параметров new(), то компилятор автоматически вызывает его для освобождения памяти. Предположим, есть следующее выражение с оператором размещения new:
Screen *ps = new (start) Screen;
Если конструктор по умолчанию класса Screen возбуждает исключение, то компилятор ищет delete() в области видимости Screen. Чтобы такой оператор был найден, типы его параметров должны соответствовать типам параметров вызванного new(). Поскольку первый параметр new() всегда имеет тип size_t, а оператора delete() – void*, то первые параметры при сравнении не учитываются. Компилятор ищет в классе Screen оператор delete() следующего вида:
void operator delete( void*, Screen* );
Если такой оператор будет найден, то он вызывается для освобождения памяти в случае, когда new() возбуждает исключение. (Иначе – не вызывается.)
Разработчик класса принимает решение, предоставлять ли delete(), соответствующий некоторому new(), в зависимости от того, выделяет ли этот оператор new() память самостоятельно или пользуется уже выделенной. В первом случае delete() необходимо включить для освобождения памяти, если конструктор возбудит исключение; иначе в нем нет необходимости.
Можно также перегрузить оператор размещения new[]() и оператор delete[]() для массивов:
class Screen {
public:
void *operator new[]( size_t );
void *operator new[]( size_t, Screen* );
void operator delete[]( void*, size_t );
void operator delete[]( void*, Screen* );
// ...
};
Оператор new[]() используется в случае, когда в выражении, содержащем new для распределения массива, заданы соответствующие аргументы размещения:
void func( Screen *start ) {
// вызывается Screen::operator new[]( size_t, Screen* )
Screen *ps = new (start) Screen[10];
// ...
}
Если при работе оператора new конструктор возбуждает исключение, то автоматически вызывается соответствующий delete[]().
Упражнение 15.9
Объясните, какие из приведенных инициализаций ошибочны:
class iStack {
public:
iStack( int capacity )
: _stack( capacity ), _top( 0 ) {}
// ...
private:
int _top;
vatcor< int > _stack;
};
(a) iStack *ps = new iStack(20);
(b) iStack *ps2 = new const iStack(15);
(c) iStack *ps3 = new iStack[ 100 ];
Упражнение 15.10
Что происходит в следующих выражениях, содержащих new и delete?
class Exercise {
public:
Exercise();
~Exercise();
};
Exercise *pe = new Exercise[20];
delete[] ps;
Измените эти выражения так, чтобы вызывались глобальные операторы new() и delete().
Упражнение 15.11
Объясните, зачем разработчик класса должен предоставлять оператор delete().
15.9. Определенные пользователем преобразования
Мы уже видели, как преобразования типов применяются к операндам встроенных типов: в разделе 4.14 этот вопрос рассматривался на примере операндов встроенных операторов, а в разделе 9.3 – на примере фактических аргументов вызванной функции для приведения их к типам формальных параметров. Рассмотрим с этой точки зрения следующие шесть операций сложения:
char ch; short sh;, int ival;
/* в каждой операции один операнд
* требует преобразования типа */
ch + ival; ival + ch;
ch + sh; ch + ch;
ival + sh; sh + ival;
Операнды ch и sh расширяются до типа int. При выполнении операции складываются два значения типа int. Расширение типа неявно выполняется компилятором и для пользователя прозрачно.
В этом разделе мы рассмотрим, как разработчик может определить собственные преобразования для объектов типа класса. Такие определенные пользователем преобразования также автоматически вызываются компилятором по мере необходимости. Чтобы показать, зачем они нужны, обратимся снова к классу SmallInt, введенному в разделе 10.9.
Напомним, что SmallInt позволяет определять объекты, способные хранить значения из того же диапазона, что unsigned char, т.е. от 0 до 255, и перехватывает ошибки выхода за его границы. Во всех остальных отношениях этот класс ведет себя точно так же, как unsigned char.
Чтобы иметь возможность складывать объекты SmallInt с другими объектами того же класса или со значениями встроенных типов, а также вычитать их, реализуем шесть операторных функций:
class SmallInt {
friend operator+( const SmallInt &, int );
friend operator-( const SmallInt &, int );
friend operator-( int, const SmallInt & );
friend operator+( int, const SmallInt & );
public:
SmallInt( int ival ) : value( ival ) { }
operator+( const SmallInt & );
operator-( const SmallInt & );
// ...
private:
int value;
};
Операторы-члены дают возможность складывать и вычитать два объекта SmallInt. Глобальные же операторы-друзья позволяют производить эти операции над объектами данного класса и объектами встроенных арифметических типов. Необходимо только шесть операторов, поскольку любой встроенный арифметический тип может быть приведен к типу int. Например, выражение
SmallInt si( 3 );
si + 3.14159
разрешается в два шага:
Константа 3.14159 типа double преобразуется в целое число 3.
Вызывается operator+(const SmallInt &,int), который возвращает значение 6.
Если мы хотим поддержать битовые и логические операции, а также операции сравнения и составные операторы присваивания, то сколько же необходимо перегрузить операторов? Сразу и не сосчитаешь. Значительно удобнее автоматически преобразовать объект класса SmallInt в объект типа int.
В языке C++ имеется механизм, позволяющий в любом классе задать набор преобразований, применимых к его объектам. Для SmallInt мы определим приведение объекта к типу int. Вот его реализация:
class SmallInt {
public:
SmallInt( int ival ) : value( ival ) { }
// конвертер
// SmallInt ==> int
operator int() { return value; }
// перегруженные операторы не нужны
private:
int value;
};
Оператор int() – это конвертер, реализующий определенное пользователем преобразование, в данном случае приведение типа класса к заданному типу int. Определение конвертера описывает, что означает преобразование и какие действия компилятор должен выполнить для его применения. Для объекта SmallInt смысл преобразования в int заключается в том, чтобы вернуть число типа int, хранящееся в члене value.
Теперь объект класса SmallInt можно использовать всюду, где допустимо использование int. Если предположить, что перегруженных операторов больше нет и в SmallInt определен конвертер в int, операция сложения
SmallInt si( 3 );
si + 3.14159
разрешается двумя шагами:
Вызывается конвертер класса SmallInt, который возвращает целое число 3.
Целое число 3 расширяется до 3.0 и складывается с константой двойной точности 3.14159, что дает 6.14159.
Такое поведение больше соответствует поведению операндов встроенных типов по сравнению с определенными ранее перегруженными операторами. Когда значение типа int складывается со значением типа double, то выполняется сложение двух чисел типа double (поскольку тип int расширяется до double) и результатом будет число того же типа.
В этой программе иллюстрируется применение класса SmallInt:
#include
#include "SmallInt.h"
int main() {
cout << "Введите SmallInt, пожалуйста: ";
while ( cin >> si1 ) {
cout << "Прочитано значение "
<< si1 << "\nОно ";
// SmallInt::operator int() вызывается дважды
cout << ( ( si1 > 127 )
? "больше, чем "
: ( ( si1 < 127 )
? "меньше, чем "
: "равно ") ) << "127\n";
cout << "\Введите SmallInt, пожалуйста \
(ctrl-d для выхода): ";
}
cout <<"До встречи\n";
}
Откомпилированная программа выдает следующие результаты:
Введите SmallInt, пожалуйста: 127
Прочитано значение 127
Оно равно 127
Введите SmallInt, пожалуйста (ctrl-d для выхода): 126
Оно меньше, чем 127
Введите SmallInt, пожалуйста (ctrl-d для выхода): 128
Оно больше, чем 127
Введите SmallInt, пожалуйста (ctrl-d для выхода): 256
*** Ошибка диапазона SmallInt: 256 ***
В реализацию класса SmallInt добавили поддержку новой функциональности:
#include
class SmallInt {
friend istream&
operator>>( istream &is, SmallInt &s );
friend ostream&
operator<<( ostream &is, const SmallInt &s )
{ return os << s.value; }
public:
SmallInt( int i=0 ) : value( rangeCheck( i ) ){}
int operator=( int i )
{ return( value = rangeCheck( i ) ); }
operator int() { return value; }
private:
int rangeCheck( int );
int value;
};
Ниже приведены определения функций-членов, находящиеся вне тела класса:
istream& operator>>( istream &is, SmallInt &si ) {
int ix;
is >> ix;
si = ix; // SmallInt::operator=(int)
return is;
}
int SmallInt::rangeCheck( int i )
{
/* если установлен хотя бы один бит, кроме первых восьми,
* то значение слишком велико; сообщить и сразу выйти */
if ( i & ~0377 ) {
cerr << "\n*** Ошибка диапазона SmallInt: "
<< i << " ***" << endl;
exit( -1 );
}
return i;
}
15.9.1. Конвертеры
Конвертер – это особый случай функции-члена класса, реализующий определенное пользователем преобразование объекта в некоторый другой тип. Конвертер объявляется в теле класса путем указания ключевого слова operator, за которым следует целевой тип преобразования.
Имя, находящееся за ключевым словом, не обязательно должно быть именем одного из встроенных типов. В показанном ниже классе Token определено несколько конвертеров. В одном из них для задания имени типа используется typedef tName, а в другом – тип класса SmallInt.
#include "SmallInt.h"
typedef char *tName;
class Token {
public:
Token( char *, int );
operator SmallInt() { return val; }
operator tName() { return name; }
operator int() { return val; }
// другие открытые члены
private:
SmallInt val;
char *name;
};
Обратите внимание, что определения конвертеров в типы SmallInt и int одинаковы. Конвертер Token::operator int() возвращает значение члена val. Поскольку val имеет тип SmallInt, то неявно применяется SmallInt::operator int() для преобразования val в тип int. Сам Token::operator int() неявно употребляется компилятором для преобразования объекта типа Token в значение типа int. Например, этот конвертер используется для неявного приведения фактических аргументов t1 и t2 типа Token к типу int формального параметра функции print():
#include "Token.h"
void print( int i )
{
cout << "print( int ) : " << i << endl;
}
Token t1( "integer constant", 127 );
Token t2( "friend", 255 );
int main()
{
print( t1 ); // t1.operator int()
print( t2 ); // t2.operator int()
return 0;
}
После компиляции и запуска программа выведет такие строки:
print( int ) : 127
print( int ) : 255
Общий вид конвертера следующий:
operator type();
где type может быть встроенным типом, типом класса или именем typedef. Конвертеры, в которых type – тип массива или функции, не допускаются. Конвертер должен быть функцией-членом. В его объявлении не должны задаваться ни тип возвращаемого значения, ни список параметров:
operator int( SmallInt & ); // ошибка: не член
class SmallInt {
public:
int operator int(); // ошибка: задан тип возвращаемого значения
operator int( int = 0 ); // ошибка: задан список параметров
// ...
};
Конвертер вызывается в результате явного преобразования типов. Если преобразуемое значение имеет тип класса, у которого есть конвертер, и в операции приведения указан тип этого конвертера, то он и вызывается:
#include "Token.h"
Token tok( "function", 78 );
// функциональная нотация: вызывается Token::operator SmallInt()
SmallInt tokVal = SmallInt( tok );
// static_cast: вызывается Token::operator tName()
char *tokName = static_cast< char * >( tok );
У конвертера Token::operator tName() может быть нежелательный побочный эффект. Попытка прямого обращения к закрытому члену Token::name помечается компилятором как ошибка:
char *tokName = tok.name; // ошибка: Token::name - закрытый член
Однако наш конвертер, разрешая пользователям непосредственно изменять Token::name, делает как раз то, от чего мы хотели защититься. Скорее всего, это не годится. Вот, например, как могла бы произойти такая модификация:
#include "Token.h"
Token tok( "function", 78 );
char *tokName = tok; // правильно: неявное преобразование
*tokname = 'P'; // но теперь в члене name находится Punction!
Мы намереваемся разрешить доступ к преобразованному объекту класса Token только для чтения. Следовательно, конвертер должен возвращать тип const char*:
typedef const char *cchar;
class Token {