Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C++ для начинающих.pdf
Скачиваний:
183
Добавлен:
01.05.2014
Размер:
3.97 Mб
Скачать

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. Глобальные же операторы-друзья позволяют производить эти операции над объектами данного класса и объектами встроенных арифметических типов. Необходимо только шесть операторов, поскольку любой встроенный арифметический тип может быть

SmallInt

si( 3 );

приведен к типу int. Например, выражение si + 3.14159

разрешается в два шага:

1.Константа 3.14159 типа double преобразуется в целое число 3.

2.Вызывается operator+(const SmallInt &,int), который возвращает значение 6.

Если мы хотим поддержать битовые и логические операции, а также операции сравнения и составные операторы присваивания, то сколько же необходимо перегрузить операторов? Сразу и не сосчитаешь. Значительно удобнее автоматически преобразовать объект класса SmallInt в объект типа int.

В языке C++ имеется механизм, позволяющий в любом классе задать набор преобразований, применимых к его объектам. Для SmallInt мы определим приведение

class SmallInt { public:

SmallInt( int ival ) : value( ival )

{}

//конвертер

//SmallInt ==> int

operator int() { return value; }

// перегруженные операторы не нужны

private:

int value;

объекта к типу int. Вот его реализация:

};

Оператор int() – это конвертер, реализующий определенное пользователем преобразование, в данном случае приведение типа класса к заданному типу int. Определение конвертера описывает, что означает преобразование и какие действия компилятор должен выполнить для его применения. Для объекта SmallInt смысл преобразования в int заключается в том, чтобы вернуть число типа int, хранящееся в члене value.

Теперь объект класса SmallInt можно использовать всюду, где допустимо использование int. Если предположить, что перегруженных операторов больше нет и в SmallInt определен конвертер в int, операция сложения

SmallInt

si( 3 );

si + 3.14159

разрешается двумя шагами:

1.Вызывается конвертер класса SmallInt, который возвращает целое число 3.

2.Целое число 3 расширяется до 3.0 и складывается с константой двойной точности 3.14159, что дает 6.14159.

Такое поведение больше соответствует поведению операндов встроенных типов по сравнению с определенными ранее перегруженными операторами. Когда значение типа int складывается со значением типа double, то выполняется сложение двух чисел типа double (поскольку тип int расширяется до double) и результатом будет число того же типа.

#include <iostream> #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:

}

Откомпилированная программа выдает следующие результаты:

Введите SmallInt, пожалуйста: 127

Прочитано значение 127 Оно равно 127

Введите SmallInt, пожалуйста (ctrl-d для выхода): 126 Оно меньше, чем 127

Введите SmallInt, пожалуйста (ctrl-d для выхода): 128 Оно больше, чем 127

Введите SmallInt, пожалуйста (ctrl-d для выхода): 256

*** Ошибка диапазона SmallInt: 256 ***

#include <iostream>

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;

В реализацию класса SmallInt добавили поддержку новой функциональности:

};

istream& operator>>( istream &is, SmallInt &si ) { int ix;

is >> ix;

// SmallInt::operator=(int)

si = ix;

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; }

{ return val; }

operator int()

// другие открытые члены 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

#include "Token.h"

void print( int i )

{

cout << "print( int ) : " << i << endl;

}

Token t1( "integer constant", 127 );

Token t2( "friend", 255 );

int main()

 

 

{

 

// t1.operator int()

print( t1 );

print( t2

);

// t2.operator int()

return 0;

 

 

типа Token к типу int формального параметра функции print():

}

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"

78

);

Token tok( "function",

char *tokName = tok;

//

правильно: неявное

преобразование

годится. Вот, например, как могла бы произойти такая модификация:

*tokname = 'P'; // но теперь в члене name находится Punction!

Мы намереваемся разрешить доступ к преобразованному объекту класса Token только для чтения. Следовательно, конвертер должен возвращать тип const char*:

typedef const char *cchar; class Token {

public:

operator cchar() { return name; } // ...

};

// ошибка: преобразование char* в const char* не допускается

char *pn = tok;

const char *pn2 = tok;

// правильно

Другое решение – заменить

в определении Token тип char* на тип string из

class Token { public:

Token( string, int );

operator SmallInt() { return val; }

operator string()

{ return

name; }

{ return val; }

operator int()

// другие открытые члены private:

SmallInt val; string name;

стандартной библиотеки C++:

};

Семантика конвертера Token::operator string() состоит в возврате копии значения (а не указателя на значение) строки, представляющей имя лексемы. Это предотвращает случайную модификацию закрытого члена name класса Token.

Должен ли целевой тип точно соответствовать типу конвертера? Например, будет ли в

extern void calc( double ); Token tok( "constant", 44 );

//Вызывается ли оператор int()? Да

//применяется стандартное преобразование int -->

double

следующем коде вызван конвертер int(), определенный в классе Token? calc( tok );

Если целевой тип (в данном случае double) не точно соответствует типу конвертера (в нашем случае int), то конвертер все равно будет вызван при условии, что существует последовательность стандартных преобразований, приводящая к целевому типу из типа конвертера. (Эти последовательности описаны в разделе 9.3.) При обращении к функции calc() вызывается Token::operator int() для преобразования tok из типа Token в

тип int. Затем для приведения результата от типа int к типу double применяется стандартное преобразование.

Вслед за определенным пользователем преобразованием допускаются только стандартные. Если для достижения целевого типа необходимо еще одно пользовательское преобразование, то компилятор не применяет никаких преобразований.