Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
шпора к КПИЯП.docx
Скачиваний:
41
Добавлен:
25.02.2016
Размер:
135.65 Кб
Скачать

2.7 Перегрузка операторов

 

Язык С++ располагает рядом встроенных типов данных, включая int, real, char и т.д. Для работы с данными этих типов используются встроенные операторы, например, сложение (+) и умножение (*). Кроме того, язык С++ позволяет добавлять и перегружать подобные операторы для пользовательских классов.

Чтобы в деталях рассмотреть процедуру перегрузки операторов, в листинге 20.1 создается новый класс Counter (счетчик). Объект Counter будет использован в качестве счетчика (сюрприз!) в циклах и других приложениях, где необходимо приращение (инкремент или декремент) числа.

Листинг 20.1 Класс Counter

#include <iostream>

using namespace std;

class Counter

{

    public:

       Counter();

       ~Counter() {}

       int GetltsVal() const { return itsVal; }

       void SetltsVal(int x) { itsVal = x; }     private:

       int itsVal;

} ; Counter::Counter():itsVal(0) 

{}

int main()

{

    Counter i;

    cout << "The value of i is " <<  i.GetltsVal() << endl;

    return 0;

}

Результат:

The value of i is 0

Анализ:

Как можно заметить, это совершенно бесполезный класс. Класс Counter обладает единственной переменной-членом типа int. Стандартный конструктор инициализирует единственную переменную-член itsVal значением 0.

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

 

Создание функции инкремента

С помощью перегрузки операторов можно восстановить утраченные функциональные возможности класса Counter. Для этого следует предусмотреть возможность увеличивать значение объекта Counter (инкремент). Для начала создадим метод инкремента (приращения), как показано в листинге 20.2.

Листинг 20.2 Добавление в класс оператора инкремента

#include <iostream> 

using namespace std; class Counter

{     public:         Counter();

        ~Counter () {}

        int GetltsVal() const { return itsVal; }

        void SetltsVal(int x) { itsVal = x; }

        void Increment() { ++itsVal; }

    private:

        int itsVal;

} ;

Counter::Counter():itsVal(0)

{}

int main()

{

    Counter i;

    cout << "The value of i is " << i.GetltsVal() << endl;

    i.Increment();

    cout << "The value of i is " << i.GetltsVal() << endl;

    return 0;

}

Результат: The value of i is 0 

The value of i is 1

Анализ:

В листинге 20.2 добавлена функция increment(). Несмотря на громоздкость, она вполне работоспособна. Программам необходим оператор ++ и, безусловно, это можно сделать.

 

Перегрузка префиксных операторов

 

Префиксные операторы могут быть перегружены. Синтаксис объявления функции имеет следующий вид:

возвращаемыйТип Operator оп()

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

void operator++ ()

 

Листинг 20.3 Перегрузка оператора ++

1: // Листинг 20.3. Класс Counter

2: // Префиксный оператор приращения 

3 :

4: #include <iostream>  

5: using namespace std;

6:

7: class Counter

8: {

9:   public:

10:     Counter();

11:     ~Counter () {}

12:     int GetltsVal() const { return itsVal; }

13:     void SetltsVal(int x) { itsVal = x; }

14:     void Increment() { ++itsVal; }

15:     void operator++ () { ++itsVal; } 

16 :

17:   private:

18:     int itsVal;

19: } ;

20:

21: Counter::Counter():

22: itsVal(0)

23: {}

24:

25: int main()

26: {

27:     Counter i;

28:     cout << "The value of i is " << i.GetltsVal() << endl;

29:     i.Increment();

30:     cout << "The value of i is " << i.GetltsVal() << endl;

31:     ++i;

32:     cout << "The value of i is " << i.GetltsVal() << endl;

33:     return 0;

34: }

Результат:

The value of i is 0

The value of i is 1

The value of i is 2

Анализ:

Оператор operator++ перегружен в строке 15. Как можно заметить, перегруженный оператор инкремента просто увеличивает значение закрытой переменной-члена itsVal. Впоследствии он используется в строке 31. Такой синтаксис очень похож на применяемый для встроенного типа int.

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

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

Counter а = ++i;

В этой строке предпринимается попытка создать новый объект а класса Counter, которому присваивается значение переменной i, перед этим увеличенное на единицу (инкремент). Встроенный конструктор копий отработает присвоение, но текущий оператор инкремента не в состоянии возвратить объект класса Counter. Он не возвращает ничего (void), а пустой объект нельзя присвоить объекту класса Counter. (Из ничего нельзя сделать нечто!)

 

Тип возвращаемого значения перегруженных операторов и функций

 

Таким образом, необходимо организовать возвращение объекта класса Counter, чтобы его можно было присвоить другому объекту класса Counter. Как это сделать? Одним из подходов является создание и возвращение временного объекта. Это показано в листинге 20.4.

 

Листинг 20.4 Возвращение временного объекта

// Листинг 20.4. operator++ возвращает временный объект

#include <iostream>  

using namespace std;

class Counter

{

    public:

        Counter();

        ~Counter() {}

        int GetltsVal() const { return itsVal; }

        void SetltsVal(int x) { itsVal = x; } 

        void Increment() { ++itsVal; }

        Counter operator++ ();

    private:

        int itsVal;

} ;

Counter::Counter(): itsVal(0) {}

Counter Counter::operator++()

{

    ++itsVal;

    Counter temp; 

    temp.SetltsVal(itsVal); 

    return temp;

}

int main()

{

    Counter i;

    cout << "The value of i is " << i.GetltsVal() << endl;

    i.Increment();

    cout << "The value of i is " << i.GetltsVal() << endl;

    ++i;

    cout << "The value of i is " << i.GetltsVal() << endl;

    Counter a = ++i;

    cout << "The value of a: " << a.GetltsVal();

    cout << " and i: " << i.GetltsVal() << endl;

    return 0;

}

Результат:

The value of i is 0

The value of i is 1

The value of i is 2

The value of a: 3 and i: 3

Анализ:

В данной версии operator++ объявлен так, что он может возвращать объекты класса Counter. В строке 29 создается временный объект temp, и ему присваивается значение текущего объекта Counter. Значение временной переменной возвращается и сразу присваивается новому объекту а.

 

Возвращение безымянных временных объектов

 

На самом деле нет необходимости присваивать имя временному объекту, как это было сделано в предыдущем листинге. Если класс Counter обладает конструктором, получающим значение, то возвращаемое значение оператора инкремента можно просто передать ему в качестве параметра. Это продемонстрировано в листинге 20.5.

 

Листинг 20.5 Возвращение безымянного временного объекта

1: // Листинг 20.5. operator++ возвращает безымянный

2: // временный объект

3: #include <iostream>  

4:

5: using namespace std;

6:

7: class Counter

8: {

9:   public: 

10:    Counter ();

11:    Counter(int val);

12:    ~Counter() {}

13:    int GetltsVal() const { return itsVal; }

14:    void SetltsVal(int x) { itsVal = x; }

15:    void Increment() { ++itsVal; }

16:    Counter operator++ ();

17:

18:  private: 

19:    int itsVal;

20: } ;

21:

22: Counter::Counter():

23: itsVal(0)

24: {}

25:

26: Counter::Counter(int val):

27: itsVal(val)

28: {}

29:

30: Counter Counter::operator++()

31: {

32:    ++itsVal;

33:    return Counter (itsVal);

34: }

35:

36: int main()

37: {

38:    Counter i;

39:    cout << "The value of i is " << i.GetltsVal() << endl;

40:    i.Increment();

41:    cout << "The value of i is " << i.GetltsVal() << endl;

42:    ++i;

43:    cout << "The value of i is " << i.GetltsVal() << endl;

44:    Counter a = ++i;

45:    cout << "The value of a: " << a.GetltsVal();

46:    cout << " and i: " << i.GetltsVal() << endl;

47:    return 0;

48: }

Результат:

The value of i is 0

 The value of i is 1

The value of i is 2

The value of a: 3 and i: 3

Анализ:

В строке 11 объявлен новый конструктор, который получает значение типа int. Его реализация находится в строках 26-28. Он инициализирует переменную itsVal : чачением, переданным в качестве параметра по значению.

Теперь реализация оператора operator++ упрощена. В строке 32 значение пере-енной itsVal увеличивается. В строке 33 создается временный объект класса :;unter, инициализируемый значением переменной itsVal и возвращаемый затем :<ак результат функции operator++.

Такое решение изящнее, но возникает вопрос: зачем вообще создавать временный :5ъект? Напомним, что каждый временный объект придется сначала создать, а затем -шчтожить, и каждая из этих операций отнимает время и ресурсы. Если объект i уже существует и содержит правильное значение, то почему бы не возвращать именно его? Эта проблема решается при помощи указателя this.

 

Использование указателя this

 

Указатель this сопутствует всем функциям-членам, включая такие перегруженные итераторы, как operator++(). Указатель this указывает и на объект i; сославшись на него, можно возвратить правильное значение, которое содержится в его перемен-ной-члене itsVal. Листинг 20.6 иллюстрирует возвращение значения по ссылке на указатель this, что позволяет избежать создания ненужного временного объекта.

 

Листинг 20.6. Возвращение указателя this

1: // Листинг 20.6. Возвращение значения указателя this

2:

3: #include <iostream> 

4:

5: using namespace std;

6:

7: class Counter

8: {

9:   public:

10:    Counter() ;

11:    ~Counter () {}

12:    int GetltsVal() const { return itsVal; }

13:    void SetltsVal(int x) { itsVal = x; }

14:    void Increment() { ++itsVal; }

15:    const Counters operator++ (); 

16 :

17:  private:

18:    int itsVal;

19: } ;

20:

21: Counter::Counter():

22: itsVal(0)

23: {};

24:

25: const Counter& Counter::operator++()

26: {

27:    ++itsVal;

28:    return *this;

29: }

30:

31: int main()

32: {

33:    Counter i;

34:    cout << "The value of i is " << i.GetltsVal() << endl;

35:    i.Increment();

36:    cout << "The value of i is " << i.GetltsVal() << endl;

37:    ++i;

38:    cout << "The value of i is " << i.GetltsVal() << endl;

39:    Counter a = ++i;

40:    cout << "The value of a: " << a.GetltsVal();

41:    cout << " and i: " << i.GetltsVal() << endl;

42:    return 0;

43: }

Результат:

The value of i is 0 The value of i is 1 The value of i is 2 The value of a: : 3 and i: 3

Анализ:

Реализация оператора operator++ в строках 25-29 была изменена так, чтобы, сославшись на указатель this, возвратить текущий объект. Это позволит присвоить объект класса Counter объекту а. Как уже говорилось, если объект резервирует область памяти, то желательно отказаться от конструктора копий, но в данном случае стандартный конструктор копий работает прекрасно.

Обратите внимание: возвращаемое значение представляет собой ссылку класса Counter, благодаря чему отпадает необходимость в создании каких-либо дополнительных временных объектов. Ссылка задана как const, поскольку ее значение не должно изменяться при использовании в функции.

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

39: Counter а = ++++i;

Эта запись представляла бы собой вызов оператора приращения (++) для результата вызова оператора приращения (а этого следует избегать).

Проведем эксперимент: изменим в объявлении и реализации (строки 15 и 25) тип возвращаемого значения на непостоянный, а строку 39 перепишем так, как было показано выше (++++i). Поместите в отладчике контрольную точку в строке 39 и запустите программу. Окажется, что здесь оператор инкремента вызывается дважды, поскольку теперь возвращаемое значение не является постоянным, и приращение может бьггь выполнено.

Для предотвращения этого объявим возвращаемое значение постоянным. Если снова изменить строки 15 и 265 на постоянные, не трогая строку 39 (++++i), то компилятор не сможет вызывать оператор приращения для постоянного объекта.

 

Перегрузка постфиксных операторов

 

До сих пор рассматривалась перегрузка префиксного оператора инкремента. А если необходимо перегрузить оператор постфиксного инкремента? Тут перед компилятором встает проблема: как различить операторы постфиксного и префиксного инкремента? По соглашению в объявлении оператора постфиксного инкремента используется целочисленный тип. Значение параметра неважно, поскольку это лишь символ того, что речь идет об операторе постфиксного инкремента.

 

Различие между префиксом и постфиксом

 

Прежде чем приступить к перегрузке оператора постфиксного инкремента, следует четко понять, чем он отличается от оператора префиксного инкремента.

Напомним, что префикс означает "сначала приращение, затем возвращение", а постфикс означает "сначала возвращение, затем приращение".

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

Давайте повторим все это еще раз. Рассмотрим следующий участок кода:

a = x++;

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

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

В листинге 20.7 показано использование обоих типов операторов.

Листинг 20.7. Префиксный и постфиксный операторы инкремента

// Листинг 10.12. Возвращение значения указателя this

#include <iostream> 

using namespace std;

class Counter

{

  public:

    Counter ();

    ~Counter() {}

    int GetltsVal() const { return itsVal; }

    void SetltsVal(int x) { itsVal = x; } 

    const Counter& operator++ (); // префикс

    const Counter operator++ (int); // постфикс

  private:

    int itsVal;

} ;

Counter::Counter():

itsVal(0) {}

const Counter& Counter::operator++()

{

    ++itsVal; 

    return *this;

}

 

const Counter Counter::operator++(int theFlag) 

{

    Counter temp(*this);

    ++itsVal; 

    return temp;

}

int main()

{

    Counter i;

    cout << "The value of i is " << i.GetltsVal() << endl;

    i++;

    cout << "The value of i is " << i.GetltsVal() << endl;

    ++i; 

    cout << "The value of i is " << i.GetltsVal() << endl;

    Counter a = ++i;

    cout << "The value of a; " << a.GetltsVal();

    cout << " and i: " << i.GetltsVal() << endl;

    a = i++;

    cout << "The value of a: " << a.GetltsVal();

    cout << " and i: " << i.GetltsVal() << endl;

    return 0;

}

Результат:

The value of i is 0

The value of i is 1

The value of i is 2

The value of a: 3 and i: 3

The value of a: 3 and i: 4

Анализ:

Постфиксный оператор объявлен в строке 15 и реализован в строках 31—36. Префиксный оператор объявлен в строке 14.

Параметр, переданный в постфиксный оператор в строке 32 (theFlag), должен сообщить о том, что это постфиксный оператор, но само значение никак не используется.

 

Перегрузка парных математических операторов

Оператор инкремента является унарным, т.е. работает только с одним объектом. Оператор суммы (+) — парный, работающий с двумя объектами. Большинство математических операторов являются парными, им передают два объекта (один текущего класса и один любого другого класса). Вполне очевидно, что перегрузка таких операторов, как сложение (+), вычитание (-), умножение (*), деление (/) и деление по модулю (%), будет существенно отличаться от перегрузки префиксных и постфиксных операторов. Так как же реализовать перегрузку оператора + для объекта Counter?

Задача заключается в том, чтобы объявить две переменные класса Counter, а затем сложить их следующим образом:

Counter varOne, varTwo, varThree; 

VarThree = VarOne + VarTwo; Теперь создадим функцию Add (), в которой объект Counter будет аргументом. Эта функция должна сложить два значения, а затем возвратить объект Counter с полученным результатом. Данный подход продемонстрирован в листинге 20.8.

Листинг 20.8 Функция Add()

1: // Листинг 20.8. Функция Add()

2:

3: #include <iostream> 

4:

5: using namespace std;

6 :

7: class Counter

8: {

9:  public: 

10:   Counter();

11:   Counter(int initialValue);

12:   ~Counter() {}

13:   int GetltsVal() const { return itsVal; }

14:   void SetltsVal(int x) { itsVal = x; }

15:   Counter Add(const Counter &) ;

16:

17:  private: 

18:   int itsVal;

19: } ;

20:

21: Counter::Counter(int initialValue):

22: itsVal(initialValue)

23: {}

24:

25: Counter::Counter():

26: itsVal(0)

27: {}

28:

29: Counter Counter::Add(const Counter & rhs)

30: {

31:    return Counter(itsVal+ rhs.GetltsVal()); 

32: }

33 :

34: int main()

35: {

36:    Counter varOne(2), varTwo(4), varThree; 

37:    varThree = varOne.Add(varTwo);

38:    cout << "varOne: " << varOne.GetltsVal() << endl; 

39:    cout << "varTwo: " << varTwo.GetltsVal() << endl; 

40:    cout << "varThree: " << varThree.GetltsVal() << endl; 

41:

42:    return 0; 

43: }

Результат:

varOne: 2

varTwo: 4

varThree: 6

Анализ:

Функция Add () объявлена в строке 15. В качестве аргумента она принимает постоянную ссылку на объект класса Counter, представляющий собой второе число, которое нужно добавить к текущему объекту. Функция возвращает объект класса Counter, представляющий собой результат суммирования, который присваивается операнду слева от оператора присвоения (=), как показано в строке 37. Здесь переменная varOne является объектом, varTwo — параметром функции Add (), а результатом будет объект varThree.

Для создания объекта varThree без исходной инициализации каким-либо значением используется стандартный конструктор. Он присваивает объекту varThree нулевое значение (строки 25-27). Поскольку переменные varOne и varTwo должны инициализироваться ненулевым значением, в строках 21-23 создан специальный конструктор. Другим решением этой проблемы является инициализация нулевым значением в стандартном конструкторе, объявленном в строке 11.

Сама функция Add () находится в строках 29-32. Она вполне работоспособна, но изяществом не отличается.

 

Перегрузка оператора суммы operator+

 

Перегрузка оператора суммы (+) сделала бы работу класса Counter более естественной. Как уже было сказано, для перегрузки операторов используется следующий синтаксис:

возвращаемыйТип Operator оп() Перегрузку оператора суммы демонстрирует листинг 20.9.

Листинг 20.9 Перегрузка оператора суммы

// Листинг 10.14. Перегрузка оператора плюс (+)

#include <iostream> 

using namespace std;

class Counter

{

  public:

    Counter();

    Counter(int initialValue);

    ~Counter() {}

    int GetltsVal() const { return itsVal; }

    void SetltsVal(int x) { itsVal = x; } 

    Counter operator+ (const Counter &); 

  private:

    int itsVal;

};

Counter::Counter(int initialValue):

itsVal(initialValue)

{}

Counter::Counter(): itsVal(0)

{}

Counter Counter::operator+ (const Counter & rhs)

{

    return Counter(itsVal + rhs.GetltsVal());

}

int main()

{

    Counter varOne(2), varTwo(4), varThree;

    varThree = varOne + varTwo; 

    cout << "varOne: " << varOne.GetltsVal() << endl;

    cout << "varTwo: " << varTwo.GetltsVal() << endl;

    cout << "varThree: " << varThree.GetltsVal() << endl; 

    return 0;

}

Результат:

varOne: 2

varTwo: 4

varThree: 6

Анализ:

Оператор суммы (operator+) объявлен в строке 15 и реализован в строках 28—31.

Сравните его с объявлением и реализацией функции Add() в предыдущем листинге. Они почти идентичны. Но синтаксис их применения совершенно различен. Ведь более естественно написать так: varThree = varOne + varTwo; нежели так:

varThree = varOne.Add(varTwo);

Изменение небольшое, но программа стала более читабельной.

В строке 36 оператор применяется так: 36:

varThree = varOne + varTwo;

а компилятор воспринимает это так:

VarThree = varOne.Operator + (varTwo);

Безусловно, можно применить и такую форму записи; для компилятора это безразлично.

Метод operator + выполняется над операндами слева от знака =, передавая результат операнду справа.

 

Проблемы перегрузки операторов

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

Некоторые операторы могут быть исключительно членами класса. Это операторы присвоения (=), индексирования ([ ]), вызова функции (()), а также косвенного обращения (->).

 

Ограничения на перегрузку операторов

Операторы для встроенных типов (например, int) не могут быть перегружены. Не может быть изменен ни порядок приоритета, ни количество операндов оператора. То есть, унарный оператор не будет работать с двумя операндами. Нельзя создать новый оператор, поэтому не удастся объявить, что ** будет оператором "возведения в степень".

Количество операндов, которыми может манипулировать оператор, является важнейшей характеристикой любого оператора. В языке С++ существуют унарные операторы, использующие только один операнд (myValue++), парные операторы, использующие два операнда (а+Ь), и всего один тройственный оператор, использующий три операнда (?). Данный оператор иногда называют троичным, поскольку это единственный оператор в языке С++, который использует три операнда (а > b? X: у).

Что можно перегружать

 

Возможность перегрузки операторов — это одно из наиболее широко используемых средств языка С++, которым иногда злоупотребляют начинающие программисты. Новичков захватывает азарт присвоения новых интересных функций обычным операторам. В результате код программы зачастую оказывается непонятным даже для создателя, не говоря о других программистах.

Безусловно, если в программе оператор + осуществляет вычитание, а оператор * — суммирование, то это может тешить самолюбие начинающего программиста, но профессионал никогда такого не допустит. Вполне естественно желание использовать оператор + для конкатенации строк и символов, а оператор / — для разделения строк, но такая перегрузка операторов таит в себе подводные рифы, на которые может совершенно неожиданно напороться программа во время выполнения. Возможно, было бы неплохо уделить больше внимания особенностям использования перегруженных операторов, однако начнем с предостережений. Прежде всего следует помнить, что главная цель перегрузки операторов — повышение эффективности программы и упрощение ее кода.

 

Оператор присвоения

Четвертая функция, предоставляемая компилятором для работы с объектами (если не заданы дополнительные функции), — это оператор присвоения (operator= ()). Он применяется всякий раз, когда объекту необходимо присвоить новое значение, например: Cat catOne(5,7);

Cat catTwo(3,4);

// ... здесь будет остальной код

catTwo = catOne; Здесь создан объект catOne, а его переменные itsAge и itsWeight инициализированы значениями 5 и 7. Затем был создан объект CatTwo и инициализирован значениями 3 и 4.

Через некоторое время объекту catTwo присваивается значение объекта catOne. Возникают вопросы: что произойдет, если переменная itsAge является указателем, и что случится с исходными значениями переменных объекта catTwo?

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

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

catTwo = catTwo;

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

Если не предусмотреть решение такой проблемы, оператор присвоения сначала очистит ячейки памяти объекта catTwo, а затем попытается присвоить объекту catTwo свои собственные значения, которых уже не будет и в помине!

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

Листинг 20.10. Оператор присвоения

1: // Листинг 20.10. Конструкторы копий

2 :

3: #include <iostream> 

4:

5: using namespace std;

6 :

7: class Cat

8: {

9:   public:

10:     Cat(); // стандартный конструктор

11:     // конструктор копий и деструктор отсутствуют!

12:     int GetAge() const { return *itsAge; }

13:     int GetWeight() const { return *itsWeight; }

14:     void SetAge(int age) { *itsAge = age; }

15:     Cat & operator=(const Cat &); 

16 :

17:   private:

18:     int *itsAge;

19:     int *itsWeight;

20: } ;

21:

22: Cat::Cat()

23: {

24:     itsAge = new int;

25:     itsWeight = new int;

26:     *itsAge = 5;

27:     *itsWeight = 9;

28: }

29:

30:

31: Cat & Cat::operator=(const Cat & rhs)

32: {

33:     if (this == &rhs)

34:        return *this;

35:     *itsAge = rhs.GetAge();

36:     *itsWeight = rhs.GetWeight();

37:     return *this;

38 : }

39:

40:

41: int main()

42: {

43:     Cat Frisky;

44:     cout << "Frisky's age: " << Frisky.GetAge() << endl;

45:     cout << "Setting Frisky to 6..." << endl;

46:     Frisky.SetAge(6);

47:     Cat Whiskers;

48:     cout << "Whiskers' age: " << Whiskers.GetAge() << endl;

49:     cout << "copying Frisky to Whiskers..." << endl;

50:     Whiskers = Frisky;

51:     cout << "Whiskers' age: " << Whiskers.GetAge() << endl;

52:     return 0;

53: }

Результат:

Frisky's age: 5

Setting Frisky to 6...

Whiskers' age: 5

copying Frisky to Whiskers...

Whiskers' age: 6

Анализ:

Листинг 20.10 вновь обращается к классу Cat, но теперь без конструктора копий и деструктора во избежание лишнего расхода памяти. Оператор присвоения объявлен в строке 15, а реализован в строках 31—38.

В строке 33 текущий (присваиваемый) объект Cat проверяется на идентичность результирующему объекту Cat. Для этого сравниваются адреса указателей rhs и this. Если они совпадают, то делать ничего не нужно вообще. Поэтому в строке 34 текущий объект просто возвращается.

Если объекты справа и слева не совпадают, то перед выходом их переменные-члены копируются в строках 35 и 36.

Пример применения оператора присвоения расположен в строке 50 основной программы, где объект Frisky класса Cat присваивается объекту Whiskers того же класса. Остальная часть кода этого листинга уже рассматривалась. Как уже упоминалось, если два объекта указывают на один и тот же адрес, то они идентичны. Безусловно, оператор равенства (==) также может быть перегружен, что позволяет применить свой собственный механизм проверки идентичности объектов.

 

Преобразование типов данных

Что происходит при попытке присвоить значение переменной одного из базовых типов, таких, как int и unsigned short, объекту пользовательского класса? Например, созданному ранее классу Counter? В листинге 20.11 вновь вернемся к классу Counter и попытаемся присвоить его объекту значение переменной типа int.

НЕ КОМПИЛИРУЙТЕ ЛИСТИНГ 20.11!!!

Листинг 20.11. Попытка присвоить объекту класса counter значение переменной типа int

1: // Листинг 20.11. Этот код нельзя компилировать! 

2 :

3: #include <iostream> 

4:

5: using namespace std;

6:

7: class Counter

8: {

9:   public:

10:    Counter();

11:    ~Counter() {}

12:    int GetltsVal() const { return itsVal; }

13:    void SetltsVal(int x) { itsVal = x; }

14:  private:

15:    int itsVal;

16: } ;

17:

18: Counter::Counter():

19: itsVal(0)

20: {}

21:

22: int main()

23: {

24:    int thelnt = 5;

25:    Counter theCtr = thelnt;

26:    cout << "theCtr: " << theCtr.GetltsVal() << endl;

27:    return 0;

28:}

Результат:

Compiler error! Unable to convert int to Counter

Анализ:

Класс Counter, объявленный в строках 7—16, содержит лишь стандартный конструктор. В объявлении класса отсутствуют методы преобразования данных типа int в данные типа Counter. В строке 24 функции main() объявлена целочисленная переменная, которой присвоено значение 5. Затем она присваивается объекту Counter. Однако содержащая присвоение строка кода приводит к ошибке. Компилятор ничего не сможет сделать, пока не получит четких инструкций о присвоении переменной-члену itsVal значения типа int.

В листинге 20.12 эта ошибка исправлена в результате создания оператора преобразования типов. Теперь добавлен еще один конструктор, который получает значение типа int и присваивает его переменной-члену itsVal создаваемого объекта класса counter.

Листинг 20.12. Преобразование типа int в Counter

// Листинг 10.17. Конструктор как оператор преобразования

#include <iostream> 

using namespace std;

class Counter {

  public:

    Counter();

    Counter(int val); 

    ~Counter() {}

    int GetltsVal() const { return itsVal; }

    void SetltsVal(int x) { itsVal = x; } 

  private:

    int itsVal;

} ;

Counter::Counter () :

itsVal(0)

{}

Counter::Counter(int val):

itsVal(val)

{}

int main()

{

    int thelnt = 5;

    Counter theCtr = thelnt;

    cout << "theCtr: " << theCtr.GetltsVal() << endl; 

    return 0; 

}

Результат:

theCtr: 5

Анализ:

Важнейшие изменения произошли в строке 11, где конструктор перегружен таким образом, чтобы получать значения типа int, а также в строках 24—26, где этот конструктор реализован. В результате конструктор сможет создать из переменной типа int объект класса Counter.

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

Этап 1. Создается объект типа Counter с именем theCtr.

Это то же, что и запись: int х = 5, где создается целочисленная переменная х, а затем инициализируется значением 5. В данном случае также создается объект theCtr класса Counter, который инициализируется переменной thelnt типа int. Этап 2. Объекту theCtr присваивается значение переменной thelnt. Но переменная thelnt имеет тип int, а не Counter! Сначала необходимо преобразовать ее в переменную типа Counter. Компилятор может произвести некоторые преобразования автоматически, но ему нужно точно указать, что от него требуется. Именно для компилятора создается конструктор класса counter, которому передается параметр типа int:

class Counter {

Counter (int x);

// ...

} ; Этот конструктор создает объект класса Counter, используя временный безымянный объект данного класса, способный принимать значения типа int. Чтобы сделать этот процесс более наглядным, предположим, что для значений типа int создается не безымянный объект, а объект класса Counter с именем was Int.

Этап 3. Присвоить значение объекта was Int объекту theCtr, что эквивалентно записи theCtr = waslnt;.

На этом этапе временный объект waslnt, созданный при запуске конструктора, замещается постоянным объектом theCtr класса Counter. Иными словами, значение временного объекта присваивается объекту theCtr.

Чтобы понять, как это происходит, следует четко уяснить принципы работы перегруженных операторов, определенных с помощью ключевого слова operator. В случае парных операторов (таких, как = или +) операнд, находящийся справа, объявляется как параметр функции оператора, заданной в конструкторе. Так, выражение а = b; становится выражением а. operator = (b);.

Но что произойдет, если изменить порядок присвоения, как в следующем примере?

1: Counter theCtr (5); 

2: int thelnt = theCtr;

3: cout << "thelnt : " << thelnt << endl;

И вновь компилятор сообщает об ошибке. Хотя сейчас ему известно, как создать временный объект Counter для значения типа int, но ему не известно, как осуществить обратный процесс.

 

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

Для решения этой и подобных проблем язык С++ обладает операторами преобразования типов, которые можно добавить в пользовательский класс. Это позволит объектам создаваемого класса осуществлять неявные преобразования встроенных типов. Листинг 10.18 демонстрирует этот подход. Кстати, операторы преобразования не устанавливают тип возвращаемого значения, несмотря на то что фактически возвращают преобразованное значение.

Листинг 20.13 Преобразование типа counter в unsigned int

1: // Листинг 20.13. Операторы преобразования

2: #include <iostream> 

3 :

4: class Counter

5: {

6:  public:

7:     Counter();

8:     Counter(int val);

9:     ~Counter() {}

10:     int GetltsVal() const { return itsVal; }

11:     void SetltsVal(int x) { itsVal = x; }

12:     operator unsigned int();

13:  private:

14:     int itsVal;

15: } ;

16:

17: Counter::Counter():

18: itsVal(0)

19: {}

20:

21: Counter::Counter(int val):

22: itsVal(val)

23: {}

24:

25: Counter::operator unsigned int ()

26: {

27:     return ( int (itsVal) );

28: }

29:

30: int main()

31: {

32:     Counter ctr(5);

33:     int thelnt = ctr; 

34:     std::cout << "thelnt: " << theInt << std::endl; 

35:     return 0;

36: }

Результат:

thelnt: 5

Анализ:

В строке 12 объявлен оператор преобразования. Обратите внимание, что он начинается ключевым словом operator и не имеет возвращаемого значения. Реализация этой функции находится в строках 25—28. Код строки 27 возвращает значение itsVal, преобразованное в значение типа int.

Теперь компилятору известно, как присвоить объекту класса значение типа int, как возвратить из объекта класса текущее значение и как присвоить его внешней переменной типа int.