Позднее связывание с использованием виртуальных функций элементов

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

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

Синтаксис определения виртуальных функций элементов очень прозрачный: добавьте слово virtual к первому определению функции элементу:


  virtual void Show();



  virtual void Hide();





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

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

Какая именно функция элемент Show будет вызвана - зависит только от класса объекта, для которого вызывается Show, даже если вызов производится через указатель на базовый класс. Например,


  Circle ACircle



  Point* APoint_рointer = &ACircle; // указатель на Circle,



      // которому присваивается



      // значение указателя на



      // базовый класс, Point



  APoint_рointer->Show();   // вызывает Circle::Show!





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

Связывание значения с функциями-элементами

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


                            V            V       



                     void clock::tick(int sec)      Динамическое



                                   ^                связывание



                     {                              с функциями



                        val += sec;                 элементами



                     }                           



                       V                         



                     clock big_ben;                 // Объявление объекта



                        ^                        



                        v     v    v



                     big_ben.tick(25);





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

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

Изучение ООП на маленьком примере

Одно из важных достоинств Borland C++ в том, что вы можете изучить новые возможности ООП, не отказываясь от уже приобретенных ранее знаний. Фактически, мы собираемся сейчас потренировать на маленьком упражнении, превращающем программу на языке Си в объектно-ориентированную С++ программу < рации '&':


         int i = 42;



         int &k = i;        // k принимает значение i



         printf("%d\n", k); // k разыменовывается автоматически, печатает 42



         k = 55;            // Присваивает значение и i, и k = 55





Здесь уточняется как это переводится на Cи:

Tурбо и Borland C++
int i = 42; int i = 42;
int *k = &i; int &k = i;
printf("%d\n", *k); printf("%d\n", k);
*k = 55; k = 55;

Использование переменных alias, как параметров

Переменные alias используются как параметры к функциям, и обеспечивают новый путь передачи ссылок. Например,


          struct rect {



            int wd, ht;



          };                 передача указателя



                        V



          int area(rect &r)



          {



             return r.wd * r.ht;



          }          ^



                        - используем селектор вместо '->'





                           Здесь нет нужды использовать '&'



          rect r;      V



          int k = area(r);



       Struct rect {int wd, ht;



       };





Переменные-ссылки, используемые в качестве аргументов

Одним из нововведений в С++ является новый вид переменных - ссылки. Ссылка - переменная, задаваемая указателем. Чтобы сделать переменную ссылкой, необходимо после описателя типа поставить операцию '&'. Ссылка схожа с переменной во всем, однако, на самом деле она совпадает с другой переменной, адрес которой указывается при объявлении ссылки.

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


                   Си                        Турбо и Borland C++







          typedef struct {          struct rect { // Определение структуры



            int wd, ht;               int wd, ht; // Ширина и высота



          } rect;                   };            // прямоугольника



                                                     Передача ссылки



                                                 v



          int area(rect *r)         int area(rect& r) // Вычисление



          {                         {            // площади прямоугольника



            return r->wd*r->ht;       return r.wd * r.ht;



          }                         }         ^



                                                 - Использование раздели-



                                                   теля вместо '->'



          rect r;                   rect r;



          int k = area(&r);         int k = area(r);





В данном примере, для вычисления площади прямоугольника, после определения структуры rect описана функция элемент area с параметром-ссылкой r. После инициализации переменной r типа rect, выполняется вызов функции элемента area с аргументом r, который будет использоваться не явно, а в качестве ссылки. Таким образом, незаметно для нас, аргумент передан в функцию элемент, через его адрес. Отметим, что rect& r, rect &r и rect & r эквивалентны.

Отличием C++ от языка Си является смена разделителя с '->' на '.'.

Использование ключевого слова void.

Тип void был введен в системы программирования Си в Начале 80-х годов. Он является стандартным согласно ANSI Си.

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

В первом случае компилятор получает информацию о необходимости и проигнорировать какое-либо выражение:


            //Простейший случай использования ключевого слова void





Более интересным является второй случай, когда void* используется как родовой указатель, т.е. может указывать на объекты любого типа. Однако его значение не может быть присвоено какому-либо другому указателю, так как компилятор не знает размер объекта на который ссылается void*. Рассмотрим несколько примеров:


            void *gp;   //родовой указатель



            int *ip;    // указатель на int



            char *cp;   // указатель на char



            gp=ip;      // корректное преобразование



            ip=gp;      //        -"-



            cp=ip       // некорректное преобразование



            *ip=15;     // корректное присваивание



            *ip=*gp;    // некорректное присваивание





Одним из основных способов применения этого типа являются применения этого типа являются формальные параметры. Функция из стандартной библиотеки memcpy, например, определена в string.h как:


            void*memcpy(voidf*s1, const void*s2, unsigned int n);





Она копирует n символов из объекта, на который ссылается s2 в объект, на который указывает s1. Таким образом она работает с объектами любого типа.

Аргументы функции элемента, принимаемые "по умолчанию"

При вызове функции элемента в С++, разрешается не указывать ее последние аргументы в списке и, таким образом, избавить программиста от необходимости указывать их каждый раз при обращении. Определить такие аргументы лучше списком в начале программы. Например:


            #define RED  0x04



            #define BLUE 0x01





Тем не менее не теряется и гибкость, поскольку, при необходимости изменить используемые по умолчанию значения, просто задаются необходимые. В следующем примере последнему аргументу функции присваивается конкретное значение. Использование знака '=' означает, что это значение может быть использовано по умолчанию. Достаточно будет его пропустить, при обращении к функции.


            Назначает по умолчанию



                      красный цвет              



                                               v



            void set_pixel(int x, int y, int c = RED)



            {



              ...



            }





            set_pixel(100,100,BLUE);  // Переопределяет цвет



                                      // установленный по умолчанию



            set_pixel(200,300);       // По умолчанию использует цвет RED



                                      // (красный)





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

Прототипы функций

Прототипы функций в С++ записываются ранее того места, где эта функция будет использована. Это позволяет компилятору контролировать типы вызываемых функций. Рассмотрим, например, ошибочную функцию по вычислению налога на добавленную стоимость:


            int nalog(int, int, int);





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


            int nalog(int priceOld, int priceNew, int nalog1);



            priceOld=150.5;



            priceNew=135.7;



            delta=nalog(priceOld, priceNew, 0.28);





Такая запись будет ошибочной в стандартном Си, так как все аргументы приведутся к типу float и будут переданы в тело функции, которая ожидает передачу чисел типа int. В С++ это выражение преобразуется согласно прототипу функции. Таким образом, небольшое изменение делает программы на С++ более надежными и позволяет избежать множества ошибок.


            int nalog(int priceOld, int priceNew, int nalog1)



            {



                 return (priceNew - priceOld)*nalog1;



            }





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

Динамическое использование свободной памяти (операции new и delete)

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

Хотя в С++ можно использовать функции динамического распределения памяти языка Си, такие, как malloc, однако, С++ содержит некоторые мощные расширения, которые облегчают и делают более надежным динамическое распределение и освобождение объектов.

Речь идет о функциях, которые можно использовать для распределения динамической памяти, - new и delete. Работа с этими функциями строится в стиле операций:


                                          операция new



                                      v



                         double *d = new double;





                         delete d;



                           ^



                               операция delete





Важность операции new заключается в возможности следить за размерами выделяемой памяти, в соответствии с используемыми типами. Без вспомогательной информации, из вышеприведенного примера ясно, что выделено восемь байт для указателя типа double.

Сравните функции new и delete с функциями Си malloc() и free():


             В Си указывается



                 размер                        C++ (размер не указывается)



                               v                                          



            double *d = malloc(8);             double *d = new double;



            free(d);                           delete d;





            char *q = malloc(sizeof(int)*10);  char *q = new int[10];



            free(q);                           delete[10] q;





Если память выделяется для массива, а не для типа данных со стандартной длиной, то используется следующий синтаксис:


                                new объект[размер]





Например, чтобы динамически выделить память для массива из 100 целых чисел с именем counts, используйте вызов:


                              counts = new int [100];





Использование new и delete не только надежней, но и удобней. Они автоматически могут вызываться конструкторами и деструкторами.

Однако, если динамический объект создан с помощью оператора new, то программист несет ответственность за его освобождение, так как С++ "не знает", нужен ли еще этот объект. Для освобождения памяти можно использовать оператор delete. При выполнении оператора delete вызывается любой определенный вами деструктор.
Еще раз уточним, что delete имеет следующий синтаксис:


                                 delete указатель;





где "указатель" - это указатель, который использовался в операторе new для выделения памяти.

Потоки ввода-вывода

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

В Турбо и Borland C++ используется новый способ работы с потоками ввода-вывода, который заключается в использовании операций '>>' и '<<'. Функции потоков сокращают время разработки программы, избавляя от необходимости непосредственно иметь дело с различными типами форматов, которые требуются для функций рrintf и scanf. Ниже показан сравнительный пример:


                        Си                        Турбо и Borland C++



                                                                            





         #include <stdio.h>                 #include <iostream.h>



         main()                             main()



         {                                  {



           int k;                             int k;



           printf("Введите число: ");         cout << "Введите число: ";



           scanf("%d", &k);                   cin >> k;



           printf("Число: %d\n", k);          cout << "Число: " << k;



         }                                  }





cout - стандартный выходной поток (по умолчанию - экран). Данные (например, значения переменных и строки) посылаются в поток с помощью операции <<. Операция << (читающаяся как "поместить в...") пересылает данные справа от нее в поток слева.

cin - стандартный входной поток (обычно - клавиатура). Значения, вводимые с клавиатуры, присваиваются переменным с помощью операции >>. Использование операций >> и << для потоков ввода/вывода является типичным примером переопределения операций в С++ (см. "Переопределение операций").

Кроме cout и cin в библиотеке iostream предопределены - cerr (стандартное устройство для вывода сообщений об ошибках, соответствующее stderr в Си) и clog (полностью буферизованная версия cerr (в Си эквивалента нет).

Реальное преимущество при работе с потоками С++ заключается в той простоте, с какой можно переопределять операторы << и >> при работе с собственными типами данных. В C++ есть два класса, istream и ostream, которые можно использовать для ввода в объекты и вывода из них. Возможно написание собственных функций элементов работы с потоками, что и демонстрируется в следующем примере:


            struct clock {          // Простая структура данных



              int hr, min, sec;



              clock(int h, int m, int s) { hr = h; min = m; sec = s; }



            };






Для переопределения << для вывода объектов типа clock требуется следующее определение:

                                          Поток вывода из объекта и '<<'



                v             v     v



            ostream& operator<<(ostream& strm, clock& c)



            {



              strm << c.hr << ":" << c.min << ":" << c.sec;



            }                      ^



                                       Возможно повторное использование



                                       операции





Заметим, что переопределенная операция << должна возвращать ostream&, то есть ссылку на ostream, Теперь можно организовать вывод для объектов типа clock следующим образом:


            clock c(12,31,55);



            cout << c;






Вывод на принтере будет следующим:

            12:31:55





Дополнительную информацию о потоках ввода-вывода можно получить в руководстве программиста Турбо и Borland C++. В нем дана детальная информация: по библиотеке iostream (определенной в файле iostream.h); о форматировании ввода и вывода; о манипуляторах потока; вводе-выводе в файл; режимах открытия файла; строковой обработке потока, а также всех классах потоков: filebuf, fstream, fstreambase, ifstream, ios, iostream, iostream_withassign, istream, istream_withassign, istrstream, ofstream, ostream, ostream_withassign, ostrstream, streambuf, strstreambase, strstreambuf, strstream.

Встроенные функции

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

Это макрос-подобные функции, которые вставляются код исполнения, непосредственно в том месте откуда они вызываются. Ниже приведен пример:


             struct rect { // Определение структуры rect (прямоугольник)



               int wd, ht; // Ширина и высота



               inline int area(int wd, int ht) { return (wd * ht); }



             };



            ...



            node1 = area(first, two);





Такая подстановка выполнит код функции сразу, экономя время необходимое для ее вызова. Встроенные функции не обязательно помещать в структуры. Они должны быть только в поле зрения транслятора. Обычно их помещают в заголовочные файлы, а не в файлы c расширением .CPP. Заметим, что в определениях класса ключевое слово inline не требуется. Чтобы излишне не увлекаться встроенными функциями, помните - наиболее целесообразно делать функцию встроенной только когда объем ее кода меньше, чем размер кода, который потребуется для вызова ее извне.

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


            inline int area(int wd, int ht)



               {



                   return (wd * ht);



               }





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

Виртуальные функции элементы

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

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

Пример класса с виртуальными функциями элементами

Определим класс shape с двумя виртуальными функциями элементами:


       class shape {



       public:



         double xo, yo;



         shape(double x, double y); // Конструктор создания shape (фигуры)



         virtual double area(void); // Функция вычисляющая поверхность



         virtual void draw(void);   // Функция рисования shape



       };





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

            Данные объекта            Таблица               Нужная



                shape               виртуальных             функция



                                      функций



                                 >               



               xo, yo                (*area)() *      >  shape::area()



                                                 



               vptr *                (*draw)() *      >  shape::draw()



                                                 





При вызове объектов shape функцией draw(), выполняется подстановка кода следующим образом:


            My_shape.draw()      > *my_shape.vptr1([]);





Каждая виртуальная функция элемент проиндексирована в таблице. Это индексирование выполняется во время компиляции.
Для создания новых классов из shape можно использовать наследование и порождение.


            class circle : public shape {



            public:



              double radius;



              circle(double x, double y, double r);



              double area(void); // Переопределяет shape::area()



              void draw(void);   // Переопределяет shape::draw()



            };





Ниже показано, как устроен будет устроен просмотр таблицы виртуальных функций элементов сейчас:

             Данные объекта           Таблица               Нужная



                circle              виртуальных             функция



                                      функций



                                 >               



               xo, yo                (*area)() *      >  circle::area()



               radius                            



                                     (*draw)() *      >  circle::draw();



               vptr *                            



                        





Класс circle копируют таблицу виртуальных функций элементов из класса shape. Каждая виртуальная функция переопределяется классом circle путем передачи входов модифицированной таблице.

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


            shape *p;           // Объявление родового указателя shape



            shape s(0,0);       // исходной точки фигуры shape



            circle c(10,10,50); // и круга circle фигуры shape





            p = &s;             // Точка фигуры shape



            p->draw();          // Вызов shape::draw()



            p = &c;             // Точка круга circle



            p->draw();          // Вызов circle::draw()





Так c помощью виртуальных функций мы можем управлять поведением объектов. Вот таков полиморфизм в действии.

Обсудим порядок объявления виртуальных функций элементов.

Объявление виртуальных функций элементов

Фактически, мы уже видели как это делается на примере класса shape, однако, рассмотрим еще раз этот процесс:


       class shape {



       public:



         double xo, yo;



         shape(double x, double y); // Конструктор создания shape (фигуры)



         virtual double area(void); // Функция вычисляющая поверхность



         virtual draw(void);        // Функция рисования shape



       };  ^



               Ключевое слово virtual





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

Когда тип не проверяется

Как вы уже могли заметить, указатель базового класса, такой как *p фигуры shape, может указывать не только на объект shape, но также и на объект circle. Это делается без указания типа. Фактически, вы можете использовать базовый указатель класса, для указания на любой порожденный объект. Например, *p может указывать на rect, на box3d, на cylinder и так далее. Вот такие возможности заложены в указателях.

Однако, преобразование неверно. Например, указатель круга circle не может указывать на объект shape. Почему? Потому что радиус radius круга отсутствует у фигуры shape. Так если вы попытались сделать из фигуры круг, без радиуса, то вероятно будет ошибка.

Дружественные функции

Обычно доступ к приватным элементам класса ограничивается методами этого класса. Но иногда возникает необходимость предоставить доступ к приватным данным класса внешним функциям. Определить такие внешние функции (которые могут даже находиться вне классов), которым будет предоставлена возможность доступа к приватным элементам класса, позволяет описание friend (дружественный). Иногда можно видеть, что переопределенная операция также описывается, как дружественная, но, обычно, дружественные функции используются редко. Их присутствие в программе обычно означает то, что иерархия классов нуждается в видоизменении.

Рассмотрим пример. Возьмем наш класс sber_bank, с приватным элементом big_bucks и добавим в него "дружественную" функцию вычисления налога - irs:


            class sber_bank {



            private:                         // Начало раздела private



               double big_bucks;               // элемент private



            public:                          // Начало раздела public



               void deposit(double bucks);     // Элемент public



               double withdraw(double bucks);  // Элемент public



               friend void irs(void);          // Дружественная функция irs



            };





Дружественную функцию irs определим следующим образом:

            void irs(void)



            {



              big_bucks -= big_bucks * 0.10; // Взять 10% от итога



            }





Отметим, что хотя мы объявили irs() внутри класса, но она не является функцией элементом! Это достигается благодаря ключевому слову friend. Но даже хотя этот не функция элемент, irs() может выполнить указанную операцию с нашими данными, имеющими тип private.

Если бы функция big_bucks была элементом другого класса (например, класса free_shop), то в описании friend нужно использовать операцию разрешения области действия:


            friend void free_shop::irs(void);





Дружественным для описанного класса можно также сделать целый класс, для чего в описании используется ключевое слово class:

            friend class check_bucks;





После этого любая функция элемент класса check_bucks может получить доступ к приватным элементам класса sber_bank. Заметим, что в С++, как и в жизни, дружественность не транзитивна: если А является другом для Б, а Б является другом для И, то отсюда не следует, что А является другом для И.

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

Заключение

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

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

Назад | Содержание | Вперед
Сайт создан в системе uCoz