Приветствую Вас Гость | RSS

ALLDev

Суббота, 21.12.2024, 15:45
Главная » Статьи » Программирование » C/C++/C#

Учебник C++. Классы – конструкторы и деструкторы. Чать 1

При определении класса имеется возможность задать для объекта начальное значение. Специальный метод класса, называемый конструктором, выполняется каждый раз, когда создается новый объект этого класса.Конструктор – это метод, имя которого совпадает с именем класса. Конструктор не возвращает никакого значения.

Для класса String имеет смысл в качестве начального значения использовать пустую строку:

class String
{
public:
 String(); // объявление конструктора
};
// определение конструктора
String::String()
{
 str = 0;
 length = 0;
}

Определив такой конструктор, мы гарантируем, что даже при создании автоматической переменной объект будет соответствующим образоминициализирован (в отличие от переменных встроенных типов).

Конструктор без аргументов называется стандартным конструктором или конструктором по умолчанию. Можно определить несколько конструкторов с различными наборами аргументов. Возможности инициализации объектов в таком случае расширяются. Для нашего класса строк было бы логично инициализировать переменную с помощью указателя на строку.

class String
{
public: 
 String(); // стандартный конструктор
 String(const char* p); 
 // дополнительный конструктор 

};
// определение второго конструктора
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("Начальное значение");
// выполняется стандартный конструктор
String* sptr = new String;
// выполняется второй конструктор
String* ssptr = new String(cp);

Копирующий конструктор

Остановимся чуть подробнее на одном из видов конструктора с аргументом, в котором в качестве аргумента выступает объект того же самого класса. Такой конструктор часто называюткопирующим , поскольку предполагается, что при его выполнении создается объект-копия другого объекта. Для класса String он может выглядеть следующим образом:

class String
{
public:
 String(const String& s);
};
String::String(const String& s)
{
 length = s.length;
 str = new char[length + 1];
 strcpy(str, s.str);
}

Очевидно, что новый объект будет копией своего аргумента. При этом новый объект независим от первоначального в том смысле, что изменение значения одного не изменяет значения другого.

// первый объект с начальным значением 
// "Astring"
String a("Astring");
// новый объект – копия первого,
// т.е. со значением "Astring"
String b(a);
// изменение значения b на "AstringAstring",
// значение объекта a не изменяется
b.Concat(a);

Столь логичное поведение объектов класса String на самом деле обусловлено наличием копирующего конструктора. Если бы его не было, компилятор создал бы его по умолчанию, и такойконструктор просто копировал бы все атрибуты класса, т.е. был бы эквивалентен:

String::String(const String& s)
{
 length = s.length;
 str = s.str;
}

При вызове метода Concat для объекта b произошло бы следующее: объект b перераспределил бы память под строку str, выделив новый участок памяти и удалив предыдущий (см. определение метода выше). Однако указатель str объекта a по-прежнему указывает на первоначальный участок памяти, только что освобожденный объектом b. Соответственно, значение объекта a испорчено.

Для класса Complex, который мы рассматривали ранее, кроме стандартного конструктора можно задать конструктор, строящий комплексное число из целых чисел:

class Complex
{
public:
 Complex();
 Complex(int rl, int im = 0);
 Complex(const Complex& c);
 // прибавить комплексное число
 Complex operator+(const Complex x) const; 
private: 
 int real; // вещественная часть
 int imaginary; // мнимая часть 

};
//
// Стандартный конструктор создает число (0,0)
//
Complex::Complex() : real(0), imaginary(0)
{}
//
// Создать комплексное число из действительной
// и мнимой частей. У второго аргумента есть 
// значение по умолчанию — мнимая часть равна 
// нулю
Complex::Complex(int rl, int im=0) :
 real(rl), imaginary(im)
{}
//
// Скопировать значение комплексного числа
//
Complex::Complex(const Complex& c) :
 real(c.real), imaginary(c.imaginary)
{}

Теперь при создании комплексных чисел происходит их инициализация:

Complex x1; // начальное значение – ноль
Complex x2(3); 
 // мнимая часть по умолчанию равна 0
 // создается действительное число 3
Complex x3(0, 1); // мнимая единица
Complex y(x3); // мнимая единица

Конструкторы, особенно копирующие, довольно часто выполняются неявно. Предположим, мы бы описали метод Concat несколько иначе:

Concat(String s);

вместо

Concat(const String& s);

т.е. использовали бы передачу аргумента по значению вместо передачи по ссылке. Конечный результат не изменился бы, однако при вызове метода

b.Concat(a)

компилятор создал бы временную переменную типа String – копию объекта a, и передал бы ее в качестве аргумента. При выходе из метода String эта переменная была бы уничтожена. Представляете, насколько снизилось бы быстродействие метода!

Второй пример вызова конструктора – неявное преобразование типа. Допустима запись вида:

b.Concat("LITERAL");

хотя сам метод определен только для аргумента – объекта типа String. Поскольку в классе String есть конструктор с аргументом – указателем на байт (а литерал – как раз константа такого типа), компилятор произведет автоматическое преобразование. Будет создана автоматическая переменная типа String с начальным значением "LITERAL", ссылка на нее будет передана в качестве аргумента метода String, а по завершении Concat временная переменная будет уничтожена.

Чтобы избежать подобного неэффективного преобразования, можно определить отдельный метод для работы с указателями:

class String
{
public:
 void Concat(const String& s);
 void Concat(const char* s);
};
void
String::Concat(const char* s)
{
 length += strlen(s);
 char* tmp = new char[length + 1];
 if (tmp == 0) {
 // обработка ошибки
 }
 strcpy(tmp, str);
 strcat(tmp, s);
 delete [] str;
 str = tmp;
}

Деструкторы

Аналогично тому, что при создании объекта выполняется конструкторпри уничтожении объекта выполняется специальный метод класса, называемыйдеструктором . Обычно деструктор   освобождает ресурсы, использованные данным объектом.

У класса может быть только один деструктор. Его имя – это имя класса, перед которым добавлен знак "тильда" ‘ ~ ’. Для объектов класса String  деструктор должен освободить память, используемую для хранения строки:

class String 
{
 ~String();
}; 
String::~String()
{
 if (str)
 delete str;
}

Если деструктор в определении класса не объявлен, то при уничтожении объекта никаких действий не производится.

Деструктор всегда вызывается перед тем, как освобождается память, выделенная под объект. Если объект типа String был создан с помощью операцииnew, то при вызове

delete str;

выполняется деструктор   ~String(), а затем освобождается память, занимаемая этим объектом. Предположим, в некой функции объявлена автоматическая переменная типа String:

int funct(void)
{
 String str;
 . . .
 return 0;
}

При выходе из функции funct по оператору return переменная str будет уничтожена: выполнится деструктор и затем освободится память, занимаемая этой переменной.

В особых случаях деструктор можно вызвать явно:

sptr->~String();

Такие вызовы встречаются довольно редко; соответствующие примеры будут рассматриваться позже, при описании переопределения операций new и delete.

Инициализация объектов

Рассмотрим более подробно, как создаются объекты. Предположим, формируется объект типа Book.

Во-первых, под объект выделяется необходимое количество памяти: либо динамически, если объект создается с помощью операции new, либо автоматически – при создании автоматической переменной, либо статически – при создании статической переменной.

Класс Book – производный от класса Item, поэтому вначале вызывается конструктор   Item.

У объекта класса Book имеются атрибуты – объекты других классов, в частности, String. После завершения конструктора базового класса будут созданы все атрибуты, т.е. вызваны ихконструкторы. По умолчанию используются стандартные конструкторы, как для базового класса, так и для атрибутов.

И только теперь очередь дошла до вызова конструктора класса Book.

В самом конце, после завершения конструктора   Book, создаются структуры, необходимые для работы виртуального механизма (отсюда следует, что в конструкторе нельзя использовать виртуальный механизм).

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

Item::Item() : taken(false), invNumber(0)
{}

В данном случае атрибутам объекта присваиваются начальные значения. Для класса Book   конструктор может выглядеть следующим образом:

Book::Book() : Item(), title("<None>"),
 author("<None>"), publisher("<None>"),
 year(-1)
{}

Вначале выполняется стандартный конструктор класса Item, а затем создаются атрибуты объекта с некими начальными значениями. Теперь предположим, что у классов Item и Book есть не только стандартные конструкторы, но и конструкторы, которые задают начальные значения атрибутов. Для класса Item   конструктор задает инвентарный номер единицы хранения.

class Item
{
public:
 Item(long in) { invNumber = in; };
 . . .
};
class Book
{
public:
 Book(long in, const String& a, 
 const String& t);
 . . .
};

Тогда конструктор класса Book имеет смысл записать так:

Book::Book(long in, const String& a, 
 const String& t) :
 Item(in), author(a), title(t)
{}

Такого же результата можно добиться и при другой записи:

Book::Book(long in, const String& a, 
 const String& t) :
 Item(in)
{
 author = a;
 title = t;
}

Однако предыдущий вариант лучше. Во втором случае вначале для атрибутов author и title объекта типа Book вызываются стандартные конструкторы. Затем программа выполнит операции присваивания новых значений. В первом же случае для каждого атрибута будет выполнен лишь один копирующий конструктор. Посмотрев на реализацию класса String, вы можете убедиться, насколько эффективнее первый вариант конструктора класса Book.

Встречается еще один случай, когда без инициализации обойтись невозможно. В качестве атрибута класса можно определить ссылку. Однако при создании ссылки ее необходимоинициализировать, поэтому в конструкторе подобного класса нужно применять инициализацию.

class A
{
public:
 A(const String& x);
private:
 String& str_ref;
};
A::A(const String& x) : str_ref(x)
{}

Создавая объект класса A, мы задаем строку, на которую он будет ссылаться. Ссылка инициализируется во время конструирования объекта. Поскольку ссылку нельзя переопределить, все время жизни объект класса A будет ссылаться на одну и ту же строку. Выбор ссылки в качестве атрибута класса обычно как раз и определяется тем, что ссылка инициализируется при создании объекта и никогда не изменяется. Тем самым дается гарантия использования ссылки на одну и ту же переменную. Значение переменной может изменяться, но сама ссылка – никогда.

Рассмотрим еще один пример использования ссылки в качестве атрибута класса. Предположим, что в нашей библиотечной системе книги, журналы, альбомы и т.д. могут храниться в разных хранилищах. Хранилище описывается объектом класса Repository. У каждого элемента хранения есть атрибут, указывающий на его хранилище. Здесь может быть два варианта. Первый вариант – элемент хранения хранится всегда в одном и том же месте, переместить книгу из одного хранилища в другое нельзя. В данном случае использование ссылки полностью оправдано:

class Repository
{
. . .
};
class Item
{
public:
 Item(Repository& rep) : 
 myRepository(rep) {};
 . . .
private:
 Repository& myRepository;
};

При создании объекта необходимо указать, где он хранится. Изменить хранилище нельзя, пока данный объект не уничтожен. Атрибут myRepository всегда ссылается на один и тот же объект.

Второй вариант заключается в том, что книги можно перемещать из одного хранилища в другое. Тогда в качестве атрибута класса Item лучше использовать указатель на Repository:

class Item
{
public:
 Item() : myRepository(0) {};
 Item(Repository* rep) : 
 myRepository(rep) {};
 void MoveItem(Repository* newRep);
 . . .
private:
 Repository* myRepository;
};

Создавая объект Item, можно указать, где он хранится, а можно и не указывать. Впоследствии можно изменить хранилище, например с помощью метода MoveItem.

При уничтожении объекта вызов деструкторов происходит в обратном порядке. Вначале вызывается деструктор самого класса, затем деструкторы атрибутов этого класса и, наконец, деструкторбазового класса.

В создании и уничтожении объектов имеется одно существенное отличие. Создавая объект, мы всегда точно знаем, какому классу он принадлежит. При уничтожении это не всегда известно.

Item* itptr;
if (type == "book")
 itptr = new Book();
else
 itptr = new Magazin();
. . .
delete itptr;

Во время компиляции неизвестно, каким будет значение переменной type и, соответственно, объект какого класса удаляется операцией delete. Поэтому компилятор может вставить вызов только деструктора базового класса.

Для того чтобы все необходимые деструкторы были вызваны, нужно воспользоваться виртуальным механизмом – объявить деструктор как в базовом классе, так и в производном, как virtual.

class Item
{
 virtual ~Item();
};
class Book
{
public:
 virtual ~Book();
};

Возникает вопрос – почему бы всегда не объявлять деструкторы виртуальными? Единственная плата за это – небольшое увеличение памяти для реализации виртуального механизма. Таким образом, не объявлять деструктор виртуальным имеет смысл только в том случае, если во всей иерархии классов нет виртуальных функций, и удаление объекта никогда не происходит через указатель на базовый класс.


Категория: C/C++/C# | Добавил: artkil (23.09.2012)
Просмотров: 9642 | Теги: деструктор, конструктор, c++ | Рейтинг: 0.0/0
Всего комментариев: 0
Имя *:
Email *:
Код *: