Практика программирования 16 ➔ Набор операторов, автоматически определяемых компилятором для каждого класса ➔ Порядок инициализации ➔ explicit ➔ Динамический массив Кувшинов Д.Р. КМиММ УрФУ Екатеринбург 2012 «Пустой» класс ● Определив класс или структуру, вы получаете «в довесок» набор определений функций-членов «по умолчанию»: – Конструктор по умолчанию (без параметров), который «ничего не делает»; – Конструктор копирования, выполняющий «побитовое» копирование всех полей; – Деструктор, который «ничего не делает»; – Оператор присваивания, выполняющий «побитовое» копирование всех полей; – Оператор взятия адреса (не-const и const версии), возвращающий this. «Пустой» класс ● class A { }; // то же самое, что: ● class A { public: A() { } // конструктор по умолчанию A(const A&) { } // пустой, копировать нечего ~A() { } A& operator= (const A&) { return *this; } // пустой A* operator&() { return this; } const A* operator&() const { return this; } }; «Пустой» класс ● ● Если определён какой-либо конструктор (например, копирования), то конструктор по умолчанию автоматически определён не будет. class A { public: A(const A&) { } }; int main() { A a; // ошибка: конструктор A::A() не определён } «Пустой» класс ● Конструктор копирования определён всегда. ● class A { public: A() { } }; int main() { A a; A b(a); // вызов конструктора копирования } «Пустой» класс ● Конструктор копирования определён всегда. ● Однако, его можно «спрятать» в private-секции. ● class A { A(const A&); public: A() { } }; int main() { A a; A b(a); // ошибка: доступ к private-члену } Запрет на копирование ● ● Чтобы действительно запретить копирование, «спрятать» придётся и operator=. Кроме того, если мы не хотим разрешать копирование и функциям класса, то можно выполнить private-наследование от класса вида class noncopyable { // private-секция: noncopyable(const noncopyable&); noncopyable& operator=(noncopyable&); public: noncopyable() { } }; ● class A : private noncopyable { … }; // теперь копировать объекты A нельзя ни в каком контексте Список инициализации ● struct A { int a; A() { a = 1; } }; ● struct B { A m; B() { } // «за кадром» вызывает A::A() для m, // так как мы определили его, // это происходит лишь для классов, но не // примитивных типов, хотя для них и определены // конструктор по умолчанию и копирующий конст-р }; ● int main() { B b; /* истинно: b.m.a == 1 */ } Список инициализации ● struct A { int a; A (int value) { a = value; } }; ● struct B { A m; B() { } // ошибка: нет конструктора по умолчанию для m! }; ● int main() { B b; /* ошибка: нельзя создать b */ } Список инициализации ● struct A { int a; A (int value) { a = value; } }; ● struct B { A m; B() { m = 0; } // m.a = 0 тоже не сработает // ошибка: нет конструктора по умолчанию для m! // ошибка: нет оператора = вида A = int! }; ● ● int main() { B b; /* ошибка: нельзя создать b */ } Чтобы передать конструктору поля параметры из конструктора класса, есть специальный механизм: список инициализации. Список инициализации ● struct A { int a; A (int value) { a = value; } }; ● struct B { A m; B() : m(0) // список инициализации: вызывает A(int) { } // тело конструктора пусто // теперь всё в порядке }; ● ● int main() { B b; /* истинно: b.m.a == 0 */ } Дополнительный плюс списков инициализации в том, что не происходит лишнего вызова конструктора по умолчанию, при создании объекта сразу вызывается нужный конструктор. Список инициализации ● ● ● ● Конструкторы (упомянутые в списках инициализации или конструкторы по умолчанию для неупомянутых) вызываются в порядке определения полей в классе. Порядок инициализации унаследованных объектов определяется порядком перечисления классовпредков. Их инициализация происходит до инициализации собственных полей класса. В списке инициализации можно вызывать конструкторы классов предков: class Base { public: Base (int); … }; class Derived : public Base { public: Derived() : Base (42) { } … }; Список инициализации ● struct A { A(int i) { cout << " A" << i; } }; ● struct B { B(int i) { cout << " B" << i; } }; ● struct C { C(int i) { cout << " C" << i; } }; ● struct D { A a; B b; C c; D(): c(1), b(2), a(3) { cout << " D" << endl; } }; ● struct E { A a; B b; E(): b(0), a(1) { cout << " E" << endl; } }; ● struct F : E, D { A a; F(): a(5) { cout << " F" << endl; } }; ● int main() { F f; } ● // вывод: // A1 B0 E // A3 B2 C1 D // A5 F ссылка на код Класс, управляющий ресурсом ● ● Если объект класса управляет некоторым ресурсом (один ресурс на один объект), например, блоком памяти, выделенным new, следует определять (или запрещать) полный набор: – конструктор по умолчанию; – копирующий конструктор; – оператор присваивания; – деструктор. Примером такого класса является динамический массив. Динамический массив ● ● ● ● Контейнер, хранящий своё содержимое в непрерывном фрагменте (массиве) динамической памяти. К каждому элементу массива можно обратиться по индексу. Размер массива может изменяться во время исполнения: элементы могут добавляться и удаляться. Сохранность указателей и ссылок на элементы при добавлении новых элементов не гарантируется (массив может переместиться в памяти при увеличении размера). Динамический массив ● ● Требуется хранить указатель на начало массива и [размер, либо указатель на фиктивный элемент за последним («конец»)]. class ArrayInt { // минимальный интерфейс int *b, *e; // начало и конец public: ArrayInt (int n = 0, int value = 0); // указать размер и значения ArrayInt (const ArrayInt&); ~ArrayInt() { delete[ ] b; } ArrayInt& operator= (const ArrayInt&); int& operator[ ] (int index) { return b[index]; } int operator[ ] (int index) const { return b[index]; } int size() const { return e – b; } // получить размер массива void resize (int n, int value = 0); // задать новый размер и значения }; Динамический массив ● ArrayInt::ArrayInt (int n, int value) : b (nullptr), e (nullptr) { if (n > 0) resize (n, value); } ● ArrayInt::ArrayInt (const ArrayInt &other) : b (new int [other.size()]) { e = b + other.size(); // копировать поэлементно for (int i = 0; i < e – b; ++i) b[i] = other[i]; } ● ArrayInt& ArrayInt::operator= (const ArrayInt &other) { if (this != &other) { delete[ ] b; b = new int [other.size()]; e = b + other.size(); for (int i = 0; i < e – b; ++i) b[i] = other[i]; } return *this; } Динамический массив // задать новый размер и значения новых элементов ● void ArrayInt::resize (int n, int value) { if (size() < n) { int *bcopy = new int [n]; for (int i = 0; i < size(); ++i) bcopy[i] = b[i]; for (int i = size(); i < n; ++i) bcopy[i] = value; delete[ ] b; b = bcopy; } e = b + n; } Динамический массив ● ● ● Чтобы уменьшить количество выделений памяти и копирований (и, таким образом, улучшить производительность), полезно иметь выделенный фрагмент памяти, больший реально задействованного размера массива. Для этого добавим третий указатель – «конец выделенного фрагмента», задающий ёмкость (capacity) контейнера. class ArrayInt { int *b, *e, *c; // начало, конец и конец фрагмента ● Соответственно поправим уже определённые функции. Добавим в интерфейс «сброс» clear(), определение ёмкости capacity(), резервирование ёмкости reserve(int) и функции добавления и удаления элементов с конца контейнера: push_back(int), pop_back(). Динамический массив ● Особенность нашей реализации: при вызове resize(n), pop_back() или clear() реального удаления хранимых объектов (вызова деструктора, если он есть) не происходит, хотя рано или поздно объекты будут удалены вместе с областью памяти, в которой они размещаются. ● Это поведение может отличаться от ожидаемого программистом поведения. Explicit ● Приведённый выше пример допускает такой, как может показаться, «странный» код: ArrayInt arr (10, 10); // десять десяток arr = 5; // а теперь arr – это пять нулей ● Благодаря возможности написать ArrayInt array(5); ● ● 5 будет автоматически (неявно, implicitly) «сконвертировано» в массив. Чтобы запретить автоматическое приведение из int, нужно объявить конструктор из int с ключевым словом explicit: explicit ArrayInt (int n = 0, int value = 0); ● Ссылка на переработанный код динамического массива