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

2vcTnguyvU

.pdf
Скачиваний:
6
Добавлен:
15.04.2023
Размер:
955.27 Кб
Скачать

Оператор delete освобождения памяти

Управление памятью является обязанностью программиста. В C++ имеется оператор delete для освобождения (возвращения в системный ресурс) памяти, предварительно выделенной оператором new. Синтаксис delete основывается на том факте, что система времени исполнения С++ сохраняет информацию о каждом вызове оператора new. Например, p и q указывают на динамически выделяемую память:

T *p, *q; // p и q являются указателями на тип T

p= new T;

//p указывает на один элемент

q = new T[n]; // q указывает на массив n элементов

Функция

delete использует указатель для освобождения памяти. В

случае освобождения массива delete применяется с оператором [],

delete p; //освобождает память переменной, на которую указывает p delete [] q; //освобождает весь массив, на который указывает q

Динамически создаваемые объекты

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

Например, рассмотрим шаблонный класс DynamicClass, имеющий статические и динамические данные-члены.

#include <iostream> using namespace std; template <class T> class DynamicClass

{

private:

// переменные типа T и указатель на тип T T member1; T *member2;

public:

// конструкторы

DynamicClass(const T& m1, const T& m2); DynamicClass(const DynamicClass<T>& obj); ~DynamicClass( ); // деструктор

// оператор присваивания

DynamicClass<T>& operator= (const DynamicClass<T>& rhs); }; 50

Этот простой класс имеет два данныхчлена. Конструктор этого класса использует параметр m1 для инициализации статического данного-члена member1. Для данного-члена member2 требуется выделение памяти типа T и инициализация ее значением m2:

// конструктор с параметрами для инициализации данного-члена template <class T>

DynamicClass<T>::DynamicClass(const T& m1, const T& m2) { // параметр m1 инициализируется статически

member1 = m1;

// выделение динамической памяти и инициализация ее значением m2 member2 = new T(m2);

cout << "Constructor: " << member1 << '/' << *member2 << endl; }

Для эффективного управления памятью необходимо освобождать динамические данные объекта в то время, когда уничтожается объект. Язык С++ предоставляет функцию-член, называемую деструктором. Для DynamicClass деструктор имеет следующее объявление:

// деструктор освобождает память, выделенную конструктором template <class T> DynamicClass<T>::~DynamicClass( ) {

cout << "Destructor: " << member1 << '/' << *member2 << endl; delete member2; }

Деструктор вызывается всякий раз при уничтожении какого-либо объекта. Когда программа завершается, все глобальные объекты или объекты, объявленные в main-программе, уничтожаются. Локальные объекты, создаваемые внутри блока, уничтожаются при выходе программы из блока. Присваивание и инициализация являются базовыми операциями, применяемыми к любому объекту. Например, создается объект класса DynamicClass X c членами-данных member1=2, member2=200;:

DynamicClass<int> X(2,200); и

DynamicClass<int> Y(0, 67);

Y=X; //данные объекта Y переписываются из X

Чтобы избежать нежелательных ошибок, надо создавать новые методы, управляющие присваиванием и инициализацией объектов. Оператор присваивания Y=X приводит к тому, что данные объекта X копируются в Y. Так как значению указателя member2 в объекте Y присваивается значение указателя member2 в объекте X, оба указателя теперь ссылаются на один и тот же участок памяти, а на динамическую память, первоначально присвоенную Y, ссылки теперь нет. Предположим, что где-то в программе первым уничтожается объект Y. Деструктор освобождает память, на которую указывает Y.member2 (и одновременно X.member2). При уничтожении объекта X вызывается его деструктор для освобождения памяти, связанной с

51

указателем-переменной X.member2, но этот блок памяти ранее уже был освобожден при уничтожении Y, поэтому использование операции delete в деструкторе для X является ошибкой. Для правильного выполнения присваивания объектов в случаях, когда это касается динамических данных, С++ позволяет перегружать оператор присваивания (=) как функцию-член. Синтаксис для перегруженного оператора присваивания в DynamicClass следующий:

DynamicClass<T>& operator = (const DynamicClass<T>& rhs); Бинарный оператор реализуется как функция-член с параметром rhs, представляющий операнд в правой части оператора. Например, Y=X; перегруженный оператор = выполняется для каждого оператора присваивания, включающего объекты типа DynamicClass. Вместо простого побитового копирования данных-членов из объекта X в объект Y, перегруженный оператор отвечает за явное присваивание всех данных, включая закрытые и открытые данные-члены, а так же данные, на которые указывают эти члены. Параметр rhs передается по константной ссылке. Таким образом, не выполняется копирование в этот параметр того, что могло быть большим объектом в правой части, и не допускается никакого изменения объекта. Важно также, что где бы ни использовалось имя шаблонного класса, необходимо добавлять <T> в конец имени класса.

Для DynamicClass оператор = должен присваивать значения данных member1 объекта rhs значению данных member1 текущего объекта и копировать содержимое, на которое указывает member2 объекта rhs, в участок памяти, на который указывает member2 текущего объекта.

// перегруженный оператор присваивания

template <class T> DynamicClass<T>& DynamicClass<T>::operator= (const DynamicClass<T>& rhs)

{

// копирование статического данного-члена из rhs в текущий объект member1 = rhs.member1;

//содержимое динамической памяти должно быть тем же, что и содержимое //rhs

*member2 = *rhs.member2;

cout << "Оператор присваивания : "<< member1 << “ “ << *member2 << endl;

return *this;

}

Зарезервированное слово this используется для возвращения ссылки на текущий объект. Каждый объект С++ имеет указатель с именем this, определяемый автоматически при создании объекта, а *this - это сам объект.

52

Поскольку оператор = возвращает ссылку на текущий объект, то можно эффективно связывать вместе два или более операторов присваивания.

Инициализация объекта - это операция, создающая новый объект, который является копией другого объекта. Подобно присваиванию, когда объект имеет динамические данные, эта операция требует особую функциючлен, называемую конструктором копирования (copy constructor). Например:

DynamicClass<int> X(3,7), Y(X);

Инициализация осуществляется не только при объявлении объектов, но и при передаче объекта функции в качестве параметра по значению, и при возвращении объекта в качестве значения функции. Например, функция funcMy() имеет передаваемый по значению параметр A типа DynamicClass<int> :

DynamiClass<int> funcMy (DynamicClass<int> A) { DynamicClass<int> obj; return obj; }

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

DynamicClass<int> X(5, 67), Y (4, 89); //объявление объектов Y=funcMy(X); // вызов funcMy копированием X в A

При выполнении возврата из funcMy создается копия obj, вызываются деструкторы для локальных объектов A и obj и копия obj возвращается как значение функции.

Создание конструктора копирования

Чтобы правильно обращаться с классами, которые выделяют динамическую память, С++ предоставляет конструктор копирования для выделения динамической памяти новому объекту и инициализации его значений данных. Конструктор копирования является функцией-членом, которая объявляется с именем класса и одним параметром и он не имеет возвращаемого значения.

template <class T>

DynamicClass<T>::DynamicClass(const DynamicClass<T>& obj) { // копировать статический данное-член из obj в текущий объект

member1 = obj.member1;

//выделить динамическую память и инициализировать ее значением

//*obj.member2

member2 = new T(*obj.member2);

cout << "Конструктор копирования : " << member1 << '/' << *member2 << endl; }

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

53

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

Создание надежного массива

Статический массив – это коллекция, содержащая фиксированное количество элементов, ссылка на которые выполняется индексным оператором. Размер статического массива устанавливается во время компиляции и не может изменяться во время исполнения приложения. Поэтому требуется создать шаблонный класс Array, содержащий список последовательных элементов любого типа данных, размер которого может быть изменен во время выполнения приложения. Этот класс должен содержать методы, реализующие индексацию и преобразование типа указателя. Чтобы был возможен индексный доступ к элементам в списке, используется перегруженный оператор индекса [], с проверкой границ массива. Полученные в результате объекты называются надежными массивами. Для того, чтобы объект массива мог использоваться с функциями, принимающими стандартные параметры массива, определяется общий оператор преобразования указателя, связывающий объект Array с обычным массивом, элементы которого – это элементы типа T.

Спецификация класса Array

 

#include <iostream>

 

 

#include <stdlib.h>

 

 

using namespace std;

 

 

#ifndef NULL

 

 

const int NULL = 0;

 

 

#endif // NULL

 

 

enum

ErrorType

{invalidArraySize,

memoryAllocationError,

indexOutOfRange};

 

 

char *errorMsg[] = { "Неверный размер массива", "Ошибка выделения

памяти ", "Неверный индекс " };

 

 

template <class T>

 

 

class Array

{

 

 

private:

 

 

 

// динамически выделяемый список размером size

T*

alist;

int size;

 

 

// метод обработки ошибок

void Error( ErrorType error, int badIndex=0) const; 54

public:

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

Array(int sz = 50); Array(const Array<T>& A); ~Array( );

//присваивание, индексация и преобразование указателя Array<T>& operator= (const Array<T>& rhs);

T& operator[](int i); operator T* ( ) const;

//операции с размером массива

int ListSize( ) const;

//

читать size

void Resize(int sz);

};

// обновлять size

Выделение памяти для класса

Конструктор класса выделяет динамический массив, элементы которого –это элементы типа T. Начальный размер массива определяется параметром sz, который имеет значение по умолчанию 50:

// конструктор

template <class T> Array<T>::Array(int sz) {

//проверка на наличие параметра неверного размера if (sz <= 0)

Error(invalidArraySize);

//присваивание размера и выделение памяти

size = sz;

alist = new T[size];

// проверка правильности выделения памяти , if (alist == NULL)

Error(memoryAllocationError);

}

 

// destructor

 

template <class T>

 

Array<T>::~Array( ) {

delete [] alist; }

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

// конструктор копирования

template <class T> Array<T>::Array(const Array<T>& X)

{ // получить размер объекта X и присвоить текущему объекту int n = X.size; size = n;

55

// выделить новую память для объекта с проверкой

alist = new T[n];

// динамически созданный массив

if (alist == NULL)

Error(memoryAllocationError);

// копировать элементы массива объекта x в текущий объект

T* srcptr = X.alist; // адрес начала

X.alist

T* destptr = alist;

// адрес начала

alist

while (n--)

// копировать список

*destptr++ = *srcptr++; }

Оператор присваивания определен следующим образом:

template <class T> Array<T>& Array<T>::operator=(const Array<T>& rhs)

{ // запись размера

size

объекта rhs

int n = rhs.size;

 

 

 

if (size != n)

{

 

 

delete [] alist;

 

// освобождается исходная память

alist = new T[n];

//

выделяется новая память

if (alist == NULL) Error(memoryAllocationError); size = n; }

// копируютя элементы массива из rhs в текущий объект T* destptr = alist; T* srcptr = rhs.alist;

while (n--)

*destptr++ = *srcptr++;

// возвращается

ссылка на текущий объект

return *this;

}

 

Проверка границ массива и перегруженный оператор []

Оператор может быть перегруженным только как функция-член и обеспечивает индексированный доступ к данным объекта. Поэтому перегруженный оператор [] будет реализован так:

template <class T> T& Array<T>::operator[] (int n) { // выполнение проверки границ массива

if (n < 0 || n > size-1) Error(indexOutOfRange,n);

// возвращается элемент из закрытого списка массива return alist[n]; }

Преобразование объекта в указатель

Преобразование указателя предоставляет возможность использовать объект Array как параметр времени исполнения в любой функции, определяющей обычный массив. Это выполняется перегрузкой оператора преобразования T*( ) , которая преобразует объект в указатель.

//оператор преобразования указателя

56

template <class T> Array<T>::operator T* ( ) const {

// возвращает адрес закрытого массива в текущем объекте return alist; }

template <class T> int Array<T>::ListSize( ) const { return size; }

Функция вывода сообщений об ошибках template <class T>

void Array<T>::Error(ErrorType error, int badIndex) const { cerr << errorMsg[error];

if (error == indexOutOfRange) cerr << badIndex; cerr << endl; exit(1); }

Изменение размера массива. Оператор изменения размера template <class T> void Array<T>::Resize(int sz)

{ //проверка нового размера массива. Выход из программы при size <= 0 if (sz <= 0)

Error(invalidArraySize);

// ничего не делать, если размер не изменился if (sz == size) return;

// запросить память для нового массива и проверить ответ системы T* newlist = new T[sz];

if (newlist == NULL) Error(memoryAllocationError); //объявить n и инициализировать значениями sz или size

int n = (sz <= size) ? sz : size;

//копировать n элементов массива из старой в новую память

T* srcptr = alist;

//

адрес начала alist

T* destptr = newlist;

// адрес начала newlist

while (n--)

// копировать список

*destptr++ = *srcptr++; //удалить старый список

delete[] alist;

// переустановить alist, чтобы он указывал на обновление члена класса size alist = newlist; size = sz; }

Косвенное обращение

Операцию косвенного обращения к члену -> можно определить как унарную постфиксную операцию. Это значит, если есть класс

class Ptr { // ...

X* operator->(); };

57

то объекты класса Ptr могут использоваться для доступа к членам класса X также, как для этой цели используются указатели:

void f(Ptr p) {

p->m = 7; // (p.operator->( ))->m = 7 }

Превращение объекта p в указатель p.operator->() никак не зависит от члена m, на который он указывает. Именно по этой причине operator->() является унарной постфиксной операцией. Однако, не вводятся новые синтаксические обозначения, так что имя члена по-прежнему должно идти после -> :

void g(Ptr p) { X* q1 = p->; // синтаксическая ошибка X* q2 = p.operator->(); }// нормально

Перегрузка операции -> прежде всего используется для создания "хитрых указателей", т.е. объектов, которые помимо использования как указатели позволяют проводить некоторые операции при каждом обращении к указываемому объекту с их помощью. Например, можно определить класс RecPtr для организации доступа к объектам класса Rec, хранимым на диске. Параметром конструктора RecPtr является имя, которое будет использоваться для поиска объекта на диске. При обращении к объекту с помощью функции RecPtr::operator->(), он переписывается в основную память, а в конце работы деструктор RecPtr записывает измененный объект обратно на диск.

class RecPtr {

Rec* in_core_address; const char* identifier; // ...

public:

RecPtr(const char* p) : identifier(p) { in_core_address = 0; } ~RecPtr() { write_to_disc(in_core_address,identifier); }

Rec* operator->();

};

 

Rec* RecPtr::operator->()

{ if (in_core_address == 0)

in_core_address = read_from_disc(identifier);

return in_core_address; }

 

 

Использовать это можно так:

 

main(int argc, const char* argv)

 

{

 

 

for (int i = argc; i; i--) {

 

 

RecPtr p(argv[i]); p->update();

} }

На самом деле, тип RecPtr должен определяться как шаблон типа, а тип структуры Record будет его параметром. Кроме того, настоящая программа будет содержать обработку ошибок и взаимодействие с диском будет организовано не столь примитивно.

58

Для обычных указателей операция -> эквивалентна операциям, использующим * и [ ]. Так, если описано Y* p , то выполняется соотношение p->m == (*p).m == p[0].m

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

class X { Y* p; public:

Y* operator->() { return p; } Y& operator*() { return *p; }

Y& operator[](int i) { return p[i]; } };

Если в классе определено более одной подобной операции, разумно будет обеспечить эквивалентность, точно так же, как разумно предусмотреть для простой переменной x некоторого класса, в котором есть операции ++, += , = и +, чтобы операции ++x и x+=1 были эквивалентны x=x+1.

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

Пример указателя на член класса

Как уже было сказано ранее, указатель на член класа отличается от указателя на класс. Указатель на тип члена класса выглядит как T::*, где T - имя класса. В С++ есть два оператора, служащих для разыменования

указателя на член класса. Вот они: .*,

->* . В

конструкциях

объект.*указатель_на_член и указатель->*

указатель_на_член сначала

производится доступ к объекту, а затем доступ к указанному

члену и его

разыменование. В следующем фрагменте показано применение этих операторов.

В файле showhide.cpp

// Указатель на член класса #include <cstdlib>

#include <iostream> using namespace std; class X {

public:

int visible; // видимая

void print ( ) { cout << "\nhide = " << hide << " visible = " << visible; } void reset ( ) { visible = hide ; }

void set ( int i ) { hide=i; } private:

59

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]