4856

Объектно-ориентированное программирование на языке С++

Книга

Информатика, кибернетика и программирование

Объектно-ориентированное программирование на языке С++. Объектно-ориентированное программирование как методология проектирования программных средств. Что такое объектно-ориентированное программирование? Объектно-ориентированное программирование...

Русский

2012-11-28

343.5 KB

75 чел.

Объектно-ориентированное программирование на языке С++

1. Объектно-ориентированное  программирование как методология проектирования программных средств.

1.1 Что такое объектно-ориентированное программирование?

Объектно-ориентированное программирование – это новый подход к программированию. По мере развития языков программирования возникали новые методы программирования. На каждом этапе создавался новый подход, который помогал программистам справляться с растущим усложнением программ.

В языках первого поколения (1954-1958 гг.) –FORTRAN1, ALGOL-58, реализуется хаотическая концепция программирования. Программам на этих языках была присуща неконтролируемая последовательность передач управления (оператор GO TO), отсутствие требования объявления данных, отсутствие подпрограмм.

К языкам второго поколения (1959-1961гг.) относят такие как FORTRAN2, ALGOL-60, COBOL, Lisp. В программах на этих языках были реализованы следующие методы программирования:

  •  описание данных;
  •  указатели для хранения адресов данных;
  •  подпрограммы как средство разбиения задачи на подзадачи и снижения уровня сложности задачи.

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

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

 

            

                                          

            

                                                                                                                 

 

Подпрограммы

Рис 1.1. Архитектура языков второго поколения.

Стрелками на рисунке обозначено воздействие подпрограмм на различные данные.

К языкам третьего поколения (1962-1970гг.) относят такие как ALGOL-68, PL/1, Pascal, Simula. В программах на этих языках были реализованы следующие методы программирования:

-структурное программирование (текст программы должен представлять собой композицию трех основных элементов: последовательность, условие, цикл, программа строится из блоков, заключенных в фигурные скобки  {  });

-передача данных в подпрограмму посредством параметров;

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

- абстракция данных (возможность определить в программе новые типы данных  со сколь угодно сложной структурой).

Архитектура языков третьего поколения показана на рис 1.2.

                       

                                                      

 

                                          

            

                                                                                                                 

 

 Подпрограммы

                        Модуль 1                   Модуль 2                         Модуль 3           

                   

                               Рис 1.2. Архитектура языков третьего поколения.

 

Таким образом, в языках второго и третьего поколений центральное место в программе занимает подпрограмма (процедура или функция в языке С). Поэтому такие языки называют процедурно-ориентированными. Процедурно-ориентированные языки позволили программисту создавать программы свыше 50000 строк длиной. Однако они оказываются несостоятельными, когда программа достигала определенной длины. Чтобы написать более сложную программу, необходим был новый подход к программированию. В итоге были разработаны принципы объектно-ориентированного программирования.

В 1970-1980 годах появились такие языки, как Smalltak, Object Pascal, C++, CLOS, ADA, Simula. Эти языки получили название объектно-ориентированных. Язык программирования С++ был разработан на основе языка С Бьярном Страуструпом и вышел за пределы его исследовательской группы в начале 80-х годов.

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

Объектно-ориентированное программирование – это методология программирования, которая основана на представлении программы в виде совокупности объектов, каждый из которых является реализацией определенного класса, а классы образуют иерархию на принципах наследуемости.

Архитектура программных систем на основе объектно-ориентированных языков показана на рис.1.3.

                        Объект 1

                

         Данные     

    

  Функции   

                                                                

                                                         

                                          Объект 2                 

                                                 Объект n

                                 

                               

                                                                  

Рис. 1.3. Архитектура программных систем на основе объектно-ориентированных языков.

            На рис.1.3 стрелками показано взаимодействие объектов.

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

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

1.2. Объект

В самом общем смысле объект – это осязаемая реальность, имеющая четко выраженное поведение.

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

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

Объект – это абстрактный тип данных, содержащий структуры данных и набор функций, обрабатывающих  эти данные.

Рассмотрим пример - фрагмент программы на С++. Предположим, что нам нужно работать с объектами – массивами типа int.

Пусть в программе имеется следующее объявление:

Листинг 1.1.

class Iarray

           {

          int maxitems;  //максимальное число элементов

          int citems;    //число элементов, размещенных в массиве

          int *items;    //указатель на массив

          public:

//конструктор –функция создания объекта класса

          Iarray(int nitems);

//деструктор - функция уничтожения объекта класса

          Iarray();

// функция занесения элемента в массив

         int putitem(int item);

// функция получения элемента из массива

        int getitem(int ind,int &item);

//функция получения фактического числа элементов в массиве

         int count() {return citems;};

                 };

Это объявление определяет не объект, а класс – новый тип с именем Iarray. В классе объявлены элементы-данные maxitems , citems, *items, описывающие свойства объекта и элементы-функции для работы с этими данными.

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

Чтобы создать объекты данного класса, следует записать:

Iarray  a(10), b(25), c(45);

Здесь объявлено 3 объекта с именами a, b, c, каждый из которых занимает определенную область в памяти (каждый объект – свою область). Свойства этих объектов являются общими. Элементы-данные классы описывают такие общие свойства объектов, как:

  •  максимальное количество элементов в массиве maxitems,
  •   число элементов, размещенных в массиве citems,
  •   адрес массива *items.

Элементы-функции класса реализуют алгоритмы обработки данных.

Таким образом, объект содержит в себе как данные, так и операции над ними.

1.3. Первая программа на С++

Листинг 1.2.

// Первая программа на С++

#include <iostream.h>  // заголовочный файл содержит определения классов:

                                    // ввода istream и вывода ostream

int main(void)

   {

     int x;

     float y;

     char str[80];

     cout <<” Введите целое число ”;   //cout – имя стандартного потока вывода

     cin >> y;                                         //cin - имя стандартного потока ввода

     cout <<” Введите число с дробной частью ”;

     cin >> x;

     cout <<” Введите строку ”;

     cin >> str;

     cout <<” Вы ввели числа: ”<< x <<”,”<< y  <<”\n”;

     cout <<” Вы ввели  строку: ”<< str <<”\n”;

     return 0;

   }

Поясним, как выполняется ввод-вывод в этой программе. В С++ по-прежнему доступны  функции ввода-вывода языка С, такие, как printf(), scanf()  и другие, однако С++ обеспечивает иной, лучший способ выполнения этих операций. В С++ ввод-вывод выполняется с использованием операций, а не библиотечных функций. Операция ввода – это >>, ее еще называют операцией извлечения из потока, а операция вывода – это<<, ее называют также операцией вставки в поток. В С эти операции являются операциями побитового сдвига  вправо и влево. В С++ они сохраняют первоначальное значение и, кроме того, используются и как операции ввода-вывода. Расширенное толкование << и >> является примером перегрузки операций. Перегрузка операций подробнее будет рассмотрена позже.

1.4. Основные принципы объектно-ориентированного программирования.

Основными принципами ООП являются: абстракция данных, инкапсуляция, полиморфизм, наследование.

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

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

Полиморфизм - возможность определить внутреннюю процедуру, исходя из типа данных.

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

Наследование – это процесс, посредством которого один объект может приобретать свойства другого. Точнее, объект может наследовать свойства другого объекта и добавлять к ним черты, характерные только для него. Наследование позволяет поддерживать концепцию иерархии классов и играет очень важную роль в ООП. В программе из листинга 1.2 наследование выражается в том, что классы  ввода istream и вывода ostream являются наследниками класса iostream.

Более подробно перечисленные принципы ООП будут рассмотрены позднее.

Контрольные вопросы

1.Какое взаимодействие между подпрограммами поддерживают языки второго и третьего поколений?

2.Какие методы программирования были реализованы в языках третьего поколения?

3.Какова архитектура языков третьего поколения?

4.Какие языки называют процедурно-ориентированными?

5.В чем сущность методологии объектно-ориентированного программирования?

6.Какова архитектура программных систем на основе объектно-ориентированных языков?

7.Приведите определение объекта, используемое в языке С++. Что содержит в себе объект?

8.Как выполняется ввод вывод данных в программах на С++?

9.Дайте характеристику основных принципов ООП.

2. НЕ ОБЪЕКТНО-ОРИЕНТИРОВАННЫЕ РАСШИРЕНИЯ С++.

2.1. Перегрузка имен функций

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

Пример использования этого механизма приведен в листинге 2.1.

    // Листинг 2.1. Перегрузка имен функций

    #include <stdio.h>

    //Прототипы трех функций с одним и тем же именем и разными

    //типами параметров  

    void print(int i);

    void print(long i);

    void print(double i);

    int main(void)

     {

      print(200);

      print(2000000L);

      print(3.14159);

      return 0;

     }

      void  (int i)

      {

      printf("%d\n",i)  ;

      }

     void print(long i)

      {

      printf("%ld\n",i)  ;

      }

     void print(double i)

     {

      printf("%lf\n",i)  ;

     }

В листинге 2.1 показано использование трех функций с именем print() и с различными типами аргументов. Вызов функции print() компилятор свяжет с тем или иным ее определением в зависимости от типа переданного аргумента.

Примечание. 

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

int print(int);

void print(int);

2.2. Динамическое  распределение памяти

Для распределения памяти под динамические данные в процессе исполнения программы можно использовать библиотечные функции языка С malloc(), farmalloc() и другие. Неудобство их заключается в том, что программист вынужден заниматься сам расчетами по выделению необходимого объема памяти.

В С++ для этого предлагается  унарный оператор new, синтаксис которого имеет вид:

Указатель на тип = new имя<(инициализатор)>;

Оператор new создает в памяти объект, тип которого определяется операндом и возвращает указатель на объект.

Например, объявление:

int *ip=new int;

создаст динамический неинициализированный объект целого типа, а объявление

int *ip=new int(3);

создаст динамический  объект целого типа с инициализацией его значением 3.

Освободить память, распределенную операцией new, можно с помощью оператора delete, например

delete ip;

Удаление указателя означает освобождение выделенной памяти для последующего ее использования.

Примечание.

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

Часто new используется для динамического распределения памяти под массив, например:

double *a;

int n=10;

a=new double[n];

или:

double *a= new double[10];

Здесь [] указывают, что распределяется память под массив объектов, а не под простую переменную.

Закончив использование массива, следует удалить его:

delete[] a;

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

int (*matrix)[10];

Такое объявление определяет matrix как указатель на массив из 10 целых. Для выделения памяти для матрицы размером 10*10 и присвоения адреса выделенной памяти переменной matrix, следует записать:

matrix= new int[10][10];

Удаление многомерных массивов любой размерности происходит так, как будто они имеют лишь одно измерение:

delete[] matrix;

2.3.Ссылки

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

С++ предлагает альтернативу указателям путем использования нового вида переменных – ссылок. Чтобы сделать переменную ссылкой, необходимо после описателя типа поставить знак &, например:

int i=0;

int &ir=i; // irэто ссылка на i

ir указывает на i подобно указателю. Оператор

cout << “ir=” << ir <<’\n’;

выведет  ir=0, то есть использование ссылки похоже на разадресацию указателя.

Ссылка обязательно должна быть инициализирована (т.е. должно быть что-то, для чего она является ссылкой).  

Важно!

Однажды инициализировав ссылку, ей нельзя присвоить другое значение!

Нельзя в той же программе, где используется ir как ссылка на i, присвоить ссылке ir адрес другого объекта т.е. ошибкой является:

int j;

ir=j;

Ссылка является алиасом, т.е. синонимом, другим именем объекта.

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

//Листинг 2.2. Передача параметров по ссылке

#include <iostream.h>

void incr(int &aa); // обьявление параметра-ссылки  в прототипе функции

void main(void)

{

int x=1;

 incr(x);   // неявная инициализация ссылки адресом переменной х

 cout << “x=” << x <<“\n”;

}

void incr(int &aa)

{

aa++;

}

Результатом работы этой программы будет x=2, так как функция получила доступ к содержимому памяти, в которой хранится переменная х посредством передачи параметра по ссылке.

Вывод.

Ссылку можно рассматривать как постоянный указатель, который всегда разадресован, поэтому для него не надо выполнять операцию разадресации.

Контрольные вопросы

1.Что означает перегрузка имен функций в С++?

2.Каким образом в С++ осуществляется динамическое распределение памяти под данные? Каким образом можно освободить память, динамически распределенную программе?

3.Что такое ссылка? В чем различие между ссылкой и указателем? В чем заключается удобство использования ссылок?

3. КЛАССЫ С++

3.1. Класс как абстрактный тип.

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

Класс-это фундаментальное понятие С++, с введением которого язык С++ становится объектно-ориентированным языком.

Класс – это тип, создаваемый программистом на основе уже существующих типов.

Можно дать и более развернутое определение класса:

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

3.2 Объявление класса.

Объявление класса можно выполнить следующим образом:

Ключ_класса   имя_класса<: базовый список>

                 {

                        список  элементов

                     };

где

      ключ класса –это одно из служебных слов class, struct, union;

      имя класса – произвольно выбираемый идентификатор;

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

Объявление класса создает уникальный тип, тип класса имя класса. Это позволяет объявить последующие объекты класса данного типа.

С++ позволяет также неполное объявление класса, например:

           class X;

           stuct Y;

           union Z;

Неполные объявления позволяют делать в программе ссылки к именам классов до того, как классы будут полностью определены.

Все компоненты класса в английском языке обозначаются термином member (член, элемент, часть). Функции, принадлежащие классу, обозначаются термином member function, а данные класса имеют название data members. В русском языке терминология, относящаяся  к классам, недостаточно устоялась, поэтому в литературе имеются многочисленные расхождения. Данные класса называют компонентными данными или элементами данных объектов класса. Принадлежащие классу функции называют методами класса или компонентными функциями.

Необязательный базовый список перечисляет базовые классы, наследником которых (производным от которых) является данный класс.

Например, объявим класс “вектор”:

Листинг 3.1.

class   V_3d

{

             // описания элементов-данных объединены в одном операторе

double   x,y,z;  // координаты вектора в трехмерной системе координат

public:

// функция – метод класса для вычисления модуля вектора

double  mod( )

   {

     return   sqrt  (x*x+y*y+z*z);  

   };

};

Таким объявлением класса определен не просто отдельный объект – трехмерный вектор, а любой произвольный вектор. Здесь задан целый класс объектов.

Примечание.

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

Служебное слово public делит тело класса на две части. Первая часть, до слова public по умолчанию является закрытой. Это означает, что имена в этой части – x, y, z могут использоваться только функциями – методами класса. Вторая, открытая часть, составляет интерфейс к объекту класса. Служебное слово public определяет, что метод класса – функция mod( ) является общедоступной, то есть может быть вызвана из любого места программы.

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

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

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

3.3. Управление  доступом  к  элементам  класса

Элементы класса получают атрибуты доступа, либо по умолчанию (в зависимости от ключа класса) либо при использовании спецификаторов доступа:

public (общедоступный),

private (собственный),

protected (защищенный).

Спецификатор public означает, что элементы общедоступны всем функциям, в том числе и не методам класса.

Спецификатор private определяет, что элементы могут быть использованы только функциями методами  и “друзьями” классов.

Спецификатор protected определяет те же права доступа, что и private, но дополнительно элементы могут быть использованы и методами- наследниками данного класса. Защищенные (protected) элементы классов нужны только в случае построения иерархии классов. Если наследование классов не используется в программе, то применение спецификатора protected эквивалентно использованию спецификатора private.

По умолчанию права доступа к элементам класса зависят от ключа класса:

- class-определяет право доступа как private;

- struct- определяет право доступа как public, но можно переопределить это умолчание при помощи спецификаторов доступа private и protected;

- union- определяет право доступа как public и переопределить  его нельзя.

Хорошим стилем  в ООП является  сокрытие данных и общедоступность методов для вызова их из любого места.

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

Листинг 3.2.

 class  Х {

                    int i;           // i  и ch  имеют по умолчанию право доступа

                    char ch;     // private

  public:

                   int j; // j и k являются общедоступными

                   int k;

  protected:

                   int l;..// l объявлен как защищенный элемент

};

struct Y

            {

int  i;   // i  по умолчанию имеет право доступа public

 private:

int j;     // j  объявлен  как защищенный элемент класса

public:

int k;    // k является общедоступным элементом

};

   union Z

{

int i;         // public по умолчанию, других

double d;  // вариантов нет

};

3.4 Объекты классов

Объект класса – это переменная, объявленная как принадлежащая классу. Для описания объекта класса используется конструкция:

имя_класса   имя объекта;

Например, можно объявить объекты класса V_3d следующим образом:

 V_3d    d, b, c, *a=&d, &a1;

Здесь

  d, b, c – объекты класса;

   – указатель на класс;

  a1 – ссылка на класс.

Для доступа к данным конкретного объекта используется конструкция:

имя объекта. имя элемента

Например, можно явно присвоить значения элементам объекта d класса V_3d:

d.x=1.34;   d.y=5.8;     d.z=8.5;

Объекты классов могут быть присвоены (если не было запрещено копирование), переданы как аргументы функции и возвращены функцией.

Действия с объектами класса выполняются с помощью методов класса.

3.5. Методы классов

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

В листинге 3.1  функция – метод класса mod() ,была, как объявлена внутри класса, так и определена. Это удобно для небольших функций.  В общем случае определение функции может располагаться вне класса. В этом случае компилятору следует указать, к какому именно классу принадлежит определяемая функция. Для определения функции вне класса в общем случае используется конструкция:

тип_возврата   имя_класса ::имя_функции( )

{

   тело функции

}

Последовательность символов в виде двойного двоеточия :: называется  операцией области видимости (или принадлежности), она показывает, к какому классу принадлежит функция. Например, в листинге 3.3 показано, как записать определение функции mod() вне  класса V_3d:

Листинг 3.3.

// определение функции mod()

double V_3d:: mod( ) //операция :: означает, что функция mod()

                                    //  принадлежит классу V_3d

 {

return   sqrt  (x*x+y*y+z*z);

 }

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

Следовательно, все методы находятся в сфере действия своего класса, даже если они определены вне этого класса.

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

имя_объекта. имя_метода( );

Например, вызов функции mod() для  объекта  d может быть записан так:

d.mod( );

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

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

                               

имя класса  *this;

this инициализируется так, что он указывает на объект, для которого была вызвана функция, в примере  вызова метода d.mod() для  объекта  d  this=&d.  

Функция-метод класса может вызываться  и через указатель на объект, например, если указатель а инициализирован адресом объекта d, то вызов функции mod() может быть записан так:

a->mod( );

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

3.6. Конструкторы

Данные, описанные внутри класса, как правило, имеют атрибут доступа private. Это означает, что данные класса доступны только методам этого класса. В таком случае следует предусмотреть функцию –метод класса для инициализации данных.

Функция создания и инициализации объектов данного класса называется конструктором.

Конструкторы обладают большинством характеристик обычных методов – их следует объявить и определить в пределах класса, либо объявить в классе, но определить вне его. Однако они обладают некоторыми уникальными свойствами:

  •  конструкторы имеют то же имя, что и класс;
  •  не имеют объявлений возврата (даже void);
  •  не могут быть унаследованы, хотя производный класс может вызывать конструкторы базового класса;
  •  вызвать конструктор тем же образом, что и обычную функцию – метод класса, нельзя. Конструктор вызывается неявно, при создании или копировании объекта данного класса.

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

Конструкторы глобальных переменных вызываются до вызова функции main(). Локальные объекты создаются, как только становится активной сфера действия переменной.

3.7. Параметры конструктора

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

имя_класса (список формальных параметров)

                                  { операторы тела конструктора  };

Такой конструктор называется конструктором, получающим параметры. Пример программы, в которой использован такой конструктор, приведен в листинге 3.4.

Листинг 3.4

       #include <iostream.h>

       #include <math.h>

     // Объявление класса “трехмерный вектор” 

        class V_3d

         {
           double x;
           double y;

           double z;

public:

// конструктор получает три параметра – ссылки на

//данные типа double

// определение конструктора находится внутри класса
V_3d( double &X, double &Y,double &Z)
{ x=X; y=Y; z=Z;  };

//объявление функции – метода класса
           
double mod();
         };  

void main(void)
            {

// Создание объекта R класса V_3d с инициализацией элементов-

// данных значениями переданных конструктору параметров
              
V_3d R(1.0,2.0,3.0);// неявный вызов конструктора
           double a;
           a=R.mod();
           cout << "\n длина вектора =" << a;
             }

//определение функции – метода класса
            
double V_3d :: mod()

 {

return sqrt(x*x+y*y+z*z);

 }

 

Конструкторы могут иметь аргументы по умолчанию. Например, конструктор

V_3d :: V_3d(double, double, double =7.6)

может принимать два или три аргумента. Если данный конструктор при инициализации объекта будет представлен с двумя аргументами, недостающий третий аргумент будет принят как значение типа double, равное 7.6. Аналогично конструктор

V_3d :: V_3d(double=5.5, double=6.3, double =7.6)

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

Конструктор по умолчанию. Конструктором по умолчанию для класса Х называется такой конструктор, который не принимает никаких аргументов: X::X(). Например:

V_3d () { x=0;  y=0; z=0;  };

Примечание

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

Конструктор копирования. Конструктор копирования для класса . Х - это такой конструктор, который может быть вызван с единственным аргументом типа Х:

Х:: Х(const Х&)

 Конструктор копирования запускается при копировании объекта данного класса, обычно в случае объявления с инициализацией объектом другого класса: Х x = y.

Если конструкторы в теле класса не были определены явно, то компилятор формирует по умолчанию конструктор без параметров и конструктор копирования.

3.8. Перегрузка конструкторов

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

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

В листинге 3.5 приведен пример программы, в которой используются перегруженные конструкторы.

Листинг 3.5. Перегрузка конструкторов

#include <iostream.h>
#include <math.h>


class V_3d
{
double x,y,z;


public:
//
конструктор по умолчанию

V_3d()  {x=0;y=0;z=0;};

//конструктор, принимающий параметры

V_3d(double x1,double y1,double z1)

{

 x=x1;

  y=y1;

  z=z1;

};

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

V_3d(const V_3d& V1);

// функция вычисления модуля

double mod()

{

return sqrt(x*x+y+y+z*z);

};

// функция для вывода элементов – данных класса

void print();
};


void main(void)
{
double a;

//создание объекта B класса V_3d

V_3d B;       //
запуск конструктора по умолчанию
cout << "координаты вектора B:\n";
B.print();    //
вызов функции – метода класса

//создание обьекта R класса V_3d

V_3d R(1.0,2.0,3.0) ;          //используется конструктор с параметрами

cout << "координаты вектора R:\n";

R.print();

a=R.mod();       //вызов метода класса

cout << "\n длина вектора R = " << a <<"\n";

V_3d C=R;   // запуск конструктора копирования для объекта C 

cout << "\nкоординаты вектора C:\n";

C.print();

}


//определение конструктора копирования

V_3d::V_3d(const V_3d& V1)
{

 x=V1.x;
y=V1.y;
z=V1.z;
}


// определение метода класса для вывода координат вектора

void V_3d::print()

{

cout << "x= " << x << "  y= " << y << "  z= " << z <<"\n";

}

Эта программа выводит на экран следующие данные:

     координаты вектора B:

     x= 0  y= 0  z= 0

     координаты вектора R:

     x= 1  y= 2  z= 3

     длина вектора R = 3.741657

     координаты вектора C:

     x= 1  y= 2  z= 3

3.9. Инициализация  объектов  класса

Существуют два способа инициализации данных объекта с помощью конструкторов:

  •  передача значений параметров в тело конструктора. Он продемонстрирован в программе из листинга 3.5.
  •  применение списка инициализаторов данных объекта. Этот список помещается между списком параметров и телом конструктора:

имя_класса (список_параметров):

                         список_инициализаторов_элементов_данных

{   тело конструктора };

Каждый инициализатор списка относится к элементу и имеет вид:

имя_элемента_данных (выражение)

Например:

class V_3d

              {

              double  x,y,z;

     public:

V_3d ( double  x1, double y1, double z1) :  x (x 1),y (y1+x1),z (z1) { };

            };

Инициализация V_3d A(2.2, 3.0, 6.2) присваиваете элементам объекта А следующие значения: A.x=2.2, A.y=5.2, A.z=6.2.

Этот  способ  предпочтительнее при использовании   механизма  наследования классов.

3.10. Деструкторы.

Деструктор- это функция уничтожения  объекта данного класса. Деструктор представляет метод с именем, совпадающим с именем класса, перед которым стоит символ ~(тильда). Это универсальный символ, обозначающий “не” (“не конструктор”). Например:

            сlass X

             {

               public:

                  ~ X();        // деструктор класса Х

              };

Деструктор не должен иметь параметров, и, как и конструктор - типа возврата.

Класс может иметь только один деструктор или не иметь вообще. Если деструктор не объявлен для класса явно, компилятор генерирует его автоматически.

Вызов деструктора выполняется неявно, когда объект выходит из своей объявленной сферы действия.

Примечание.

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

class Massiv {

  int maxitems ;         // Максимальное количество элементов массива

  int citems ;              // число элементов, размещенных в массиве

int *items ;           // Указатель на массив

  public:

//Конструктор

Massiv( int nitems)

{

   maxitems = nitems;

  items = new int [ nitems ];

  citems = 0 ;

}

//Деструктор

~ Massiv( )

{

 delete items ;};

};

void main (void)

{

..............

Massiv  *p = new Massiv (100) ;

 delete p ;

..............

}

В этом примере память для объекта была распределена динамически с помощью операции   new. В теле деструктора один оператор, освобождающий память, выделенную под размещение массива типа int при создании объекта класса Massiv.

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

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

Контрольные вопросы

1.Что такое класс? Как выполнить объявление класса. 2. Каким образом элементы класса получают атрибуты доступа?

3.Что такое объект класса и как выполнить объявление объекта класса в программе?

4. Что такое методы класса? Каковы особенности объявления и  определения методов класса? Что такое указатель this? Как выполняется вызов функции – метода класса?

5. Какие функции называются конструкторами? Каков механизм передачи параметров конструктору?

6.Как выполняется инициализация объектов класса?

7.Каким образом осуществляется уничтожение объектов класса?

4. ПЕРЕГРУЗКА ОПЕРАЦИЙ

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

   .          - прямой выбор компонента структурированного объекта;

 .*          - обращение к компоненту через указатель на него;

 ::          - операция указания области видимости;

 ?:          - условная операция;

      sizeof        - операция вычисления размера в байтах;

      #  и  ##   - препроцессорные операции.

Перегрузка операций выполняется с помощью операций - функций.

Синтаксис объявления прототипа операции – функции:

Тип_возвращаемого значения  operator знак_операции

                                        (список параметров операции-функции);

Если принять, что конструкция operator знак_операции есть имя некоторой функции, то прототип операции-функции подобен прототипу обычной функции языка С.

4.1. Перегрузка бинарных операций

Рассмотрим перегрузку бинарных операций на примере простой программы, показанной в листинге 4.1. В этой программе  показана перегрузка операций сложения + и присваивания =  в  классе V_3d, содержащем координаты вектора в трехмерном пространстве.

Листинг 4.1

          #include <iostream.h>

                   class V_3d

                 {

                    double x,y,z;

                   public:

//конструктор по умолчанию

V_3d(){x=0;y=0;z=0;};

//конструктор, принимающий параметры

V_3d(double x1,double y1,double z1)

{  x=x1; y=y1; z=z1; };

//вывод вектора

void print();

// функция для перегрузки +

V_3d operator +(V_3d t);

// функция для перегрузки =

V_3d operator =(V_3d t);

};

void main(void)

{

//создание обьектов а, b, d, s  класса V_3d

 V_3d a(2,3,5);   //запускается конструктор с параметрами

 V_3d b(5,6,5);

 V_3d d,s;  // запускается конструктор по умолчанию

cout <<    "координаты вектора a:\n";

 a.print();

 cout << "координаты вектора b:\n";

 b.print();

 d =a+b;      //демонстрация  перегруженной операции сложения векторов a  и b

   cout << "\n сумма векторов a и b :\n";

d.print();

       s=a;  // демонстрация перегруженной операции присваивания

       cout << "\n координаты вектора s\n";

       s.print();

    }

//метод класса для вывода координат вектора

void V_3d::print()

{

cout << "x= " << x << "  y= " << y << "  z= " << z <<"\n";

}

// определение функции - оператора +

V_3d  V_3d::operator +(V_3d t)

{

  V_3d c;

  c.x=x+t.x;  c.y=y+t.y;  c.z=z+t.z;

  return c;

  }

  // определение функции - оператора=

 V_3d V_3d::operator =(V_3d t)

{

  x=t.x;

  y=t.y;

  z=t.z;

  return *this;

}

Эта программа выводит на экран следующие данные:

       координаты вектора a:

       x= 2  y= 3  z= 5

      координаты вектора b:

      x= 5  y= 6  z= 5

       сумма векторов a и b :

       x= 7  y= 9  z= 10

        координаты вектора s

       x= 2  y= 3  z= 5

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

d=a+b;

эквивалентна следующему явному вызову операции - функции:

d = a. operator + ( b );

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

Вывод.

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

4.2. Перегрузка унарных операций

Можно перегрузить унарные операции, такие, как ++ или --. При перегрузке унарных операций с помощью функций – методов класса функция-оператор не имеет параметров. Унарная операция выполняется над объектом, осуществляющим вызов функции-оператора путем неявной передачи указателя this. 

Пусть имеется следующее определение класса:

class V_3d

{

int x,y ,z ;

 public:

 // прототип функции-оператора для перегрузки префиксной операции инкремента

V_3d operator ++ ( );   

// прототип функции-оператора для перегрузки постфиксной операции инкремента

V_3d operator ++( int );

// прототипы других функций

//........

}

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

Определение  функции для перегрузки префиксной операции инкремента:

          V-3d V_3d :: operator ++( )

          {

               x ++ ;

               y ++ ;

               z ++ ;

//возвращает  this - указатель на объект, для которого вызвана эта функция-оператор

return  *this ;

}

Если затем в программе будет создан объект класса V_3d:

V_3d   c ;

То возможно следующее увеличение с:

++ c;

Определение  функции для перегрузки постфиксной операции инкремента:

 V_3d  V_ 3d :: operator  ++( int )

       {

           V_3d tmp ;

          tmp = *this ;

           x ++;

           y ++;

           z ++;

// Возвращает старое значение объекта.

return tmp ;

           }

Замечание для постфиксной операции:

она увеличивает значение элементов-данных объекта на единицу, но возвращает старое значение объекта.

4.3 Друзья классов

В разделе 3.3  было показано, что механизм управления доступом позволяет выделить общедоступные (public), защищенные (protected) и собственные (private) элементы классов. Защищенные элементы доступны внутри класса и в производных классах. Собственные элементы локализованы в классе и недоступны извне. С помощью общедоступных элементов реализуется взаимодействие класса с любыми частями программы.

Имеется еще одна возможность расширить интерфейс класса. Ее обеспечивают дружественные функции.

Дружественной функцией класса называется функция, которая, не являясь методом класса, имеет право доступа к private и protected элементам класса.

Функция не может стать другом класса “без его согласия”. Для получения прав друга функция должна быть описана в теле класса со спецификатором friend. 

4.3.1. Использование дружественных функций при перегрузке операций

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

Листинг 4.2. Перегрузка операций с помощью дружественных функций.

 #include <iostream.h>

 #include <math.h>

class V_3d

{

int x,y,z;

       public:

      //конструктор по умолчанию

       V_3d(){x=0;y=0;z=0;};

//конструктор, принимающий параметры

V_3d(int x1,int y1,int z1)

         {

          x=x1;  y=y1;  z=z1;

         };

 

   //вывод вектора

    void print();

   // функция-друг для перегрузки +

   friend V_3d operator +(V_3d op1, V_3d op2);

  // функция-друг для перегрузки ++

   friend V_3d operator ++(V_3d &op1);

// функция для перегрузки =

   V_3d operator =( V_3d t);

    };

 

  void main(void)

  {

//создание объектов а, b, d, s  класса V_3d

      V_3d a(2,3,5);

      V_3d b(5,6,5);

      V_3d d,s;

      cout << "координаты вектора a:\n";

      a.print();

      cout << "координаты вектора b:\n";

      b.print();

      d=a+b;  //демонстрация  перегруженной

              // операции сложения векторов a  и b

      cout << "\n сумма векторов a и b :\n";

     d.print();

      s=a;  // демонстрация перегруженной операции присваивания

      cout << "\n координаты вектора s\n";

      s.print();

      ++s;

      cout << "\n координаты вектора s после увеличения на 1\n";

      s.print();

    }

//метод класса для вывода координат вектора

 void V_3d::print()

 {

  cout << "x= " << x << "  y= " << y << "  z= " << z <<"\n";

 }

// определение  дружественной функции - оператора +

   V_3d operator +(V_3d op1,V_3d op2)

 {  

   V_3d c;

  c.x=op1.x+op2.x;

  c.y=op1.y+op2.y;

  c.z=op1.z+op2.z;

  return c;

  }

  // определение  дружественной функции - оператора ++

   V_3d operator ++(V_3d &op1)

   {

   op1.x++;

   op1.y++;

   op1.z++;

   return op1;

   }

  // определение функции - оператора=

   V_3d V_3d::operator =(V_3d t)

   {

  x=t.x;

  y=t.y;

  z=t.z;

  return *this;

   }

    Результаты работы этой программы:

       координаты вектора a :

       x= 2  y= 3  z= 5

      координаты вектора b:

       x= 5  y= 6  z= 5

      сумма векторов a и b :

      x= 7  y= 9  z= 10

      координаты вектора s

        x= 2  y= 3  z= 5

        координаты вектора s после увеличения на 1

                x= 3  y= 4  z= 6

Из этой программы видно, что дружественной функции operator + передаются оба операнда. Левый операнд передается в переменной op1, а правый – в переменной op2. Дружественной функции operator ++ передается один операнд – ссылка на объект, так как она должна модифицировать элементы – данные объекта.

Рекомендация:

Для перегрузки операций следует пользоваться функциями-методами. Функции-друзья предназначаются в С++ большей частью для специальных ситуаций.

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

Дружественная функция при вызове не получает указателя this. Объекты классов должны передаваться дружественной функции только явно через аппарат параметров. При вызове дружественной функции нельзя использовать синтаксис вызова функций – методов класса:

имя объекта. имя_функции

и

указатель_на_объект ->имя_функции,

так как дружественная функция не является методом класса. Именно поэтому на дружественную функцию не распространяется и действие спецификаторов доступа (public, private, protected). Место размещения прототипа дружественной функции внутри определения класса безразлично. Права доступа дружественной функции не изменяются и не зависят от спецификаторов доступа.

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

class Y

{.....

void f1( X& );

void f2 ( X* );

 .....

};

class X

{

friend Y;     // все функции класса Y являются друзьями класса X

 int i;

 void f3 ( );  // функции f1 и f2 класса Y являются друзьями класса X

                     // и имеют доступ к private элементам i и f3 класса X

};

4.4. Перегрузка операций >>и << для ввода-вывода встроенных типов

В файле <iostream.h> определены классы ostream и istream, в которых объявлены функции-операторы как методы классов, перегружающие действие побитовых операций >> и  <<  для организации  неформатированного ввода и вывода данных базовых типов языка С++.

Класс istream определен как:

       class istream

             {

             //   .....

     public:

         istream & operator >> ( char * );   //символьная строка

         istream & operator >> ( char & );  //символ

         istream & operator >> (short & );

         istream & operator >> ( int & );

         istream & operator >> ( long & );

         istream & operator >>  ( float & );

         istream & operator >> (double &);

         // …

}

Класс ostream определен как:

class ostream

{

//.....

public:

ostream & operator << ( char * );

ostream & operator << ( int );

ostream & operator << ( long );

ostream & operator << ( double );

ostream &  put ( char * );

}

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

4.5. Перегрузка операций >> и << для ввода, вывода типов, объявленных пользователем

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

Листинг 4.3

#include <iostream.h>

             class V_3d

              {

              int x,y,z;

              public:

    //конструктор по умолчанию

         V_3d(){x=0;y=0;z=0;};

      //конструктор, принимающий параметры

          V_3d(int x1,int y1,int z1)

            {

           x=x1;y=y1;z=z1;

             };

      //функция друг для вывода вектора

  friend ostream &operator <<(ostream &stream, V_3d obj);

            //функция друг для ввода вектора

            friend istream &operator >>(istream &stream, V_3d &obj);

            // функция-друг для перегрузки +

           friend V_3d operator +(V_3d op1, V_3d op2);

         // функция-друг для перегрузки ++

           friend V_3d operator ++(V_3d &op1);

           // функция для перегрузки =

           V_3d operator =( V_3d t);

            };

                   void main(void)

                 {

                  //создание обьектов а, b, d, s  класса V_3d

              V_3d a;

              V_3d b;

              V_3d d,s;

               cout << " Введите координаты x,y,z вектора a: \n";

               cin >> a;

               cout << "координаты вектора a:\n";

               cout << a;

               cout << " Введите координаты x,y,z вектора b: \n";

               cin >> b;

               cout << "координаты вектора b:\n";

               cout << b;

              d=a+b; //демонстрация  перегруженной операции сложения векторов a  и b

               cout << "\n сумма векторов a и b :\n";

               cout << d;

               s=a;  // демонстрация перегруженной операции присваивания

               cout << "\n координаты вектора s\n";

               ++s;

               cout << s;

               cout << "\n координаты вектора s после увеличения на 1\n";

               cout << s;

                     }

 // определение функции друга для вывода вектора

  ostream &operator <<(ostream &stream, V_3d obj)

  {

  stream << "x=" << obj.x <<", " ;

  stream << "y=" << obj.y <<", " ;

  stream << "z=" << obj.z <<"\n" ;

  return stream;    //возврат потока

  }

   //определение  функции друга для ввода вектора

     istream &operator >>(istream &stream, V_3d &obj)

     {

      stream >>  obj.x >> obj.y  >> obj.z;

      return stream;

     }

// определение  дружественной функции - оператора +

   V_3d operator +(V_3d op1,V_3d op2)

{

  V_3d c;

  c.x=op1.x+op2.x;

  c.y=op1.y+op2.y;

  c.z=op1.z+op2.z;

  return c;

  }

  // определение  дружественной функции - оператора ++

   V_3d operator ++(V_3d &op1)

   {

   op1.x++;

   op1.y++;

   op1.z++;

   return op1;

  }

  // определение функции - оператора=

 V_3d V_3d::operator =(V_3d t)

{

  x=t.x;

  y=t.y;

  z=t.z;

  return *this;

}

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

Перегруженные функции operator >> и operator << не являются методами класса V_3d, это функции-друзья класса. Действительно, ни функция вставки, ни функция извлечения не могут быть методами класса. Причина заключается в том, что если функция-оператор является методом класса, то левым операндом, неявно передаваемым с использованием указателя this служит объект того класса, который осуществляет вызов функции-оператора. И нет способа изменить такой порядок. Левым же аргументом функций операторов, перегружающих операции >> и <<является ссылка на потоки ввода или вывода, которые не принадлежат классу, создаваемому пользователем в программе. Поэтому перегруженные функции операторы operator >> и operator << не могут быть методами класса, созданного пользователем. Это позволяет сохранить неизменным принцип инкапсуляции ООП.

Контрольные вопросы

1.Каким образом осуществляется перегрузка операций в С++?

2.Каков синтаксис объявления оператора – функции?

3.Каков механизм передачи параметров оператору – функции при перегрузке бинарных операций?

4.Почему при перегрузке унарных операций оператор-функция не имеет параметров?

5.Какая функция называется другом класса?

6.В чем особенности передачи параметров дружественной функции при перегрузке унарных и бинарных операций?

7. Как выполняется перегрузка операций >> и << для ввода и вывода встроенных типов языка?

8. Как выполняется перегрузка операций >> и << для ввода и вывода типов, объявленных пользователем?

5. ПРОИЗВОДНЫЕ КЛАССЫ

5.1. Наследование свойств как принцип ООП. Объявление производного класса

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

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

Наследование позволяет абстрагировать некоторое общее или схожее поведение различных объектов в одном базовом классе.

Основная форма синтаксиса объявления производного класса:

            сlass   имя _производного _класса : < режим доступа >

            имя _ базового _ класса

                         {  

                             //............

                         };

Пример:

              class  A {                                                      

                              //…

                             };

                      class X: public A

                        {                                                      

                              //…

                          };

Здесь  A – базовый класс, X- производный.

5.2. Режимы доступа при наследовании

Режим  определяет уровень доступа к элементам базового класса внутри производного. При объявлении производного класса он не является обязательным и может быть privaite, public, protected.

Если режим доступа опущен, то по умолчанию это privaite. В таблице 5.1 описаны основные режима доступа при наследовании.

Таблица 5.1.Режимы доступа при наследовании.

Режим доступа в   базовом классе.

Спецификатор доступа.

Доступ в производном классе.

protected

public

public

protected

public

private

public

Не доступны

protected

public

private

private

protected

public

protected

protected

protected

private

protected

Не доступны

private

private

Не доступны

Примечание:     private  элементы базового класса остаются для него собственными и не доступны в производном классе.

Пример:

// Базовый класс

сlass B{

 int a;

 pubic:

 int b,c;

 int Bfunc( );

 };

// Производный класс.

сlass X: public B{

  int d;

  public:

  int e;

  int Xfunc( );

 };

// Внешняя функция

  int Efunc( X & x1 );

Xfunc( ) имеет доступ :

  •  к b,c Bfunc() – наследуются как public – компоненты;
  •  к d,e, Xfunc() – собственные компоненты;

Efunc()имеет доступ :

  •  из класса B: к b,c, Bfunc() как public – компонентам;
  •  из класса X: к e, Xfunc()

Даже если открытые члены базового класса становятся закрытыми при наследовании с использованием спецификатора private, они по-прежнему доступны внутри производного класса. Рассмотрим пример программы из листинга 5.1.

Листинг 5.1.

// Пример наследования со спецификатором private

        #include <iostream.h>

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

class base {

int x;

public:

void setx(int n) { x = n; }

void showx() { cout << "x="<< x << '\n'; }

};

// Наследование через private

class derived : private base {

int y;

public:

// setx доступна внутри derived

void setxy(int n, int m) { setx(n); y = m; }

// showx доступна внутри derived

void showxy() { showx(); cout <<"y="<< y << '\n'; }

};

main()

{

derived ob;

ob.setxy(10, 20);

ob.showxy();

return 0;

}

Результаты работы этой программы:

         x=10

         y=20

В данном случае функции showx() и setx() доступны внутри производного класса, так как они наследуется из базового и становятся закрытыми членами производного класса.

5.3. Инициализация наследуемых членов

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

Если конструктору базового класса должны быть переданы аргументы, они должны быть заданы в списке аргументов конструктора производного класса. Таким образом, список аргументов конструктора порожденного класса состоит из аргументов для конструктора порожденного класса и аргументов для конструктора базового класса.

Основная форма определения конструктора производного класса:

конструктор_производного_класса (список аргументов) : базовый класс1 (список аргументов),........, базовый класс N (список аргументов)

                   {...

                      // тело конструктора...........

                    };

Конструкторы базовых классов  вызываются перед конструированием  любых элементов производных классов.

Рассмотрим пример программы, показанной в листинге  5.2. В этой программе  реализовано простое наследование, когда у производного класса имеется только один базовый.

Листинг 5.2. Простое наследование.

#include <iostream.h>

#include <stdlib.h>

#include <time.h>

  // Базовый класс является моделью массива и содержит элементы-

  // данные,описывающие свойства массива любого типа

  //

       class  basearray

         {

     protected:

  int maxitems;  //максимальное число элементов

  int citems;    //число элементов, размещенных в массиве

  public:

  //конструктор

basearray(int nitems)

{

 maxitems=nitems;citems=0;};

};

  // производный класс в качестве элемента-данного содержит

  //  указатель на массив элементов конкретного типа

  class iarray:public basearray

{

  int *items;    //указатель на массив

  public:

  //конструктор

  iarray(int nitems);

  //деструктор

  ~iarray();

  //занесение элемента в массив

  int putitem(int item);

  //получение элемента из массива

  int getitem(int ind,int &item);

  //получение фактического числа элементов в массиве

  int count() {return citems;};

};

//определение конструктора производного класса

// перед определением тела конструктора производного класса

// присутствует вызов конструктора базового класса

// : обеспечивает передачу параметров конструктору

// базового класса

iarray::iarray(int nitems):basearray(nitems)

{

 items=new int[nitems];

}

//деструктор

iarray::~iarray()

{

 delete items;

}

//занесение элемента в массив

int iarray::putitem(int item)

{

 if(

 citems<maxitems){

   items[citems]=item;

   citems++;

   return 0;

   }

 else

   return -1;

}

//получение элемента из массива

int iarray::getitem(int ind,int & item)

{

  if(ind>=0 && ind<citems){

    item=items[ind];

    return 0;

  }

  else

    return -1;

}

void  main(void)

{

iarray m(100);  //создан объект производного класса

 int i,j,k;

 int n;

 randomize();

 for(i=0;i<10;i++)

   m.putitem(rand());

 n = m.count();

 for(i=0;i<n;i++){

 m.getitem(i,j);

 cout<<"индекс =  "<<i<<"  элемент = "<<j<<"\n";

 }

 }

Результаты работы этой программы:

индекс =  0  элемент = 27926

индекс =  1  элемент = 28543

индекс =  2  элемент = 22385

индекс =  3  элемент = 29846

индекс =  4  элемент = 27573

индекс =  5  элемент = 21881

индекс =  6  элемент = 23107

индекс =  7  элемент = 30123

индекс =  8  элемент = 28465

индекс =  9  элемент = 12165

5.4. Множественное наследование

Производный класс может иметь не один, а множество базовых классов. Это называется множественным наследованием. Когда наследуется множество базовых классов, конструкторы выполняются слева направо в порядке, задаваемом в объявлении производного класса. Деструкторы выполняются в обратном порядке.

Пример. Рассмотрим схему наследования, при которой производный класс прямо наследует два базовых :

               BASE1                BASE 2

 

             

                               TOP 

Class Base1

   {

          int x;

          public:

     Base1(int i) {x = i ; };

     };

Class Base2

       {

         int x;

         public:

     Base2(int i): x(i){ };

       };

Class Top : public Base1, public Base2

      {

       int a, b;

         public:

    Top(int i, int j) : Base1(i*5), Base2(j+i), a(i) {b = j; }

       };

Здесь символ :, стоящий  в  определении производного класса перед именем конструктора базового класса обеспечивает механизм передачи параметров конструктору базового класса.

Создадим объект производного класса .

Top Z(1, 2);

При таком создании объекта Z производного класса вначале будет вызван конструктор первого базового класса Base1, что приведет к инициализации его  собственного элемента-данного x значением 5, затем конструктор второго базового класса Base2, его собственный элемент данное x получит значение 3, и, наконец, будет вызван конструктор производного класса Top и его элементы данные получат значения а=1,b=2.

Пример законченной программы, иллюстрирующий множественное наследование, показан в листинге 5.3

Листинг 5.3. Множественное  наследование.

#include <iostream.h>

class B1 {

public:

B1() { cout << "Работа конструктора B1\n"; }

~B1() { cout << "Работа деструктора B1\n"; }

};

class B2 {

int b;

public:

B2() { cout << "Работа конструктора B2\n"; }

~B2() { cout << "Работа деструктора B2\n"; }

};

// Наследование двух базовых классов.

class D : public B1, public B2 {

public:

D() { cout << "Работа конструктора D\n"; }

~D() { cout << "Работа деструктора D\n"; }

};

main()

{

D ob;

return 0;

}

Результаты работы этой программы:

Работа конструктора B1

Работа конструктора B2

Работа конструктора D

Работа деструктора D

Работа деструктора B2

Работа деструктора B1

Из примера видно, что базовые классы инициализируются в последовательности их объявления, затем происходит инициализация производных классов тоже в последовательности их объявления. Деструкторы вызываются в обратном порядке.

Важно:

Конструкторы базового класса должны быть объявлены с атрибутами public или protected для того, чтобы обеспечить возможность их вызова из производного класса.

Контрольные вопросы

1.В чем сущность механизма наследования как принципа ООП?

2.Как объявляется производный класс?

3.Как осуществляется доступ к элементам базового класса из производного при наследовании?

4.Каким образом выполняется инициализация наследуемых членов?

5.Как выполняются конструкторы базовых классов при множественном наследовании?

6.СИСТЕМА ВВОДА –ВЫВОДА С++

6.1. Базовые положения системы ввода-вывода С++

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

Поток – это логическое устройство, которое выдает и принимает информацию.

Когда программа на С++ начинает выполняться, автоматически открываются четыре потока, показанные в таблице 6.1.

                                              Таблица 6.1. Стандартные потоки С++.

Поток

Значение

Устройство по умолчанию

cin

Стандартный ввод

Клавиатура

cout

Стандартный вывод

Экран

cerr

Стандартная ошибка

Экран

clog

Буферизуемая версия cerr

Экран

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

Система ввода-вывода С++ поддерживается заголовочным файлом iostream.h. В этом файле задана иерархия классов.

Библиотека потоковых классов построена на основе двух базовых классов: ios и streambuf. Класс streambuf обеспечивает буферизацию данных во всех производных классах, которыми пользуется программист. Обращаться к его методам из прикладных программ обычно не нужно. Класс streambuf обеспечивает взаимодействие создаваемых потоков с физическими устройствами. Класс ios и производные классы содержат указатель на класс streambuf. Методы и данные класса streambuf программист явно не использует. Этот класс нужен другим классам библиотеки ввода-вывода.

Упрощенная схема иерархии потоковых классов показана на рис.6.1.

              

                        

                                                       

                        

Рис. 6.1. Упрощенная схема иерархии потоковых классов.

Названия классов, показанных на рис. 6.1:

ios                        -базовый потоковый класс;

istream                - класс входных потоков;

iostream             - класс двунаправленных потоков ввода-вывода;

ifstream              - класс входных файловых потоков;

fstream              - класс двунаправленных файловых потоков;

оfstream               - класс выходных файловых потоков.

6.2. Форматирование ввода-вывода с помощью функций-членов класса ios

Имеется 2 способа форматирования ввода-вывода:

  1.  с помощью функций-методов класса ios;
  2.  с помощью манипуляторов – специальных операций, вставляемых непосредственно в поток вывода.

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

Для установки флагов форматирования используется функция setf(), формат которой имеет вид:

long setf(long flags);

Эта функция возвращает  предыдущее значение флага, устанавливая его новое значение равным значению, заданному параметром flags. При этом все остальные флаги остаются неизменными. Например, для задания выравнивания поля вывода по правому краю следует установить флаг правого выравнивания right:

 

сout.setf(ios::right);

Замечание.

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

С помощью операции побитового ИЛИ можно установить одновременно столько флагов, сколько нужно. Например, можно установить флаг выравнивания поля вывода по левому краю left и флаг вставки пробелов между знаком и цифрами числами internal в одном вызове функции setf:

сout.setf(ios::left   |  ios:: internal);

В листинге 6.1 приведен пример программы, в которой используются некоторые флаги формата.

Листинг 6.1. Флаги формата.

#include <iostream.h>

main()

{// вывод с использованием установок по умолчанию

cout << 123.23 << " привет " << 100 << '\n';

 cout << 10 << ' ' << -10 << '\n';

cout << 100.0 << '\n';

 // теперь меняем формат

   // флаг hex для вывода в шестнадцатеричной системе

   // флаг scientific для вывода в чисел с плавающей точкой

   // в научной нотации (в экспоненциальном виде)

 cout.setf(ios::hex | ios::scientific);

 cout << 123.23 << " привет " << 100 << '\n';

  // установка флага showpos приводит к появлению

  // знака + перед положительными десятичными величинами

      cout.setf(ios::showpos);

cout << 10 << ' ' << -10 << '\n';

  //флаг showpoint приводит к выводу десятичной точки и нулей

  //справа для всех чисел с плавающей запятой вне зависимости

  // от того, нужно это или нет

  // флаг fixed задает вывод чисел с плавающей запятой

  // в обычном виде, с шестью цифрами после запятой

 cout.setf(ios::showpoint | ios::fixed);

 cout << 100.0;

return 0;

}

Эта программа выводит следующее:

123.23 привет 100

10 -10

100

1.2323e+02 привет 64

a fffffff6

+100.000000

Кроме флагов формата, существуют три функции-члена, определяемые в классе ios, которые определяют параметры формата: ширину поля, точность и символ заполнения. Этими функциями являются:

width(),  precision() и fill(). 

По умолчанию, ширина поля равна числу выводимых символов. Функция width(), прототип которой имеет вид:

int width( int w);

позволяет задать ширину поля, равную w символов. Однако, если размер выводимого данного превосходит заданную ширину поля, то будет использоваться такая ширина поля, которая необходима для вывода этого данного.

С помощью функции precision() можно изменить количество цифр после десятичной точки при выводе значения типа float ( по умолчанию выводится шесть цифр). Прототип этой функции имеет вид:

int precision( int p);

где p – это точность, т.е. число выводимых цифр после запятой.

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

char fill (char ch);

В листинге 6.2 приведен пример программы, которая иллюстрирует работу функций width(),  precision() и fill(). 

Листинг 6.2.

#include <iostream.h>

main()

{

cout.width(10); // установка минимальной ширины поля

 cout << "Привет" << '\n';//по умолчанию выравнивание по правому краю

 cout.fill('%'); // установка символа заполнения

cout.width(10); // установка минимальной ширины поля

 cout << "Привет" << '\n'; // по умолчанию выравнивание по правому краю

cout.setf(ios::left); // выравнивание по левому краю

cout.width(10); // установка минимальной ширины поля

cout << "Привет" << '\n'; // выравнивание по левому краю

 cout.width(10); // установка минимальной ширины поля

cout << 123.234567 << '\n'; // по умолчанию выводится 6 цифр после точки

cout.width(10); // установка минимальной ширины поля

cout.precision(3); // установка точности

cout << 123.234567 << '\n'; // точность равна 3 цифры после точки

return 0;

}

 

Результаты работы этой программы

   Привет

   %%%%Привет

   Привет%%%%

   123.234567

   123.235%%%

Следует обратить внимание на то, что ширина поля устанавливается перед каждым оператором вывода.

6.3 Манипуляторы ввода – вывода

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

Наиболее часто используемые манипуляторы показаны в табл.6.2. Для доступа к манипуляторам с параметрами (таким, как setw()), необходимо включить в программу файл iomanip.h.  В этом нет необходимости при использовании манипуляторов без параметров.

Примечание.

Манипулятор ввода-вывода влияет только на поток, который является частью выражения ввода-вывода, содержащего манипулятор.

В листинге 6.3 показан пример использования манипуляторов для форматирования операций вставки в поток.

                                                Таблица 6.2. Манипуляторы ввода-вывода.

Манипулятор

Назначение

Ввод-вывод

dec

Вывод в десятичной системе счисления.

Вывод

endl

Вывод символа новой строки и флэширование (очистка буфера потока)

Вывод

ends

Вывод нуля (NULL)

Вывод

flush

Флэширование

Вывод

hex

Вывод в шестнадцатеричной системе счисления.

Вывод

oct

Вывод в восьмеричной системе счисления.

Вывод

setfill(int ch)

Устанавливает символ заполнения ch.

Вывод

setprecision( int p)

Задает число цифр после десятичной точки

Вывод

setw(int w)

Задает w позиций ширины поля

Вывод

ws

Пропуск начальных побелов

Ввод

Листинг 6.3.Использование манипуляторов при выводе данных.

#include <iostream.h>

#include <iomanip.h>

void main(void)

{

int i=44;

float f=3456.13246;

cout <<"Вывод вещественного  с точностью, заданной по умолчанию\n";

cout << f;

// задание точности вывода для вещественного: 2 знака после запяиой

cout <<"\nВывод вещественного  с заданной точностью\n";

 cout <<setprecision(4);

cout <<f;

 cout <<"\nВывод целого в различных системах счисления:\n";

cout << "десятичное i=" << setw(4) << i <<"\n";

cout << "шестнадцатеричное i=" << hex << i <<"\n";

cout << "восьмеричное i=" << oct << i <<"\n";

cout <<dec;

//установка символа-заполнителя  и ширины поля вывода

cout << setfill('*') <<setw(20) << "ПРИВЕТ";

cout<< setfill('*') <<setw(20)<<endl;

}

Результаты работы этой программы имеют вид:

Вывод вещественного  с точностью, заданной по умолчанию

3456.132568

Вывод вещественного  с заданной точностью

3456.1326

Вывод целого в различных системах счисления:

десятичное i=  44

шестнадцатеричное i=2c

восьмеричное i=54

**************ПРИВЕТ*******************

6.4 Организация файлового ввода-вывода

Файловый ввод-вывод поддерживают классы:

ifstream – производный от istream, наследует операции извлечения из потока >>;

ofstrem - производный от ostream, наследует операции вставки << в поток ;

fstream - производный от iostream.

В программы, использующие эти классы, следует включить заголовочный файл

< fstream.h>.

В С++ файл открывается посредством его связывания с потоком. Имеется три типа потоков: входной, выходной и входной-выходной потоки. Перед тем, как открыть файл, нужно создать поток, например:

ifstream f1;             // создание файлового потока для ввода данных;

ofstream f2;         // создание файлового потока для вывода данных;

fstream f3;            // создание файлового потока для ввода  и вывода данных.

После создания потока его можно привязать к файлу с помощью функции open(). Прототип этой функции имеет вид:

void open(char* filename, int mode, int access);

где:

filename – имя файла, может включать путь;

mode задает режим открытия файла, может принимать одно (или более) из следующих значений:

         ios::app          // добавление в конец файла, только для вывода;

         ios::ate            // позиционирование в конец файла;

         ios::binary     // открытие в двоичном режиме;

         ios::in            // открытие файла для ввода;

         ios::nocreate  // функция open вернет ошибку, если файл не существует;

         ios::noreplace // функция open вернет ошибку, если файл уже существует;

         ios::out            // открытие файла для вывода;

         ios::trunc        // удаление содержимого ранее существовавшего файла с тем              

                                  // же  названием и усечение его до нулевой длины;

По умолчанию mode для ifstream равен ios::in, а для ofstream он равен ios::out.

access задает права доступа к файлу. В среде DOS и Windows этот параметр обычно соответствует кодам атрибутов файлов и может иметь значения, показанные в таблице 6.3.

                                  Таблица 6.3. Коды атрибутов файлов.

Атрибут

Значение

0

Обычный файл со свободным доступом

1

Файл только для чтения

2

Скрытый файл

4

Системный файл

8

Архивный файл

По умолчанию значение access равно нулю.

Например:

f1.open(“test.txt”,  ios::nocreate);   

f2.open(“c:\\user\\bi62\\dan.dat”, ios::out, 0);

f3.open( “test”, ios::in | ios::out);

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

f2.open(“c:\\user\\bi62\\dan.dat”);

Примечание.

Если поток открывается как для ввода, так и для вывода, это поток f3 в примере, то использовать значение mode по умолчанию нельзя.

Если open() завершилась с ошибкой, поток будет равен нулю. Поэтому перед использованием файла следует убедиться в его успешном открытии, например:

        if (!f2)

           {

          cout  << “ Ошибка открытия файла \n” ;

            exit (-1);    // Завершение  программы  с кодом ошибки

            }

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

ifstream f1 ( “test.txt” );    // открытие файла для ввода;

ofstream f2 (“c:\\user\\bi62\\dan.dat”); // открытие файла для вывода.

Для закрытия файла следует использовать функцию-метод close(), например:

f1.close();

f2.close();

Функция close() не имеет параметров и возвращаемого значения.

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

После открытия текстового файла, чтобы читать из файла или писать в него, достаточно воспользоваться операторами <<  и >>.

Для определения момента достижения конца файла следует использовать функцию-метод eof(), имеющую следующий прототип:

int eof();

При достижении конца файла она возвращает ненулевое число, в противном случае возвращает ноль.

В листинге 6.4 показан пример программы, выполняющей копирование файла и вывод числа скопированных символов. При запуске этой программы на исполнение из командной строки должны быть переданы имена копируемого файла и файла-копии из командной строки, например, если имя файла программы copyf.exe, и следует скопировать файл dan.dat в файл dancopy.dat, то командная строка должна иметь вид:

copyf     dan.dat dancopy.dat

Листинг 6.4. Копирование текстовых файлов и вывод числа скопированных символов.

#include <iostream.h>

#include <fstream.h>

main(int argc, char *argv[ ])

 {

if(argc!=3) {

              cout << "Использование: CPY <input> <output>\n";

                  return 1;

  }

       ifstream fin(argv[ 1 ]);           // открытие входного файла

       ofstream fout(argv[ 2 ]);       // создание выходного файла

if(!fin) {

cout << "Входной файл открыть невозможно\n";

return 1;

}

if(!fout) {

cout << "Выходной файл открыть невозможно\n";

return 1;

}

  char ch;

 unsigned count = 0;

                  // сброс флага skipws 

 fin.unsetf(ios::skipws); // не пропускать пробелы

                // выполнять, пока не будет достигнут конец файла

 while(!fin.eof()) {

  fin >> ch;

  fout << ch;

  count++;

 }

 cout << "Число скопированных байтов: " << count << '\n';

 return 0;

}

Примечание. Когда установлен флаг skipws, при выполнении ввода в поток, начальные невидимые символы (пробелы, табуляции и символы новой строки) отбрасываются. Когда флаг skipws сброшен, невидимые символы не отбрасываются. Результат, выводимый этой программой, может отличаться от того, который выводится операционной системой, поскольку некоторые символы могут интерпретироваться иначе. Когда считывается последовательность возврат каретки/перевод строки, она преобразуется в символ новой строки. При выводе новая строка считается одним символом, но снова преобразуется в последовательность возврат каретки/перевод строки.

Контрольные вопросы

1.Какие классы поддерживают ввод – вывод в С++?

2.Назовите потоки, открываемые для исполняемой программы.

3.Какова схема иерархии классов ввода – вывода?

4. Назовите способы организации форматированного ввода – вывода.

5.Какой флаг следует использовать для выравнивания поля вывода по левому краю?

6. С помощью каких функций можно определить ширину поля вывода, точность и символ заполнения?

7. Какие классы поддерживают файловый ввод-вывод?

8. Какие типы файловых потоков существую и С++?

9 Как выполняется открытие файла? Перечислите режимы открытия файла.

7. ВИРТУАЛЬНЫЕ ФУНКЦИИ

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

  •   на этапе компиляции - посредством перегрузки операций и функций;
  •  во время выполнения программы – посредством виртуальных функций.  

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

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

7.1 Задача, приводящая к виртуальным функциям

Рассмотрим следующую задачу.

Пусть  имеется базовый класс Graphics Object, описывающий произвольную фигуру на экране без конкретизации ее вида:

class Graphics Object {

   // элементы – данные класса

……………

 public:

     // методы класса, необходимые для работы с любым графическим объектом

void Build();        // построить

void Display();    // показать на экране

// другие методы

…………..

};

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

class Circle: public Graphics Object {

// элементы – данные класса

……………

public:

// методы класса, необходимые для работы с окружностью

void  Build();    // построить окружность

void  Display();  // показать на экране

// другие методы

…………..

};

Создадим объект производного класса с именем A;

Circle A;

Вызов функции-метода для отображения на экране созданного объекта:

A.Build();

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

Для того чтобы при вызове A.Build() вызывался метод из производного класса следует в базовом классе объявить функцию Build() как виртуальную:

class Graphics Object {

   // элементы – данные класса

……………

 public:

     // методы класса, необходимые для работы с любым графическим объектом

virtual void  Build();        // построить

virtual  void Display();    // показать на экране

// другие методы

…………..

};

В производном классе повторять слово virtual нет необходимости, хотя ошибки при этом не возникает.

7.2 Полиморфизм и позднее связывание

Если функция в базовом классе объявлена как виртуальная, то ее вызовы будут обрабатываться методом “позднего” (динамического) связывания. Ключевое слово virtual  предписывает компилятору генерировать некоторую дополнительную информацию о функции.

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

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

В листинге 7.1 приведен пример программы, показывающей, как при помощи виртуальных функций можно реализовать полиморфное поведение классов X  и Y.

Листинг 7.1.

#include <iostream.h>

// Базовый класс

class X

 {

public:

virtual double A(double x)

{ return x*x;}

double B(double x)

{ return A(x)/2;  }

 };

// Производный класс

class Y :public X

 {

public :

   double A(double x)

{ return x*x*x;}

};

void main (void)

{

Y y;

cout <<y.B(3) <<endl;

 }

Эта программа выведет правильное значение  13.5, потому что в результате вызова наследуемой функции X::B, вызывающей функцию A, в качестве функции А во время выполнения программ будет использована замещающая функция Y::A.

7.3 Указатели на классы и виртуальные функции

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

Например, являются правильными следующие операторы:

base *p;                        // указатель базового класса

base base_ob;              // объект базового класса

derived derived_ob;     // объект производного класса

p=&base_ob;            // р указывает на объект базового класса

p=& derived_ob;      // р указывает на объект производного класса

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

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

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

Листинг 7.2

#include <stdio.h>

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

class Base

{

public:

//обьявление виртуальной функции

virtual void Method()

{

printf("Базовый\n");

};

};

//производный класс

class Nas:public Base

{

public:

void Method()         // virtual здесь допустимо, но избыточно

{

printf(" Производный \n");

};

};

// Внешняя функция  содержит в качестве параметра указатель

// на базовый класс 

void fn(Base* basePtr);

  void main(void)

  {

  Base base;

  Nas nas;

  printf("Базовый -\n");

  fn(&base);    // вызов  из функции fn виртуальной функции из базового класса

  printf("Производный -\n");

  fn(&nas);   // вызов из функции fn виртуальной функции из производного класса

  }

// определение внешней функции

 void fn(Base* basePtr)

{

  basePtr->Method();   // вызов функции через указатель на базовый класс

  }

  

Результаты работы этой программы имеют вид:

Базовый -

Базовый

Производный -

Производный

В приведенной программе при вызове внешней функции fn() в качестве аргумента ей  передается первый раз &base – адрес объекта базового класса, а второй раз &nas - адрес объекта производного класса. В определении же функции fn() стоит указатель на базовый класс. Это значит, что объект производного класса рассматривается как объект его базового класса. Таким образом, тип адресуемого через указатель объекта определяет, какая версия виртуальной функции вызывается, причем это решение принимается во время выполнения программы.

7.4. Особенности работы с виртуальными функциями

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

Например:

// Базовый класс

class B  {

public:

virtual void vf1();

virtual void vf2();

virtual void vf3();

void f();

};

// Производный класс

class D:public B   {

public:

virtual void vf1();  // виртуальная функция, причем слово virtual избыточно

void vf2(  int  );      // не virtual, т.к. использован другой список параметров

char  vf3();               // не virtual, т.к. изменен тип возврата

void f();              // не virtual, т.к. она не объявлена в базовом классе как virtual

}

7.5 Чисто виртуальные функции и абстрактные классы

     Виртуальная функция, объявленная в базовом классе, часто никогда не используется для объектов базового класса, т.е. не имеет определения.

Вернемся к классу Graphics Object, определение которого было приведено в п.7.1. В нем была объявлена виртуальная функция  Build() для построения геометрической фигуры. Эта функция должна быть первый раз объявлена и, следовательно, определена в базовом классе. Но абстрактную геометрическую фигуру построить нельзя, следовательно определение тела этой функции должно быть пустым:

void Graphics Object :: Build()   {       };

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

virtual тип имя_функции (список параметров=0;

Например определение класса Graphics Object может быть изменено следующим образом:

class Graphics Object {

   // элементы – данные класса

……………

 public:

     // методы класса, необходимые для работы с любым графическим объектом

virtual void  Build() =0;        // построить

virtual  void Display();=0;    // показать на экране

// другие методы

…………..

};

В классах производных от класса Graphics Object, при наличии своих версий виртуальных функций Build() и Display() они должны быть либо определены, либо, в свою очередь, объявлены как чисто виртуальные функции.

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

Чисто виртуальные функции полезны в следующем: они позволяют установить контроль со стороны компилятора за ошибочным созданием объектов “фиктивных” типов, таких, как Graphics Object в нашем примере.

Класс с одной или большим количеством чисто виртуальных функций называется абстрактным классом.

Правила языка С++ запрещают создание объектов таких типов. Абстрактный класс может быть использован только как базовый для последующих порождений новых классов.

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

Контрольные вопросы

1.Какой принцип ООП реализует механизм виртуальных функций?

2.Когда следует использовать механизм виртуальных функций в программе?

3.В чем сущность метода динамического связывания?

4.В чем смысл идеи “один интерфейс, множество методов”?

5.В чем состоят особенности вызова виртуальной функции через указатель на класс?

6.Когда может быть проигнорирован механизм виртуальных функций?

7. Что такое абстрактный класс? Можно ли создавать объекты абстрактных классов?

8.ШАБЛОНЫ ФУНКЦИЙ И КЛАССОВ

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

8.1. Шаблоны функций

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

Рассмотрим следующую задачу. Нужно написать функцию cub() для возведения в третью степень передаваемого аргумента.

Можно использовать механизм перегрузки функций, и три (или более) функций будут иметь одно и то же имя, но отличаться типом аргумента и типом возвращаемого значения. Нам придется для каждого типа создавать свое тело функции, причем отличаться функции будут только типами аргументов.

Шаблоны функций обычно объявляются в заголовочном файле.

Общий вид объявления шаблона функции:

template < class T > тип возвращаемого значения имя (список параметров)

          {

          //тело функции

             }

Здесь T - имя типа, параметр шаблона. Это указание компилятору, что T - определяемое пользователем имя типа.

Необходим, по крайней мере, один параметр T для передачи функции данных для обработки. Можно  задать указатель (T * param) или  ссылку (T & param).

Функция может объявлять несколько параметров и возвращать значение параметрического типа, например:

    template < class T> T f(int a, T b)

{    //   тело функции     }

В листинге 8.1 приведен пример программы, решающей поставленную задачу.

Листинг 8.1

// Шаблон функции для возведения в куб параметра функции

template <class T>

T cub( T X)

     {

       return X*X*X;

      };

void main(void)

{

int i=15; float f=3.12;

double x=3.1e2

cout << “целое” << cub(i) <<”\n”;

cout <<”вещественное” << cub(f) <<”\n”;

cout <<”двойной точности” << cub(x) <<”\n”;

return 0;

}

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

Листинг 8.2

//Шаблон функции, осуществляющей перестановку двух переданных параметров.

void Swap ( SwapType &x, SwapType &y)

{

SwapType temp;

Temp=x;

x=y;

y=temp;

}

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

Листинг 8.3.

// Шаблон функции для нахождения максимального из двух величин

template < class T1, class T2 >

T1 max( T1 x, T2 y)

{

     if( x>y)

      return x;

       else

        return y;

}

Рассмотрим, вопрос о  типе возвращаемого значения функции, приведенной в листинге 8.3.

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

void main(void)

{

int a, double b;

// операторы

max( b, a);

// операторы

}

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

8.2.Шаблоны классов

Шаблон  класса представляет собой скелет обобщенного класса.

Синтаксис объявления шаблонного класса:

template < список_аргументов_шаблона >

class имя_класса

{

 //тело класса

};

Каждый аргумент является:

  1.  либо именем типа, за которым следует идентификатор, например:

                                    float a;

  1.  либо ключевым словом class, за которым следует идентификатор, обозначающий параметризированный тип, например:

                                   class T.

Общий вид определения функции - метода шаблонного класса

template <список_аргументов_шаблона>

тип_результата  имя_класса  <список_аргументов_шаблона>

:: имя_функции(список_аргументов_функции)

     {

       // операторы

      }

Объявление объекта шаблонного класса:

имя_класса_шаблона <список_аргументов_шаблона>  имя объекта;

Пример использования шаблонного класса для организации очереди объектов параметризированного типа приведен в листинге 8.4.

Листинг 8.4.

// Использование шаблона класса для организации очереди объектов

//параметризированного типа

#include <iostream.h>

// обьявление шаблона класса 

template <class T, int size>

 class queue{

    T *q; //элемент-данное параметризированного типа

    int sloc,rloc;//начало и конец очереди

 public:

 queue(void); //конструктор

 ~queue(void); //деструктор

 void qput(T i); // помещение элемента в очередь  

 T qget(void);  // извлечение элемента из очереди

 };

//*******************************************

 //определение членов функций шаблона класса

 // конструктор

 template <class T,int size>

  queue<T,size>::queue(void)

   {

   if( !(q=new T[size])) {cout << "Недостаточно памяти\n";

   return;

   }

   sloc=rloc=0;

   cout <<"Очередь размера "<< size <<"  инициализирована\n";

   }

  //деструктор

  template <class T,int size> queue<T,size>::~queue(void)

  {

  delete q;

  cout << "Очередь разрушена\n";}

  //Помещение элемента в очередь

 template <class T,int size>

 void  queue<T,size>::qput(T i)

 { if(sloc==size)

 { cout <<" Очередь полна\n";

 return;

 }

 q[sloc++]=i;

 }

 //Извлечение элемента из очереди

 template <class T,int size>

 T  queue<T,size>::qget(void)

 { if(rloc==sloc)

 { cout <<" Очередь пуста\n";

 return 0;

 }

  return q[rloc++];

 }

/*************************************************/

 int main(void)

 {

 // Обьявление двух обьектов шаблонного класса

 // с массивами различных типов и размеров

 queue<int,5> a;

 queue<double,200> b;

 a.qput(10);a.qput(23);

 b.qput(1.129);b.qput(5.55);

 cout<< a.qget()<<" "<<a.qget()<<" ";

 cout<< b.qget()<<" "<<b.qget()<<"\n";

 const int s=10;

 // Обьявление указателя на обьект шаблонного класса

 queue<long double, s> *pq;

 pq=new queue<long double,s>; //динамическое создание обьекта

 if(!pq){ cout << "Недостаточно памяти\n";

 return 0;

 }

 else

 cout <<" Обьект класса queue создан\n";

 for(int i=0;i<s; i++)

 pq->qput(i/2.0+i);// заполнение очереди

 for( i=0; i<s; i++)

 cout << pq->qget() << " ";// просмотр элементов

 cout <<"\n";

 delete  pq;

}

Контрольные вопросы

1.Какой принцип ООП реализуется с помощью шаблонных функций и классов?

2.Когда целесообразно использовать шаблоны функций? Каков общий вид объявления шаблона функции?

3. Каков синтаксис объявления шаблонного класса? В чем особенности определения функции - метода шаблонного класса? Как объявить объект шаблонного класса?


 

А также другие работы, которые могут Вас заинтересовать

407. Разработка сети кампуса с выходом во внешнюю среду 955 KB
  Список оборудования и линий связи. Сети кампуса объединяют множество сетей различных отделов одного предприятия в пределах одного здания или в пределах одной территории. Сеть разрабатывалась на основе структурированной кабельной системы.
408. Схема модификации резонаторного фильтра для использования в полосовых структурно-перекрытых реализациях фильтров 178.5 KB
  Коэффициенты передачи в выходные узлы можно вычислить методом графов, так как данная схема довольно проста. Формула Мейсона представляет собой отношение произведения коэффициентов передачи ветвей. Для вычисления γ12 выделим в отдельную схему элементы и связи между ними.
409. Анализ и прогнозирование деятельности предприятия ремонтная организация 413 KB
  Возможности организации (резюме). Правовое обеспечение деятельности организации. Стратегия финансирования. Организационный план, конкуренция и рынок сбыта. Оценка рисков и страхования.
410. Исследование таблично заданной функции 669.5 KB
  Дана система линейных алгебраических уравнений шестого порядка. Найти ее решение методом простых итераций с заданной точностью E. Выполнить проверку истинности полученного решения. Метод простых итераций.
411. Разработка программы Кафе с использованием классов на языке программирования С# 417.5 KB
  Автоматизация деятельности кафе на основе объектно-ориентированного подхода, а также получение навыков в реализации этого подхода, проектировании и реализации схемы данных. Проектирование иерархии классов и интерфейсов на основе выделенных сущностей.
412. Проектирование передаточного и кулачкового механизма зубострогального станка 516.5 KB
  Проектирование передаточного зубчатого механизма зубострогального станка. Расчет выходных характеристик и координат профиля кулачка. Расчет вспомогательных элементов (радиуса ролика и пружины). Синтез эвольвентной зубчатой передачи.
413. Инженерные решения в разработке месторождений полезных ископаемых открытым способом 320.5 KB
  Решения инженерных задач горной промышленности. Принятия управленческих и проектных решений в недропользовании на примере открытых горных работ, формирующих наибольшую нагрузку на окружающую среду.
414. Теория и практика социальной психологии 621 KB
  Психологические характеристики социальных групп, психология личности, закономерности общения и деятельность людей, взаимодействия больших (наций, групп) и малых социальных групп, межличностные отношения и развитие социальных установок.
415. Розробка електронної системи бібліотечної картотеки 621 KB
  Створення електронної системи бібліотеки, де всі дані про читачів і книги зібрані в базі даних, і куди при необхідності можна легко заносити, змінювати та видаляти дані. Електронна система з одержання, та повернення книг.