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

Язык программирования C++. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
1 Мб
Скачать
☆
автоматическая переменная типа 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 деструктор должен освободить память, используемую для
Язык программирования C++Фридман А.Л.
131
хранения строки:
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();
Язык программирования C++Фридман А.Л.
132
Такие вызовы встречаются довольно редко; соответствующие примеры будут рассматриваться позже, при описании переопределения операций
new и delete.
Инициализация объектов
Рассмотрим более подробно, как создаются объекты. Предположим, формируется объект типа Book.
Во-первых, под объект выделяется необходимое количество памяти: либо динамически, если объект создается с помощью операции new, либо автоматически – при создании автоматической переменной, либо статически – при создании статической переменной.
Класс Book – производный от класса Item, поэтому вначале вызывается конструктор Item.
У объекта класса Book имеются атрибуты – объекты других классов, в частности, String. После завершения конструктора базового класса будут созданы все атрибуты, т.е. вызваны их конструкторы. По умолчанию используются стандартные конструкторы, как для базового класса, так и для атрибутов.
И только теперь очередь дошла до вызова конструктора класса Book.
В самом конце, после завершения конструктора Book, создаются структуры, необходимые для работы виртуального механизма (отсюда следует, что в конструкторе нельзя использовать виртуальный механизм).
Вызов конструкторов базового класса и конструкторов для атрибутов класса можно задать явно. Особенно это важно, если есть необходимость либо использовать нестандартные конструкторы, либо присвоить начальные значения атрибутам класса. Вызов конструкторов записывается после имени конструктора класса после двоеточия. Друг от друга вызовы отделяются запятой. Такой список называется списком инициализации или просто инициализацией :
Item::Item() : taken(false), invNumber(0)
Язык программирования C++Фридман А.Л.
133
{}
В данном случае атрибутам объекта присваиваются начальные значения. Для класса 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) {}
Такого же результата можно добиться и при другой записи:
Язык программирования C++Фридман А.Л.
134
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 будет ссылаться на одну и ту же строку. Выбор ссылки в качестве атрибута класса обычно как раз и определяется тем, что ссылка инициализируется при создании объекта и никогда не изменяется. Тем самым дается гарантия использования ссылки на одну и ту же
Язык программирования C++Фридман А.Л.
135
переменную. Значение переменной может изменяться, но сама ссылка – никогда.
Рассмотрим еще один пример использования ссылки в качестве атрибута класса. Предположим, что в нашей библиотечной системе книги, журналы, альбомы и т.д. могут храниться в разных хранилищах. Хранилище описывается объектом класса Repository. У каждого элемента хранения есть атрибут, указывающий на его хранилище. Здесь может быть два варианта. Первый вариант – элемент хранения хранится всегда в одном и том же месте, переместить книгу из одного хранилища в другое нельзя. В данном случае использование ссылки полностью оправдано:
class Repository { . . . }; class Item { public: Item(Repository& rep) : myRepository(rep) {}; . . . private: Repository& myRepository; };
При создании объекта необходимо указать, где он хранится. Изменить хранилище нельзя, пока данный объект не уничтожен. Атрибут
myRepository всегда ссылается на один и тот же объект.
Второй вариант заключается в том, что книги можно перемещать из одного хранилища в другое. Тогда в качестве атрибута класса Item лучше использовать указатель на Repository:
class Item { public: Item() : myRepository(0) {};
Язык программирования C++Фридман А.Л.
136
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 {
Язык программирования C++Фридман А.Л.
137
virtual ~Item(); }; class Book { public: virtual ~Book(); };
Возникает вопрос – почему бы всегда не объявлять деструкторы виртуальными? Единственная плата за это – небольшое увеличение памяти для реализации виртуального механизма. Таким образом, не объявлять деструктор виртуальным имеет смысл только в том случае, если во всей иерархии классов нет виртуальных функций, и удаление объекта никогда не происходит через указатель на базовый класс.
Операции new и delete
Выделение памяти под объекты некоего класса производится либо при создании переменных типа этого класса, либо с помощью операции new. Эти операции, как и другие операции класса, можно переопределить.
Прежде всего, рассмотрим модификацию операции new, которая уже определена в самом языке. (Точнее, она определена в стандартной библиотеке языка Си++.) Эта операция не выделяет память, а лишь создает объект на заранее выделенном участке памяти. Форма операции следующая:
new (адрес) имя_класса (аргументы_конструктора)
Перед именем класса в круглых скобках указывается адрес, по которому должен располагаться создаваемый объект. Фактически, такая операция
new не выделяет памяти, а лишь создает объект по указанному адресу,
выполняя его конструктор. Соответственно, можно не выполнять операцию delete для этого объекта, а лишь вызвать его деструктор перед тем, как поместить новый объект на то же место памяти.
char memory_chunk[4096];
Язык программирования C++Фридман А.Л.
138
Book* bp = new (memory_chunk) Book; . . . bp->~Book(); Magazin* mp = new (memory_chunk) Magazin; . . . mp->~Magazin();
В этом примере никакой потери памяти не происходит. Память выделена один раз, объявлением массива memory_chunk. Операции
new создают объекты в начале этого массива (разумеется, мы
предполагаем, что 4096 байтов для объектов достаточно). Когда объект становится ненужным, явно вызывается его деструктор и на том же месте создается новый объект.
Любой класс может использовать два вида операций new и delete – глобальную и определенную для класса. Если класс и ни один из его базовых классов, как прямых, так и косвенных, не определяет операцию
new, то используется глобальная операция new. Глобальная операция new всегда используется для выделения памяти под встроенные типы и
под массивы (независимо от того, объекты какого класса составляют массив).
Если класс определит операцию new, то для всех экземпляров этого класса и любых классов, производных от него, глобальная операция будет переопределена, и будет использоваться new данного класса. Если нужно использовать именно глобальную операцию, можно перед new поставить два двоеточия ::new.
Вид стандартной операции new следующий:
class A { void* operator new(size_t size); };
Аргумент size задает размер необходимой памяти в байтах. size_t – это тип целого, подходящий для установления размера объектов в данной реализации языка, определенный через typedef. Чаще всего это тип long. Аргумент операции new явно при ее вызове не задается.
Язык программирования C++Фридман А.Л.
139
Компилятор сам его подставляет, исходя из размера создаваемого объекта.
Реализация операции new, которая совпадает со стандартной, выглядит просто:
void* A::operator new(size_t size) { return ::new char[size]; }
В классе может быть определено несколько операций new с различными дополнительными аргументами. При вызове new эти аргументы указываются сразу после ключевого слова new в скобках до имени типа. Компилятор добавляет от себя еще один аргумент – размер памяти, и затем вызывает соответствующую операцию. Описанная выше модификация new, помещающая объект по определенному адресу, имеет вид:
void* operator new(void* addr, size_t size);
Предположим, мы хотим определить такую операцию, которая будет инициализировать каждый байт выделенной памяти каким-либо числом.
class A { void* operator new(char init, size_t size); }; void* A::operator new(char init, size_t size) { char* result = ::new char[size]; if (result) { for (size_t i = 0; i < size; i++) result[i] = init; } return result;
Язык программирования C++Фридман А.Л.
140
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]