Глава 14. Классы

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

Особенности классов

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

Конструкторы и деструкторы

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

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

В следующей программе продемонстрировано создание простейших конструкто­ра и деструктора. В данном случае они лишь сигнализируют соответственно о создании и удалении объекта класса coins. Обратите внимание на то, что обе функции вызываются автоматически: конструктор — в строке

coins cash_in_cents;

а деструктор — после завершения функции main().

//
//  coins.cpp
//  В этой программе на языке C++ демонстрируется создание конструктора
//  и деструктора. Данная программа вычисляет, каким набором монет
//  достоинством 25,10,5 и 1 копейка можно представить заданную денежную
//  сумму.
//

#include <iostream.h>

const int QUARTER = 25;
const int DIME = 10;
const int NICKEL = 5;

class coins {    int number;

public: coins ()

{ cout << "Начало вычислений.\n";} // конструктор
~coins ()

{ cout << "\nКонец вычислений."; }  // деструктор
void get_cents(int); int quarter_conversion(void) ;
int dime_conversion(int); int nickel conversion(int);

};

void coins::get_cents(int cents) {

number = cents;        cout << number << " копеек состоит из таких монет:" << endl; )

int coins::quarter_conversion()

{

cout << number / QUARTER << " — достоинством 25, return (number % QUARTER) ;

}

int coins::dime_conversion(int d) {

cout<< d/ DIME<< " — достоинством 10,

return(d % DIME);

}           

int coins::nickel_conversion(int n) {

cout<< n/ NICKEL<< " — достоинством 5 и

return(n % NICKEL); }

main ()

int с, d, n, p;

cout<< "Задайте денежную сумму в копейках: "; cin>> с;

// создание объекта cash_in_cents класса coins
coins cash_in_cents;

cash_in_cents.get_cents(с); d = cash_in_cents.quarter_conversion(); n = cash_in_cents .dime_conversion (d); p = cash_in_cents.nickel_conversion(n); cout << p << " — достоинством 1.";

return(0); }

Вот как будут выглядеть результаты работы программы:

Задайте денежную сумму в копейках: 159

Начало вычислений.

159 копеек состоит из таких монет:

6 — достоинством 25, 0 — достоинством 10,

1 — достоинством 5 и 4 — достоинством 1.

Конец вычислений.       В функции get_cents{) заданная пользователем денежная сумма записывается в переменную number класса coins. Функция quarter_conversion() делит значение numberна 25 (константа QUARTER), вычисляя тем самым, сколько монет достоинством 25 копеек "умещается" в заданной сумме. В программу возвращается остаток от деления, который далее, в функции dime_conversion(), делится уже на 10 (константа dime). Снова возвращается остаток, на этот раз он передается в функцию nickel_conversion(), где делится на 5 (константа NICKEL). Последний остаток определяет количество однокопеечных монет.

Инициализация переменных-членов с помощью конструктора

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

coins() {

cout<< "Начало вычислений.\n";

number= 431; } // конструктор

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

classcoins {

intnumber= 431; // недопустимая инициализация

}

Вот почему все подобные операции приходится помещать в конструктор.

Резервирование и освобождение памяти

Другой важной задачей, решаемой с помощью конструктора, является выделение динамической памяти. В следующем примере конструктор с помощью оператора newрезервирует блок памяти для указателя string1. Освобождение занятой памяти выполняет деструктор при удалении объекта. Этой цели служит оператор delete.

class string_operation { char *string1; int string_len;

public:

string_operation(char*)

( string1 = new char[string_len]; } ~string_operation()

{ delete stringl; }  ' void input_data(char*); void output_data(char*); );

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

Области памяти, занятые данными базовых типов, таких как intили float, освобождаются системой автоматически и не требуют помощи конструктора и деструктора.

Перегрузка функций-членов класса

Функции-члены класса можно перегружать так же, как и любые другие функции в C++, т.е. в классе допускается существование нескольких функций с одинаковым именем. Правильный вариант функции автоматически выбирается компилятором в зависимости от количества и типов аргументов, заданных в прототипе функции. В следующей программе создается перегруженная функция number() класса absolute_value, которая возвращает абсолютное значение как целочисленных аргумен­тов, так и аргументов с плавающей запятой. В первом случае для этого используется библиотечная функция abs(),принимающая аргумент типа int, во втором — fabs ( ) , принимающая аргумент типа double.

//
//  absolute. cpp
//  Эта программа на языке C++ демонстрирует использование перегруженных
//  функций-членов класса. Программа вычисляет абсолютное значение чисел
//  типа int и double.

#include <iostream.h>

#include <math.h>    // содержит прототипы функций abs() и fabs()

class absolute__value { public:

int number (int);

double number (double) ;

int absolute_value::number(int test_data) return(abs(test_data));

double absolute_value::number(double test_data)

return(fabs(test_data)); }

main()

absolute_value neg_number;

cout<< "Абсолютное значение числа -583 равно << neg_number.number(-583) << endl;

cout<< "Абсолютное значение числа -583.1749 равно "

<< neg_number. number (-583.1749)<< endl; return (0); )

Вот какими будут результаты работы программы:

Абсолютное значение числа -583 равно     583 Абсолютное значение числа -583.1749 равно 583.175   

В приводимой далее программе в функцию trig_calc() передается значение угла в одном из двух форматов: числовом или строковом. Программа вычисляет синус, косинус и тангенс угла.

//
//  overload. срр
//  Эта программа на языке C++ содержит пример перегруженной функции,
//  принимающей значение угла как в числовом виде, так и в формате
//  градусы/минуты/секунды.
//

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

const double DEG_TO_RAD = 0.0174532925;

class trigonometric { double angle;

public:

void trig_calc(double);

void trig_calc(char *); };

void trigonometric::trig_calc(double degrees) !

angle = degrees;

cout << "\nДля угла " << angle << " градусов:" << endl;
cout << "синус равен " << sin (angle * DEG__TO_RAD) << endl;
cout << "косинус равен " << cos(angle * DEG_TO_RAD) << endl;
cout << "тангенс равен " << tan(angle * DEG_TO_RAD) << endl; }

void trigonometric::trig_calc(char *dat) (

char *deg, *min, *sec;

deg = strtok(dat, "d");

min = strtok(0, "m");

sec = strtok(0,"s");

angle = atof(deg) + atof(min)/60.0 + atof (sec)/360.0;

cout<< "\nДля угла". << angle << " градусов:" << endl;
cout << "синус равен ' " << sin(angle * DEG_TO_RAD) << endl;
cout << "косинус равен " << cos(angle * DEG_TO_RAD) << endl;
cout << "тангенс равен " << tan (angle * DEG_TO_RAD) << endl; }

main ()

(   trigonometric data;

data.trig_calc(75.0) ;

char str1[]   =  "35d 75m 20s"; data.trig_calc(str1) ;

data.trig_calc(145.72);

char str2[l= "65d45m 30s"; data.trig_calc (str2) ;

return(0); }

В программе используется библиотечная функция strtok() , прототип которой находится в файле STRING.H. Эта функция сканирует строку, разбивая ее на лексемы, признаком конца которых служит один из символов, перечисленных во втором аргументе. Длина каждой лексемы может быть произвольной. Функция возвращает указатель на первую обнаруженную лексему. Лексемы можно читать одну за другой путем последовательного вызова функции strtok( ) . Дело в том, что после каждой лексемы она вставляет в строку символ \0 вместо символа-разделителя. Если при следующем вызове в качестве первого аргумента указать 0, функция продолжит чтение строки с этого места. Когда в строке больше нет лексем, возвращается нулевой указатель.

Рассмотренная программа позволяет задавать значение угла в виде строки с указанием градусов, минут и секунд. Признаком конца первой лексемы, содержащей количество градусов, служит буква d, второй лексемы — т, третьей — s. Каждая извлеченная лексема преобразуется в число с помощью стандартной библиотечной функции atof() из файла МАТН.Н.

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

Для угла 75 градусов:
синус  равен 0.965926
косинус равен 0.258819
тангенс равен 3.73205

Для угла 36.3056 градусов:
синус  равен 0.592091
косинус равен 0.805871
тангенс равен 0.734722

Для угла 145.72 градусов:
синус  равен 0.563238
косинус равен -0.826295
тангенс равен -0.681642

Для угла 65.8333 градусов:
синус  равен 0.912358
косинус равен 0.409392
тангенс равен 2.22857

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

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

//
//  friend.cpp
//  Эта программа на языке C++ демонстрирует использование дружественных
//  функций. Программа получает от системы информацию о текущей дате и
//  времени и вычисляет количество секунд, прошедших после полуночи.
//

#include <iostream.h>

#include <time.h>      // содержит прототипы функций time(),localtime(),

// asctime(),а также описания структур tm и time_t

class time_class {

long sees;

friend long present_time(time_class);

// дружественная функция public:

time_class(tm *);

time_class::time_class(tm *timer) {

secs = timer->tm_hour*3600 + timer->tm_min*60 + timer->tm_sec; }

long present_time(time_class); // прототип main()

{

// получение данных о дате и времени от системы 

time_t ltime; tm *ptr;

time(&ltime);

ptr = localtime(&ltime);

time_class tz(ptr);

cout<< "Текущие дата и время: " << asctime(ptr) << endl;
cout<< "Число секунд, прошедших после полуночи: "

<< present_time(tz) << endl; return (0);

}                                     

long present_time (time_class tz)               
{

return(tz.secs);

}
Давайте подробнее рассмотрим процесс получения данных о дате и времени.

time_t ltime;
tm *ptr;

time (&ltime) ; ptr = localtime(&ltime) ;

Сначала создается переменная ltime типа time_t, которая инициализируется значением, возвращаемым функцией time(). Эта функция вычисляет количество секунд, прошедших с даты 1-е января 1970 г. 00:00:00 по Гринвичу, в соответствии с показаниями системных часов. Тип данных time_tспециально предназначен для хранения подобного рода значений. Он лучше всего подходит для выполнения такой операции, как сравнение дат, поскольку содержит, по сути, единственное число, которое легко сравнивать с другим аналогичным числом.

Далее создается указатель на структуру tm, предназначенную для хранения даты в понятном для человека виде:

structtm {                                              
inttm_sec;  // секунды (0—59)                                 
inttm_min;  // минуты (0—59)                                 
inttm_hour; // часы (0—23)
inttm_mday; // день месяца (1—31)
inttm_mon;  // номер месяца (0—11;январь =0)
inttm_year; // год (текущий год минус 1900)
inttm_wday; // номер дня недели (0—6; воскресенье =0)
inttm_yday; // день года (0—365;1-е января = 0)
inttm_isdst; // положительное число, если осуществлен переход на летнее время;

// 0, если летнее время еще не введено;

// отрицательное число, если информация о летнем времени

// отсутствует };

Функция localtime(), в качестве аргумента принимающая адрес значения типа time_t, возвращает указатель на структуру tm, содержащую информацию о текущем времени в том часовом поясе, который установлен в системе. Имеется схожая с ней функция gmtime(), вычисляющая текущее время по Гринвичу.

Полученная структура передается объекту tzкласса time_class, а точнее — конструктору этого класса, вызываемому в строке

time_class tz(ptr);

В конструкторе из структуры tmизвлекаются поля tm_sec, tm_minи tm_hour и по несложной формуле вычисляется количество секунд, прошедших после полуночи.

sees = timer->tm_hour*3600  +  timer->tm_min*60 +  timer->tm_sec;                                    

Функция asсtime()  преобразует структуру tm в строку вида

день месяц число часы:минуты:секунды год\n\0

Например:

MonAug10 13:12:21 1998

Дружественная классу time_classфункция present_time()   получает доступ к переменной secsэтого класса и возвращает ее в программу. Программа выводит на экран две строки следующего вида:

Текущие дата и время: MonAug10 09:31:141998

Число секунд, прошедших после полуночи: 34274    

 Указатель this

Ключевое слово this выступает в роли неявно заданного указателя на текущий объект:

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

Указатель thisдоступен только в функциях-членах классов (а также структур) и позволяет сослаться на объект, для которого вызвана функция. Чаще всего он находит применение в конструкторах, выполняющих резервирование памяти:

class имякласса   {
.
.
.
public:

имя_класса(int  size)    (   this = new(size);   }

~имя__класса (void) ; };

Перегрузка операторов

В C++ разрешается осуществлять перегрузку не только функций, но и операторов. В создаваемом классе можно изменить свойства большинства стандартных операторов, таких как +, -, * и /, заставив их работать не только с данными базовых типов, но также с объектами.

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

Для перегрузки операторов применяется ключевое слово operator:

тип operator оператор (список_параметров)

В таблице 14.1 перечислены операторы, перегрузка которых допустима в C++.

Таблица 14.1. Операторы, которые могут быть перегружены

+        -          *         /         %       ^

!        =          <         >        +=      -=

^=      &=         |=        <<        >>      <<=

<=     >=          &&       | |       ++        --

( )     []         new       delete     &        |

 ~     *=          /=        %=       >>=      ==
!=      ,          ->        ->*

На перегрузку операторов накладываются следующие ограничения: невозможно изменить приоритет оператора и порядок группировки его операндов;


Следующая программа наглядно иллюстрирует концепцию перегрузки операторов, осуществляя суммирование углов, заданных с помощью строк формата

градусыd минутыm секундыs

Выделение из строки значащих частей осуществляет уже знакомая нам функция strtok(). Признаками конца лексем служат буквы d, mи s.

//
//  angles.cpp
//  Эта программа на языке C++ содержит пример перегрузки оператора +.
//

#include <iostream.h>

#include <string.h>

include <stdlib.h> // функция atoi()

class angle_value {

int degrees, minutes, seconds;

public:                          
angle_value () { degrees =0,

minutes =0,

seconds = 0; } // стандартный конструктор
angle_value(char *);                 
int get_degrees () ;
int get_minutes();
int get_seconds();
angle_value operator +(angle_value); // перегруженный оператор

};

angle_value::angle_value(char *angle_sum) {

// выделение значащих частей строки и преобразование их в целые числа

degrees = atoi(strtok(angle_sum, "d"));

minutes = atoi(strtok(0,"m"));

seconds = atoi(strtok(0,"s"));       

}        

int angle_value::get_degrees() return degrees;

int angle_value::get_minutes0

return minutes;

}                                                        .

int angle_value::get_seconds() return seconds;

angle_value angle_value::
operator+(angle_value angle_sum) angle_value ang;

ang.seconds = (seconds + angle_sum.seconds) % 60; ang.minutes = ((seconds+ angle_sum.seconds) / 60 +

minutes + angle_sum.minutes) % 60; ang.degrees = ((seconds+ angle_sum.seconds) / 60 +

minutes + angle_sum.minutes) / 60;
ang.degrees += degrees + angle_sum.degrees;

return ang;       
main()                   

char str1[]= "37d15m 56s";        angle_value angle1(str1);

char str2[]= "lOd44m 44s"; angle_value angle2(str2);

char str3[]= "75d 17m 59s";

angle_value angles(str3);                                    

char str4[]= "130d 32m 54s";          
angle_value angle4(str4);

angle_value sum_of_angles;                 

sum_of_angles = angle1 + angle2 + angle3 + angle4;

cout<< "Сумма углов равна "

<< sum_of_angles.get_degrees() << "d "

<< sum_of_angles.get_minutes() << "m "

<< sum_of_angles,get_seconds() << "s"

<< endl;

return(0);                                  
}

Следующий фрагмент программы отвечает за раздельное суммирование градусов, минут и секунд. Здесь применяется еще не перегруженный оператор +:

angle_value ang;

ang.seconds = (seconds + angle_sum.seconds) % 60; ang.minutes = ((seconds + angle_sum.seconds) / 60 +

minutes + angle_sum.minutes) % 60;
ang.degrees = ((seconds+ angle_sum.seconds) / 60 +

minutes + angle_sum.minutes) / 60;         
ang.degrees += degrees + angle_sum.degrees;

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

Программа выдаст на экран такую строку:

Сумма углов равна 253d 51m 33s

Проверьте, правильно ли вычислен результат.

Производные классы

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

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

class имя_производного_класса : (public/private/protected)

имя_базового_класса
{...};

Тип наследования public означает, что все открытые и защищенные члены базового класса становятся таковыми производного класса. Тип наследования private означает, что все открытые и защищенные члены базового класса становятся закрытыми членами производного класса. Тип наследования protected означает, что все открытые и защищенные члены базового класса становятся защищенными членами производного класса.

В следующей программе показан пример создания открытых производных классов. Родительский класс consumer содержит общие данные о клиенте: имя, адрес, город, штат и почтовый индекс. От базового класса порождаются два производных класса: airlineи rental_car. Первый из них хранит сведения о расстоянии, покрытом клиентом на арендованном самолете, а второй — сведения о расстоянии, покрытом на арендованном автомобиле.

//
//  derclass.cpp
//  Эта программа на языке C++ демонстрирует использование производных классов.
//

#include <iostream.h>
#include <string.h>

char newline;

class consumer { char name[60],

street[60], city[20], state[15], zip[10]; public:

void data_output(void); void data_input(void); };

void consumer::data_output() {

cout << "Имя:    " << name << endl;

cout << "Улица*  " << street << endl;

cout << "Город:  " << city << endl;

cout << "Штат:   " << state << endl;

cout << "Индекс: " << zip << endl;

void consumer: :data_input () {

cout<< "Введите полное имя клиента: ";

cin.get (name, 59,'\n')>>'

cin.get(newline); // пропуск символа новой строки

cout << "Введите адрес: "; cin.get(street, 59,'\n'); cin.get(newline);

cout << "Введите город: ";                         
cin.get(city, 19,'\n');
cin.get(newline) ;

cout<< "Введите название штата:   ";

cin.get(state, 14, '\n');

cin.get (newline);

cout << "Введите почтовый индекс: ";   
  cin.get(zip, 9,'\n');    
  cin.get(newline);

}

class airline : public consumer {

char airline_type[20];

float acc_air_miles; public:

void airline_consumer();

void disp_air_mileage(); };

void airline : :airline_consumer () {

data_input ( ) ;

cout << "Введите тип авиалиний : " ;
cin.get (airline_type, 19,'\n'); cin.get (newline) ;

cout << "Введите расстояние, покрытое в авиарейсах: ";
cin >> acc_air_miles; cin.get (newline) ; }

void airline : :disp_air_mileage () {

data_output ( ) ;

cout << "Тип авиалиний: "" << airline_type << endl;

cout << "Суммарное расстояние: " << acc_air_miles << endl;

class rental_car : public consumer {

char rental_car_type[20] ;

float acc_road_miles; public:

void rental_car_consumer ( ) ;

void disp_road_mileage () ;

};

void rental_car::rental_car_consumer() {

data_input();

cout<< "Введите марку автомобиля: ";

cin.get(rental_car_type, 19,'\n');

cin.get (newline) ;                  

cout << "Введите суммарный автопробег: ";
cin >> acc_road_miles;                                     
cin.get(newline);
}

void rental_car::disp_road_mileage() {

data_output();

cout << "Марка автомобиля: "
<< rental_car_type   << endl; cout << "Суммарный автопробег: " << acc_road_miles << endl;

}     

main()

airline cons1; rental_car cons2;

cout << "\n—Аренда самолета—\n";        cons1.airline_consumer();

cout << "\n—Аренда автомобиля—\n";                          
cons2.rental_car_consumer();

cout<< "\n—Аренда самолета—\n";

cons1.disp_air_mileage();

cout<< "\n—Аренда автомобиля—\n";

cons2 . disp_road_mileage () ;

return(0);     

}

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