Конструкторы и деструкторы
Конструктор — это функция, которая вызывается при создании объекта. Как правило, некоторую часть объекта, прежде чем его можно будет использовать, необходимо инициализировать. Например, рассмотрим класс queue (он представлен выше в этой главе). Прежде чем класс queue можно будет использовать, переменным rloc и sloc нужно присвоить нулевые значения. В данном конкретном случае это требование выполнялось с помощью функции init(). Но, поскольку требование инициализации членов класса весьма распространено, в C++ предусмотрена реализация этой возможности при создании объектов класса. Такая автоматическая инициализация выполняется благодаря использованию конструктора.
Конструктор — это специальная функция, которая является членом класса и имя которой совпадает с именем класса. Вот, например, как стал выглядеть класс queue после переделки, связанной с применением конструктора для инициализации его членов.
// Определение класса queue.
class queue {
int q[100];
int sloc, rloc;
public:
queue(); // конструктор
void qput(int i);
int qget();
};
Обратите внимание на то, что в объявлении конструктора queue() отсутствует тип возвращаемого значения. В C++ конструкторы не возвращают значений и, следовательно, нет смысла в указании их типа. (При этом нельзя указывать даже тип void.)
Теперь приведем код функции queue().
// Определение конструктора.
queue::queue()
{
sloc = rloc = 0;
cout << "Очередь инициализирована.\n";
}
В данном случае при выполнении конструктора выводится сообщение Очередь инициализирована., которое служит исключительно иллюстративным целям. На практике же в большинстве случаев конструкторы не выводят никаких сообщений.
Конструктор объекта вызывается при создании объекта. Это означает, что он вызывается при выполнении инструкции объявления объекта. Конструкторы глобальных объектов вызываются в самом начале выполнения программы, еще до обращения к функции main(). Что касается локальных объектов, то их конструкторы вызываются каждый раз, когда встречается объявление такого объекта.
Деструктор
Деструктор — это функция, которая вызывается при разрушении объекта. Дополнением к конструктору служит деструктор. Во многих случаях при разрушении объекту необходимо выполнить некоторое действие или даже некоторую последовательность действий. Локальные объекты создаются при входе в блок, в котором они определены, и разрушаются при выходе из него. Глобальные объекты разрушаются при завершении программы. Существует множество факторов, обуславливающих необходимость деструктора. Например, объект должен освободить ранее выделенную для него память. В C++ именно деструктору поручается обработка процесса дезактивизации объекта. Имя деструктора совпадает с именем конструктора, но предваряется символом "~" Подобно конструкторам деструкторы не возвращают значений, а следовательно, в их объявлениях отсутствует тип возвращаемого значения.
Рассмотрим уже знакомый нам класс queue, но теперь он содержит конструктор и деструктор. (Справедливости ради отметим, что классу queue деструктор, по сути, не нужен, а его наличие здесь можно оправдать лишь иллюстративными целями.)
// Определение класса queue.
class queue {
int q[100];
int sloc, rloc;
public:
queue(); // конструктор
~queue(); // деструктор
void qput(int i);
int qget();
};
// Определение конструктора.
queue::queue()
{
sloc = rloc = 0;
cout << "Очередь инициализирована.\n";
}
// Определение деструктора.
queue::~queue()
{
cout << "Очередь разрушена.\n";
}
Вот как выглядит новая версия программы реализации очереди, в которой
демонстрируется использование конструктора и деструктора.
// Демонстрация использования конструктора и деструктора.
#include <iostream>
using namespace std;
// Определение класса queue.
class queue {
int q[100];
int sloc, rloc;
public:
queue(); // конструктор
~queue(); // деструктор
void qput(int i);
int qget();
};
// Определение конструктора.
queue::queue()
{
sloc = rloc = 0;
cout << "Очередь инициализирована.\n";
}
// Определение деструктора.
queue::~queue()
{
cout << "Очередь разрушена.\n";
}
// Занесение в очередь целочисленного значения.
void queue::qput(int i)
{
if(sloc==100) {
cout << "Очередь заполнена.\n";
return;
}
sloc++;
q[sloc] = i;
}
// Извлечение из очереди целочисленного значения.
int queue::qget()
{
if(rloc == sloc) {
cout << "Очередь пуста.\n";
return 0;
}
rloc++;
return q[rloc];
}
int main()
{
queue a, b; // Создание двух объектов класса queue.
a.qput(10);
b.qput(19);
a.qput(20);
b.qput(1);
cout << a.qget() << " ";
cout << a.qget() << "\n";
cout << b.qget() << " ";
cout << b.qget() << "\n";
return 0;
}
При выполнении этой программы получаются такие результаты.
Очередь инициализирована.
Очередь инициализирована.
10 20
19 1
Очередь разрушена.
Очередь разрушена.
Параметризованные конструкторы
Конструктор может иметь параметры. С их помощью при создании объекта членам данных (переменным класса) можно присвоить некоторые начальные значения, определяемые в программе. Это реализуется путем передачи аргументов конструктору объекта. В следующем примере мы усовершенствуем класс queue так, чтобы он принимал аргументы, которые будут служить идентификационными номерами (ID) очереди. Прежде всего необходимо внести изменения в определение класса queue. Теперь оно выглядит так.
// Определение класса queue.
class queue {
int q[100];
int sloc, rloc;
int who; // содержит идентификационный номер очереди
public:
queue(int id); // параметризованный конструктор
~queue(); // деструктор
void qput(int i);
int qget();
};
Переменная who используется для хранения идентификационного номера (ID) создаваемой программой очереди. Ее реальное значение определяется значением, передаваемым конструктору в качестве параметра id, при создании переменной типа queue. Конструктор queue() выглядит теперь следующим образом.
// Определение конструктора. queue::queue(int id) { sloc = rloc = 0; who = id; cout << "Очередь " << who << " инициализирована.\n"; }
Чтобы передать аргумент конструктору, необходимо связать этот аргумент с объектом при объявлении объекта. C++ поддерживает два способа реализации такого связывания. Вот как выглядит первый способ.
queue а = queue (101);
В этом объявлении создается очередь с именем a, которой передается значение (идентификационный номер) 101. Но эта форма (в таком контексте) используется редко, поскольку второй способ имеет более короткую запись и удобнее для использования. Во втором способе аргумент должен следовать за именем объекта и заключаться в круглые скобки. Например, следующая инструкция эквивалентна предыдущему объявлению,
queue а (101);
Это самый распространенный способ объявления параметризованных объектов. Опираясь на этот метод, приведем общий формат передачи аргументов конструкторам.
тип_класса имя_переменной(список_аргументов);
Здесь элемент список_аргументов представляет собой список разделенных запятыми аргументов, передаваемых конструктору.
На заметку. Формально между двумя приведенными выше формами инициализации существует небольшое различие, которое вы поймете при дальнейшем чтении этой книги. Но это различие не влияет на результаты выполнения программ, представленных в этой главе.
В следующей версии программы организации очереди демонстрируется использование параметризованного конструктора.
// Использование параметризованного конструктора.
#include <iostream>
using namespace std;
// Определение класса queue.
class queue {
int q[100];
int sloc, rloc;
int who; // содержит идентификационный номер очереди
public:
queue(int id); // параметризованный конструктор
~queue(); // деструктор
void qput(int i);
int qget();
};
// Определение конструктора.
queue::queue(int id)
{
sloc = rloc = 0;
who = id;
cout << "Очередь " << who << " инициализирована.\n";
}
// Определение деструктора.
queue::~queue()
{
cout << "Очередь " << who << " разрушена.\n";
}
// Занесение в очередь целочисленного значения.
void queue::qput(int i)
{
if(sloc==100) {
cout << "Очередь заполнена.\n";
return;
}
sloc++;
q[sloc] = i;
}
// Извлечение из очереди целочисленного значения.
int queue::qget()
{
if(rloc == sloc) {
cout << "Очередь пуста.\n";
return 0;
}
rloc++;
return q[rloc];
}
int main()
{
queue a(1), b(2); // Создание двух объектов класса queue.
a.qput(10);
b.qput(19);
a.qput(20);
return 0;
}
При выполнении эта версия программы генерирует такие результаты:
Очередь 1 инициализирована.
Очередь 2 инициализирована.
10 20 19 1
Очередь 2 разрушена.
Очередь 1 разрушена.
Как видно из кода функции main(), очереди, связанной с именем а, присваивается идентификационный номер 1, а очереди, связанной с именем b, — идентификационный номер 2.
Несмотря на то что в примере с использованием класса queue при создании объекта передается только один аргумент, в общем случае возможна передача двух аргументов и более. В следующем примере объектам типа widget передается два значения.
#include <iostream>
using namespace std;
class widget {
int i;
int j;
public:
widget(int a, int b);
void put_widget();
};
// Передаем 2 аргумента конструктору widget().
widget::widget(int a, int b)
{
i = a; j = b;
}
void widget::put_widget()
{
cout << i << " " << j << "\n";
}
int main()
{
widget x(10, 20), y(0, 0);
x.put_widget();
у.put_widget();
return 0;
}
Важно! В отличие от конструкторов, деструкторы не могут иметь параметров.
Причину понять нетрудно: не существует средств передачи аргументов объекту, который
разрушается. Если же у вас возникнет та редкая ситуация, когда при вызове деструктора
вашему объекту необходимо получить доступ к некоторым данным, определяемым только
во время выполнения программы, создайте для этой цели специальную переменную. Затем
непосредственно перед разрушением объекта установите эту переменную равной нужному
значению.
При выполнении эта программа отображает следующие результаты.
10 20
0 0
Альтернативный вариант инициализации объекта
Если конструктор принимает только один параметр, можно использовать альтернативный способ инициализации членов объекта. Рассмотрим следующую программу.
#include <iostream>
using namespace std;
class myclass {
int a;
public:
myclass(int x);
int get_a();
};
myclass::myclass(int x)
{
a = x;
}
int myclass::get_a()
{
return a;
}
int main()
{
myclass ob = 4; // вызов функции myclass(4)
cout << ob.get_a();
return 0;
}
Здесь конструктор для объектов класса myclass принимает только один параметр. Обратите внимание на то, как в функции main() объявляется объект ob. Для этого используется такой формат объявления:
myclass ob = 4;
В этой форме инициализации объекта число 4 автоматически передается параметру х при вызове конструктора myclass(). Другими словами, эта инструкция объявления обрабатывается компилятором так, как если бы она была записана следующим образом.
myclass ob = myclass(4);
В общем случае, если у вас есть конструктор, который принимает только один аргумент, для инициализации объекта вы можете использовать либо вариант ob(х), либо вариант ob=х. Дело в том, что при создании конструктора с одним аргументом неявно создается преобразование из типа этого аргумента в тип этого класса.
Помните, что показанный здесь альтернативный способ инициализации объектов применяется только к конструкторам, которые имеют только один параметр.
Классы и структуры — родственные типы
Как упоминалось в предыдущей главе, в C++ структура также обладает объектноориентированными возможностями. В сущности, классы и структуры можно назвать близкими родственниками. За одним исключением, они взаимозаменяемы, поскольку структура также может включать данные и код, который манипулирует этими данными точно так же, как это может делать класс. Единственное различие между С++-структурой и С++-классом состоит в том, что по умолчанию члены класса являются закрытыми, а члены структуры — открытыми. В остальном же структуры и классы имеют одинаковое назначение. На самом деле в соответствии с формальным синтаксисом C++ объявление структуры создает тип класса. Рассмотрим пример структуры со свойствами, подобными свойствам класса
// Использование структуры для создания класса.
#include <iostream>
using namespace std;
struct cl {
int get_i(); // Эти члены открыты (public)
void put_i(int j); // по умолчанию.
private:
int i;
};
int cl::get_i()
{
return i;
}
void cl::put_i(int j)
{
i = j;
}
int main()
{
cl s;
s.put_i (10);
cout << s.get_i();
return 0;
}
В этой программе определяется тип структуры с именем cl, в которой функции-члены get_i() и put_i() являются открытыми (public), а член данных i — закрытым (private). Обратите внимание на то, что в структурах для объявления закрытых членов используется ключевое слово private.
Ключевое слово private используется для объявления закрытых членов класса. В следующем примере показана эквивалентная программа, которая использует вместо типа struct тип class.
// Использование типа class вместо типа struct.
#include <iostream>
using namespace std;
class cl {
int i; // закрытый член по умолчанию
public:
int get_i();
void put_i(int j);
};
int cl::get_i()
{
return i;
}
void cl::put_i(int j)
{
i = j;
}
int main()
{
cl s;
s.put_i(10);
cout << s.get_i();
return 0;
}
Иногда С++-программисты к структурам, которые не содержат функции-члены, применяют термин POD-struct.
С++-программисты тип class используют главным образом для определения формы объекта, который содержит функции-члены, а тип struct — для создания объектов, которые содержат только члены данных. Иногда для описания структуры, которая не содержит функции-члены, используется аббревиатура POD (Plain Old Data).
Сравнение структур с классами
Тот факт, что и структуры, и классы обладают практически идентичными возможностями, создает впечатление избыточности. Многие новички в программировании на C++ недоумевают, почему в нем существует такое очевидное дублирование. Нередко приходится слышать предложения отказаться от ненужного ключевого слова (class или struct) и оставить только одно из них.
Ответ на эту цепь рассуждений лежит в происхождении языка C++ от С и намерении сохранить C++ совместимым снизу вверх с С. В соответствии с современным определением C++ стандартная С-структура одновременно является совершенно законной С++-структурой. В языке С, который не содержит ключевых слов public или private, все члены структуры являются открытыми. Вот почему и члены С++-структур по умолчанию являются открытыми (а не закрытыми). Поскольку конструкция типа class специально разработана для поддержки инкапсуляции, есть определенный смысл в том, чтобы по умолчанию ее члены были закрытыми. Следовательно, чтобы избежать несовместимости с языком С в этом вопросе, аспекты доступа, действующие по умолчанию, менять было нельзя, поэтому и решено было добавить новое ключевое слово. Но в перспективе можно говорить о более веской причине для отделения структур от классов. Поскольку тип class синтаксически отделен от типа struct, определение класса вполне открыто для эволюционных изменений, которые синтаксически могут оказаться несовместимыми с С-подобными структурами. Поскольку мы имеем дело с двумя отдельными типами, будущее направление развития языка C++ не обременяется "моральными обязательствами", связанными с совместимостью с С-структурами. Под "занавес" этой темы отметим следующее. Структура определяет тип класса. Следовательно, структура является классом. На этом настаивал создатель языка C++, Бьерн Страуструп. Он полагал, что если структура и классы будут более или менее эквивалентны, то переход от С к C++ станет проще. И история доказала его правоту!
Объединения и классы — родственные типы
Тот факт, что структуры и классы — родственны, обычно никого не удивляет; однако вы можете удивиться, узнав, что объединения также связаны с классами "близкими отношениями". Согласно определению C++ объединение — это, по сути, тот же класс, в котором все члены данных хранятся в одной и той же области. (Таким образом, объединение также определяет тип класса.) Объединение может содержать конструктор и деструктор, а также функции-члены. Конечно же, члены объединения по умолчанию открыты (public), а не закрыты (private).
Рассмотрим программу, в которой объединение используется для отображения символов, составляющих содержимое старшего и младшего байтов короткого целочисленного значения (предполагается, что короткие целочисленные значения занимают в памяти компьютера два байта).
// Создание класса на основе объединения.
#include <iostream>
using namespace std;
union u_type {
u_type(short int a); // открытые члены по умолчанию
void showchars();
short int i;
char ch[2];
};
// конструктор
u_type::u_type(short int a)
{
i = a;
}
// Отображение символов, составляющих значение типа short int.
void u_type::showchars()
{
cout << ch[0] << " ";
cout << ch[1] << "\n";
}
int main()
{
u_type u(1000);
u.showchars();
return 0;
}
Подобно структуре, С++-объединение также произошло от своего С-предшественника. Но в C++ оно имеет более широкие "классовые" возможности. Однако лишь то, что C++ наделяет "свои" объединения более могучими средствами и большей степенью гибкости, не означает, что вы непременно должны их использовать. Если вас вполне устраивает объединение с традиционным стилем поведения, вы вольны именно таким его и использовать. Но в случаях, когда можно инкапсулировать данные объединения вместе с функциями, которые их обрабатывают, все же стоит воспользоваться С++-возможностями, что придаст вашей программе дополнительные преимущества.
Встраиваемые функции
Прежде чем мы продолжим освоение класса, сделаем небольшое, но важное отступление. Оно не относится конкретно к объектно-ориентированному программированию, но является очень полезным средством C++, которое довольно часто используется в определениях классов. Речь идет о встраиваемой, или подставляемой, функции (inline function). Встраиваемой называется функция, код которой подставляется в то место строки программы, из которого она вызывается, т.е. вызов такой функции заменяется ее кодом. Существует два способа создания встраиваемой функции. Первый состоит в использовании модификатора inline. Например, чтобы создать встраиваемую функцию f(), которая возвращает int-значение и не принимает ни одного параметра, достаточно объявить ее таким образом.
inline int f()
{
// ...
}
Модификатор inline должен предварять все остальные аспекты объявления функции. Встраиваемая функция — это небольшая (по объему кода) функция, код которой подставляется вместо ее вызова.
Причиной существования встраиваемых функций является эффективность. Ведь при каждом вызове обычной функции должна быть выполнена целая последовательность инструкций, связанных с обработкой самого вызова, включающего помещение ее аргументов в стек, и с возвратом из функции. В некоторых случаях значительное количество циклов центрального процессора используется именно для выполнения этих действий. Но если функция встраивается в строку программы, подобные системные затраты попросту отсутствуют, и общая скорость выполнения программы возрастает. Если же встраиваемая функция оказывается не такой уж маленькой, общий размер программы может существенно увеличиться. Поэтому лучше всего в качестве встраиваемых использовать только очень маленькие функции, а те, что побольше, — оформлять в виде обычных.
Продемонстрируем использование встраиваемой функции на примере следующей
программы.
#include <iostream>
using namespace std;
class cl {
int i; // закрытый член по умолчанию
public:
int get_i();
void put_i(int j);
};
inline int cl::get_i()
{
return i;
}
inline void cl::put_i(int j)
{
i = j;
}
int main()
{
cl s;
s.put_i(10);
cout << s.get_i();
return 0;
}
Здесь вместо вызова функций get_i() и put_i() подставляется их код. Так, в функции
main() строка
s.put_i(10);
функционально эквивалентна следующей инструкции присваивания:
s.i = 10;
Поскольку переменная i по умолчанию закрыта в рамках класса cl, эта строка не может реально существовать в коде функции main(), но за счет встраивания функции put_i() мы достигли того же результата, одновременно избавившись от затрат системных ресурсов, связанных с вызовом функции.
Важно понимать, что в действительности использование модификатора inline является запросом, а не командой, по которой компилятор сгенерирует встраиваемый (inline-) код. Существуют различные ситуации, которые могут не позволить компилятору удовлетворить наш запрос. Вот несколько примеров
■ Некоторые компиляторы не генерируют встраиваемый код, если соответствующая функция содержит цикл, конструкцию switch или инструкцию goto. ■ Чаще всего встраиваемыми не могут быть рекурсивные функции. ■ Как правило, встраивание "не проходит" для функций, которые содержат статические (static) переменные
Узелок на память. Ограничения на использование встраиваемых функций зависят от конкретной реализации системы, поэтому, чтобы узнать, какие ограничения имеют место в вашем случае, обратитесь к документации, прилагаемой к вашему компилятору.
Использование встраиваемых функций в определении класса
Существует еще один способ создания встраиваемой функции. Он состоит в определении кода для функции-члена класса в самом объявлении класса. Любая функция, которая определяется в объявлении класса, автоматически становится встраиваемой. В этом случае необязательно предварять ее объявление ключевым словом inline. Например, предыдущую программу можно переписать в таком виде.
#include <iostream>
using namespace std;
class cl {
int i; // закрытый член по умолчанию
public:
// автоматически встраиваемые функции
int get_i() { return i; }
void put_i(int j) { i = j; }
};
int main()
{
s.put_i(10);
cout << s.get_i();
return 0;
}
Здесь функции get_i() и put_i() определены в теле объявления класса cl и автоматически
являются встраиваемыми.
Обратите внимание на то, как выглядит код функций, определенных "внутри" класса cl.
Для очень небольших по объему функций такое представление кода отражает обычный
стиль языка C++. Однако можно сформатировать эти функции и таким образом.
class cl {
int i; // закрытый член по умолчанию
public:
// встраиваемые функции
int get_i()
{
return i;
}
void put_i(int j)
{
i = j;
}
};
В общем случае небольшие функции (как представленные в этом примере) определяются
в объявлении класса. Это соглашение применяется и к остальным примерам данной книги.
Важно! Определение небольших функций-членов в объявлении класса — обычная
практика в С++-программировании. И дело даже не в средстве автоматического
встраивания, а просто в удобстве. Вряд ли вы встретите в профессиональных программах,
чтобы короткие функции-члены определялись вне их класса.