Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
39
Добавлен:
16.04.2013
Размер:
99.33 Кб
Скачать

Классы с деструкторами

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

Давайте добавим в наш пример ch_stack деструктор.

//ch_stack с конструктором и деструктором

class ch_stack {

public:

ch_stack(); //конструктор по умолчанию

explicit ch_stack(int size) : max_len(size), top(EMPTY)

{ assert(size > 0); s = new char[size];

assert(s != 0);}

ch_stack(const ch_stack& str); //копирующий конструктор

ch_stack(int size, const char str[]);

~ch_stack() { delete [] s; } //деструктор

......

private:

enum { EMPTY = -1 };

char* s;

int max_len;

int top;

};

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

Пример: динамически размещаемые строки

Языку C++ недостает собственного строкового типа. Стандартная библиотека предоставляет шаблонный строковый класс — тип, который используется все чаще и чаще. Старое строковое представление выглядело как указатель на char. В этом представлении конец строки обозначался нулевым символом ' \0 '.

В этом разделе мы разработаем полезныи строковый АТД, в котором длина объявлена как private.

Реализация будет использовать строковые функции из библиотеки string.h для оперирования вышеупомянутым представлением строк с помощью указателей.

//Реализация динамически распределяемых строк

class my_string {

public:

my_string() : Ien(0)

{ s = new char [1]; assert (s != 0); s[0] = 0; }

my_string(const my_string& str); //копирующий конструктор

my_string(const char* p); //преобразующий конструктор

~my_string() { delete [] s; }

void assign(const my_string& str);

void print() const { cout << s << endl; }

void concat (const my_string& a, const my_string& b) ;

private:

char* s;

int len;

};

my_string::my_string(const my_string& str) : len(str.len)

{

s = new char(len + 1]

assert(s != 0);

strcpy(s, str.s);

}

void my_string::assign(const my_string& str)

{

if (this = = &str) //a = = а; ничего не делаем

return;

delete [] s;

len = str.len;

s = new char[len + 1] ;

assert(s != 0) ;

strcpy(s, str.s) ;

}

void my_string::concat(const my_string& a,

const my_string& b)

{

char* temp = new char[a.len + b.len + 1] ;

len = a.len + b.len;

strcpy(temp, a.s);

strcat(temp, b.s);

delete [] s;

s = new char[len + 1] ;

assert(s != 0) ;

strcpy(s, temp) ;

}

Этот тип позволяет объявлять строки my_string, присваивать с помощью копирования одну my_string другой, печатать строки и объединять две строки типа my_string. Скрытое представление является указателем на char и содержит переменную len, в которой хранится текущая длина строки типа my_string.

Следующая программа тестирует класс my_string, выполняя конкатенацию нескольких строк типа my_string.

В файле string5. срр

int main()

{

char* str = "Громче всех скрипит то колесо,\n";

my_string a(str), b, author ("Джош Биллингс\n"),

both, quote;

b.assign("которое смазывают\n");

both.concat(a, b);

quote.concat (both, author);

quote.print();

}

Эта программа распечатает следующее:

Громче всех скрипит то колесо,

которое смазывают

Джош Биллингс

Мы намеренно использовали несколько объявлений, чтобы показать, как будут вызываться разные конструкторы. Переменные my_string b, both и quote используют конструктор по умолчанию. Объявление для author использует конструктор с аргументом типа char*. Конкатенация выполняется в два этапа. Сначала my_string строки a и b объединяются в both. Потом выполняется конкатенация строк both и author. Затем печатается вся цитата.

Конструктор my_string: :my_string (const char*) вызывается для создания и инициализации объектов а и author. Этот же конструктор вызывается неявно как операция преобразования при вызове my_string: :assign() для литерала "которое смазывают \n”

Сложные объекты могут быть созданы из более простых путем объединения их отношениями включать как часть.

#include "vectl.h"

class pair_vect {

public:

pair_vect(int i) : a(i), b(i) , size(i){ }

int& first_element(int i) ;

int& second_element(int i) ;

int ub()const (return size - 1;}

private:

vect a, b;

int size;

};

int& pair_vect: : first_element (int i)

{ return a.element(i); }

int& pair_vect::second_element(int i)

{ return b.element(i); }

Заметьте, что конструктор pair_vect сводится к вызову трех инициализаторов. Инициализаторы vect-членов а и b вызывают vect :vect (int). Используем этот тип данных для построения таблицы соответствия возраста и веса.

int main()

{

int i ;

pair_vect age_weight(5); //возраст и вес

cout << "таблица возраста и веса \n";

for (i = 0; i <= age_weight.ub(); ++i) {

age_weight.first_element(i) = 21 + i;

age_weight.second_element(i) = 135 + i;

cout << age_weight.first_element(i) <<", "

<< age_weight. second_element(i) << endl;

}

}

Пример: односвязный список

Разработаем тип данных для односвязного списка. Он является прототипом для многих динамических АТД, которые называются ссылающимися на себя структурами (self-referential structure). Такие типы данных содержат члены-указатели, которые ссылаются на объекты своего собственного типа. Они служат основой для многих полезных контейнерных классов. Следующее объявление реализует подобный тип:

class slist { //односвязный список

public:

slist () : h(0) { } //0 означает пустой список

~slist() { release(); }

void prepend(char с); //добавление в голову списка

void del() ;

slistelem* first() const { return h; }

void print() const;

void release();

private:

slistelem* h; //голова списка

};

struct slistelem {

char data;

slistelem* next;

};

Операции над списком

1. prepend: добавляет в голову списка

2. first: возвращает указатель на первый элемент

3. print: печатает содержимое списка

4. del: удаляет первый элемент

5. release: уничтожает список

Связующий член next указывает на следующий slistelem в списке. Переменная data в этом примере является простой переменной, но она может быть заменена на сложный тип, способный хранить целую порцию информации. Конструктор инициализует голову списка slist (указатель h ) значением 0, которое называется постоянной нулевого указателя (null pointer constant) и может быть присвоено указателю любого типа. В связных списках она обычно обозначает пустой список или значение «конец списка». Функция-член prepend () строит структуру списка:

void slist::prepend(char с)

{

slistelem* temp = new slistelem; //создание элемента

assert(temp != 0);

temp -> next = h; //связь с slist

temp -> data = c;

h = temp; //изменение головы списка

}

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

Функция-член del ( ) выполняет обратную задачу:

void slist::del( )

{

slistelem* temp = h;

h = h -> next; //предполагается не пустой slist

delete temp;

}

Она освобождает память, занимаемую первым элементом списка. Это делается с помощью оператора delete, направленного на голову slist (указатель h). Новая голова списка — это значение члена next прежней головы. Эту функцию можно модифицировать так, чтобы она работала и с пустым списком, не вызывая прерывания программы (см. упражнение 18 на стр. 188).

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

void slist::print() const //объект не изменяется

{

slistelem* temp = h;

while (temp != 0) { //обнаружение конца списка

cout << temp -> data <<"->";

temp = temp -> next;

}

cout << "\n# # #" << endl;

}

//освобождение памяти, занимаемой элементами

void slist::release()

slist:: ~slist()

{

cout <<"вызван деструктор" << endl;

release() ;

}

int main()

{

slist* p;

{

slist w;

w.prepend('A' ) ;

w.prepend(' B' ) ;

w.print() ;

w.del();

w.print() ;

p = &w;

p -> print() ;

cout << "выход из внутреннего блока" << endl;

}

//p -> print(); ведет себя в зависимости от системы

cout << "выход из внешнего блока" << endl;

}

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

В -> А ->

# # #

А ->

# # #

А ->

# # #

выход из внутреннего блока

вызван деструктор

выход из внешнего блока

Первый вызов print () выведет двухэлементный список slist, содержащий В и А. После выполнения операции del список содержит один элемент, хранящий А. Находящемуся во внешнем блоке указателю на slist р присваивается адрес переменной slist w. Если к списку обращаются через р из внутреннего блока, печатается А. Этот вывод показывает, что деструктор удаляет переменную w при выходе из блока.

Второй вызов функции slist: :print () закомментирован, поскольку результат зависит от системы. Разыменование р здесь вызовет ошибку этапа выполнения, потому что объект по адресу, на который он ссылается, разрушен процедурой (удаления.

Двумерные массивы

|В стандартном С нет подлинных многомерных массивов. Вместо этого программист должен проявлять осторожность при представлении подобной абстрактной структуры данных указателем на указатель, на…, на базовый тип. В C++ программист может реализовывать гибкие, безопасные динамические многомерные массивы. Продемонстрируем это, разработав тип двумерного динамического массива matrix (матрица). Обратите внимание на его сходство с классом vect.

//Безопасный тип двумерного массива matrix

class matrix {

public:

matrix (int dl, int d2) ;

~matrix() ;

int ubl() const { return(sl - 1); }

int ub2() const { return(s2 - 1) ; }

int& element(int i, int j);

private:

int** p;

int sl, s2;

};

Тип matrix имеет по каждому измерению размер и соответствующую верхнюю границу, доступ к которой открыт. Скрытое представление использует указатель на указатель на тип int. Здесь будет храниться базовый адрес массива указателей на int, в которых в свою очередь содержатся базовые адреса для каждой строки.

matrix::matrix(int dl, int d2) : sl(dl), s2(d2)

{

assert (dl > 0 && d2 > 0) ;

p = new int*[sl] ;

assert(p != 0) ;

for (int i = 0; i < sl; ++i){

p[i] = new int[s2];

assert(p[i] != 0) ;

}

}

matrix::-matrix()

{

for (int i = 0; i <= ubl(); ++i)

delete p(i];

delete [ ] p;

Так же как и в типе twod из раздела 5.9, «Контейнеры и доступ к их содержимому», на стр. 147, конструктор размещает массив указателей на int. Числом элементов этого массива служит значение s1 Затем конструктор итеративно размещает массив целых, на которые указывает каждый из элементов p[i]. Таким образом, из свободной памяти выделяется пространство для sl*s2 целых, и плюс к тому, пространство для sl указателей. Деструктор освобождает память в обратном порядке. Подобная схема распространяется и на массивы с более высокой размерностью.

Для получения lvalue элемента в этом двумерном массиве необходимы два индексных аргумента:

int& matrix::element(int i, int j)

{

assert(i >= 0 && i <= ub1() && j >= 0 && j <= ub2());

return p[i] [j] ;

}

Оба аргумента проверяются на то, находятся ли они в пределах границ. Это — обобщение случая с одним индексом.

Строки, использующие семантику ссылок

В следующем примере мы создадим класс my_string, имеющий ссылочную семантику копирования. Этот класс использует поверхностное копирование, поскольку копирование заменено присваиванием указателей. Показанная техника обычна для агрегатов данных подобного типа. Будем использовать класс str_obj для создания значений фактических объектов. Тип str_obj является необходимой деталью реализации my_string. Данная деталь не может быть помещена непосредственно в my_string без разрушения отношения (возможно, многие-к-одному) между объектами типа my_string и ссылочными значениями типа str_obj. Значения my_string содержатся в str_obj, который является дополнительным классом только лишь для использования классом my_string. Открыто используемый класс my_string управляет экземплярами str_obj, и иногда называется управляющим классом (handler class).

//Строки my_string с подсчетом ссылок

#include <string.h>

#include <iostream.h>

#include <assert.h>

class str_obj {

public:

int len, ref_cnt;

char* s;

str_obj() : len(0), ref_cnt(l)

{ s = new char[l]; assert(s != 0); s[0] = 0; }

str_obj(const char* p) : ref_cnt(l)

{ len = strlen(p); s = new char[len + 1] ;

assert(s != 0); strcpy(s, p); }

~str_obj() { delete []s; }

};

Класс str_obj объявляет объекты, которые используются my_string. Обратите внимание, что класс str_obj применяется в основном для создания и уничтожения объектов, использующих свободную память. При создании str_obj переменная ref_cnt инициализуется единицей.

class my_string {

public:

my_string() { st = new str_obj; assert(st != 0);}

my_string(const char* p)

{ st = new str_obj(p); assert(st != 0);}

my_string(const my_string& str)

{ st = str.st; st -> ref_cnt++; }

~my_string() ;

void assign(const my_string& str);

void print() const { cout << st -> s; }

private:

str_obj* st;

};

Клиент будет использовать объекты типа my_string. Эти объекты реализованы как указатели st назначения типа str_obj. Обратите внимание на копирующий конструктор этого класса и на то, как он применяет семантику ссылок для создания копии. Семантика функции assign () демонстрирует некоторые тонкости использования счетчика ссылок:

void my_string::assign(const my_string& str)

{

if (str.st != st) {

if (--st -> ref_cnt = = 0)

delete st;

st = str.st;

st -> ref_cnt++;

}

}

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

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

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

my_string:: ~ my_string ()

{

if (--st -> ref_cnt = =0)

delete st;

}

Соседние файлы в папке Тельминов (мб)