Попробуйте часть кода до слова main заменить на #include "stdafx.h" //1 #include <iostream> //2 using namespace std; //3 |
Классы как средство создания больших программных комплексов
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::show15.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;} };
Если в производном классе хотя бы одна из чисто виртуальных функций не переопределяется, то производный класс продолжает оставаться абстрактным и попытка создать объект (экземпляр класса) будет пресечена компилятором.