§29 Перегрузка операций. Друзья класса. Указатель this. Деструктор

Перегрузка операций

В 10 классе мы с вами познакомились с понятием перегрузка функции. Суть его в том, что функция, в зависимости от сигнатуры (количество и типы аргументов), может вести себя по разному. Такое явление мы назвали функциональным полиморфизмом. В C++ полиморфизм присущ и операциям. Например, в зависимости от количества и типов аргументов, операции "*", "-", "&", ">>" будут выполнять совершенные разные действия. С++ позволяет использовать перегрузку операций и для абстрактных типов. Это не должно являться для вас большой неожиданностью, так как мы не могли пройти мимо понятия функтора, а для его создания использовалась перегрузка операции – вызова функции (). Для реализации перегрузки операции в классе разработчика используется специальный метод, заголовок которого имеет следующий синтаксис:

тип operator символ_операции (параметры);

где символ операции – это обозначение любой перегружаемой операции.
Для чего это нужно? Все очень просто. Используя хорошо знакомые нам операции мы можем придать нашему коду более естественный и понятный вид. В тоже время скрыть (инкапсулировать) реализацию сложного алгоритма внутри класса. Как вы знаете в C++ такая операция невозможна:

string S1, S2;
S2 = S1 * 10;

Получаем ошибку: no match for «operator*».... Однако, мы можем описать класс в котором такая операция будет выполняться. Для этого применяется перегруженная операция "*". Обратите внимание, что порядок операндов здесь будет важен. Так будет правильно: myStr * 5, а так – будет ошибка: 5 * myStr (о передаче первого операнда неявно см. ниже).
Программа 29.1

#include <iostream>
#include <string>
using namespace std;

class Factor {
private:
	string s;
public:
	Factor (string &S) {
		s = S;
	}
	string operator* (const int &n) {
		string temp;
		for (int i = 0; i < n; i++)
			temp += s;
		return temp;
	}
};

int main() {
	string test;
	cout << "S => ";
	getline(cin, test);
	Factor myStr(test);
	cout << myStr * 5 << endl;
	return 0;
}
S => ЁЁЁ
ЁЁЁЁЁЁЁЁЁЁЁЁЁЁЁ

На перегрузку операций накладываются некоторые ограничения. Так нельзя перегружать следующие операции: "sizeof", ".", ".*", "::", ":?" (тернарная), операции приведения типа. Не допускается изменение приоритетов операций. Можно использовать лишь те символы операций, которые известны в C++. Нельзя нарушать правила синтаксиса исходной операции.
Поскольку операции подразделяются на две категории унарные и бинарные рассмотрим примеры использования тех и других.

Бинарные операции

Бинарная операция требует наличия двух операндов. Синтаксис метода перегрузки бинарной операции имеет вид:

тип operator символ_операции (параметр1, параметр2);

В программе 29.1 нетрудно заметить, что метод перегруженной операции имеет только один параметр. Все верно! Дело в том, что первый операнд передается неявно как вызывающий объект. Часто перегруженные операции не являются членами класса, а являются дружественными функциями (друзья класса). В этом случае, количество операндов должно точно соответствовать типу операции (для бинарной их должно быть два). Для того, чтобы функцию определить как друг класса необходимо использовать спецификатор доступа friend.
Создадим класс в котором перегружаются операции <<, >> (вставки и извлечения). Справедливости ради заметим, что эти потоковые функции также являются перегруженными операциями побитового сдвига. Итак, нам нужны эти операции для тех же целей, что и для обычных переменных - для ввода и вывода. Адаптируем эти операции для ввода и вывода массива vector.
Программа 29.2

#include <iostream>
#include <vector>
using namespace std;

class Boo {
private:
	vector<int> V;
public:
	Boo (vector<int> &vec) : V(vec) {}
	friend ostream& operator<< (ostream&, Boo&);
	friend istream& operator>> (istream&, Boo&);
	size_t len() {
		return V.size();
	}
	int &arr(size_t n) {
		return V.at(n);
	}
	void ini(int &n) {
		if (!V.empty()) {
			V.clear();
			V.reserve(n);
		} else {
			V.resize(n);
		}
	}
};

int main() {
	int x;
	vector<int> myV;
	cout << "Введите размер массива => ";
	cin >> x;
	Boo myMass(myV);
	myMass.ini(x);
	cin >> myMass;
	cout << myMass;
	return 0;
}
ostream& operator<< (ostream &out, Boo &m) {
	for (size_t i = 0; i < m.len(); i++)
		out << m.arr(i);
    return out;
}
istream& operator>> (istream &in, Boo &m) {
	for (size_t i = 0; i < m.len(); i++)
		in >> m.arr(i);
	return in;
}

Примечание. Методы перегрузки операции по существу не являются членами класса (поэтому операции "." и "::" не используются), но им предоставляются те же права доступа, что и функциям-членам.
Почему перегрузки функций ввода/вывода необходимо объявлять как друзья класса? Причина заключается в том, что функции библиотеки iostream (как и любой другой библиотеки STD) не могут быть членами классов создаваемых разработчиком. В тоже время нельзя добавлять свои методы в библиотечные классы. Но мы можем создавать объекты библиотечных классов в методах своих классов, сделав их дружественными.
В программе 29.2 первым параметром выступает объект класса iostream. Второй аргумент - это объект нашего класса Boo. В циклах формируются соответствующие потоки, которые по окончанию работы циклов, передаются как ссылки. В основной программе эти ссылки становятся аргументами функций стандартных потоковых объектов.

Указатель this

В нестатических функциях-членах класса (а также в struct или union) доступен указатель this. Он указывает на объект, для которого вызывается функция-член. Поскольку this - указатель, то при обращении к полям класса применяется операция "стрелка" - "->".
Примечание. Коли мы затронули понятие статический член, дадим краткое пояснение. Члены класса (поля и методы) объявленные с квалификатором static будут иметь только одну копию данных для всех объектов класса. Статические члены класса удобно использовать, когда они являются закрытыми членами. Но, как уже сказано, указатель this в этих членах недоступен.
Программа 29.3

#include <iostream>
using namespace std;

class MyClass {
private:
	int t;
public:
	MyClass (int t) : t(t) {}
	int foo;
	int demo () {
		foo = 2 * t++; 
		// Аналогично
		this -> foo = 2 * t++;
		// Аналогично
		(*this).foo = 2 * t++;
		return foo;
	}

};

int main() {
	MyClass Boo(5);
	cout << Boo.demo() << endl; // 14
	return 0;
}

В программе 29.3 приводятся три равносильных варианта одной и той же инструкции. Становится очевидным, что указатель this используется здесь неявно. Также неявно передается this когда один метод класса вызывает другой. (Неявно передавался указатель this и в программе 29.1, мы обратили на это внимание). В каких случаях этот указатель можно применить с пользой? Пример такой программы мы составим ниже.

Унарные операции

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

// глобальная функция или статический член
тип operator символ_операции (тип);
// метод класса 
тип operator символ_операции();

Реализуем перегрузки двух операций постфиксного инкремента operator++ и разыменования operator*. Реализация перегрузки постфиксного и префиксного инкремента различается по существу самой операции. В первом случае возвращается сохраненное состояние объекта (*this). Во втором сам объект *this. (Нужно сказать, что и в первом, и во втором случаях можно обойтись и без указателя this; в комментариях мы это показали). Что конкретно будет инкрементироваться - зависит от поставленной задачи. В нашей новой программе мы создадим свой "умный указатель" и будем инкрементировать этот указатель, перемещаясь по массиву. Поскольку для вывода элементов (а не адресов) нам нужна операция разыменования, то мы перегрузим и её.
Программа 29.4

class Boo {
private:
	int* t;
	int n;
public:
	Boo (int *T, int N) : t(T), n(N) {}
	Boo operator++(int) {
		// Сохраняем состояние объекта
		Boo oldValue = *this;
		// Что равносильно
		// Boo oldValue(t, n);
		// Инкрементируем
		this -> t++;
		// Что равносильно t++
		// Но возвращаем сохранение
		return oldValue;
	}
	int& operator*() {
		return *t;
	}
	friend ostream& operator<<(ostream&, const Boo&);
};

int main() {
	int ar[5] {2, 1, 8, 3, 4};
	Boo s(ar, 5);
	for (int i = 0; i < 5; i++)
		cout << s++ << " ";
	return 0;
}

ostream& operator<<(ostream &out, const Boo &m) {
		out << *m.t;
	    return out;
}

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

Boo& operator++() {
    this -> ++t;
    return *this;
}

Деструкторы

Деструктор - это специальный метод класса предназначенный для удаления объекта. Как конструктор автоматически вызывается при создании объекта, так и деструктор автоматически вызывается для его уничтожения. Как конструктор, так и деструктор имеет форму по умолчанию. Имя деструктора тоже совпадает с именем класса, но предваряется символом "~".

class MyClass {
public:
    ~MyClass() {
    // Инструкции деструктора
    }
};

Деструктор применяется явно тогда, когда необходимо освободить ресурсы с помощью инструкции delete, выделенные с помощью инструкции new. Для классов контейнеров существуют встроенные деструкторы, так что их вызывать намеренно не обязательно (но не запрещено). Если есть необходимость удалить какой-либо объект контейнера до момента его автоматического удаления (когда он выйдет из области видимости), то деструктор - это самое подходящее место для этой операции. Если вы используете динамические небезопасные встроенные массивы, то их удалять нужно вручную, в обязательном порядке. Из этого следует, что лучшим решением будет использование встроенных типов, таких как string или vector. Приведем небольшой код, иллюстрирующий работу с деструктором, в заключение.
Программа 29.5

#include <iostream>
#include <cstring>
using namespace std;

class Boo {
private:
	char* T;
public:
	Boo (char* S) {
		T = new char [20];
		strcpy(T, S);
	}
	~Boo () {
		delete T;
	}
	void rotStr() {
		int l = strlen(T);
		for (int i = 0; i < l / 2; i++) {
			char temp = T[i];
			T[i] = T[l - i - 1];
			T[l - i - 1] = temp;
		}
	}
	void print() {
		cout << T << endl;
	}
};

int main() {
	char s[20];
	cin.getline(s, 20);
	Boo Q(s);
	Q.print();
	Q.rotStr();
	Q.print();
	return 0;
}
Print Friendly, PDF & Email

Comments are closed.