Конструктор по умолчанию

advertisement
Практика программирования
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);
●
Ссылка на переработанный код динамического массива
Download