Описываются возможности C++ по работе с наследованием (virtual, override, final). Описываются механизмы работы с константными переменными и методами (const, mutable, constexpr). Описываются возможности по перегрузке операторов (operator).
2. 7.
сохраняемость
Сохраняемость -‐ способность
объекта
существовать
во
времени,
переживая
породивший
его
процесс,
и
(или)
в
пространстве,
перемещаясь
из
своего
первоначального
адресного
пространства.
4. А
что
если
объект
сложный?
граф
объектов
Задача
сериализации объекта,
включающего
только
поля
из
элементарных
типов
значений
и
строк,
не
представляет
принципиальных
трудностей.
Для
такого
объекта
в
ходе
сериализации в
поток
записываются
сами
значения
всех
полей
объекта.
Однако
в
общем
случае
объект
содержит
ссылки
на
другие
объекты,
которые,
в
свою
очередь,
могут
ссылаться
друг
на
друга,
образуя
так
называемый
граф
объектов
(object graph).
4
class SampleClass
{
SampleClass fieldA = nullptr;
SampleClass fieldB = nullptr;
}
...
SampleClass root = new SampleClass();
SampleClass child1 = new SampleClass();
SampleClass child2 = new SampleClass();
root.fieldA = child1;
root.fieldB = child2;
child1.fieldA = child2;
5. Просто
пронумеруем
объекты
почти
как
ссылки
в
C++
1. ObjectID ?
В
ходе
сериализации объектам
должны
быть
поставлены
в
соответствие
некоторые
идентификаторы,
и
в
хранилище
отдельно
сохраняется
список
объектов,
отмеченный
идентификаторами,
а
при
сериализации вместо
ссылок
записываются
идентификаторы
ссылаемых
объектов.
2. Виртуальный
адрес!
В
программе
роль
идентификатора
объекта
выполняет
его
адрес,
но
вместо
него
обычно
удобнее
назначить
некоторые
идентификаторы
в
процедуре
сериализации для
более
легкого
чтения
человеком
полученного
образа.
3. Hash-‐map
…
В
ходе
сериализации нужно
ввести
список
адресов
уже
записанных
объектов,
как
для
ведения
списка
идентификаторов,
так
и
для
обнаружения
возможных
циклов
при
обходе
графа
методом
в
глубину.
5
6. Пример
Example14_Serialize
1. class A {
2. public:
3. A(const char* value) : next(nullptr), name(value) {};
4. A(std::ifstream &is) {
5. bool is_next;
6. is >> name;
7. is >> is_next;
8. if (is_next) next = new A(is);}
9. void Serialize(std::ofstream &os) {
10. os << name << std::endl;
11. if (next != nullptr) {
12. os << true;
13. next->Serialize(os);} else os << false;}
14. void SetNext(A* value) {
15. next = value; }
16. virtual ~A() { delete next; }
17.private:
18. std::string name;
19. A* next;
20.};
8. Временные
объекты
Вре́менные объекты — в
C++
объекты,
которые
компилятор
создаёт
автоматически
по
ходу
вычисления
выражений.
Такие
объекты
не
имеют
имени
и
уничтожаются
сразу
же,
как
только
в
них
исчезает
потребность.
Пример:
string r = string("1") + "2" + "3";
string r, tmp1, tmp2, tmp3;
tmp1.ctor("1");
tmp2.ctor();
tmp2 = tmp1 + "2";
tmp3.ctor();
tmp3 = tmp2 + "3";
r.ctor(tmp3);
tmp3.dtor();
tmp2.dtor();
tmp1.dtor();
9. Модификаторы
функций
Example15_Virtual
Ключевое
слово virtual опционально
и
поэтому
немного
затрудняло
чтение
кода,
заставляя
вечно
возвращаться
в
вершину
иерархии
наследования,
чтобы
посмотреть
объявлен
ли
виртуальным
тот
или
иной
метод.
Типовые
ошибки: Изменение
сигнатуры
метода
в
наследнике.
Модификатор
override позволяет
указать
компилятору,
что
мы
хотим
переопределить
виртуальный
метод.
Если
мы
ошиблись
в
описании
сигнатуры
метода
– то
компилятор
выдаст
нам
ошибку.
Этот
модификатор
влияет
только
на
проверки
в
момент
компиляции.
10. Модификатор
final
Example28_Final
Модификатор
final,
указывающий
что
производный
класс
не
должен
переопределять
виртуальный
метод.
Работает
только
с
модификатором
virtual.
Т.е.
Создавать
«копию»
функции
в
классе-‐
наследнике
с
помощью
этой
техники
запретить
нельзя.
Применяется,
в
случае
если
нужно
запретить
дальнейшее
переопределение
метода
в
дальнейших
наследниках
наследника
(очевидно,
что
в
родительском
классе
такой
модификатор
ставить
бессмысленно).
11. Модификатор
const
Example29_Const
сonst,
применительно
к
методу
(например,
void
foo
const {})
означает,
что
метод
не
может
вызывать
другие
методы
без
модификатора
const и
не
может
менять
значения
атрибутов
объекта.
class A{
int a;
void fooA2() {}
void fooA () const{
fooA2(); //ошибка
a=7; // ошибка
}
12. mutable
Example33_Mutable
Ключевое
слово
mutable позволяет
специфицировать
атрибуты
класса,
которые
могут
меняться
из
const
методов.
Иногда
необходимо
иметь
такие
атрибуты
для
«служебных
целей»,
например,
подсчитывать
число
обращений
к
методу
и
т.д.
13. Const
Example23_Const
1. int a=100;//два обычных объекта типа int
2. int b=222;
3. int *const P2=&a; //Константный указатель
4. *P2=987; //Менять значение разрешено
5. //P2=&b; //Но изменять адрес не разрешается
6. const int *P1=&a; //Указатель на константу
7. //*P1=110; //Менять значение нельзя
8. P1=&b; //Но менять адрес разрешено
9. const int *const P3=&a;//Константный указатель на константу
10.//*P3=155; //Изменять нельзя ни значение
11.//P3=&b; //Ни адрес к которому такой указатель привязан
14. Constexpr
Example31_constexpr
Позволяет
создавать
выражения,
вычисляемые
на
этапе
компиляции
(позволяет
упростить
код).
Ключевое
слово
constexpr,
добавленное
в
C++11,
перед
функцией
означает,
что
если
значения
параметров
возможно
посчитать
на
этапе
компиляции,
то
возвращаемое
значение
также
должно
посчитаться
на
этапе
компиляции.
Если
значение
хотя
бы
одного
параметра
будет
неизвестно
на
этапе
компиляции,
то
функция
будет
запущена
в
runtime (а
не
будет
выведена
ошибка
компиляции).
constexpr int sum (int a, int b){
return a + b;
}
void func(){
constexpr int c = sum (5, 12); // значение переменной будет посчитано на этапе
компиляции
}
15. Inheritance
/
наследование
Example24_MultipleInheritance
class temporary { /* ... */ };
class secretary : public
employee { /* ... */ };
class tsec
: public temporary, public
secretary { /* ... */ };
class consultant
: public temporary, public
manager { /* ... */ };
В
С++
в
отличии
от
большинства
других
объектно-‐ориентированных
языков
есть
множественное
наследование!
Очень
плохо,
если
у
двух
родителей
класса
есть
общий
родитель!
16. Ссылка
на
себя
Считается,
что
в
каждой
функции-‐члене
класса
X
указатель
this описан
неявно
как
X *const this;
class X {
int m;
public:
int readm() { return this->m; }
};
18. Виртуальные
деструкторы
Example26_VirtualDestructor
C++
вызывает
деструктор
для
текущего
типа
и
для
всех
его
родителей.
Однако,
если
мы
работаем
с
указателями
то
можем
попасть
в
неприятную
ситуацию,
когда
вызываем
оператор
delete
у
указателя,
предварительно
приведя
его
к
типу
родителя.
В
этом
случае,
вообще
говоря,
у
наследников
деструктор
вызван
не
будет (например,
если
у
родителя
нет
деструктора).
Однако
если
объявить
деструктор
базового
класса
как
virtual
то
будут
вызваны
деструкторы
всех
классов.
Всегда
объявляй
деструктор
как
virtual!
19. Последовательность
вызова
конструкторов
и
деструкторов
Конструкторы
вызываются
начиная
от
родителя
к
наследнику.
Деструкторы
вызываются
начиная
от
наследника
к
родителю.
class A {}
class B : A {}
class C: B{}
Конструкторы:
A, B, C
Деструкторы:
~C, ~B,~A
20. Явные
и
неявные
конструкторы
Example20_explicit
В
С++
существуют
явные
и
неявные
конструкторы;
преобразования,
определенные
с
помощью
конструктора
со
спецификатором
explicit могут
использоваться
только
при
явном
преобразовании,
в
то
время,
как
другие
конструкторы
могут
использоваться
также
и
в
неявных
преобразованиях.
Например:
// "обычный конструктор" определяет неявное преобразование
class A { S(int); };
A a1(1); // ok
A a2 = 1; // ok
void Foo(A);
Foo(1); // ok (но это может привести к неприятным
class E { explicit E(int); }; // явный конструктор
E e1(1); // ok
E e2 = 1; // ошибка (хотя это обычно является сюрпризом)
void f(E);
f(1); // ошибка (защищает от сюрпризов – например,
22. Прежде
чем
начать
• Это
механизм,
при
неумелом
использовании
которого
можно
полностью
запутать
код.
• Непонятный
код
– причина
сложных
ошибок!
• Перегруженные
операции
помогают
определить
«свойства»
созданного
вами
класса,
но
не
алгоритма
работы
с
классами!
23. Перегрузка
операций
Можно
описать
функции,
для
описания
следующих
операций:
+
-‐ *
/
%
^
&
|
~
!
=
<
>
+=
-‐=
*=
/=
%=
^=
&=
|=
<<
>>
>>=
<<=
==
!=
<=
>=
&&
||
++
-‐-‐ -‐>*
,
-‐>
[]
()
new
delete
Нельзя
изменить
приоритеты
этих
операций,
равно
как
и
синтаксические
правила
для
выражений.
Так,
нельзя
определить
унарную
операцию
%
,
также
как
и
бинарную
операцию
!.
24. Синтаксис
type operator operator-symbol ( parameter-list )
Ключевое слово operator позволяет перегружать операции. Например:
• Перегрузка
унарных
операторов:
◦ ret-‐type operatorop ( arg )
◦ где
ret-‐type и
op соответствуют
описанию
для
функций-‐членов
операторов,
а
arg — аргумент
типа
класса,
с
которым
необходимо
выполнить
операцию.
• Перегрузка
бинарных
операторов
◦ ret-‐type operatorop( arg1, arg2 )
◦ где
ret-‐type и
op — элементы,
описанные
для
функций
операторов
членов,
а
arg1
и
arg2
—
аргументы.
Хотя
бы
один
из
аргументов
должен
принадлежать
типу
класса.
26. Префиксные
и
постфиксные
операторы
++
и
-‐-‐
Операторы
инкремента
и
декремента
относятся
к
особой
категории,
поскольку
имеется
два
варианта
каждого
из
них:
• преинкрементный
и
постинкрементный
операторы;
• предекрементный
и
постдекрементный
операторы.
При
написании
функций
перегруженных
операторов
полезно
реализовать
отдельные
версии
для
префиксной
и
постфиксной
форм
этих
операторов.
Для
различения
двух
вариантов
используется
следующее
правило:
префиксная форма
оператора
объявляется
точно
так
же,
как
и
любой
другой
унарный
оператор;
в
постфиксной форме
принимается
дополнительный
аргумент
типа
int.
Пример:
friend Point& operator++( Point& ) // Prefix increment
friend Point& operator++( Point&, int ) // Postfix increment
friend Point& operator--( Point& ) // Prefix decrement
friend Point& operator--( Point&, int ) // Postfix decrement
29. Переопределение
оператора
присваивания
Example19_OperatorAssign
Он
должен
быть
нестатической
функцией-‐членом.
Никакой
оператор
operator= не
может
быть
объявлен
как
функция,
не
являющаяся
членом.
Он
не
наследуется
производными
классами.
Компилятор
может
создавать
для
типов
классов
функции
operator= по
умолчанию,
если
они
не
существуют.
В
этом
случае
оператор
равно
применяется
к
каждому
члену
класса.
30. Спецификатор
delete и
default
Example27_Delete
Иногда
очень
важно
сделать
так,
что
бы
у
объекта
был
только
один
экземпляр.
Т.е.,
что
бы
его
нельзя
было
скопировать.
Сейчас
стандартная
идиома
«запрещения
копирования»
может
быть
явно
выражена
следующим
образом:
class X {
// ...
X& operator=(const X&) = delete; // Запрет копирования
X(const X&) = delete; // запрет копирование в момент конструирования
};
Такая
конструкция
запрещает
компилятору
«создавать»
конструкторы
и
оператор
копирования
«по
умолчанию».
Ключевое
слово
=default,
наоборот,
указывает
что
мы
хотим
что
бы
компилятор
использовал
операцию
«по-‐
умолчанию».
Вообще
говоря,
она
является
избыточной.
31. Делаем
функтор
CPP_Examples21_OperatorFunctor
Функторы
в
C++
являются
сокращением
от
"функциональные
объекты".
Функциональный
объект
является
экземпляром
класса
С++,
в
котором
определён
operator().
Если
вы
определите
operator() для
C++
класса,
то
вы
получите
объект,
который
действует
как
функция,
но
может
также
хранить
состояние.
class A{
private:
int _value;
public: A(int a) : _value(a) {};
A operator()(int a) {
std::cout << _value+a << std::endl;
return A(_value+a);
}
};
32. Литералы
Литерал — это
некоторое
выражение,
создающее
объект.
В
языке
С++
существуют
литералы
для
разных
встроенных
типов
(2.14
Literals):
123
//
int
1.2
//
double
1.2F
//
float
'a'
//
char
1ULL
//
unsigned
long
long
0xD0
//
unsigned
int в
шестнадцатеричном
формате
"as"
//
string
33. Пользовательские
литералы
Example22_Literal
Должны
начинаться
с
подчеркивания:
OutputType operator "" _suffix(unsigned long long);
Конструктор
типа
должен
так
же
иметь
спецификатор
constexpr
Могут
иметь
следующие
параметры:
const char*
unsigned long long int
long double
char
wchar_t
char16_t
char32_t
const char*, std::size_t
const wchar_t*, std::size_t
const char16_t*, std::size_t
const char32_t*, std::size_t