Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Курс лекций Языки программирования.doc
Скачиваний:
9
Добавлен:
01.04.2025
Размер:
1.42 Mб
Скачать

Конструктор умолчания.

Конструктор класса Stack можно легко превратить в конструктор умолчания, задав значение по умолчанию:

struct Stack {

char* body;

int top;

int size;

Stack(int sz=10);

};

Все правила указания параметров в конструкторах и классификации конструкторов в зависимости от вида параметров, относятся и к случаю, когда есть параметр по умолчанию. Т.е. если есть конструктор с одним параметром, и у этого параметра есть значение по умолчанию, то одновременно получаются сразу два конструктора – конструктор по умолчанию и конструктор преобразования. В данном случае, становится понятно, что надо вызывать, когда мы пишем просто new Stack() – вызовется new Stack(10). Аналогично, при описании Stack x вызовется конструктор с параметром 10. Инициализацию параметров по умолчанию следует писать в прототипе функции, а не при описании ее реализации. Правила задания параметров по умолчанию относятся не только к конструкторам, но и к обычным функциям.

Зачем нужны конструкторы по умолчанию? Вернемся к примеру класса стек, который является подклассом класса X. С каким значением будет вызван конструктор S в конструкторе X? Откуда компилятор узнает, с каким параметром инициализировать стек. Тут-то и приходит на помощь конструктор умолчания. Т.е. если компилятор не знает, с каким значением нужно вызывать конструктор S, то он вызывает конструктор по умолчанию.

В некоторых случаях наличие конструктора по умолчанию провоцирует ошибки, например, десяти элементов стека может не хватить. Когда нас заставляют писать какое-то значение, мы, по крайней мере, задумываемся о должном размере стека. Для стека не имеет смысл писать конструктор умолчания, кроме специфических задач. Но как быть, если у подкласса нет конструктора умолчания? Что будет подставлять компилятор в качестве значения параметра? В случае отсутствия конструктора по умолчанию компилятор выдаст ошибку. Очевидно, должен быть способ указания параметра в данном случае.

сlass X{

int i;

float f;

Stack S;

};

Параметр подкласса S указывается в конструкторе класса Х (пусть это будет конструктор умолчания), например при описании тела конструктора, следующим образом: X::X():i(0),f(0.0),S(16){…};. В данном случае мы еще заодно можем инициализировать переменные i и f. Однако будьте осторожны с такой инициализацией, особенно если таким образом нужно проинициализировать несколько конструкторов, потому что априори неизвестно, в каком порядке эти конструкторы вызовет компилятор. Ни в коем случае не пишите конструкторы, которые зависят от глобальных переменных, и в этом смысле, могут зависеть от порядка своего выполнения. Конструктор должен быть вещью в себе, т.е. либо он должен быть конструктором умолчания, либо получать какие-то параметры.

Конструктор копирования.

Конструкторы копирования нужны, прежде всего, в том случае, когда объект создается на основе объекта этого же класса, например, при копировании стека:

Stack S(256);

Stack S1=S;

Это типичный случай вызова конструктора копирования. Аналогично (с точки зрения синтаксиса) можно инициализировать, например, целую переменную int i=j. Однако инициализация стека семантически отличается от инициализации целой переменной. При отсутствии концепции конструктора копирования, семантика структуры данных достаточно не однозначна. Обычное присваивание означает побитовое копирование структуры данных, и два стека будут ссылаться на одно и тоже тело. В этом случае, любая операция Push или Pop нарушает целостность стека. Еще хуже, когда один стек уничтожится, и возникнет один из самых омерзительных случаев – висячая ссылка.

При работе со сложными структурами данных, возникает проблема копирования. Что означает копирование двух объектов сложной структуры? Тут различают два вида копирования: поверхностное (побитовое) и глубокое. Для стека поверхностное копирование не годится, это приводит к труднообнаружимым ошибкам. Для стека требуется глубокое копирование, когда создается отдельный экземпляр тела стека, но содержащий те же значения, что и в исходном стеке. Динамические структуры данных должны копироваться только глубоким видом копирования.

Языки программирования Модула-2 и Оберон никак не решают эту проблему, оставляя ее программисту. Как же дело обстоит в С++. Понятно, что компилятор сам не может решить, к какому виду копирования прибегнуть. С++ следует идеологии языка Си, который понравился программистам за то, что он их нигде не обманывает (компилятор нигде ничего неявно не вставляет). С++ тоже почти никогда не обманывает. Когда программист видит, что структуре данных необходимо глубокое копировании, то он сам обязан написать конструктор копирования. Например, у конструктора копирования стека будет следующее тело:

Stack (Stack &S) {

body = new char[size=S.size];

top = S.top;

memcpy(body, S.body, top*sizeof(char));

};

Конструктор копирования вызывается всегда при такой инициализации: Stack S1=S; (Stack S1(S) – то же самое). Но этот конструктор нужен не только здесь. Вспомним специфику передачи параметров по значению. Пусть есть такая функция:

void f(Stack X) {…};

Когда происходит передача параметров по значению, то в системном стеке заводится место под переменную типа Stack и создается локальный экземпляр передаваемого параметра. Объект любого класса инициализируется только с помощью конструктора, и в данном случае компилятором неявно вызовется конструктор копирования. Т.е. в данном случае, в теле функции как бы (т.е. это эквивалентно) заводится локальная переменная типа Stack, которая инициализируется стеком, переданным по ссылке.

Теперь понятно, что мы никогда не можем написать конструктор копирования вида X(Stack X), потому что в этом случае этот конструктор будет вызывать сам себя бесконечно. Такие объявления запрещены. Именно по этому параметрами конструктора копирования могут быть только ссылки на класс.

Будьте осторожны с передачей параметра по значению. Например, в MFC есть класс CString, работа с которым ведется исключительно в динамической памяти, поэтому при копировании, конкатенации строк никогда не может быть переполнения памяти. Но если вы все время пишите функции вида void S(CString c), то все строки, которые вы передаете, передаются по значению, т.е. всякий раз вызывается конструктор копирования, использующий глубокое копирование с помощью менеджера памяти. Т.е. возникают накладные расходы. Здесь мощность языка С++ может обернуться против программиста. В таких случаях часто имеет смысл передавать параметры по ссылке, чтобы избежать глубокого копирования.

Теперь мы понимаем, что у конструктора копирования, как и у конструктора умолчания, есть своя семантика. В чем еще отличие этих конструкторов? В языке С++ каждый класс имеет хотя бы один конструктор. Откуда они берутся? Мы писали такую структуру:

struct Complex {

double Re, Im;

};

Где здесь конструктор? В случае, если в классе отсутствуют какие-либо конструкторы, то по умолчанию генерируется конструктор умолчания. В данном случае, он ничего не делает, потому что у этого класса нет никаких подобъектов, а также отсутствует наследование (но накладных расходов не будет, потому что компилятор достаточно умен, чтобы это оптимизировать). Конструктор умолчания не везде нужен. Например он не нужен для стека, потому что нам необходимо, чтобы компилятор выдал ошибку, если мы не проинициализируем стек явно. Поэтому, конструктор умолчания генерируется только тогда, когда в классе нет никаких других конструкторов.

В том случае, если конструктор копирования не описан явно, то он генерируется неявно. Этот конструктор выполняет побитовое копирование. Если бы при описании стека сразу задавалась бы его длина (char body[50]), то мы могли бы обойтись только конструктором копирования, сгенерированным компилятором.