Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

hkjCJgcQqF

.pdf
Скачиваний:
2
Добавлен:
15.04.2023
Размер:
810.94 Кб
Скачать

Определения конвертеров в

типы

MyInt и

int

одинаковы.

Конвертер Person::operator int()

возвращает значение

члена val.

Поскольку val имеет тип MyInt, то неявно применяется

MyInt::operator

int() для преобразования val в тип int. Сам Person::operator int() неявно употребляется компилятором для преобразования объекта типа Person в значение типа int. Например, этот конвертер используется для неявного приведения фактических аргументов объектов p1 и p2 типа Person к типу int формального параметра функции print():

#include "Person.h" void print( int i )

{

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

}

Person p1( "Ivanov ", 18 );

Person p2 ( "Petrov ", 25 ); int main( )

{

print(p1 );

// p1.operator int()

print( p2 );

// p2.operator int()

return 0; }

 

В следующем примере вызывается Person::operator MyInt() и

Person::operator tName()

#include "Person.h"

Person

student( "Ivanov", 18 );

MyInt

studentVal = MyInt( student );

// static_cast: вызывается Person::operator tName() char * studentName = static_cast< char * >( student );

У конвертера Person::operator tName() может быть нежелательный побочный эффект. Попытка прямого обращения к закрытому члену Person::name помечается компилятором как ошибка:

char *studentName = student.name;

// ошибка: Person::name - закрытый член

Но конвертер разрешает пользователям непосредственно изменять Person::name, как правило, это не годится. Вот, например, как могла бы произойти такая модификация:

#include "Person.h"

Person employee ( "Ivanov", 48 );

char *empName = employee; // правильно: неявное преобразование *empname = 'A'; // но теперь в члене name находится Avanov Обычно разрешается доступ к преобразованному объекту класса

Person только для чтения. Следовательно, конвертер должен возвращать тип const char*:

typedef const char *cchar;

83

class Person { public:

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

};

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

const char *pn2 = employee; // правильно

Другое решение – заменить в определении Person тип char* на тип string из стандартной библиотеки C++:

class Person { public:

Person ( string, int );

operator MyInt() { return val; }

operator string()

{ return name; }

operator int()

{ return val; }

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

private:

 

MyInt val; string name;

};

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

Должен ли целевой тип точно соответствовать типу конвертера? Например, будет ли в следующем коде вызван конвертер int(), определенный в классе Person?

extern void calc( double ); Person obj( "XXX", 44 );

// применяется стандартное преобразование int --> double calc( obj );

Если целевой тип (в данном случае double) не точно соответствует типу конвертера (в нашем случае int), то конвертер все равно будет вызван при условии, что существует последовательность стандартных преобразований, приводящая к целевому типу из типа конвертера. При обращении к функции calc() вызывается Person::operator int() для преобразования obj из типа Person в тип int. Затем для приведения результата от типа int к типу double применяется стандартное преобразование. Вслед за определенным пользователем преобразованием допускаются только стандартные. Если для достижения целевого типа необходимо еще одно пользовательское преобразование, то компилятор не применяет никаких преобразований.

84

Конструктор как конвертер

Набор конструкторов класса, принимающих единственный параметр, например, MyInt(int) класса MyInt, определяет множество неявных преобразований в значения типа MyInt. Так, конструктор MyInt(int) преобразует значения типа int в значения типа MyInt.

extern void calc( MyInt ); int i;

//необходимо преобразовать i в значение типа MyInt

//это достигается применением MyInt(int)

calc( i );

При вызове calc(i) число i преобразуется в значение типа MyInt с помощью конструктора MyInt(int), вызванного компилятором для создания временного объекта нужного типа. Затем копия этого объекта передается в calc(), как если бы вызов функции был записан в форме:

Псевдокод на C++

// создается временный объект типа MyInt

{

MyInt temp = MyInt( i ); calc( temp );

}

Фигурные скобки в этом примере обозначают время жизни данного объекта: он уничтожается при выходе из функции. Типом параметра конструктора может быть тип некоторого класса:

class Number { public:

//создание значения типа Number из значения типа SmallInt Number( const MyInt & );

//...};

Втаком случае значение типа MyInt можно использовать всюду, где допустимо значение типа Number:

extern void func( Number ); MyInt mi(27);

int main()

{ // вызывается Number( const MyInt & ) func( mi );

//...}

Если конструктор используется для выполнения неявного преобразования, то должен ли тип его параметра точно соответствовать типу подлежащего преобразованию значения? Например, будет ли в следующем коде вызван MyInt(int), определенный в классе MyInt, для приведения dobj к типу SmallInt?

extern void calc( MyInt );

85

double dobj;

// dobj преобразуется приведением от double к int стандартным преобразованием

calc( dobj );

Если необходимо, то к фактическому аргументу применяется последовательность стандартных преобразований до того как вызвать конструктор, выполняющий определенное пользователем преобразование. При обращении к функции calc() употребляется стандартное преобразование dobj из типа double в тип int. Затем уже для приведения результата к типу MyInt вызывается MyInt(int).

Компилятор неявно использует конструктор с единственным параметром для преобразования его типа в тип класса, к которому принадлежит конструктор. Однако, иногда удобнее, чтобы конструктор Number(const MyInt&) можно было вызывать только для инициализации объекта типа Number значением типа MyInt, но ни в коем случае не для выполнения неявных преобразований. Чтобы избежать такого употребления конструктора, объявим его явным (explicit):

class Number { public:

//никогда не использовать для неявных преобразований explicit Number( const MyInt & );

//...};

Компилятор никогда не применяет явные конструкторы для выполнения неявных преобразований типов:

extern void func( Number ); MyInt mi(27);

int main()

{ // ошибка: не существует неявного преобразования из MyInt в Number

func(mi ); // ...

}

Однако, такой конструктор все же можно использовать для преобразования типов, если оно запрошено явно в форме оператора приведения типа:

MyInt mi(27); int main()

{ // ошибка: не существует неявного преобразования из MyInt в Number

func( mi );

func( Number( mi ) ); // правильно: приведение типа

func( static_cast< Number >( mi ) ); // правильно: приведение типа

}

86

Выбор преобразования

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

Последовательность определенных пользователем преобразований – это комбинация определенного пользователем и стандартного преобразования, которая необходима для приведения значения к целевому типу. Такая последовательность имеет вид:

Последовательность стандартных преобразований ;

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

Последовательность стандартных преобразований, где определенное пользователем преобразование реализуется конвертером либо

конструктором.

Не исключено, что для преобразования исходного значения в целевой тип существует две разных последовательности пользовательских преобразований, и тогда компилятор должен выбрать из них лучшую. В классе разрешается определять много конвертеров. Например, в классе Number их два: operator int() и operator float(), причем оба способны преобразовать объект типа Number в значение типа float. Естественно, можно воспользоваться конвертером Person ::operator float() для прямой трансформации. Но и Person::operator int() тоже подходит, так как результат его применения имеет тип int и, следовательно, может быть преобразован в тип float с помощью стандартного преобразования. Является ли трансформация неоднозначной, если имеется несколько таких последовательностей? Или какую-то из них можно предпочесть остальным?

class Number { public:

operator float(); operator int(); // ...

};

Number num;

float ff = num; // operator float()

В таких случаях выбор наилучшей последовательности определенных пользователем преобразований основан на анализе последовательности преобразований, которая применяется после конвертера. В предыдущем примере можно применить такие две последовательности:

87

operator float() -> точное соответствие operator int() -> стандартное преобразование

Точное соответствие лучше стандартного преобразования. Поэтому первая последовательность лучше второй. Может случиться так, что для преобразования значения в целевой тип применимы два разных конструктора. В этом случае анализируется последовательность стандартных преобразований, предшествующая вызову конструктора:

class MyInt { public:

MyInt( int ival ) : value( ival ) { } MyInt( double dval )

:value( static_cast< int >( dval ) );

{}

};

extern void manip( const MyInt & ); int main() {

double dobj;

manip( dobj ); // правильно: SmallInt( double )

}

Здесь в классе MyInt определено два конструктора – MyInt(int) и MyInt(double), которые можно использовать для изменения значения типа double в объект типа MyInt: MyInt(double) трансформирует double в MyInt напрямую, а MyInt(int) работает с результатом стандартного преобразования double в int. Таким образом, имеются две последовательности определенных пользователем преобразований:

точное соответствие -> MyInt( double )

стандартное преобразование -> MyInt( int )

Поскольку точное соответствие лучше стандартного преобразования, то выбирается конструктор MyInt(double).

Не всегда удается решить, какая последовательность лучше. Может случиться, что все они одинаково хороши, и это означает, что преобразование неоднозначно. В таком случае компилятор не применяет никаких неявных преобразований. Например, если в классе Number есть два конвертера:

class Number { public:

operator float(); operator int(); // ...};

long lval = num;

Невозможно неявно преобразовать объект типа Number в тип long. Следующая инструкция вызывает ошибку компиляции, так как выбор

88

последовательности определенных пользователем преобразований неоднозначен:

// ошибка: можно применить как float(), так и int()

Для трансформации num в значение типа long применимы две такие последовательности:

operator float() -> стандартное преобразование operator int() -> стандартное преобразование

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

// правильно: явное приведение типа long lval = static_cast< int >( num );

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

class MyInt { public:

MyInt( const Number & ); // ...

};

class Number { public:

operator MyInt(); // ...

};

extern void compute( MyInt ); extern Number num;

compute( num ); // ошибка: возможно два преобразования

Аргумент num преобразуется в тип MyInt двумя разными способами: с помощью конструктора MyInt::MyInt(const Number&) либо с помощью конвертера Number::operator MyInt(). Поскольку оба изменения одинаково хороши, вызов считается ошибкой.

Для разрешения неоднозначности можно явно вызвать конвертер класса Number:

// правильно: явный вызов устраняет неоднозначность compute( num.operator MyInt() );

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

compute( MyInt( num ) );

89

// ошибка: по-прежнему неоднозначно Наличие большого числа подобных конвертеров и конструкторов

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

Лабораторная работа № 3 Тема: Перегруженные операторы

Цель: Изучить использование перегруженных операторов в классе Rational

Краткие теоретические сведения

Класс Rational дает хороший пример перегрузки арифметических, операций отношения, преобразования типов между целыми значениями и действительными числами и простого определения для перегруженного потока ввода/вывода.

Рациональные числа –это множество частных P/Q, где P и Q –это целые, а Q≠0. Число P называется числителем или нумератором (numerator), а число Q –знаменателем или деноминатором (denominator). Рациональное число представляет один член коллекции эквивалентных чисел. Например: 2/3=10/15=50/75 . //Эквивалентные рациональные числа.

Один член коллекции имеет редуцированную форму, в которой числитель и знаменатель не имеют общего делителя. Чтобы создать редуцированную форму какого-либо числа, числитель и знаменатель нужно разделить на их наибольший общий делитель (GCD, greatest common denominator).

Класс Rational

В классе используются данные-члены нумератор и деноминатор для описания рационального числа. Операторы потокового ввода и вывода реализуются с использованием перегрузки дружественной функции. Преобразование между целыми (или действительными) и рациональными числами и преобразование между рациональными и действительными числами используют функции конструктора и операторы преобразования.

Спецификация класса Rational: // заголовочный файл rational.h #ifndef RATIONAL_CLASS #define RATIONAL_CLASS

#include <iostream> #include <stdlib.h> using namespace std; class Rational

{

private:

90

// определяет рациональное число как числитель/ знаменатель long num, den;

// закрытый конструктор, используемый арифметическими операторами

// numerator или denominator Rational(long num, long denom);

// Функции-утилиты void Standardize(void);

long gcd(long m, long n) const; public:

//конструкторы преобразуют:

//int->Rational, double->Rational Rational(int num=0, int denom=1); Rational(double x);

//rational ввод/вывод

friend istream& operator>> (istream& istr, Rational &r);

friend ostream& operator<< (ostream& ostr, const Rational& d);

//бинарные операторы: add(+), subtract (-), multiply (*), divide (/)

//рациональная форма - (+/-)numerator/denominator. т.е. denominator

//всегда положителен в стандартной форме

Rational operator+ (Rational r) const;

Rational operator- (Rational r) const;

Rational operator* (Rational r) const;

Rational operator/ (Rational r) const;

//унарный минус (изменяет знак) Rational operator- (void) const;

//операторы отношения

int operator< (Rational r) const; int operator<= (Rational r) const; int operator== (Rational r) const; int operator!= (Rational r) const; int operator> (Rational r) const; int operator>= (Rational r) const;

//оператор преобразования : Rational в double operator double( ) const;

//методы-утилиты

long GetNumerator( ) const; //для доступа к данному numenator long GetDenominator( ) const; //для доступа к данному denominator void Reduce( ); //преобразует

};

Закрытый метод Standardize преобразует рациональное число в «нормальную форму» с положительным деноминатором. Конструкторы

91

используют функцию Standardize() для преобразования числа в нормальную форму. void Rational::Standardize(void)

{

if (den < 0)

{

// сделать denominator >= 0 и изменить знак в numerator den = -den;

num = -num;

}

}

Метод gcd() возвращает наибольший общий делитель двух целых. Вычисляется наибольший общий делитель GCD чисел m и n. Используется редуцированная форма:

long Rational::gcd(long m, long n) const

{

long remainder = m % n; while (remainder > 0)

{

m = n;

n = remainder; remainder = m % n;

}

return n;

}

Конструкторрациональное число в форме num/den. Если используется одно значение num , то значение по умолчанию den=1 и число преобразуется в эквивалентное num/1.

Rational::Rational(int p, int q): num(p), den(q)

{

if (den == 0) {

cerr << "Нулевой деноминатор (A Zero denominator is invalid)" << endl; exit(1); }

}

Rational::Rational(long p, long q): num(p), den(q)

{

if (den == 0) {

cerr << "Нулевой деноминатор (A Zero denominator is invalid)" << endl; exit(1); }

}

// конструктор преобразует X тип double к типу Rational

Rational::Rational(double x)

{

double val1, val2;

92

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]