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

Материал из SEWiki
Перейти к: навигация, поиск

Общая информация

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

  1. Вместо get(i) удобнее использовать [i].
  2. a.plus(b) или plus(a, b) удобнее заменить на a + b.
  3. Вместо p.get()->print() удобнее использовать p->print().
  4. Обобщённое программирование.

В С++ существуют следующие операторы:

  • +a -a
  • + - / * %
  • += -= /= *= %=
  • && || !
  • = == !=
  • & | ~
  • a-> *a &a
  • >> <<
  • ,
  • . ::
  • ()?():()
  • [] ()
  • a++ ++a a-- --a
  • > < >= <=
  • new, delete
  • приведение к типу

Два последних типа операторов будут разобраны в отдельной лекции.

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

Для перегрузки операторов характерны следующие особенности:

  • Для того, чтобы можно было перегрузить оператор, необходимо, чтобы хотя бы один из аргументов был пользовательского типа.
  • Большую часть операторов можно перегружать как внутри класса, так и снаружи, причём их не нужно определять одновременно и там, и там. Однако некоторые операторы могут быть определены только внутри класса, а именно =, a->, [], ().
  • Для операторов && и || применяется логика "ленивых" вычислений. При перегрузке этих операторов она теряется.
  • Оператор -> должен обязательно возвращать указатель.
  • Операторов () может быть сколько угодно, с различными сигнатурами.
  • Оператор , в качестве результата возвращает тип и значение самого правого выражения. Перегружается он очень редко. Пример использования:
a = f(), g();
//сначала вычисляется f(), потом g(), и a=f()

Пример перегрузки оператора бинарного сложения

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

BigNum operator+ (BigNum const &a, BigNum const &b)
{
	...
}

А так будет выглядеть определение внутри:

BigNum BigNum::operator+ (BigNum const &b) const
{
	...
}
//первым аргументом в данном случае является *this
//метод не меняет значение, поэтому const

Пример перегрузки оператора +=

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

BigNum & operator+= (BigNum &a, BigNum const &b)
{
	...
	return a;
}

А определение внутри класса такое:

BigNum & BigNum::operator+= (BigNum const &b) const
{
	...
	return *this;
}

В частности, можно передавать операнды различных типов (например, для перегрузки BigNum+int).

Другие примеры

Определение унарного минуса снаружи класса:

BigNum operator- (BigNum const &a)
{
	...
}

Он же - внутри класса:

BigNum BigNum::operator- () const
{
	...
}

Далее показана разница между определением методов ++a и a++:

BigNum & operator++ (BigNum &a);
BigNum & operator++ (BigNum &a, size_t);
//во втором случае используется фиктивный параметр, 
//который нужен только для разделения префиксного и постфиксного инкрементов
BigNum BigNum::operator[] (size_t i); 
BigNum BigNum::operator() (...); 
...
BigNum b;
b(17, 34);

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

Array

struct Array
{
	int operator[] (size_t i) const
	{
		assert (i<size_);
		return data_[i];
	}
	int & operator[] (size_t i)
	{
		return data_[i];
	}
	void swap (Array &a)
	{
		std::swap(a.data_, data_);
		std::swap(a.size_, size_);
	}
	//swap меняет значения местами 
	Array & operator= (Array const &a)
	{
		if (this != &a)
			Array(a).swap(*this);
		return *this;			
	}
};

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

Для использования ostream и istream достаточно подключить

#include <iosfwd>;

т.к. iostream весьма и весьма велик.

Array a;
std::cout << a;
std::ostream &operator<< (std::ostream & os, Array const &a); 
//этот и следующий операторы могут быть переопределены только снаружи класса 
std::istream &operator>> (std::ostream & is, Array &a); 
//при чтении объект изменяется (std::cin >> a)

Замечание: swap(Array(a)) не сработает, потому что swap принимает константную ссылку, а временные объекты нельзя передавать по константной ссылке.

BigNum

Оператор += лучше определять внутри класса, а оператор + — через += снаружи, как показано в примере:

struct BigNum
{
	BigNum & operator+= (BigNum &b)
	{
		...
	}
	...
}

BigNum operator+ (BigNum a, BigNum const &b)
{
	return a+=b;
}
//+, определённый снаружи, может быть вызван от аргументов,
//которые может быть приведены к типу
BigNum a(10); //должен быть BigNum(int)
a+20;         //работает и внутри, и снаружи
20+a;         //работает только снаружи
bool operator== (BigNum const &a, BigNum const &b)
{
	...
}
bool operator!= (BigNum const &a, BigNum const &b)
{
	return !(a==b);
}

Вообще говоря, достаточно определить < и == и легко получить все остальные сравнения. Например, (a<=b) выражается как (!(b<a))

Умные указатели

{
	smart_pstr p = new ...
}

В данном примере рассматривается перегрузка операторов p-> и *p.

struct smart_pstr
{
	string *p_;
	...
	string * operator-> () const
	{
		return p_;
	}
	string & operator* () const
	{
		return *p_;
	}
};
//предположим, что мы переопределим оператор ниже:
operator== (smart_pstr, smart_pstr)
...
smart_pstr p;
string *q;
p == q; //произойдёт неявное преобразование типа,
//создастся smart_pstr(q), а потом он будет удалён
//для того, чтобы этого избежать, нужно конструктор сделать explicit
//(т.е. запретить неявное преобразование типов)