search

Перегрузка конструкторов

Несмотря на выполнение конструкторами уникальных действий, они не сильно отличаются от функций других типов и также могут подвергаться перегрузке. Чтобы перегрузить конструктор класса, достаточно объявить его во всех нужных форматах и определить каждое действие, связанное с соответствующим форматом. Например, в следующей программе объявляется класс timer, который действует как вычитающий таймер.

При создании объекта типа timer таймеру присваивается некоторое начальное значение времени. При вызове функции run() таймер выполняет счет в обратном порядке до нуля, а затем подает звуковой сигнал. В этом примере конструктор перегружается трижды, предоставляя тем самым возможность задавать время как в секундах (причем либо числом, либо строкой), так и в минутах и секундах (с помощью двух целочисленных значений). В этой программе используется стандартная библиотечная функция clock(), которая возвращает количество сигналов, принятых от системных часов с момента начала выполнения программы. Вот как выглядит прототип этой функции:


clock_t clock();	

Тип clock_t представляет собой разновидность длинного целочисленного типа. Операция деления значения, возвращаемого функцией clock(), на значение CLOCKS_PER_SEC позволяет преобразовать результат в секунды. Как прототип для функции clock(), так и определение константы CLOCKS_PER_SEC принадлежат заголовку <ctime>.


// Использование перегруженных конструкторов.
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
class timer{
int seconds;
public:
// секунды, задаваемые в виде строки
timer(char *t) { seconds = atoi (t); }
// секунды, задаваемые в виде целого числа
timer(int t) { seconds = t; }
// время, задаваемое в минутах и секундах
timer(int min, int sec) { seconds = min*60 + sec; }
void run();
};
void timer::run()
{
clock_t t1;
t1 = clock();
while( (clock()/CLOCKS_PER_SEC - t1/CLOCKS_PER_SEC) < seconds);
cout << "\a"; // звуковой сигнал
}
int main()
{
timer a (10), b("20"), c(1, 10);
a.run(); // отсчет 10 секунд
b.run(); // отсчет 20 секунд
c.run(); // отсчет 1 минуты и 10 секунд
return 0;
}	

При создании в функции main() объектов а, b и с класса timer им присваиваются начальные значения тремя различными способами, поддерживаемыми перегруженными функциями конструкторов. В каждом случае вызывается конструктор, который соответствует заданному списку параметров и потому надлежащим образом инициализирует "свой" объект.

На примере предыдущей программы вы, возможно, не оценили значимость перегрузки функций конструктора, поскольку здесь можно было обойтись единым способом задания временного интервала. Но если бы вы создавали библиотеку классов на заказ, то вам стоило бы предусмотреть набор конструкторов, охватывающий самый широкий спектр различных форматов инициализации, тем самым обеспечив других программистов наиболее подходящими для их программ форматами. Кроме того, как будет показано ниже, в C++ существует атрибут, который делает перегруженные конструкторы особенно ценным средством инициализации объектов.

Динамическая инициализация

В C++ как локальные, так и глобальные переменные можно инициализировать во время выполнения программы. Этот процесс иногда называют динамической инициализацией. До сих пор в большинстве инструкций инициализации, представленных в этой книге, использовались константы. Однако переменную можно также инициализировать во время выполнения программы, используя любое С++-выражение, действительное на момент объявления этой переменной. Это означает, что переменную можно инициализировать с помощью других переменных и/или вызовов функций при условии, что в момент выполнения инструкции объявления общее выражение инициализации имеет действительное значение. Например, следующие варианты инициализации переменных абсолютно допустимы в C++,


int n = strlen(str);
double arc = sin(theta);
float d = 1.02 * count / deltax;

Применение динамической инициализации к конструкторам

Подобно простым переменным, объекты можно инициализировать динамически при их создании. Это средство позволяет создавать объект нужного типа с использованием информации, которая становится известной только во время выполнения программы. Чтобы показать, как работает механизм динамической инициализации, модифицируем программу реализации таймера, приведенную в предыдущем разделе.

Вспомните, что в первом примере программы таймера мы не получили большого преимущества от перегрузки конструктора timer(), поскольку все объекты этого типа инициализировались с помощью констант, известных во время компиляции программы. Но в случаях, когда объект необходимо инициализировать во время выполнения программы, можно получить существенный выигрыш от наличия множества разных форматов инициализации. Это позволяет программисту выбрать из существующих конструкторов тот, который наиболее точно соответствуют текущему формату данных. Например, в следующей версии программы таймера для создания двух объектов b и с используется динамическая инициализация.


// Демонстрация динамической инициализации.
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
class timer{
int seconds;
public:
// секунды, задаваемые в виде строки
timer(char *t) { seconds = atoi(t); }
// секунды, задаваемые в виде целого числа
timer(int t) { seconds = t; }
// время, задаваемое в минутах и секундах
timer(int min, int sec) { seconds = min*60 + sec; }
void run();
};
void timer::run()
{
clock_t t1;
t1 = clock();
while((clock()/CLOCKS_PER_SEC - t1/CLOCKS_PER_SEC) < seconds);
cout << "\a"; // звуковой сигнал
}
int main()
{
timer a(10);
a.run();
cout << "Введите количество секунд: ";
char str[80];
cin >> str;
timer b(str); // инициализация в динамике
b.run();
cout << "Введите минуты и секунды: ";
int min, sec;
cin >> min >> sec;
timer с(min, sec); // инициализация в динамике
c.run();
return 0;
}

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

Механизм перегрузки конструкторов способствует понижению уровня сложности программ, позволяя создавать объекты наиболее естественным для их применения образом. Поскольку существует три наиболее распространенных способа передачи объекту значений временных интервалов, имеет смысл позаботиться о том, чтобы конструктор timer() был перегружен для реализации каждого из этих способов. При этом перегрузка конструктора timer() для приема значения, выраженного в днях или наносекундах, вряд ли себя оправдает. Загромождение кода конструкторами для обработки редко возникающих ситуаций оказывает, как правило, дестабилизирующее влияние на программу.

Узелок на память. Разрабатывая перегруженные конструкторы, необходимо определиться в том, какие ситуации важно предусмотреть, а какие можно и не учитывать.

Присваивание объектов

Если два объекта имеют одинаковый тип (т.е. оба они — объекты одного класса), то один объект можно присвоить другому. Для присваивания недостаточно, чтобы два класса были физически подобны; имена классов, объекты которых участвуют в операции присваивания, должны совпадать. Если один объект присваивается другому, то по умолчанию данные первого объекта поразрядно копируются во второй. Присваивание объектов демонстрируется в следующей программе.


// Демонстрация присваивания объектов.
#include <iostream>
using namespace std;
class myclass {
int a, b;
public:
void setab(int i, int j) { a = i, b = j; }
void showab();
};
void myclass::showab()
{
cout << "а равно " << a << '\n';
cout << "b равно " << b << '\n';
}
int main()
{
myclass ob1, ob2;
ob1.setab(10, 20);
ob2.setab(0, 0);
cout << "Объект ob1 до присваивания: \n";
ob1.showab();
cout << "Объект ob2 до присваивания: \n";
ob2.showab();
cout << ' \n';
ob2 = ob1; // Присваиваем объект ob1 объекту ob2.
cout << "Объект ob1 после присваивания: \n";
ob1.showab();
cout << "Объект ob2 после присваивания: \n";
ob2.showab();
return 0;
}
При выполнении программа генерирует такие результаты.
Объект ob1 до присваивания:
а равно 10
b равно 20
Объект ob2 до присваивания:
а равно 0
b равно 0
Объект ob1 после присваивания:
а равно 10
b равно 20
Объект ob2 после присваивания:
а равно 10
b равно 20	

По умолчанию все данные из одного объекта присваиваются другому путем создания поразрядной копии. (Другими словами, создается точный дубликат объекта.) Но, как будет показано ниже, оператор присваивания можно перегрузить, определив собственные операции присваивания.

Узелок на память. Присваивание одного объекта другому просто делает их данные идентичными, но эти два объекта остаются совершенно независимыми. Следовательно, последующая модификация данных одного объекта не оказывает никакого влияния на данные другого.

Передача объектов функциям

Объект можно передать функции точно так же, как значение любого другого типа данных. Объекты передаются функциям путем использования обычного С++-соглашения о передаче параметров по значению. Таким образом, функции передается не сам объект, а его копия. Следовательно, изменения, внесенные в объект при выполнении функции, не оказывают никакого влияния на объект, используемый в качестве аргумента для функции.


Этот механизм демонстрируется в следующей программе.
#include <iostream>
using namespace std;
class OBJ {
int i;
public:
void set_i(int x) { i = x; }
void out_i() { cout << i << " "; }
};
void f(OBJ x)
{
x.out_i(); // Выводит число.
х.set_i(100); // Устанавливает только локальную копию.
x.out_i(); // Выводит число 100.
}
int main()
{
OBJ о;
о.set_i(10);
f(о);
o.out_i(); // По-прежнему выводит число 10, значение
переменной i не изменилось.
return 0;
}
Вот как выглядят результаты выполнения этой программы.
10 100 10
Как подтверждают эти результаты, модификация объекта x в функции f() не влияет на
объект o в функции main().