ТОП 10:

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



 

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

Количество операндов, которыми может манипулировать оператор, — важная характеристика каждого оператора. Различают операторы, используемые с одним операндом (например, оператор инкремента: myValue++), и операторы, для работы которых необходимо указать два операнда (например, оператор суммирования: a+b). Сразу тремя операндами управляет только условный оператор ?, синтаксис использования которого показан в следующем примере: (а > b ? x : у).

 

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

 

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

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

 

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

 

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

 

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

 

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

CAT catOne(5,7);

CAT catTwo(3,4);

//...другие строки программы

catTwo = catOne

В данном примере создан объект catOne, переменной которого itsAge присвоено значение 5, а переменной itsWeigth — 7. Затем создается объект catTwo со значениями переменных соответственно 3 и 4.

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

Работа с переменными-членами, которые хранят свои значения в области динамической памяти, рассматривалась ранее при обсуждении использования конструктора- копировщика (см. также рис. 10.1 и 10.2).

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

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

catTwo = catTwo

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

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

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

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

1: // Листинг 10.15.

2: // Конструктор-копировщик

3:

4: #include <iostream.h>

5:

6: class CAT

7: {

8: public:

9: CAT(); // конструктор по умолчанию

10: // конструктор-копировщик и деструктор пропущены!

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

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

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

14: CAT & operator=(const CAT &);

15:

16: private:

17: int *itsAge;

18: int *itsWeight;

19: };

20:

21: CAT::CAT()

22: {

23: itsAge = new int;

24: itsWeight = new int;

25: *itsAge = 5;

26: *itsWeight = 9;

27: }

28:

29:

30: CAT & CAT::operator=(const CAT & rhs)

31: {

32: if (this == &rhs)

33: return *this;

34: *itsAge = rhs.GetAge();

35: *itsWeight = rhs.GetWeight();

36: return *this;

37: }

38:

39:

40: int main()

41: {

42: CAT frisky;

43: cout << "frisky's age: " << frisky.GetAge() << endl;

44: cout << "Setting frisky to 6...\n";

45: frisky.SetAge(6);

46: CAT whiskers;

47: cout << "whiskers' age: " << whiskers.GetAge() << endl;

48: cout << "copying frisky to whiskers...\n";

49: whiskers = frisky;

50: cout << "whiskers' age: " << whiskers.GetAge() << endl;

51: return 0;

52: }

 

Результат:

frisky's age: 5

Setting frisky to 6. . .

whiskers' age: 5

copying frisky to whiskers...

whiskers' age: 6

 

Анализ: В листинге 10.15 вновь используется класс CAT. Чтобы не повторяться, в данном коде пропущены объявления конструктора-копировщика и деструктора. В строке 14 объявляется оператор присваивания, определение которого представлено в строках 30—37.

В строке 32 выполняется проверка того, не является ли объект, которому будет присвоено значение, тем же самым объектом класса CAT, чье значение будет присвоено. Чтобы проверить это, сравниваются адреса в указателях rhs и this.

Безусловно, оператор присваивания (=) может быть произвольно перегружен таким образом, чтобы отвечать представлениям программиста, что означает равенство объектов.

 

 

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

 

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

 

Предупреждение: Листинг 10.16 не компилируйте!

 

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

1: // Листинг 10.16.

2: // Эту программу не компилируйте!

3:

4: int

5: #include <iostream.h>

6:

7: class Counter

8: {

9: public:

10: Counter();

11: ~Counter(){ }

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

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

14: private:

15: int itsVal;

16:

17: };

18:

19: Counter::Counter():

20: itsVal(0)

21: { }

22:

23: int main()

24: {

25: int theShort = 5;

26; Counter theCtr = theShort;

27: cout << "theCtr: " << theCtr.GetItsVal() << endl;

28: return 0;

29: }

 

Результат:

Компилятор покажет сообщение об ошибке, поскольку не сможет преобразовать тип int в Counter.

 

Анализ: Класс Counter, определенный в строках 7—17, содержит только один конструктор, заданный по умолчанию. В нем не определено ни одного метода преобразования данных типа int в тип Counter, поэтому компилятор обнаруживает ошибку в строке 26. Компилятор ничего не сможет поделать, пока не получит четких инструкций, что данные типа int необходимо взять и присвоить переменной-члену itsVal.

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

 

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

1: // Листинг 10.17.

2: // Использование конструктора в качестве оператора преобразования типа

3:

4: int

5: #include <iostream.h>

6:

7: class Counter

8: {

9: public:

10: Counter();

11: Counter(int val);

12: ~Counter(){ }

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

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

15: private:

16: int itsVal;

17:

18: };

19:

20: Counter::Counter():

21: itsVal(0)

22: { }

23:

24: Counter::Counter(intval):

25: itsVal(val)

26: { }

27:

28:

29: int main()

30: {

31: int theShort = 5;

32: Counter theCtr = theShort;

33: cout << "theCtr: " << theCtr.GetItsVal() << endl;

34: return 0;

35: }

 

Результат:

the Ctr: 5

 

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

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

Шаг 1: создание переменной класса Counter с именем theCtr.

Это то же самое, что записать: int x = 5, где создается целочисленная переменная x и ей присваивается значение 5. Но в нашем случае создается объект theCtr класса Counter, который инициализируется переменной theShortTHna short int.

Шаг 2: присвоение объекту theCtr значения переменной theShort.

Но переменная относится к типу short, а не Counter! Первое, что нужно сделать, — это преобразовать ее к типу Counter. Компилятор может делать некоторые преобразования автоматически, но ему нужно точно указать, чего от него хотят. Именно для инструктирования компилятора создается конструктор класса Counter, который содержит единственный параметр, например типа short:

class Counter

{

Counter (short int x);

// ...

};

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

Шаг 3: присвоение значения объекта wasShort объекту theCtr, что эквивалентно записи

"theCtr = wasShort";

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

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

а = b

объявляется как

a.operator=(b);

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

1: Counter theCtr(5);

2: int theShort = theCtr;

3: cout << "theShort : " << theShort << endl;

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

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

 

Чтобы разрешить эту и подобные ей проблемы, в C++ есть специальные операторы преобразования типов, которые можно добавить в пользовательский класс. В результате появится возможность явного преобразования типа пользовательского класса к любому из базовых типов данных языка программирования. Реализация этой возможности показана в листинге 10.18. Только одно замечание: в операторах преобразований не задается тип возврата. Даже если их работа напоминает возврат функции, в действительности они возвращают преобразованное значение.

Листинг 10.18. Преобразования данных типа Counter в тип unsigned short()

1: #include <iostream.h>

2:

3: class Counter

4: {

5: public:

6: Counter();

7: Counter(int val);

8: ~Counter(){ }

9: int GetItsVal()const { return itsVal; }

10: void SetItsVal(int x) { itsVal = x; }

11: operator unsigned short();

12: private:

13: int itsVal;

14:

15: };

16:

17: Counter::Counter():

18: itsVal(0)

19: { }

20:

21: Counter::Counter(int val):

22: itsVal(val)

23: { }

24:

25: Counter::operator unsigned short ()

26: {

27: return ( int (itsVal) );

28: }

29:

30: int main()

31: {

32: Counter ctr(5);

33: int theShort = ctr;

34: cout << "theShort: " << theShort << endl;

35: return 0;

36: }

 

Результат:

theShort: 5

 

Анализ: В строке 11 объявляется оператор преобразования типа. Обратите внимание, что в нем не указан тип возврата. Функция оператора преобразования выполняется в строках 25—28. В строке 27 возвращается значение объекта itsVal, преобразованное в тип int.

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

 

 

Резюме

 

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

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

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

Хотя в языке C++ можно произвольно перегружать все операторы, настоятельно рекомендуем не создавать таких операторов, функции которых противоречат их традиционному использованию. Кроме того, невозможно изменить ассоциативность оператора, а также создавать собственные операторы, не представленные в языке C++.

Указатель this ссылается на текущий объект и является невидимым параметром для всех функций-членов. Разыменованный указатель this часто возвращается перегруженными операторами.

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

 

Вопросы и ответы

 

Зачем использовать значения, заданные по умолчанию, если можно перегрузить функцию?

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

Почему бы тогда постоянно не использовать только значения, заданные по умолчанию?

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

Какие переменные-члены следует инициализировать одновременно с инициализацией конструктора, а какие оставлять для тела конструктора?

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

Может ли перегруженная функция содержать параметры, заданные по умолчанию?

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

Почему одни функции-члены определяются в описании класса, а другие нет?

Если функция определяется в описании класса, то далее она используется в режиме inline. Впрочем, встраивание кода функции по месту вызова происходит только в

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

 

Коллоквиум

 

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

Контрольные вопросы

 

1. Если вы перегрузили функцию-член, как потом можно будет различить разные варианты функции?

2. Какая разница между определением и объявлением?

3. Когда вызывается конструктор-копировщик?

4. Когда вызывается деструктор?

5. Чем отличается конструктор-копировщик от оператора присваивания (=)?

6. Что представляет собой указатель this?

7. Как отличается перегрузка операторов предварительного и последующего действия?

8. Можно ли перегрузить operator+ для переменных типа short int?

9. Допускается ли в C++ перегрузка operator++ таким образом, чтобы он выполнял в классе операцию декремента?

10. Как устанавливается тип возврата в объявлениях функций операторов преобразования типов?

 

Упражнения

 

1. Представьте объявление класса SimpleCircle с единственной переменой-членом itsRadius. В классе должны использоваться конструктор и деструктор, заданные по умолчанию, а также метод установки радиуса.

2. Используя класс, созданный в упражнении 1, с помощью конструктора, заданного по умолчанию, инициализируйте переменную itsRadius значением 5.

3. Добавьте в класс новый конструктор, который присваивает значение своего параметра переменной itsRadius.

4. Перегрузите операторы преинкремента и постинкремента для использования в вашем классе SimpleCircle с переменной itsRadius.

5. Измените SimpleCircle таким образом, чтобы сохранять itsRadius в динамической области памяти и фиксировать существующие методы.

6. Создайте в классе SimpleCircle конструктор-копировщик.

7. Перегрузите в классе SimpleCircle оператор присваивания.

8. Напишите программу, которая создает два объекта класса SimpleCircle. Для создания одного объекта используйте конструктор, заданный по умолчанию, а второму экземпляру при объявлении присвойте значение 9. С каждым из объектов используйте оператор инкремента и выведите полученные значения на печать. Наконец, присвойте значение одного объекта другому объекту и выведите результат на печать.

9. Жучки: что неправильно в следующем примере использования оператора присваивания?

SQUARE SQUARE::operator=(const SQARE & rhs)

{

itsSide = new int;

*itsSide = rgs.GetSide();

return << this;

}

10. Жучки: что неправильно в следующем примере использования оператора суммирования?

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

{

itsval += rhs.GetItsVal();

return *this;

}

 

День 11-й. Наследование

 

Фундаментальной основой человеческого мышления является поиск, выявление и построение взаимоотношений между различными концепциями. Чтобы постичь хитросплетения отношений между вещами и явлениями, мы используем иерархические построения, матрицы, сети и прочие средства визуализации. Чтобы лучше выразить суть отношений между объектами, в C++ используется иерархическая система наследования. Сегодня вы узнаете:

• Что представляет собой наследование

• Как произвести один класс из другого

• Что такое защищенный доступ и как его использовать

• Что такое виртуальные функции

Что такое наследование

 

Что такое собака? Что вы видите, когда смотрите на своего питомца? Я вижу четыре лапы, обслуживающие зубастую пасть. Биолог увидит систему взаимодействующих органов, физик — стройную систему атомов и совокупность разных видов энергии, а ученый, занимающийся систематикой млекопитающих, — типичного представителя вида Canis familiaris.

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

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

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

Иерархия и наследование

 

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

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

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

 

Рис. 11.1. Иерархия млекопитающих

В C++ иерархичность реализована в концепции классов, где один класс может происходить, или наследоваться от класса более высокого уровня. В наследовании классов реализуются принципы их иерархической подчиненности. Предположим, мы производим новый класс Dog (Собака) от класса Mammal (Млекопитающее). Другими словами, класс Mammal является базовым для класса Dog. Точно так же, как описание вида собака несет в себе признаки, детализирующие описание млекопитающих в целом, так и класс Dog содержит ряд методов и данных, дополняющих методы и данные, которые представлены в классе Mammal.

Как правило, с базовым классом связано несколько производных классов. Поскольку собаки, кошки и лошади являются представителями млекопитающих, то с точки зрения C++ можно сказать, что все эти классы произведены от класса Mammal.

 

Царство животных

 

Чтобы более наглядно раскрыть смысл наследования классов, рассмотрим эту тему на примере отношений между многочисленными представителями животного мира. Представим себе, что программисту, поступил заказ на создание детской игры " Ферма" .

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

 







Последнее изменение этой страницы: 2016-12-10; Нарушение авторского права страницы

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.235.172.213 (0.035 с.)