Отправляет email-рассылки с помощью сервиса Sendsay

C++ для всех

  Все выпуски  

C++ для всех


Информационный Канал Subscribe.Ru


C++ для всех. Выпуск 3

Классы. Механизм сокрытия данных. Иерархия классов. Конструкторы. Деструкторы. Структуры.

Класс является ключевой и самой востребованной конструкцией языка С++. Определение класса, по-моему, придумывать не стоит - возьмем готовую короткую, но достаточную формулировку из издания Герба Саттера "Решение сложных задач на С++": "класс описывает множество данных вместе с функциями, оперирующими этими данными". Объявляется класс с помощью ключевого слова class:


 class MyClass;

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


 class MyClass{
 };

 class MyClass1 : public MyClass{
 };

 class MyClass2 : private MyClass{
 };

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


 class MyClass{
 public:
  void calcValue(){ /*...*/  }
  int getValue()const{ return value; }
 private:
  int value;
 };

Данные-члены и функции-члены в классе могут быть объявлены в одной из трех областей, которые именуются ключевыми словами: public, protected, private (члены, объявленные вне этих областей, относятся к private). Рассмотрим каждую из этих областей:

  • public - все члены, объявленные в этой области будут видны из любого контекста вызова. Согласно концепции объектно-ориентированного программирования, крайне не рекомендуется в секции public располагать данные-члены, т.к. кто угодно получит возможность бесконтрольно изменять значения этих переменных. Как правило, здесь располагают открытые интерфейсные функции.
  • protected - члены этой области будут видны только в производных классах и friend-классах(непосредственно о наследованиии и дружественном объявлении поговорим несколько позже, поэтому данную информацию просто примите к сведению). Здесь нужно объявлять те члены, которые понадобятся производным классам.
  • private - все, что перечислено в этой области, будет доступно только в функциях-членах данного класса (и friend-классах). Здесь принято объявлять те данные и функции, которые необходимы для обработки данных или выполнения каких-либо действий только внутри данного класса.
  • 
     class MyString{
     public:
      MyString():name(0){}
      
      void setName(const char *nm){   
       int ssize = strlen(nm)+1;
       name = (char*)realloc(nm,ssize);
       fast_memcpy(name,nm,ssize);
      }
      const char *name()const{ return name; }
    
     protected:
      void fast_memcpy(void *d,const void *s,int size){ /*...*/ }
      
     private:
      char *name;
     };
    

    Теперь поговорим об иерархии классов и наследовании.

    В языке С++ определен механизм наследования, который позволяет передавать свойства базового класса всем наследуемым классам, при этом наследование происходит с полным соблюдением прав доступа к членам базового класса. Данный механизм позволяет создать базовый класс с определенными общими свойствами, и от него произвести ряд логически сходных классов.

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

    В определении будут использованы виртуальные и абстрактные функции, описание которых чуть ниже.

    Для начала нам нужно выделить общий интерфейс всех геометрических фигур.

    Итак, все наши геометрические фигуры должны себя нарисовать, повернуть и переместить на определенный интервал:

    
     class Color{};
    
     class GBase{
     public:
      GBase(const Color &c):color_i(c){}
      virtual ~GBase(){}
    
      virtual void  setColor(const Color &c){ color=c; }
      const Color& color()const{ return color_i; }  
    
     protected:
      virtual void  draw() const = 0;
      virtual void  rotate() = 0;
      virtual void  move(int dx,int dy) = 0;
    
      Color color_i; 
     };
    

    Функции draw(), rotate() и move() в приведенном контексте объявлены абстрактными, данное объявление обязывает объявить и реализовать эти функции наследниками. Благодаря абстрактным функциям объект базового класса GBase создать невозможно. Ключевое слово virtual говорит о том, что данная функция в любом классе может быть замещена аналогичной функцией производного класса (о механизме виртуальных функций поговорим несколько позже). Функции setColor() и color() могут использоваться в любом контексте, поэтому они объявлены public. Переменная color_i вынесена в область protected для свободного доступа из производных классов (не объектов!) только для ускорения доступа, более сложные типы, управление которыми осуществляется с помощью функций-членов данного класса, следует располагать в private-секции.

    Теперь напишем классы прямоугольника и квадрата:

    
     class GRectangle : public GBase
     {
     public:
      GRectangle(int x_,int y_,int w_,int h_,const Color &c)
       :GBase(c),x(x_),y(y_),w(w_),h(h_){}
      
      virtual void setWidth(int w_){ if(w!=w_){ w=w_; draw(); } }
      int width()const{ return w; }
      virtual void setHeight(int h_){ if(h!=h_){ h=h_; draw(); } }
      int height()const{ return h; }
     
     protected:
      virtual void draw()const{ /*рисуем картинку исходя из координат*/ }
      virtual void rotate(){ /*...*/ draw(); } 
      virtual void move(int dx,int dy){ x+=dx; y+=dy; draw(); }  
     
      int x,y; //координаты левого верхнего угла
      int w,h; //ширина и высота
     };
    
     class GSquare : public GRectangle
     {
     public:
      GSquare(int x_,int y_,int w_,const Color &c)
       :GRectangle(x_,y_,w_,w_,c){}
    
      virtual void rotate(){} //ничего не поворачиваем
    
      virtual void setWidth(int w_){ if(w!=w_){ h=w=w_; draw(); } }
      virtual void setHeight(int h_){ if(h!=h_){ w=h=h_; draw(); } }
     };
    

    По-правде говоря, данный пример не так прост как кажется. Спорный вопрос что от чего наследовать: GSquare от GRectangle или наоборот. В вышеприведенном случае в классе GSquare мы имеем две функции setWidth и setHeight, название которых не особо вяжется с квадратом.

    Посмотрите на объявление class GRectangle : public GBase. После двоеточия стоит ключевое слово public - это слово указывает на способ наследования интерфейса базового класса. Короче говоря, на все члены базового класса, которые видны в данном классе, дополнительно накладываются ограничения этой указанной области видимости (или доступа - кому что нравится). Рассморим короткий пример:

    
     class T1{
     public:
      int val;
     };
     class T2 : public T1{ /*...*/ }; 
    
     class T3 : protected T2{ /*..*/ };
     class T4 : public T3{
     public:
      void setValue(int v){ val=v; }
     };
    
     class T5 : private T1{ /*..*/ }
     class T6 : public T5{
     public:
      void setValue(int v){ val=v; } //это уже не прокатит
     };
     
     int main(){
      T1 t1;
      t1.val = 0; //работает
      
      T2 t2;
      t2.val = 0; //работает
    
      T3 t3;
      t3.val = 0; //не компилируется, val уже в области protected
    
      T4 t4;
      t4.setValue(0); //работает
    
      T5 t5;
      t5.val = 0; //не компилируется, val уже в области private
    
      T6 t6;
      t6.setValue(0); //не компилируется, val нет в T6
      return 0;
     }
    

    Рассмотрим теперь краткий пример использования классов геометрических фигур:

    
     int main()
     {
      Color cl;
      
      //ошибка компиляции, - присутствуют абстрактные классы
      //GBase bs(cl);
      
      GRectangle grec(0,0,10,20,cl); //правильно
      GSquare gsquare(20,30,10,cl);  //правильно
      
      //ошибка компиляции, - не определен конструктор по умолчанию
      GRectangle grec1;
      
      return 0;
     }
    

    В вышеприведенном примере нас интересует последняя ошибка компиляции, связанная с конструктором. Что же такое конструктор?

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

    
     class MyClass{
     public:
      MyClass():value(0){} //конструктор по умолчанию
      MyClass(int v):value(v){}
      MyClass(const MyClass &s){ value = s.value; }  
    
     private:
      int value;
     };
    
    
     class MyClass1{ //конструктор по умолчанию
     public:
      MyClass1(int v=0):value(v){}
     private:
      int value;
     };
    
     int main()
     {
      MyClass t; //вызывается конструктор по умолчанию
      MyClass t1(10); //вызывается конструктор MyClass(int v)
      MyClass t2(t); //вызывается конструктор MyClass(const MyClass &s)
    
      MyClass1 v1; //вызывается конструктор MyClass1(int v=0)
      MyClass1 v1(10);//вызывается конструктор MyClass1(int v=0)
    
      return 0;
     }
    

    Посмотрите на способ инициализации переменной value в определении конструктора MyClass:

    
     class MyClass{
     public:
      MyClass():value(0){} //конструктор по умолчанию
     ...
    

    Назначение переменным-членам класса определенных значений называется инициализацией, а переменные, перечисленные через запятую после объявления класса, называются списком инициализации.

    
     class SBase{
     public:
      SBase():val(0){ std::cerr << "SBase()" << '\n'; }  
      SBase(const SBase &s):val(s.value()){ { std::cerr << "SBase(const SBase &s)" << '\n'; } }
    
      int value()const{ return val; }
    
      const SBase& operator=(const SBase &s){
       val = s.val;
       std::cerr << "const SBase& operator=(const SBase &s)\n";
      }
    
     private:
      int val;
     };
    
    
     1)_______________________
    
     class NewClass{
     public:
      NewClass():v1(0),v2(0),v3(0){}
      NewClass(int v1_,int v2_,int v3_,const SBase &s):v1(v1_),v2(v2_),v3(v3_),sbase(s){}
     
     private:
      int v1,v2,v3;
      SBase sbase;
     };
    

    Но инициализировать данные можно и другим способом:

    
     2)_______________________
     class NewClass{
     public:
      NewClass(){
       v1 = 0;
       v2 = 0;
       v3 = 0;
      }
      NewClass(int v1_,int v2_,int v3_,const SBase &s){
       v1 = v1_;
       v2 = v2_;
       v3 = v3_;
       sbase = s; 
      }
     
     private:
      int v1,v2,v3;
      SBase sbase;
     };
    

    Тогда спрашивается, какая между ними разница? А разница такая: во втором случае мы имеем вызов лишнего конструктора по умолчанию для всех переменных, которые инициализируется иначе чем через список инициализации. Для простых объектов это мелочи, но для сложных вызов лишнего конструктора может привести к значительным накладным расходам и, к тому же, абсолютно не нужным расходам. Кроме того, происходит вызов оператора =, реализация которого в сложных классах предусматривает предварительную очистку прежних данных, т.е. к расходам можно еще добавить различные проверки, реализованные в операторе присваивания. Посмотрите пример, здесь показаны два варианта создания объекта класса NewClass для двух рассмотренных случаев - со списком инициализации и без него:

    
     1)________________________________________________________
     int main(){
      SBase s;
      std::cerr << "----------------------------------\n";
      NewClass nc(1,2,3,s);
      
      return 0;
     }
    
     На экране увидим:
    
     SBase()
     ----------------------------------
     SBase(const SBase &s)
     
    
     2)________________________________________________________
     int main(){
      SBase s;
      std::cerr << "----------------------------------\n";
      NewClass nc(1,2,3,s);
      
      return 0;
     }
    
     На экране увидим:
    
     SBase()
     ----------------------------------
     SBase()
     const SBase& operator=(const SBase &s)
     
     имеем лишний вызов оператора =
    

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

    
     class MemAllocator{
      int msize;
      char *membuf;
     public:
      MemAllocator(int size):msize(size),membuf((char*)malloc(msize)){}
     };
    
     Работает, а теперь изменим порядок членов-данных:
     class MemAllocator{
      char *membuf;
      int msize;
     public:
      MemAllocator(int size):msize(size),membuf((char*)malloc(msize)){}
     };
     На момент выделения msize содержит неизвестное значение, поэтому
     выделенный размер совсем не того размера который ожидаем.
    
     Следует делать так:
     class MemAllocator{
      int msize;
      char *membuf;
     public:
      MemAllocator(int size):msize(size),membuf((char*)malloc(size)){}
     };
    

    Пока по конструкторам все, рассмотрим деструкторы.

    Аналогично конструкторам, для каждого класса определяется деструктор, соответственно, если деструктор не определяете Вы, его определяет компилятор. Как и конструктор, деструктор не может возвращать значения, кроме того, в деструктор невозможно передать параметры. Деструктор у класса может быть только один.

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

    
     class MyClass{
     public:
      MyClass(int size):buffer(new char[size]){}
      ~MyClass(){ delete buffer; }
      
     private:
      char *buffer;
     };
    

    К отличительным особенностям деструктора следует отнести то, что деструктор не может генерировать исключения.

    Деструктор вызывается в двух случаях: при завершении блока {} для автоматического объекта и при вызове функции delete применительно к указателю на объект. Также можно вызвать деструктор самостоятельно, но этот способ применяется только при ручном управлении памятью и будет рассмотрен в следующих выпусках. После вызова деструктора использовать объект нельзя - объекта уже не существует.

    
     class STest{
     public:
      STest(){ std::cerr << "конструктор\n"; }
      ~STest(){ std::cerr << "деструктор\n"; }  
     };
    
     int main(){
      {
       STest s;
      }
      std::cerr << "------------------\n";
      STest *p = new STest();
      delete p;
      return 0;
     }
    

    На этом с деструкторами покончим, пока...


    В языке С++, также как и в С широко используются структуры. Структура - это класс, у которого все члены находятся в секции public. Отсюда вытекает и способ использования,- в С++ структуры следует использовать как наборы данных и не более. Единственное, что целесообразно, это определять при необходимости в структуре конструктор и деструктор, а также операторы сравнения и присваивания. Если же у Вас структура обрастает функциями доступа и модификации, то лучше реализовать все это через класс, т.к. полный доступ к членам структуры может привести со временем к проблемам, кроме того, использование структуры ничего не дает в плане скорости.

    Обозначается структура ключевым словом struct:

    
     struct STest{
      int val;
      double y;
      
      STest(int v=0,double y_=0):val(v),y(y_){}
     };
    

    На этом закончим третий выпуск. Пожелания, предложения, вопросы прошу на iqsoft@cg.ukrtel.net

    В следующем выпуске: Виртуальные функции. Виртуальные деструкторы.


    (с) Юрий Гордиенко iqsoft@cg.ukrtel.net


    http://subscribe.ru/
    E-mail: ask@subscribe.ru
    Отписаться
    Убрать рекламу

    В избранное