§28 Классы. Атрибуты. Экземпляр. Конструктор

Класс как абстрактный тип данных

При моделировании какого-либо процесса, явления, сущности нашего мира мы создаем некоторую модель объекта. Эта модель является абстракцией реального объекта. При моделировании мы отвлекаемся от несущественных сторон, свойств, связей этого объекта, с целью выделения его наиболее существенных, закономерных признаков. Под абстракцией в объектно-ориентированного программирования (ООП) понимают возможность работы с объектом, не вдаваясь в особенности его реализации (разделение интерфейса и реализации). Уже известным примером работы с объектами в программе является работа с обычными переменными.
В 10 классе мы познакомились с фундаментальными типами, такими как char, short, int, double. Каждый из этих типов определяет:

  • интервал допустимых значений данных этого типа;
  • операции применяемые с данными этого типа;
  • способ организации памяти для хранения этих данных.

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

int a;

Данная инструкция говорит о том, что мы получили объект a (экземпляр) типа (класса) int. Поэтому программисты часто называют переменные – объектами.
Для создания моделей объектов в C++ используется классы. Классы представляют собой абстрактные типы данных (abstract data type), которые создаются разработчиком программы.

Описание класса

Для создания класса используется следующая синтаксическая конструкция:

class MyClass {
[private:]
// элементы в этой секции доступны только из класса; 
// это область доступа по умолчанию
public:
// элементы в этой секции доступны из любой части программы
};

где MyClass – это имя класса (абстрактного типа).
Примечание. В имени класса, по общему соглашению, первая буква должна быть заглавная.
Спецификаторы доступа private (необязательно) и public управляют доступом к элементам класса. Для класса (в отличие от структуры) все его элементы недоступны, т. е. невидимы из-вне (по умолчанию), поэтому спецификатор private, можно не использовать.
Обратите внимание на то, что, в отличие от блока функции, блок описания класса заканчивается точкой с запятой. Большинство IDE вставляют ";" в конце блока автоматически.

Члены класса

К членам класса относятся:

  • данные-члены (переменные-члены)
  • функции-члены
Функции-члены класса называются методами класса. А переменные-члены часто называются полями (или атрибутами) класса.
Не допускается инициализировать поля класса при описании. Инициализация производится в конструкторе класса (см. ниже), либо после создания объекта класса.
Секция private содержит члены класса, которые могут быть доступны только через функции-члены описанные в секции public.
Секция public содержит члены класса, которые образуют открытый интерфейс класса.
Начнем рассмотрение данного вопроса с простой программы. В программе 28.1 мы определили класс с именем A. В этом классе одно приватное поле Big и один метод sum
Программа 28.1

#include <iostream>
using namespace std;

class A {
private:
	int Big;
public:
	void sum(int x, int y) {
		Big = x + y;
	}
};

int main() {
	A obj;
	obj.sum(2, 3);
	return 0;
}

Экземпляры класса

Объекты класса определяются точно также, как определяются переменные фундаментальных типов. Имя класса выступает как абстрактный тип. После указания типа следует идентификатор переменной абстрактного типа. Объект иначе называется экземпляром класса. В программе 28.1 мы имеем один экземпляр класса A: obj. Существуют два способа получения экземпляров класса. Первый способ аналогичен объявлению обычной переменной. При этом автоматически выделяется (а после завершения работы – удаляется) память, достаточная для хранения всех полей класса. Второй способ основан на использовании операции new которая, как нам известно (см. здесь), запрашивает выделение динамической памяти.
Примечание. Динамическая память должна быть освобождена (во избежании утечек памяти), когда работа с объектом будет завершена.
Область видимости не позволяет вызывать методы класса непосредственно, по имени, как обычную функцию. Для того, чтобы появилась возможность изменять объекты, посредством методов, используется операция “точка” или операция доступа к члену класса. Вызывая метод мы должны указать имя объекта, с которым связан данный метод, и имя самого вызываемого метода, разделенные точкой (obj.sum). Поскольку метод – это функция, то за именем метода следуют (). Общий синтаксис вызова метода для определенного объекта таков:

объект.метод(аргументы)

В этой программе ничего полезного не происходит, поскольку после изменения состояния атрибута Big программа завершает свою работу. “Оживим” наш объект. Для этого введем методы инициализации. В классе, по общепринятой практике, присутствуют два метода с префиксами set и get. set-метод используется для инициализации скрытых полей, а get-метод позволяющий получить значения этих полей.
Программа 28.2

#include <iostream>
using namespace std;

class A {
private:
	int Big;
	int x;
	int y;
public:
	void set_X(int dig1) {
		x = dig1;
	}
	void set_Y(int dig2) {
		y = dig2;
	}
	void num(int &dig1, int &dig2) {
		Big = dig1 + dig2;
	}
	int Sum() {
		num(x, y);
		return Big;
	}
};

int main() {
	A obj;
	obj.set_X(2);
	obj.set_Y(3);
	cout << obj.Sum() << endl;

	return 0;
}

Эта программа для иллюстрации, большого смысла в ней конечно же нет. Зачем нужен set-метод? Ответ придет позже, сам собой, при разработке большой программы, когда потребуется проводить различные манипуляции с полями класса, например, проверять допустимость значения атрибута. С другой стороны, обращение к этим методам в главной функции выглядит очень некрасиво. Для того, чтобы скрыть обращение к set-методам воспользуемся важным компонентом архитектуры класса – конструктором.

Конструктор

Конструктор – это специальный метод имя которого совпадает с именем класса. В отличие от других методов класса конструктор не имеет спецификатора типа (в том числе и void) поскольку он ничего не возвращает. Задача конструктора – инициализация атрибутов (полей) класса. Конструктор всегда вызывается при создании объекта класса. Даже если пользователь не создал конструктор явно, он создается неявно (конструктор по умолчанию). Конструктор, как и любой метод класса может быть перегруженным, т.е. можно создавать несколько конструкторов в одном классе.
Программа 28.3

#include <iostream>
using namespace std;

class A {
private:
	int Big;
	int x;
	int y;
public:
	A () { // Конструктор по умолчанию
		x = 0, y = 0, Big = 0;
	}
	A (int dig1, int dig2) {
		x = dig1,
		y = dig2,
		Big = 0;
	}

	void num(int &dig1, int &dig2) {
		Big = dig1 + dig2;
	}
	int Sum() {
		num(x, y);
		return Big;
	}
};

int main() {
	int X, Y;
	cout << "X = "; cin >> X;
	cout << "Y = "; cin >> Y;
	A obj (X, Y);
	cout << obj.Sum() << endl;
	return 0;
}

Очень часто используют другу форму конструктора в виде списка инициализации:

A (int dig1, int dig2) :
	x {dig1}, y {dig2}, Big {0}  {}

Примечание. Константа, ссылка или класс, для которых нет конструктора по умолчанию должны быть инициализированы в списке инициализации. Напоминаем, что если в списке инициализации используются {} вместо (), то компилятор не допустит сужение типа.
Методы класса (в том числе и конструктор) могут быть определены как в самом классе, так и за его пределами. По общепринятой практике, определение методов класса выносится за пределы класса. Если метод будет определяться вне класса, то в самом классе должен находиться прототип функции-члена.

Область видимости

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

Тип имя_класса::имя_метода(параметры){...}

Синтаксические правила, существующие для описания функции, равно относятся и к описанию метода вне класса: количество параметров, их тип и порядок должны совпадать с таковыми в прототипе метода, находящегося в блоке класса.
Теперь настало время написать что-нибудь более полезное. Давайте научим наш объект складывать сверх-большие числа. Мы уже обсуждали этот вопрос, когда говорили, что размер целочисленного типа не очень большой, так что даже 19 (десятичных) разрядов (ull), при выполнении очень точных вычислений, может не хватить (точнее 264 - 1).
Программа 28.4

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

class BigNumber {
private:
	string x;
	string y;
	const int base;
	const int len;
public:
	// Конструктор
	BigNumber (string dig1, string dig2) :
		x (dig1), y (dig2),
		base(1000000000), len(9) {}

	// Прототип метода суммирования
	void sum(vector<unsigned>&, 
			 vector<unsigned>&, 
			 vector<unsigned>&);
	// Прототип метода получения строки - большого числа
	string Add(void);
	// Прототип get-метода для суммирования
	void getSumBig(void);

	vector<unsigned> BigX;
	vector<unsigned> BigY;
	vector<unsigned> Res;

	void setBig() {
		// Очищаем если они были заняты
		if (!BigX.empty()) BigX.clear();
		if (!BigY.empty()) BigY.clear();
		// Заполняем
		input(x, BigX);
		input(y, BigY);
		// Запрос на оптимизацию
		BigX.shrink_to_fit();
		BigY.shrink_to_fit();
	}
	//====================================================
	// Ввод длинного числа str => mas_unsigned
	//====================================================
	void input(string &S, vector<unsigned> &V) {
		for (int i = S.size(); i > 0; i -= len)
			// Записываем в вектор перевернутым
			if (i < len)
				V.push_back(stoul(S.substr(0, i)));
			else
				V.push_back(stoul(S.substr(i - len, len)));
	}
	//====================================================
	// Вывод длинного числа mas_unsigned => str
	//====================================================
	string BigToStr() {
		string temp;
		int n = Res.size() - 1;
		for (int i = n; i >= 0; i--) {
			size_t e = 0;
			string r = to_string(Res[i]);
			if (r.size() < size_t(len) && i < n)
				while (e < size_t(len) - r.size()) {
					e++;
					temp += "0";
				}
			temp.append(r);
		}
		return temp;
	}
};

int main() {
	string X, Y;
	cout << "X = ";
	getline(cin, X);
	cout << "Y = ";
	getline(cin, Y);
	BigNumber Num(X, Y);
	cout << "X + Y = " << Num.Add() << endl;
	return 0;
}

//=======================================================
// Сумма двух больших целых чисел
//=======================================================
void BigNumber::sum(vector<unsigned> &V1,
		 	 	 	vector<unsigned> &V2,
					vector<unsigned> &Vbig) {
	size_t carry = 0;
	// Смотрим какое длиннее
	size_t q = V1.size() > V2.size() ?
		   V1.size() : V2.size();
	Vbig.reserve(q);
	for (size_t i = 0; i < q; i++) {
		Vbig.push_back(V1[i] + V2[i] + carry);
		carry = Vbig[i] / base;
		Vbig[i] = Vbig[i] % base;
	}
	if (carry > 0) {
		q++;
		Vbig.push_back(carry);
	}
}
//===========================================================
// Из того, что сделали, собираем нашу операцию
//===========================================================
string BigNumber::Add() {
	setBig();
	getSumBig();
	return BigToStr();
}
//===========================================================
// get-метод суммы
//===========================================================
void BigNumber::getSumBig() {
	if (!Res.empty()) Res.clear();
	sum(BigX, BigY, Res);
	Res.shrink_to_fit();
}

Примечание. Здесь get и set – методы используются не для инициализации скрытых полей, а для работы с числовыми массивами.
В программе 28.4 мы вынесли за пределы класса те методы, которые относятся к операции сложения двух больших чисел и оставили общие методы впрок. Поэтому класс BigNumber можно расширить, добавив в него методы вычитания, умножения, деления и сравнения больших чисел, сделав, таким образом, этот класс универсальным (это вы сделаете в качестве домашней работы). Идеи алгоритмов для реализации таких задач без труда можно найти в сети. Для более естественного использования этих операций можно воспользоваться специальными методами, которые называются “перегрузкой операции”. С этим понятием мы уже знакомы. Детально об этом мы поговорим уже на следующем уроке.

Print Friendly, PDF & Email

Comments are closed.