}
class matrix {
vect** p;
int size1, size2;
public:
matrix( int, int );
vect& operator [] ( int );
~matrix();
friend ostream& operator << ( ostream&, matrix& );
friend vect operator * ( matrix& m, vect& v );
friend vect operator * ( vect& v, matrix& m );
};
matrix :: matrix( int n1, int n2 )
{
if ( n1 < 0 ) ERR("Ошибка в первой размерности матрицы",n1);
if ( n2 < 0 ) ERR("Ошибка во второй размерности матрицы",n2);
p = new vect* [ size1 = n1 ];
for ( int i=0; i<n1; i++ ) p[i] = new vect(n2);
size2 = n2;
}
vect& matrix :: operator [] ( int n1 )
{
if ( n1 < 1 || n1 > size1 )
ERR("Ошибка в первом номере элемента матрицы",n1);
return ( *p[n1-1] );
}
matrix :: ~matrix()
{
for( int i=0; i<size1; i++) delete p[i];
delete p;
}
ostream& operator << (ostream& s, matrix& m)
{
int i,j;
for( i=0; i<m.size1; i++ ) s << *m.p[i];
return s;
}
vect operator * ( matrix& m, vect& v )
{
if( m.size2 != v.size )
ERR("Ошибка: различные размерности матрицы и вектора");
vect r(m.size1);
for( int i=0; i<m.size1; i++ ) {
r.p[i]=0.;
for ( int j=0; j<v.size; j++ )
r.p[i]+=m.p[i]-->p[j]*v.p[j]; }
return r;
}
vect operator * ( vect& v, matrix& m )
{
if( m.size1 != v.size )
ERR("Ошибка: различные размерности матрицы и вектора");
vect r(m.size2);
for( int i=0; i<m.size2; i++ ) {
r.p[i] = 0.;
for ( int j=0; j<v.size; j++ ) r.p[i]+=m.p[j]-->p[i]*v.p[j];}
return r;
}
void main( void )
{
vect a(3);
matrix b(3,3);
a[1]=1.; a[2]=2.; a[3]=3.;
b[1][1]=1.; b[1][2]=2.; b[1][3]=3.;
b[2][1]=4.; b[2][2]=5.; b[2][3]=6.;
b[3][1]=7.; b[3][2]=8.; b[3][3]=9.;
cout << "Матрица:\n" << b;
cout << "Вектор:\n" << a;
cout << "Результат умножения матрицы на вектор:\n" << b * a;
cout << "Результат умножения вектора на матрицу:\n" << a * b;
}
В результате выполнения этой программы получено
Матрица:
1 2 3
4 5 6
7 8 9
Вектор:
1 2 3
Результат умножения матрицы на вектор:
14 32 50
Результат умножения вектора на матрицу:
30 36 42
Присутствие в описании класса vect конструктора vect( vect& ) может показаться излишним, поскольку нигде в программе он явно не используется. Дело в том, что в отсутствие такого конструктора обращение к функции operator* приведет к побитовому копированию информации, хра¬нимой в локальной переменной r и возвращаемой в качестве результата, в (“рабочую”) переменную, заводимую транслятором в основной программе для результата работы этой функции. После такого копирования поле p этой (“рабочей”) переменной будет содержать тот же адрес памяти, что и поле p локальной переменной r. Но последние действия функции operator* будут состоять в неявном вызове деструктора для локальной переменной r, что, с одной стороны, может привести к потере или порче информации по адресу, хранящейся в указателе p, а с другой стороны, повторное освобождение памяти при неявном вызове деструктора для “рабочей” переменной также может привести к некорректному завершению программы. Поэтому в описание класса vect и помещен конструктор vect(vect&), который позволяет корректным образом обрабатывать возвращение результата выполнения функции.
По тем же причинам для того, чтобы иметь возможность написать следующую последовательность операторов:
vect с(3);
c = a * b;
cout << "Результат умножения a * b" << c;,
где a и b переменные, определенные в main(), следует в описание класса vect поместить описание функции operator=, позволяющую корректным образом реализовать операцию присваивания, например, в виде:
vect vect::operator= ( vect& v )
{
delete p;
p = new float [ size = v.size ];
for( int i=0; i<v.size; i++) p[i] = v.p[i];
return *this;
}
Данный вид функции допускает также выражения вида
vect a(3), b(3), c(3);
a = b = c;
Производные классы
Множество полезных структур данных являются вариантами друг друга. Поэтому в С++ конструирование новых классов возможно на основе уже существующих. Такие классы называются производными. Классы, на основе которых строятся производные классы, обычно называют базовыми. Производные классы, наследуя свойства базовых, строятся путем введения новых членов, перегрузки существующих функций и изменения порядка доступа к членам базового класса. Наследственность - важное свойство объектно-ориентирован¬но¬го программирования.
Описание производного класса должно выглядеть так:
Optional- правило наследования производным классом членов базового класса, которое может задаваться как public, private или protected.
Правило наследования производным классом базового может усилить степень скрытости данных, но не ослабить ее. Т.е. в случае наследования с правилом public скрытые и доступные члены базового класса будут содержаться соответственно в приватном и открытом разделах производного класса, а в случае private - и те и другие будут закрытыми членами производного класса. И в том и в другом случае, как обычно, закрытые члены базового класса будут доступны только через дружественные функции и функции члены базового класса.
Расширить права доступа к некоторым наследуемым членам производного класса, суженные правилом наследования, до их состояния в базовом классе можно явно перечислив такие члены в соответствующем разделе производного класса:
class A {
public:
int a,b;
void f();
};
class B : private A {
public:
A::a;
A::f;};
void main( void )
{
B a;
a.a = 0;
a.f();
a.b = 1; // ошибка; поле из приватного раздела
}
Выполнение программы:
class A {
public:
void f() { cout << "Функция класса А\n"};
};
class B : public A {
public:
void f() { cout << "Функция класса B\n"};
};
class C : public B { };
void main( void )
{
C c;
c.f();
}
дает результат
Функция класса B.
Если производный класс наследует несколько функций с одинаковым именем, то обращение будет строится к той из них, которая принадлежит “самому производному” классу.
В качестве примера построения производного класса рассмотрим следующую программу:
#include <iostream.h>
#include <dos.h>
class Time {
time t;
public:
Time() { gettime( &t ); }
void print();
};
void Time :: print()
{
cout << "Текущее время - " << (int)t.ti_hour << ":";
cout << (int)t.ti_min << ":" << (int)t.ti_sec << ".";
cout << (int)t.ti_hund << "\n";
}
class Date : public Time {
date d;
public:
Date() : Time() { getdate( &d ); }
void print();
};
void Date :: print()
{
cout << "Сегодня " << (int)d.da_day << "." << (int)d.da_mon;
cout << "." << d.da_year << " ";
Time :: print();
}
void main( void )
{
Time a;
a.print();
delay(60000); // задержка в одну минуту
Date b;
b.print();
}
Класс Date является производным от класса Time. Наследуя от него данные о времени, этот класс содержит также данные о текущей дате. Структуры time и date описаны в файле dos.h и используются для получения данных от библиотечных программ. Результатом выполнения программы
Текущее время - 19:18:20.61
Сегодня 12.7.1991 Текущее время - 19:19:20.64
является выдача двух функций print для переменных a и b различных классов. Причем функция print класса Date обращается к одноименной функции класса Time. Чтобы не возникло рекурсии при описании функции класса Date, имя Time::print указано полностью.
Хотя в классе Date и есть член t, содержащий данные о времени, он является недоступным даже для функций членов этого класса. Поэтому, чтобы получить информацию о времени, мы не могли обратиться напрямую к полям структуры t, и нам пришлось обратиться к функции Time::print. Это могло бы привести к значительным затруднениям при желании воспользоваться частью информации, хранящейся в t.
К счастью, эти трудности легко преодолимы, т.к. на самом деле при описании классов может существовать третий раздел, о котором до сих пор речь не шла. Этот раздел вводится ключевым словом protected. Члены этого раздела в базовом классе обладают теми же свойствами, что и члены раздела private. В производном же классе, непосредственно выводимом из базового, эта информация, хотя и является скрытой, все же доступна для функций членов и дружественных функций производного класса.
И, если бы мы захотели изменить функцию print класса Date, это мож-но было бы сделать, например, так:
class Time {
protected:
time t;
public:
Time() { gettime( &t ); }
void print();
};
class Date : public Time {
date d;
public:
Date() : Time() { getdate( &d ); }
void print();
};
void Date::print()
{
cout << "Сейчас - " << (int)t.ti_hour << ":" << (int)t.ti_min << " ";
cout <<(int)d.da_day << "." <<(int)d.da_mon << "." << d.da_year << "\n";
}
и выполнение той же программы дало бы результат
Текущее время - 15:34:27.44
Сейчас - 15:34 13.7.1991
Производный класс в случае открытого наследования является подтипом базового класса, поэтому во многих случаях переменная производного класса может трактоваться как переменная базового класса, а указатели, имеющие тип указателя на базовый класс, могут указывать на объекты, имеющие тип производного класса.
Например, вполне возможно было бы в рамках рассматриваемой программы выполнить присваивание
a = b;
( хотя ошибкой было бы b = a; ) или написать такую программу:
void main( void )
{
Time a;
delay( 20000 );
Date b;
Time* c;
c = &a;
c --> print();
c = &b;
c --> print();
}
Однако результат ее выполнения:
Текущее время - 17:44:18.41
Текущее время - 17:44:38.46
может кого-то и не удовлетворить. Дело в том, что при описании функций print, которое присутствует в классах Time и Date, транслятором при компиляции программы независимо от того, на объект какого класса в данный момент ссылается указатель, строится ссылка на функцию print того же класса, что и тип указателя. Другими словами, транслятором еще на этапе компиляции строится обращение только к одной функции вне зависимости от текущего значения указателя (в данном случае оба раза использовалась функция базового класса).
Если этот "недостаток" должен быть ликвидирован, то об этом необходимо специальным образом уведомить компилятор. Признаком необходимости в процессе выполнения программы в зависимости от того, на объект какого класса ссылается указатель, строить ссылку к перегруженной функции именно этого класса, служит ключевое слово virtual, присутствующее перед названиями таких функций в описании классов.
Описание классов Time и Date в этом случае выглядело бы так:
class Time {
protected:
time t;
public:
Time() { gettime( &t ); }
virtual void print();
};
class Date : public Time {
date d;
public:
Date() : Time() { getdate( &d ); }
virtual void print();
};
а результат выполнения программы, содержащей указатели, был бы уже таким:
Текущее время - 17:51:14.25
Сейчас - 17:51 14.7.1991 ,
поскольку выбор функции, к которой строится обращение, осуществляет-ся уже в процессе выполнения программы в зависимости от текущего значения указателя.
Другим примером иллюстрирующим важность механизма виртуальных функций является следующий фрагмент:
#include <iostream.h>
#include <string.h>
class name {
char* ptrf;
public:
name(const char*);
virtual ~name() { delete ptrf; }
};
name::name(const char* s)
{
char* p = ptrf = new char[strlen(s)+1]; while(*p++=*s++);
}
class full_name: public name {
char* ptrs;
public:
full_name(const char*, const char*)
~full_name() { delete ptrs; }
};
full_name::full_name(const char* s1, const char* s2) : name(s1)
{
char* p = ptrs = new char[strlen(s2)+1]; while(*p++=*s2++);
}
void main( void )
{
char *s1="Николай", *s2="Константинович";
name* ptr = new full_name(s1,s2);
delete ptr;
}
Дело в том, что если не описать деструктор базового класса как виртуальную функцию, то при выполнении оператора delete ptr будет вызываться деструктор базового класса, т.е. будет освобождена лишь память занимаемая именем. Объ¬явление деструктора класса name виртуальной функцией позволяет освобождать всю занятую память под переменную класса full_name корректным образом.
Абстрактные классы
Рассмотрим пример:
#include <iostream.h>
class figure {
intn;
float*x,*y;
public:
figure( int k ) { x=new float[n=k]; y=new float[n]; }
virtual float nm_of_pnts() = 0;
friend ostream& operator<< ( ostream&, figure* );
~figure() { delete x; delete y; }
};
ostream& operator<< (ostream& c, figure* f)
{
return c << "Число вершин фигуры " << f->nm_of_pnts() << "\n";
}
class triangle : public figure {
public:
triangle() : figure(3) {}
float nm_of_pnts() { return 3; }
};
class square : public figure {
public:
square() : figure(4) {}
float nm_of_pnts() { return 4; }
};
void main( void )
{
triangle* a = new triangle ;
square* b = new square ;
cout << a;
cout << b;
delete a;
delete b;
}
Описание функции nm_of_pnts() в классе figure выглядит довольно необычно. Приведенное описание сообщает компилятору о том, что данная функция хотя и присутствует в описании класса, определяться для него не будет. Для каждого класса, являющегося наследником данного класса, описывается своя функция с указанным именем. Такое описание позволило нам, не вдаваясь в подробности реализации функций nm_of_pnts() для каждого из классов-наслед¬ников, еще на этапе разработ-ки базового класса создать общую для всех этих классов функцию operator<< (ostream&, figure*). Класс, содержащий обсуждаемые описа-ния функций, принято называть абстрактным. Дело в том. что в программе не может существовать ни одного объекта такого класса (хотя, как мы видели выше, вполне возможно существование указателей на такие объекты). Используются абстрактные классы только для построения на их основе новых типов данных.
Множественное наследование
Вообще говоря, производный класс может наследовать свойства нескольких базовых классов. Описание таких классов выглядит обычным образом; при этом базовые классы в описании наследника перечисляются через запятую.
#include <iostream.h>
class X {
public:
int i;
virtual void print() { cout << "Содержимое поля i класса X - " << i << "\n"; }
void b() { cout << "Функция b() класса X\n"; }
virtual void c() { cout << "Виртуальная Функция c() класса X\n"; }