При первом знакомстве с указателями в C++ (см. Указатели в C++. Часть 1 ) может сложиться упрощённое представление, что указатели могут указывать только на отдельные переменные встроенных (скалярных) типов C++, и что это просто ещё одна, альтернативная форма доступа к таким переменным. В таком применении указатели были бы приятным дополнением языка, но с весьма ограниченными возможностями.
При более внимательном изучении указателей C++ мы обнаруживаем, что указатель может быть адресом размещения (указывать на) любого допустимого объекта в программе: структуры, объекта класса, массива, функции, или опять же указателя на некоторый объект, или указателя на указатель и так далее… Это делает указатели чуть ли не самым мощным инструментом программиста на C++ … но и самым опасным в смысле возможных скрытых ошибок.
Рассмотрим такие варианты подробнее. Самым простым вариантом будет использование указателей на составные объекты (объекты классов и структуры). Но уже такое использование указателей открывает широкие перспективы в C++, как мы сейчас увидим.
Итак, мы уже знаем, что описание класса не создаёт никаких новых объектов данных в программе — это только описание некоторого шаблона, нового типа данных, согласно которому будут создаваться реальные объекты данных.
При создании (объявлении) новых объектов данных мы можем вычислять адрес этих объектов и присваивать их указателям на объекты этого класса. Напишем простейшую программу, оперирующую с указателями на объекты некоторого класса (файл ex1.cc):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #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; } |
Здесь мы видим относительно новую в наших темах конструкцию:
1 | my( int numb ) : number( numb ) {}; |
Это конструктор нового класса my, но с параметром создания. При вызове он вызывает конструктор родительского класса (number(numb)), передавая ему это же значение параметра. Следующие далее скобки {} обрамляют пустой блок кода, который означает, что ничего более сверх вызова родительского конструктора делать не нужно.
Вспоминаем, что вся последовательность конструкторов всех родительских классов – вызывается (в порядке обратном наследованию) при вызове конструктора порождённого класса, но это только для конструкторов без параметров. В случае параметризированных конструкторов родиелей вам прийдётся вызывать явно.
Но мы на этом отвлеклись в сторону от предмета нашего изложения… А теперь самое время компилировать и посмотреть выполнение полученной нами программы:
Пока ничего принципиально нового, и всё это сильно похоже на то, как мы работали бы с указателями на переменные типа double, скажем.
Вспомним, в дополнение, что оператор new для динамического создания нового объекта:
а) вызывает менеджер динамического управления памяти и выделяет новый объем под размещение такого объекта;
б) вызывает конструктор соответствующего класса (типа данных) для начальной разметки (инициализации) выделенной памяти. Слегка модифицируем свой пример (файл ex2.cc):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #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 игрового сценария):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #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++ называется абстрактный класс: класс, в котором определена хотя бы одна виртуальная функция с определением вида:
1 | virtual double area( void ) = 0; |
В показанном классе таких функций аж 3. Естественно, что создать объект абстрактного класса невозможно, в нём есть функции, тело которых не определено. Попытка объявления объектов такого класса вызовет синтаксическую ошибку. Но от такого класса можно наследовать, создавать производные классы, которые унаследуют общие свойства родового абстрактного (например, координаты x и y центра фигуры и её размер r).
Определим 3 производных от figure класса: круг, квадрат и равносторонний треугольник:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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 геометрических объектов, над которыми можем выполнять единообразные (виртуальные) действия, не взирая на их различную природу:
1 2 3 4 5 6 7 | 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++, что требуют отдельного подробного рассмотрения. Об это поговорим в одном из следующих уроков.
По теме – указатель указывает на начало области памяти (хранит адрес). И нечего дальше писать про это, т.к. при использовании указателя ты просто берешь значение по этому адресу и приводишь его к нужному типу (любому).
Если и раскрывать тему – то надо ИМХО написать про то, какие проблемы решали указатели (не касаясь ООП, т.к. тут эта тема почти не раскрывалась) и написать по мотивам указатели vs ссылки (тема уже раскрыта у Маерса, можно взять там основу).
Но ты как то резко ты перешла к полиморфизму. Это не плохо, но юзерам, которые читали только твой сайт теперь мало что понятно. До этого, описывая классы ты не писала про виртуальные функции и даже про наследование. А тут вдруг на читателя упало все это и абстрактные классы.
В конце статьи ты описала полиморфизм – я думаю это стоит выделить в отдельную статью. Но перед этим написать статью про “Наследование классов в C++” (это последняя тема в твоем плане на главной странице)
Автор статьи – не админ сайта )) Простим ему это, так как он не перечитывал все наши статьи
Вот согласен. В начале про указатели, а потом какогото фига про полиморфизм. Как-то слишком в кучу всё.
Согласен. Последние статьи из раздела классов трудные.
Скажите, а кто админ? Лилия или Игорь? Я всегда думал, что от имени админа пишет Игорь.
А, что означают строки? В первом листинге
pm1->show();
pm2->show();
pm3->show();
Объект класса (типа) my – m1.
Указатель на этот объект – pm1.
m1.show() – вызов метода (функции) show() класса my для объекта m1.
pm1->show() – вызов точно того же метода show() класса my через указатель на объект m1.
Вообще-то m1 – это один объект, а pm1 – это указатель на совсем другой объект того же класса (в динамической памяти).
это на листинге 2.
1p1rlh
Подскажите как устранить эту ошибку?
Ошибка (активно) E0020 идентификатор “M_PI” не определен Project1