Изменить стиль страницы

6.2.3 Операции и определяемые пользователем типы

Функция операция должна или быть членом, или получать в качестве параметра по меньшей мере один объект класса (фунциям, которые переопределяют операции new и delete, это длать необязательно). Это правило гарантирует, что пользовтель не может изменить смысл никакого выражения, не включающего в себя определенного пользователем типа. В часности, невозможно определить функцию, которая действует ислючительно на указатели.

Функция операция, первым параметром которой предполагется основной встроенный тип, не может быть функцией членом. Рассмотрим, например, сложение комплексной переменной aa с целым 2: aa+2, при подходящим образом описанной функции члне, может быть проинтерпретировано как aa.operator+(2), но с 2+aa это не может быть сделано, потому что нет такого класса int, для которого можно было бы определить + так, чтобы это означало 2.operator+(aa). Даже если бы такой тип был, то для

того, чтобы обработать и 2+aa и aa+2, понадобилось бы две различных функции члена. Так как компилятор не знает смысла +, определяемого пользователем, то не может предполагать, что он коммутативен, и интерпретировать 2+aa как aa+2. С этим примером могут легко справиться функции друзья.

Все функции операции по определению перегружены. Функция операция задает новый смысл операции в дополнение к встроеному определению, и может существовать несколько функций опраций с одним и тем же именем, если в типах их параметров имеются отличия, различимые для компилятора, чтобы он мог различать их при обращении (см. #4.6.7).

6.3 Определяемое пользователем преобразование типа

Приведенная во введении реализация комплексных чисел слишком ограничена, чтобы она могла устроить кого-либо, потому ее нужно расширить. Это будет в основном повторением описанных выше методов. Например:

class complex (* double re, im; public: complex(double r, double i) (* re=r; im=i; *)

friend complex operator+(complex, complex); friend complex operator+(complex, double); friend complex operator+(double, complex);

friend complex operator-(complex, complex); friend complex operator-(complex, double); friend complex operator-(double, complex); complex operator-() // унарный -

friend complex operator*(complex, complex); friend complex operator*(complex, double); friend complex operator*(double, complex);

// ... *);

Теперь, имея описание complex, мы можем написать:

void f() (* complex a(1,1), b(2,2), c(3,3), d(4,4), e(5,5); a = -b-c; b = c*2.0*c; c = (d+e)*a; *)

Но писать функцию для каждого сочетания complex и double, как это делалось выше для operator+(), невыносимо нудно. Кроме того, близкие к реальности средства комплексной арифметики должны предоставлять по меньшей мере дюжину таких функций. Посмотрите, например, на тип complex, описанный в «complex.h».

6.3.1 Конструкторы

Альтернативу использованию нескольких функций (перегрженных) составляет описание конструктора, который по заданнму double создает complex. Например:

class complex (* // ... complex(double r) (* re=r; im=0; *) *);

Конструктор, требующий только один параметр, необязтельно вызывать явно:

complex z1 = complex(23); complex z2 = 23;

И z1, и z2 будут инициализированы вызовом complex(23).

Конструктор – это предписание, как создавать значение данного типа. Когда требуется значение типа, и когда такое значение может быть создано конструктором, тогда, если такое значение дается для присваивания, вызывается конструктор. Например, класс complex можно было бы описать так:

class complex (* double re, im; public: complex(double r, double i = 0) (* re=r; im=i; *)

friend complex operator+(complex, complex); friend complex operator*(complex, complex); *);

и действия, в которые будут входить переменные complex и целые константы, стали бы допустимы. Целая константа будет интерпретироваться как complex с нулевой мнимой частью. Наример, a=b*2 означает:

a=operator*( b, complex( double(2), double(0) ) )

Определенное пользователем преобразование типа применется неявно только тогда, когда оно является единственным.

Объект, сконструированный с помощью явного или неявного вызова конструктора, является автоматическим и будет уничтжен при первой возможности, обычно сразу же после оператора, в котором он был создан.

6.3.2 Операции преобразования

Использование конструктора для задания преобразования типа является удобным, но имеет следствия, которые могут окзаться нежелательными:

1. Не может быть неявного преобразования из определеного пользователем типа в основной тип (поскольку осноные типы не являются классами)

2. Невозможно задать преобразование из нового типа в старый, не изменяя описание старого

3. Невозможно иметь конструктор с одним параметром, не имея при этом преобразования.

Последнее не является серьезной проблемой, а с первыми двумя можно справиться, определив для исходного типа операцию преобразования. Функция член X::operator T(), где T – имя тпа, определяет преобразование из X в T. Например, можно опрделить тип tiny (крошечный), который может иметь значение только в диапазоне 0...63, но все равно может свободно сочтаться в целыми в арифметических операциях:

class tiny (* char v; int assign(int i) (*return v=(i amp;~63) ? (error(«ошибка диапазона»),0):i;*) public: tiny(int i) (* assign(i); *) tiny(tiny amp; i) (* v = t.v; *) int operator=(tiny amp; i) (* return v = t.v; *) int operator=(int i) (* return assign(i); *) operator int() (* return v; *) *)

Диапазон значения проверяется всегда, когда tiny иницилизируется int, и всегда, когда ему присваивается int. Одно tiny может присваиваться другому без проверки диапазона. Чтбы разрешить выполнять над переменными tiny обычные целые операции, определяется tiny::operator int(), неявное преобрзование из tiny в int. Всегда, когда в том месте, где требется int, появляется tiny, используется соответствующее ему int. Например:

void main() (* tiny c1 = 2; tiny c2 = 62; tiny c3 = c2 – c1; // c3 = 60 tiny c4 = c3; // нет проверки диапазона (необязательна) int i = c1 + c2; // i = 64 c1 = c2 + 2 * c1; // ошибка диапазона: c1 = 0 (а не 66) c2 = c1 -i; // ошибка диапазона: c2 = 0 c3 = c2; // нет проверки диапазона (необязательна) *)

Тип вектор из tiny может оказаться более полезным, покольку он экономит пространство. Чтобы сделать этот тип более удобным в обращении, можно использовать операцию индексировния.

Другое применение определяемых операций преобразования – это типы, которые предоставляют нестандартные представления чисел (арифметика по основанию 100, арифметика, арифметика с фиксированной точкой, двоично-десятичное представление и т.п.). При этом обычно переопределяются такие операции, как + и *.

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

Типы istream и ostream опираются на функцию преобразовния, чтобы сделать возможными такие операторы, как

while (cin»»x) cout««x;

Действие ввода cin»»x выше возвращает istream amp;. Это знчение неявно преобразуется к значению, которое указывает сотояние cin, а уже это значение может проверяться оператором while (см. #8.4.2). Однако определять преобразование из оного типа в другой так, что при этом теряется информация, обычно не стоит.