Классы

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

Ликбез по лекции 8 октября


На прошлой лекции были рассмотрены следующие вопросы касающиеся классов:

  • Методы
  • Конструкторы
  • Деструкторы
  • Конструкторы копирования
  • Оператор присваивания

Поставим задачу:

Необходимо написать класс рациональных чисел. Для начала напишем класс с 2 переменными m_num(числитель), m_denom(знаменатель):

struct Rational 
{ 
	int m_num; 
	int m_denom; 
};

Добавим конструктор

struct Rational 
{ 
	int m_num; 
	int m_denom; 
	Rational(): m_num(0), m_denom(1)    // конструктор по умолчанию 
	{} 

	Rational(int n): m_num(n), m_denom(1)  // конструктор для целых чисел
	{} 

	Rational(int a, int b): m_num(a), m_denom(b) // конструктор для дробей 
	{} 
};

Что бы избежать повторения, можно сделать 3 конструкторов 1 с параметрами по умолчанию, таким образом получаем:

struct Rational 
{ 
	int m_num; 
	int m_denom; 
	Rational(int a = 0, int b = 1): m_num(a), m_denom(b) 
	{
		normalize();  // получим несократимую дробь.
	} 

	void normalize();  // метод для сокращения дробей 
};

Но получившийся класс не является надежным. Рассмотрим его недостатки: переменные m_num, m_denom являются открытыми, следовательно их можно изменить из другого модуля и это изменения будет сложно отследить. Таким образом может произойти нарушение инварианта у класса. Для решения этой проблемы необходимо установить идентификаторы доступа.

В с++ существует 3 идентификатора.

  • public — методы, переменные могут быть вызваны/изменены из кода другого модуля.
  • private — методы, переменные могут вызывать только из модуля где они определены
  • protected — будет рассказано позже.

Пример:

struct Rational 
{ 
private: 
	int m_num; 
	int m_denom; 
public: 
	Rational(int a = 0, int b = 1): m_num(a), m_denom(b)   // конструктор для дробей 
	{} 
};

Теперь, из другого модуля переменные не могут быть изменены. Таким образом, вызовы типа

Rational r(4, 5); 
r.m_num = 10;  // будет ошибка

не позволят изменить переменные.

Вопрос:

Как корректно уметь получать, изменять и отслеживать изменения private переменных.

Ответ:

Необходимо для каждой переменной завести пару методов get/set, которые будут устанавливать и получать соответствующие значения. К примеру:

struct Rational 
{ 
private: 
	int m_num; 
	int m_denom; 
public: 
	Rational(int a = 0, int b = 1): m_num(a), m_denom(b) 
	{} 
	
	void setNum(int a); 
	int getNum(); 
	void setDenom(int b); 
	int getDenom(); 
};

Замечание 1

Рассмотрим конструктор со списоком инициализации членов класса в данной задачи, то есть:

Rational(int a = 0, int b = 1): m_num(a), m_denom(b) // конструктор для дробей 
{}

Вопрос:

Что будет если изменить порядок инициализации, то есть

Rational(int a = 0, int b = 1): m_denom(b), m_num(a) 
{}

Ответ:

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

struct Rational 
{ 
private: 
	int m_num; 
	int m_denom; 
public: 
	Rational(int a = 0, int b = 1): m_num(a), m_denom(b) 
	{} 
};

Всегда будет сначала будет инициализирована переменная m_num, а затем m_denom.

Вопрос:

В каком порядке будет происходить вызов деструкторов полей.

Ответ:

Вызов деструкторов происходит в порядке обратном вызову конструкторов. К примеру

struct Rational 
{ 
private: 
	int m_num; 
	int m_denom; 
public: 
	Rational(int a = 0, int b = 1): m_num(a), m_denom(b)  // конструктор для дробей 
	{} 
};

Сначала будет вызван деструктор для m_denom, а затем для m_num. Замечание 2(дополнение для оператора присваивания):

Может быть опасно присваивать объект самому себе, для запрета необходимо при перегрузке оператора присваивания делать сравнение с *this.

Замечание 3

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

  • int num_; // то есть после имени переменной класса необходимо ставит _
  • int _num; // то есть перед имени переменной класса необходимо ставит _, но если имя

вашей переменной начинается с большой буквы(к примеру _GHB), происходит конфликт имен

  • int myNum;

Замечание 4

Идентификаторы доступа private, public не являются сигнатурой.

Пример:

struct Rational 
{ 
private: 
	int m_num; 
	int m_denom; 
public: 
	Rational(int a = 0, int b = 1); 
	int getNum(); 
}; 
Rational::Rational(int a, int b) : m_num(a), m_denom(b) 
{} 
int Rational::getNum() 
{ 
return m_num; 
}

Замечание 5

Если объявление функции находить в файле *,h, то функция по умолчанию становиться inline. Рассмотрим пример

struct Rational 
{ 
private: 
	int m_num; 
	int m_denom; 
public: 
	Rational(int a = 0, int b = 1): m_num(a), m_denom(b)  // конструктор для дробей 
	{} 
	
	int getNum() 
	{ 
		return m_num; 
	} 
};

То есть теперь getNum() теперь является inline, то есть по стандарту, код может быть с оптимизирован компилятором, и в результате, метод getNum может автоматически быть встроеным в код.

Замечание 6

ODR — one defenition rule Это значит что не должно быть в программе двух inline функции с одинаковыми именам. Так же для каждой inline функции должна быть известна её реализация.