Лекции.ИНФО


Освобождение памяти, адресуемой указателем р.



Эта программа содержит принципиальную ошибку. И вот почему: при создании в функции main() объекта a выделяется область памяти, адрес которой присваивается указателю а.р . При передаче функции display() объект a копируется в параметр ob. Это означает, что оба объекта (a и ob) будут иметь одинаковое значение для указателя р.

Другими словами, в обоих объектах (в оригинале и его копии) член данных p будет указывать на одну и ту же динамически выделенную область памяти. По завершении функции display() объект ob разрушается, и его разрушение сопровождается вызовом деструктора. Деструктор освобождает область памяти, адресуемую указателем ob.р. Но ведь эта (уже освобожденная) область памяти — та же самая область, на которую все еще указывает член данных (исходного объекта) a.p! Налицо серьезная ошибка.

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

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

// Одно из решений проблемы передачи объектов.

#include <iostream>

#include <cstdlib>

using namespace std;

class myclass {

  int *p;

 public:

  myclass(int i);

  ~myclass();

  int getval() { return *p; }

};

Myclass::myclass(int i)

{

 cout << "Выделение памяти, адресуемой указателем p.";

 р = new int;

 *p = i;

}

myclass::~myclass()

{

 cout <<"Освобождение памяти, адресуемой указателем p.";

 delete p;

}

/* Эта функция HE создает проблем. Поскольку объект ob теперь передается по ссылке, копия аргумента не создается, а следовательно, объект не выходит из области видимости по завершении функции display().

*/

void display(myclass &ob)

{

 cout << ob.getval() << '';

}

Int main()

{

 myclass a(10);

 display(a);

 return 0;

}

Результаты выполнения этой версии программы выглядят гораздо лучше предыдущих.

Выделение памяти, адресуемой указателем р.

Освобождение памяти, адресуемой указателем р.

Как видите, здесь деструктор вызывается только один раз, поскольку при передаче по ссылке аргумента функции display() копия объекта не создается.

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

Возвращение объектов функциями

Если объекты можно передавать функциям, то "с таким же успехом" функции могут возвращать объекты. Чтобы функция могла вернуть объект, во-первых, необходимо объявить в качестве типа возвращаемого ею значения тип соответствующего класса. Во-вторых, нужно обеспечить возврат объекта этого типа с помощью обычной инструкции return. Рассмотрим пример функции, которая возвращает объект.

// Использование функции, которая возвращает объект.

#include <iostream>

#include <cstring>

using namespace std;

class sample {

  char s[80];

 public:

  void show() { cout << s << ""; }

  void set(char *str) { strcpy(s, str); }

};

// Эта функция возвращает объект типа sample.

Sample input()

{

 char instr[80];

 sample str;

 cout << "Введите строку: ";

  cin >> instr;

 str.set(instr);

 return str;

}

Int main()

{

 sample ob;

 // Присваиваем объект, возвращаемый

 // функцией input(), объекту ob.

 ob = input();

 ob.show();

 return 0;

}

В этом примере функция input() создает локальный объект str класса sample, а затем считывает строку с клавиатуры. Эта строка копируется в строку str.s, после чего объект str возвращается функцией input() и присваивается объекту ob в функции main().

Потенциальная проблема при возвращении объектов функциями

Относительно возвращения объектов функциями важно понимать следующее. Если функция возвращает объект класса, она автоматически создает временный объект, который хранит возвращаемое значение. Именно этот объект реально и возвращается функцией. После возврата значения объект разрушается. Разрушение временного объекта в некоторых ситуациях может вызвать непредвиденные побочные эффекты. Например, если объект, возвращаемый функцией, имеет деструктор, который освобождает динамически выделяемую память, эта память будет освобождена даже в том случае, если объект, получающий возвращаемое функцией значение, все еще ее использует. Рассмотрим следующую некорректную версию предыдущей программы.

// Ошибка, генерируемая при возвращении объекта функцией.

#include <iostream>

#include <cstring>

#include <cstdlib>

using namespace std;

class sample {

  char *s;

 public:

  sample() { s = 0; }

  ~sample() {

   if(s) delete [] s;

   cout << "Освобождение s-памяти.";

  }

  void show() { cout << s << ""; }

  void set(char *str);

};

// Загрузка строки.

void sample::set(char *str)

{

 s = new char[strlen(str)+1];

 strcpy(s, str);

}

// Эта функция возвращает объект типа sample.

Sample input()

{

 char instr[80];

 sample str;

 cout << "Введите строку: ";

  cin >> instr;

 str.set(instr);

 return str;

}

Int main()

{

 sample ob;

 // Присваиваем объект, возвращаемый

 // функцией input(), объекту ob.

 ob = input(); // Эта инструкция генерирует ошибку!!!!

 ob.show(); // Отображение "мусора".

 return 0;

}

Результаты выполнения этой программы выглядят таким образом.

Введите строку: Привет

Освобождение s-памяти.

Освобождение s-памяти.

Здесь мусор

Освобождение s-памяти.

Обратите внимание на то, что деструктор класса sample вызывается три раза! В первый раз он вызывается при выходе локального объекта str из области видимости в момент возвращения из функции input(). Второй вызов деструктора ~sample() происходит тогда, когда разрушается временный объект, возвращаемый функцией input(). Когда функция возвращает объект, автоматически генерируется невидимый (для вас) временный объект, который хранит возвращаемое значение. В данном случае этот объект просто представляет собой побитовую копию объекта str, который является значением, возвращаемым из функции. Следовательно, после возвращения из функции выполняется деструктор временного объекта. Поскольку область памяти, выделенная для хранения строки, вводимой пользователем, уже была освобождена (причем дважды!), при вызове функции show() на экран выведется "мусор". (Вы можете не увидеть вывод на экран "мусора". Это зависит от того, как ваш компилятор реализует динамическое выделение памяти. Однако ошибка все равно здесь присутствует.) Наконец, по завершении программы вызывается деструктор объекта ob (в функции main()). Ситуация здесь осложняется тем, что при первом вызове деструктора освобождается память, выделенная для хранения строки, получаемой функцией input(). Таким образом, само по себе плохо не только то, что остальные два обращения к деструктору класса sample попытаются освободить уже освобожденную область динамически выделяемой памяти, но они также могут разрушить систему динамического распределения памяти.

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

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

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

Для начала еще раз сформулируем проблемы, для решения которых мы хотим определить конструктор копии. При передаче объекта функции создается побитовая (т.е. точная) копия этого объекта, которая передается параметру этой функции. Однако возможны ситуации, когда такая идентичная копия нежелательна. Например, если оригинальный объект содержит указатель на выделяемую динамически память, то и указатель, принадлежащий копии, также будет ссылаться на ту же область памяти. Следовательно, если копия внесет изменения в содержимое этой области памяти, эти изменения коснутся также оригинального объекта! Более того, при завершении функции копия будет разрушена (с вызовом деструктора). Это может нежелательным образом сказаться на исходном объекте.

Аналогичная ситуация возникает при возврате объекта из функции. Компилятор генерирует временный объект, который будет хранить копию значения, возвращаемого функцией. (Это делается автоматически, и без нашего на то согласия.) Этот временный объект выходит за пределы области видимости сразу же, как только инициатору вызова этой функции будет возвращено "обещанное" значение, после чего незамедлительно вызывается деструктор временного объекта. Но если этот деструктор разрушит что-либо нужное для выполняемого далее кода, последствия будут печальны.

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

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

Прежде чем подробнее знакомиться с использованием конструктора копии, важно понимать, что в C++ определено два отдельных вида ситуаций, в которых значение одного объекта передается другому. Первой такой ситуацией является присваивание, а второй — инициализация. Инициализация может выполняться тремя способами, т.е. в случаях, когда:

■ один объект явно инициализирует другой объект, как, например, в объявлении;

■ копия объекта передается параметру функции;

■ генерируется временный объект (чаще всего в качестве значения, возвращаемого функцией).









Читайте также:

Последнее изменение этой страницы: 2016-03-17; Просмотров: 146;


lektsia.info 2017 год. Все права принадлежат их авторам! Главная