Классы
Ликбез по лекции 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 функции должна быть известна её реализация.