Указатели на объекты

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

При более внимательном изучении указателей C++ мы обнаруживаем, что указатель может быть адресом размещения (указывать на) любого допустимого объекта в программе: структуры, объекта класса, массива, функции, или опять же указателя на некоторый объект, или указателя на указатель и так далее… Это делает указатели чуть ли не самым мощным инструментом программиста на C++ … но и самым опасным в смысле возможных скрытых ошибок.

Рассмотрим такие варианты подробнее. Самым простым вариантом будет использование указателей на составные объекты (объекты классов и структуры). Но уже такое использование указателей открывает широкие перспективы в C++, как мы сейчас увидим.

Итак, мы уже знаем, что описание класса не создаёт никаких новых объектов данных в программе — это только описание некоторого шаблона, нового типа данных, согласно которому будут создаваться реальные объекты данных. При создании (объявлении) новых объектов данных мы можем вычислять адрес этих объектов и присваивать их указателям на объекты этого класса. Напишем простейшую программу, оперирующую с указателями на объекты некоторого класса (файл ex1.cc):

#include <iostream>

using namespace std;

 

class my {   // наш собственный класс

private:

   int number;

public:

   my( int numb ) : number( numb ) {};

   void show( void ) {

      cout << "my number is " << number << endl;

   }

};

 

int main( int argc, char **argv, char **envp ) {

   my m1( 10 ), m2( 15 ), m3 ( 25 );       // 3 объекта нового класса

   my *pm1 = &m1, *pm2 = &m2, *pm3 = &m3;  // 3 указателя на эти же объекты

   m1.show();

   m2.show();

   m3.show();

   pm1->show();

   pm2->show();

   pm3->show();

   return 0;

}

Здесь мы видим относительно новую в наших темах конструкцию:

my( int numb ) : number( numb ) {};

Это конструктор нового класса my, но с параметром создания. При вызове он вызывает конструктор родительского класса (number(numb)), передавая ему это же значение параметра. Следующие далее скобки {} обрамляют пустой блок кода, который означает, что ничего более сверх вызова родительского конструктора делать не нужно. Вспоминаем, что вся последовательность конструкторов всех родительских классов — вызывается (в порядке обратном наследованию) при вызове конструктора порождённого класса, но это только для конструкторов без параметров. В случае параметризированных конструкторов родиелей вам прийдётся вызывать явно.

Но мы на этом отвлеклись в сторону от предмета нашего изложения… А теперь самое время компилировать и посмотреть выполнение полученной нами программы:

Пока ничего принципиально нового, и всё это сильно похоже на то, как мы работали бы с указателями на переменные типа double, скажем.

Вспомним, в дополнение, что оператор new для динамического создания нового объекта:

а) вызывает менеджер динамического управления памяти и выделяет новый объем под размещение такого объекта;

б) вызывает конструктор соответствующего класса (типа данных) для начальной разметки (инициализации) выделенной памяти. Слегка модифицируем свой пример (файл ex2.cc):

#include <iostream>

using namespace std;

 

class my {

private:

   int number;

public:

   my( int numb ) : number( numb ) {};

   void show( void ) {

      cout << "my number is " << number << endl;

   }

};

 

int main( int argc, char **argv, char **envp ) {

   my m1( 10 ), m2( 15 ), m3 ( 25 );

   my *pm1 = new my( 10 ),

      *pm2 = new my( 15 ),

      *pm3 = new my( 25 );

   pm1->show();

   pm2->show();

   pm3->show();

   delete pm1;

   delete pm2;

   delete pm3;

   return 0;

}

Сборка и выполнение:

Пока ещё не произошло никаких радикальных изменений …, но обратим внимание на то, что в программе (а это могла бы быть сколь угодно большая программа) вообще не объявлены и не используются в явном виде объекты — программа оперирует только указателями на объекты.

И, наконец, мы приближаемся к той технике в использовании указателей на объекты, которая делает указатели самым мощным инструментом работы с классами и их объектами в C++. А именно: виртуализация функций-членов класса и полиморфизм. Понятие полиморфизма (изменчивости формы, многоликости) — одно из основных направлений развития C++ от его предшественника языка C. Оно состоит в том, что прародитель целого семейства наследуемых классовобъявляет некоторые свои функции-методы как virtual. А в разных наследуемых классах эти функции-методы переопределяются по-разному. Это означает, что в различающихся наследуемых классах (родственных) функция-метод с одним и тем же именем будет выполняться несколько различающимся способом, в зависимости от конкретного класса в котором она используется.

Продемонстрируем эту технику на примере. Создадим (файл ex3.cc) класс, описывающий вообще любые плоские фигуры на плоскости (это могут быть элементы некоторого 2D игрового сценария):

#include <iostream>

#include <math.h>

using namespace std;

 

class figure {                            // абстрактный класс

protected:

   double x, y;                           // центр: x, y

   double r;                              // масштаб фигуры

public:

   figure( double r ) { x = y = 0.0; this->r = r; }

   // далее - 3 чистые (пустые) виртуальные функции:

   virtual const char *title( void ) = 0;

   virtual double area( void ) = 0;      

   virtual double perimeter( void ) = 0;

   void show( void ) {

      cout << "фигура " << title() << ": площадь=" << area()

           << ", периметр=" << perimeter() << endl;

   };

};

Здесь перед нами образец того, что в C++ называется абстрактный класс: класс, в котором определена хотя бы одна виртуальная функция с определением вида:

virtual double area( void ) = 0;

В показанном классе таких функций аж 3. Естественно, что создать объект абстрактного класса невозможно, в нём есть функции, тело которых не определено. Попытка объявления объектов такого класса вызовет синтаксическую ошибку. Но от такого класса можно наследовать, создавать производные классы, которые унаследуют общие свойства родового абстрактного (например, координаты x и y центра фигуры и её размер r).

Определим 3 производных от figure класса: круг, квадрат и равносторонний треугольник:

class circle : public figure {            // класс всех кругов

public:

   circle( double r ) : figure( r ) {};  

   const char *title( void ) { return "круг"; };

   virtual double area( void ) { return M_PI * r * r; };

   virtual double perimeter( void ) { return 2. * M_PI * r; };

};

class square : public figure {            // класс всех квадратов

public:

   square( double r ) : figure( r ) {};  

   const char *title( void ) { return "квадрат"; };

   virtual double area( void ) { return r * r; };

   virtual double perimeter( void ) { return 4. * r; };

};

class triangle : public figure {          // класс всех равносторонних треугольников

public:

   triangle( double r ) : figure( r ) {};  

   const char *title( void ) { return "треугольник"; };

   virtual double area( void ) { return sqrt( 3. ) * r * r / 4.; };

   virtual double perimeter( void ) { return 3. * r; };

};

Теперь мы готовы создать программу, для которой строились все эти приготовления: создать произвольное число различных 2D геометрических объектов, над которыми можем выполнять единообразные (виртуальные) действия, не взирая на их различную природу:

int main( int argc, char **argv, char **envp ) {

   figure *fgs[] = { new circle( 3 ), new square( 3 ), new triangle( 3 ) };

   int n = sizeof( fgs ) / sizeof( fgs[ 0 ] );

   for( int i = 0; i < n; i++ ) fgs[ i ]->show();

   for( int i = 0; i < n; i++ ) delete fgs[ i ];

   return 0;

}

На этом простейшем примере показано то, что в объектной модели языка C++ называется полиморфизм. И это свойство является одним из самых мощных выразительных инструментов языка C++. И реализуется эта техника всегда через указатели на объекты (figure*).

Вот как будет выглядеть компиляция и выполнение нашего примера (ex3.cc) в терминале операционной системы Linux при использовании GCC компилятора с языка C++ (это будет лишний раз подтверждением того, что программирование на языке C++ в меньшей мере зависит от операционной системы):

Ещё раз обратимся к коду показанного примера, и лишний раз зафиксируем то чрезвычайно важное обстоятельство, что указатели C++ всегда типизированы: указатель не может быть «указателем на что-то». Язык C++ — это язык со строгой именной типизацией. Типом же указателя является: указатель на тип указываемой ним переменной, например «указатель на double». Указатели на различные типы несовместимы между собой по присвоению и сравнению.

Техника виртуальных функций и полиморфизма являются настолько основными для всей философии C++, что требуют отдельного подробного рассмотрения.