Цель работы: изучение назначения и свойств конструкторов и деструкторов класса; ознакомление с различными типами конструкторов; приобретение навыков программирования классов с конструкторами и деструкторами.
Теоретическая часть
Любому объекту класса требуется память и некоторое начальное значение. Чтобы клиент класса мог использовать объекты как собственные типы, класс нуждается в механизме создания и уничтожения объектов. Эта проблема в С++ решается с помощью специальных функций-членов класса: конструктора и деструктора.
Конструктор – это функция-член класса, имя которой совпадает с именем класса. Ниже перечислены основные свойства конструктора:
1. Конструктор не возвращает значение, даже void. Нельзя создавать указатель на конструктор.
2. Класс может иметь несколько конструкторов с разными параметрами. Параметры конструктора могут иметь любой тип, кроме этого же класса.
3. Если класс не содержит явного определения конструктора, компилятор создает его автоматически.
4. Конструктор не наследуется.
5. Конструктор нельзя описать с модификаторами const,virtual и static.
6. Конструктор для глобальных объектов вызывается до вызова функции main(). Локальные объекты создаются, как только становится активной область их действия. Конструктор запускается и при создании временного объекта (например, при передаче объекта из функции).
Ниже представлен небольшой класс с конструктором.
#include <iostream.h>
сlass A
{ int a;
public:
A(); // объявление конструктора
void Print ();
};
A::A() // определение конструктора
{a=5; }
void A::Print ()
{cout <<a;}
main ()
{A ob; //создание объекта
ob.Print ();
return 0;}
В этом примере класс включает две открытые функции: А() и Print ().
Функция А() – это конструктор без параметров. Имя функции совпадает с именем класса. Определение конструктора находится вне класса, поэтому использована операция доступа к области видимости (::). Конструктор инициирует закрытую переменную a значением 5. Вызов конструктора производится при объявлении объекта класса
А ob;
и это приведет к выполнению записанных в конструкторе действий.
Функцией, обратной конструктору, является деструктор. Эта функция вызывается при удалении объекта. Имя деструктора совпадает с именем класса, но с символом ~ (тильда) в начале. Ниже перечислены основные свойства деструктора:
1. Деструктор не возвращает значение, даже void.
2. Класс может иметь только один деструктор.
3. Если класс не содержит явного определения деструктора, компилятор создает его автоматически.
4. Деструктор не может иметь параметров.
5. Деструктор не наследуется.
6. Деструктор не может быть объявлен с модификатором const, static или virtual.
7. Деструктор для глобальных объектов вызывается при завершении программы. Локальные объекты удаляются тогда, когда они выходят из области видимости.
Пример класса с деструктором приведен ниже.
#include <iostream.h>
сlass A
{ int a;
public:
A() {a=10;}
~A() {}
void Print () {cout <<a;}
};
main ()
{A ob;
ob.Print ();
return 0;
}
В этом классе три функции: конструктор А(), деструктор ~A() и функция Print (). Все функции определены в классе. Деструктор не выполняет каких-либо действий, кроме удаления объекта.
Фактически как конструктор, так и деструктор могут выполнять любой тип операций. Однако применение конструктора или деструктора для действий, прямо не связанных с инициализацией объектов и их удалением, является очень плохим стилем программирования.
Конструктор, не требующий аргументов, называется конструктором по умолчанию. Это может быть конструктор с пустым списком аргументов, или конструктор, у которого все аргументы имеют значения по умолчанию. Конструктор по умолчанию имеет специальное назначение при инициализации массивов объектов своего класса. Если у класса нет конструктора, компилятор предоставляет конструктор по умолчанию. Если конструктор у класса есть, но нет конструктора по умолчанию, размещение массива вызовет синтаксическую ошибку.
Как было отмечено выше, класс может содержать несколько конструкторов. Можно определить в классе несколько конструкторов с различными наборами аргументов. Возможности инициализации объектов в таком случае расширяются.
Создадим класс, в котором объявим два конструктора.
сlass String
{ char * str;
int length;
public:
String (); // конструктор по умолчанию
String (const char *p); // конструктор с параметром
};
// определение первого конструктора
String::String ()
{
str =0;
length=0;
}
// определение второго конструктора
String:: String (const char *p)
{
length = strlen(p);
str = new char [length +1];
if (str==0)
{
//обработка ошибки выделения динамической памяти
}
strcpy(str, p); // копирование строки
}
Теперь можно, создавая переменные типа String, инициализировать их тем или иным образом:
char *cp;
String s1; // выполняется конструктор по умолчанию
String s2 (“начальное значение”); // выполняется 2-й конструктор
String *sptr = new String; //выполняется конструктор по умолчанию
String * ssptr = new String (cp); //выполняется 2-й конструктор
Существует специальный синтаксис для инициализации отдельных частей объекта с помощью конструктора. Инициализация осуществляется не в теле конструктора, а с помощью списка инициализации элементов. Список инициализации отделяется двоеточием от заголовка определения функции и предшествует телу конструктора. Отдельные члены инициализируются по следующему формату:
имя_члена_класса (значение)
и перечисляются через запятую.
Преобразуем класс А так, чтобы в нем был конструктор со списком инициализации:
сlass A
{ int a,b;
public:
A(int x, int y);
};
A::A(int x, int y): a(x), b(y)
{ }
Список инициализации является единственным методом инициализации данных – констант и ссылок. Если членом класса является объект, конструктор которого требует задания значений одного или нескольких параметров, то единственно возможным способом его инициализации также является список инициализации.
В С++ имеется специальный вид конструктора, который называется конструктор копирования. Он получает в качестве единственного параметра объект этого же класса. При выполнении копирующего конструктора создается объект – копия существующего объекта. Такой конструктор может понадобиться либо при создании нового объекта с инициализацией другим объектом, либо при передаче объекта в функцию по значению; либо при возврате объекта из функции.
Если в классе не определен конструктор копирования, то при необходимости он создается автоматически. Такой конструктор выполняет копирование с помощью почленной инициализации. Она может не работать при некоторых обстоятельствах для сложных агрегатов с членами, являющимися указателями. В таких случаях указатель может оказаться связанным с объектом, который уже автоматически удален, так как вышел за пределы области видимости. Процесс дублирования значения указателя, а не объекта, на который он указывает, может привести к аномалиям. Поэтому в таких случаях для классов желательно явно определить копирующий конструктор.
Для класса String он может выглядеть следующим образом:
сlass String
{
char * str;
int length;
public:
String (const char *p);
String (const String & s); // копирующий конструктор
};
String:: String (const String & s)
{ length = s.length;
str = new char [length +1];
strcpy(str, s.str);
}
main()
{
//создание первого объекта
String ob (“Строка”);
// создание нового объекта – копии первого
String ob1(ob);
return 0;
}
Таким образом, новый объект является копией своего аргумента. При этом новый объект независим от первого в том смысле, что изменение значения одного не изменяет значения другого. Столь логичное поведение объектов класса String на самом деле обусловлено наличием копирующего конструктора. Если бы его не было, компилятор создал бы его по умолчанию, и такой конструктор просто копировал бы все атрибуты класса, т.е. был бы эквивалентен:
String:: String (const String & s)
{ length=s.length;
str = s.str;
}
Еще раз отметим, что для объектов любого класса, использующих в своей реализации указатели, надо задавать копирующий конструктор явно.
Одна из задач объектно-ориентированного программирования на С++ состоит в интеграции определяемых пользователем типов данных и встроенных типов. Для этого существует механизм, позволяющий функциям – членам обеспечивать преобразования. Эти преобразования могут быть как явными, так и неявными. Конструкторы с одним параметром автоматически являются функциями преобразования. Они преобразуют тип параметра к типу класса.
Следующий пример это демонстрирует.
сlass A
{
int a;
public:
A (); // конструктор по умолчанию
A (int); // конструктор преобразования типа int
A (long); // конструктор преобразования типа long
};
A:: A () { a=0;}
A:: A (int _a) {a =_a;}
A:: A (double _a) {a =_a;}
//некоторая функция, параметром которой является объект класса
void f (A & ob)
{
// тело функции
}
main ()
{
f (10); // преобразование int в объект класса А
f (10.5); // преобразование double в объект класса А
A ob;
f (ob); // преобразование не нужно
return 0;
}
В рассмотренном примере осуществляется неявное преобразование типа данных. Неявное потому, что исходный код не вызывает явно конструктор для преобразования. С этой целью компилятор каждый раз будет создавать временный объект класса А, которому и будет передавать параметр, переданный в функцию.
Этот же способ преобразования типа данных может быть использован и для преобразования из одного класса в другой.
Определенные пользователем преобразования типа применяются компилятором в следующих ситуациях:
- при инициализации объекта;
- при вызове функции;
- при возврате функцией значений.
Недавно в языке С++ появилось ключевое слово explicit. Оно представляет собой спецификатор объявления, который может применяться только в объявлениях конструкторов (но не в определениях конструкторов вне класса). Конструктор, объявленный со спецификатором explicit, не принимает участия в неявных преобразованиях типа. Он может быть использован только в том случае, когда никакого преобразования типа аргументов не требуется.
Нет смысла применять ключевое слово explicit к конструкторам с несколькими параметрами, так как такие конструкторы не принимают участия в неявных преобразованиях типа.