Нижегородский государственный университет им. Н.И.Лобачевского
Опубликован: 25.11.2008 | Доступ: свободный | Студентов: 9592 / 1296 | Оценка: 4.06 / 3.66 | Длительность: 21:16:00
Лекция 16:

Классы как средство создания больших программных комплексов

< Лекция 15 || Лекция 16: 123 || Лекция 17 >

15.1.3. Динамическое создание и удаление объектов

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

class A {...};    //объявление класса
..............
A *ps=new A;      //объявление указателя и создание объекта типа A
A* *pa=new A[20]; //объявление указателя и создание массива объектов
...............
delete ps;        //удаление объекта по указателю ps
delete [] pa;     //удаление массива объектов по указателю pa

Фактически, выполнение оператора new эквивалентно вызову конструктора класса, а обращение к оператору delete на автомате означает вызов деструктора. Создание одиночных объектов может быть совмещено с инициализацией объекта, если в классе предусмотрен соответствующий конструктор:

A *ptr1=new A(5);//создание объекта и вызов конструктора инициализации

Массив создаваемых объектов проинициализировать таким же образом нельзя.

В ранних версиях C++ для создания и уничтожения динамических объектов использовали обращения к функциям malloc ( запрос памяти ) и free ( освобождение памяти ). Неудобство применения этих функций по сравнению с операторами new/delete заключается в том, что для запроса памяти нужно знать количество байт, занимаемых объектом в оперативной памяти. Конечно, это не так уж и сложно – существует функция sizeof, с помощью которой длину объекта можно определить. Второе неудобство заключается в том, что функция malloc выдает указатель типа void* и его еще надо преобразовать к типу указателя на объект класса.

Довольно распространенная ситуация, которая может оказаться потенциальным источником ошибок, возникает в процессе создания и удаления динамических объектов. Она заключается в том, что после уничтожения объекта, связанного, например, с указателем ps, этот указатель не чистится. Если после удаления объекта сделать попытку что-то прочитать или записать по этому указателю, то поведение программы предсказать трудно. Поэтому достаточно разумным правилом является засылка нуля в указатель разрушаемого объекта:

delete ps;
  ps=NULL; 	//или ps=0;

15.1.4. Виртуальные функции

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

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

Рассмотрим пример, в котором базовый класс B содержит защищенное поле n и отображает его содержимое на экране. Производный класс D1 отображает квадрат доставшегося по наследству поля. Еще один класс D2, порожденный тем же родителем B, отображает куб своего наследства.

#include <iostream.h>
#include <conio.h>
class B {
public:
  B(int k):n(k){}	//конструктор инициализации
  virtual void show(){cout<<n<<endl;}	//виртуальная функция
protected:
  int n;
};
class D1: public B {
public:
  D1(int k):B(k){}	// конструктор инициализации
  virtual void show(){cout<<n*n<<endl;}
};
class D2: public B {
public:
  D2(int k):B(k){}	// конструктор инициализации
  virtual void show(){cout<<n*n*n<<endl;}
};
void main()
{ B bb(2),*ptr;
  D1 dd1(2);
  D2 dd2(2);
  ptr=&bb;
  ptr->show();
  ptr=&dd1;
  ptr->show();
  ptr=&dd2;
  ptr->show();
  getch();
}
//=== Результат работы ===
2	//результат работы функции B::show
4	//результат работы функции D1::show
8	//результат работы функции D2::show
15.2.

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

B *bptr;
  D1 dd1(2);
  bptr=&dd1;        //вниз по иерархии классов без преобразования
  D1 *dptr;
  dptr=(D1 *)bptr;  //вверх по иерархии с преобразованием типа

15.1.5. Виртуальные деструкторы

Когда существует иерархия производных классов, и мы создаем массив динамических указателей на объекты разных производных классов, то при уничтожении такого рода объектов могут возникнуть проблемы. Продемонстрируем это на примере иерархии геометрических фигур: Shape (фигура базового класса), Circle (окружность, производная от Shape ) и Rectangle (прямоугольник, производный от Shape ):

#include <iostream.h>
class Shape {
public:
  Shape();		//конструктор по умолчанию
  ~Shape();	//стандартный деструктор
  virtual void show() {cout <<"Shape"<<endl;
};
class Circle: public Shape {
  int xc,yc,r;	//координаты центра и радиус
public:
  Circle(int x,int y,int R):xc(x),yc(y),r(R) {}	//конструктор
  ~Circle();	//стандартный деструктор
  void show() {cout<<"x="<<xc<<" 
    y="<<yc<<" r="<<r<<endl;
};
class Rectangle: public Shape {
  int x1,y1,x2,y2;	//координаты противоположных вершин
public:
  Rectangle(int ix1,int iy1,int ix2,int iy2):
		 x1(ix1),y1(iy1),x2(ux2),y2(iy2) {}	//конструктор
  ~Rectangle();	//стандартный деструктор

Создаем массив указателей на объекты базового класса и присваиваем им адреса динамически создаваемых объектов:

Shape *ptr_s[2];
  ptr_s[0]=new Circle(20,20,10)
  ptr_s[1]=new Rectangle(20,40,50,50);

Поработали с временно созданными объектами, и пришла пора их удалить. Попытка сделать это следующим способом ни к чему хорошему не приведет:

for(int i=0; i<2; i++) delete ptr_s[i];

Причина заключается в том, что для удаления этих фигур будет вызван деструктор класса Shape (именно на объекты этого класса был объявлен массив указателей ptr_s ). А ресурсы, занятые окружностью и прямоугольником, при этом не будут освобождены. Выход из создавшегося положения довольно простой – надо объявить деструктор базового класса виртуальным ( virtual ~Shape(); ). Тогда автоматически виртуальными станут и деструкторы производных классов (хотя деструкторы и не наследуются). И все проблемы, связанные с утечкой памяти, будут решены.

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

15.1.6. Чистые виртуальные функции и абстрактные классы

Чистая виртуальная функция не совершает никаких действий, и ее описание выглядит следующим образом:

virtual тип name_f(тип1 a1,тип2 a2,...)=0;

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

Объявим абстрактным класс Shape (Геометрическая Фигура), в состав которого включим две чистые виртуальные функции – определение площади фигуры ( Get_Area ) и определение периметра фигуры ( Get_Perim ).

class Shape {
public:
  Shape(){}	//конструктор
  virtual double Get_Area()=0;
  virtual double Get_Perim()=0;
};
class Rectangle: public Shape {
  double w,h;	//ширина и высота
public:
  Rectangle(double w1,double h1):w(w1),h(h1) {}
  double Get_Area() {return w*h;}
  double Get_Perim() {return 2*w+2*h);}
};
class Circle: public Shape {
  double r;	//радиус
public:
  Circle(double r1):r(r1) {}
  double Get_Area() {return M_PI*r*r;}
  double Get_Perim() {return 2*M_PI*r;}
};

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

< Лекция 15 || Лекция 16: 123 || Лекция 17 >
Alexey Ku
Alexey Ku

Попробуйте часть кода до слова main заменить на 

#include "stdafx.h" //1

#include <iostream> //2
#include <conio.h>

using namespace std; //3

Александр Талеев
Александр Талеев

#include <iostream.h>
#include <conio.h>
int main(void)
{
int a,b,max;
cout << "a=5";
cin >> a;
cout <<"b=3";
cin >> b;
if(a>b) max=a;
else max=b;
cout <<" max="<<max;
getch();
return 0;
}

при запуске в visual express выдает ошибки 

Ошибка    1    error C1083: Не удается открыть файл включение: iostream.h: No such file or directory    c:\users\саня\documents\visual studio 2012\projects\проект3\проект3\исходный код.cpp    1    1    Проект3

    2    IntelliSense: не удается открыть источник файл "iostream.h"    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    1    1    Проект3

    3    IntelliSense: идентификатор "cout" не определен    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    6    1    Проект3

    4    IntelliSense: идентификатор "cin" не определен    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    7    1    Проект3

при создании файла я выбрал пустой проект. Может нужно было выбрать консольное приложение?