Пример
#include <iostream.h>
#include <math.h>
class complex {
double re, im ;
public:
complex(double r, double i) { re = r; im = i; }
static void print( complex& );
void print() { cout << "(" << re << "," << im << ")\n"; }
static double mod( complex& a ) { return sqrt( a.re*a.re + a.im*a.im ); }
};
void complex::print( complex& a )
{
cout << "{" << a.re << "," << a.im << "}\n";
}
void main( void )
{
complex a(1.,2.), b(3.,4.);
cout << "Обращение к обычной функции: "; a.print();
cout << "Обращение к статической функции: "; a.print(b);
cout << "Модуль числа: " << complex::mod(b) << "\n";
// возможно и так
}
Результат выполнения:
Обращение к обычной функции: (1,2)
Обращение к статической функции: {3,4}
Модуль числа: 5
Константные функции члены класса
Рассмотрим программу
class complex {
double re, im ;
public:
complex(double r, double i) { re = r; im = i; }
void print();
};
void complex::print()
{
cout << "(" << re << "," << im << ")\n";
}
void main( void )
{
const complex a(1.,2.);
a.print();
}
При компиляции этой безобидной, на первый взгляд, программы транслятор выдаст предупреждение об ошибке. У него нет уверенности в том, что функция print не попытается изменить объект a, который, по его определению, должен быть неизменным. Поэтому все функции относительно, которых предполагается возможность их работы с объектами типа const, должны иметь особые полномочия на этот случай. Сообщение о таких возможностях функции дает слово const, которое указывается в заголовке функции. Такие функции имеют доступ к объектам класса любого вида (как к обычным, так и к константным).
В нашем случае описание класса выглядело бы так:
class complex {
double re, im ;
public:
complex ( double, double );
void print() const ;
};
Указатели на функции
В языке C описание указателя на функцию и его использование в простейшем случае могло бы выглядеть следующим образом:
#include <stdio.h>
f(a) int a; { return a*a; }
void main()
{int (*pf)() = f;
printf("%d\n",(*pf)(5));
}
В С++ такое описание привело бы к ошибке, поскольку следовало бы описать и типы аргументов указателя на функцию, т.е. аналогичная программа на С++ выглядела бы так:
#include <iostream.h>
int f(int a) { return a*a; }
void main( void )
{int (*pf)(int) = f;
cout << (*pf)(5) << "\n";
}
Аналогичным образом вводятся и указатели на функции члены класса.
Например, для константной функции print класса complex описание и использование указателя могло бы выглядеть так:
class complex {
double re, im ;
public:
complex ( double, double );
void print() const ;
};
void main( void )
{const complex a(1.,2.), *b = &a ;
void (complex::*pmf)() const = complex::print;
(a.*pmf)();
(b-->*pmf)();
}
Как заметил Страустрап: "Синтаксис на вид не слишком привлекателен, но о синтаксисе С, который служил образцом, этого тоже не скажешь".
Вложенные классы
Классы могут быть вложенными, т.е. класс может быть описан внутри другого класса. Правила обращения с такими классами ничем не отличаются от обычных классов.
Членами класса могут быть также объекты другого класса. Например, если определен класс vect
class vect {
float* a;
int sz;
public:
vect ( int );
};
то класс mvect, состоящий из трех векторов, мог бы быть описан так:
class mvect {
vect a, b, c;
pulic:
mvect( int sz ) : a(sz), b(sz), c(sz) {}
};
Необычным кажется описание конструктора класса mvect. В данном случае тело конструктора состоит из пустого блока, однако в заголовке (после двоеточия) указывается, что должен быть вызван конструктор класса vect для каждого из элементов a, b и c. Обращение к конструкторам производится в порядке написания слева направо, т.е. сначала - для a, и в последнюю очередь - для c.
Преобразование типов
Как уже упоминалось, переменные и константы основных типов могут сочетаться в выражениях языка. При этом при вычислении выражения будет проводиться преобразование типов таким образом, чтобы потери информации были минимальны. Например, в выражении
int i = 2; double s = 3.14; int j = s + i;
при вычислении j значение переменной i будет неявно преобразовано к типу double, а затем результат вычисления суммы, преобразованный к ти-пу int, будет присвоен переменной j.
Преобразование типов может быть указано и явно. В форме традиционной для С, например, это могло бы быть записано так:
j = s + (double)i;
в С++ то же самое могло бы быть записано также в виде
j = s + double(i);
Последняя форма записи преобразования типа называется функциональной, т.к. по семантике она очень похожа на обращение к функции. Указанные две формы преобразования типов не совсем эквивалентны. Так, например, возможна запись
p = (int*)q;
в то время как запись
p = int*(q);
приведет к ошибке.
До сих пор шла речь о преобразовании друг в друга основных типов. Куда более актуален вопрос о преобразовании типов, определенных пользователем.
Функциональная форма записи преобразования типов наводит на мысль, что определяемый в некотором классе пользователем конструктор с одним аргументом фактически является заданием правила преобразования от типа аргумента к типу этого класса.
Например, для класса
class complex {
double re, im;
publc:
complex() { re = im = 0.; }
complex( double r, double i = 0 ) { re = r; im = i; }
};
приведенный конструктор есть правило преобразования вещественного числа в комплексное, которое может быть использовано как явно
complex a;
a = complex(2.0); ,
так и неявно
complex a;
a = 5.;
Хотелось бы, конечно, иметь возможность вводить преобразования, определенных пользователем типов к основным. Простейший выход - это дополнение конструкторами встроенных типов. Однако, поскольку такой возможности нет, пользователь должен описать для определяемого типа данных функцию специального вида
Operator Type() { . . . }
Здесь Type - тип, по отношению к которому определяется преобразование пользовательского типа.
Например, для следующим образом определенного типа complex:
class complex {
double re, im;
public:
complex() { re = im = 0; }
complex( double r, double i = 0 ) { re = r; im = i; }
operator double() { return sqrt( re*re + im*im ); }
void print() { cout << re << "+" << im << "i"; }
};
результат выполнения программы
complex greater( complex a, complex b )
{
return a > b ? a : b;
}
void main ( void )
{
double m;
complex a(3,4), b, max;
b = 4;
m = a;
cout << "Модуль комплексного числа ";
a.print();
cout << " равен " << m << "\n";
max = greater( a, b );
cout << " Большее число - "; max.print();
cout << "\n";
}
такой
Модуль комплексного числа 3+4i равен 5
Большее число - 3+4i
В функции greater производится сравнение двух вещественных чисел, к которым неявно преобразуются комплексные числа a и b, а в качестве результата возвращается комплексное число с большим модулем.
Дружественные функции
Для повышения эффективности работы программы, часто использующиеся функции, было бы разумно сделать членами класса. Но нередко на практике встречаются ситуации, когда такие функции должны работать с объектами различных классов. Например, если определены два класса: вектор и матрица, то функция умножения матрицы на вектор должна эффективно работать как с тем, так и с другим классом. Именно для случаев, когда функция должна была бы быть членом нескольких классов, используется механизм дружественных функций. Функция, друг класса, имеет те же права по отношению к данным, что и член класса. Ее прототип должен быть указан в описании класса с префиксом friend. В то же время функция может быть дружественной по отношению к нескольким классам.
Пример
#include <iostream.h>
#include <stdlib.h>
inline void ERR(char* s,int i) { cerr << s << " " << i << "\n"; exit(1); }
inline void ERR(char* s) { cerr << s << " " << "\n"; exit(1); }
class matrix;
class vect {
float* p;
int size;
public:
vect ( int );
float& get_item ( int );
void print();
friend vect mvm(matrix& m, vect& v);
~vect();идентификатор макрос память деструктор
};
vect :: vect( int n )
{
if ( n < 0 ) ERR("Ошибка в размере вектора",n);
p = new float [ size = n ];
}
float& vect :: get_item( int n )
{
if ( n < 1 || n > size ) ERR("Ошибка в номере элемента вектора",n);
return p[n-1];
}
void vect :: print()
{
for ( int i=0; i<size; i++ ) cout << "\t" << p[i] << "\n";
}
vect :: ~vect()
{
delete p;
}
class matrix {
float** p;
int size1, size2;
public:
matrix( int, int );
float& get_item ( int, int );
void print();
friend vect mvm( matrix& m, vect& v );
~matrix();
};
matrix :: matrix( int n1, int n2 )
{
if (n1 < 0) ERR("Ошибка в первой размерности матрицы",n1);
if (n2 < 0) ERR("Ошибка во второй размерности матрицы",n2);
p = new float* [ size1 = n1 ];
for ( int i=0; i<n1; i++ ) p[i] = new float [ n2 ];
size2 = n2;
}
float& matrix :: get_item( int n1, int n2 )
{
if ( n1 < 1 || n1 > size1 )
ERR("Ошибка в первом номере элемента матрицы",n1);
if ( n2 < 1 || n2 > size2 )
ERR("Ошибка во втором номере элемента вектора",n2);
return ( p[n1-1][n2-1] );
}
void matrix :: print()
{
int i, j;
for( i=0; i<size1; i++ ) { cout << "\t";
for( j=0; j<size2; j++ ) cout << p[i][j] << " ";
cout << "\n"; }
}
matrix :: ~matrix()
{
for( int i=0; i<size1; i++ ) delete p[i];
delete p;
}
vect mvm( 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][j]*v.p[j]; }
return r;
}
void main( void )
{
vect a(2), c(3);
matrix b(3,2);
a.get_item(1) = 1.; a.get_item(2) = 2.;
b.get_item(1,1) = 1.; b.get_item(1,2) = 2.;
b.get_item(2,1) = 3.; b.get_item(2,2) = 4.;
b.get_item(3,1) = 5.; b.get_item(3,2) = 6.;
cout << "Матрица:\n";
b.print();
cout << "Вектор:\n";
a.print();
c = mvm(b,a);
cout << "Результат:\n";
c.print();
}
Результат выполнения программы
Матрица:
1 2
3 4
5 6
Вектор:
1
2
Результат:
5
11
17
Функция mvm, выполняющая умножение матрицы на вектор, имеет доступ к скрытым данным обоих классов. Поскольку эта функция должна была быть описана как дружественная в каждом из классов и, кроме того, она имеет аргументы обоих типов, то имя одного из классов (в данном случае это matrix) должно быть описано до определения класса.
Переопределение операторов
В приведенном выше примере для умножения матрицы на вектор нам пришлось обращаться к функции mvm:
c = mvm(b,a);
Более элегантно выглядела бы запись
c = b * a;
но так можно было бы написать в случае, если бы в языке были определены объекты матрица и вектор, а также операция умножения матрицы на вектор. Поскольку эти объекты были созданы нами, и априори в языке не определены, то для того чтобы иметь возможность воспользоваться операцией "*", ее следует доопределить на классы созданных нами объектов. Такая возможность в С++ дается ключевым словом operator, которое также использовалось для определения правил преобразования типов.
В С++ может быть расширена область определения почти всех операторов. Эти правила не распространяются только на операторы: запятая, точка, sizeof и условный оператор "?:,". Кроме того, не различаются префиксная и постфиксная формы переопределенных операторов увеличения (++) и уменьшения (--). Приоритет выполнения операций сохраняется таким, как он определен в С++, и изменен пользователем быть не может.
Для расширения области определения оператора на новый класс объектов должна быть определена специальная функция, к которой при необходимости использовать новые возможности оператора производится обращение. Например, заменив заголовок функции mvm, мы получили бы описание оператора умножения матрицы на вектор, которое могло бы выглядеть так:
vect operator * ( matrix&, vect& )
. . .
Конечно, изменится и описание классов, где будет стоять вместо описания дружественной классу функции описание
friend vect operator * ( matrix&, vect& ); .
Заметим, что в С++ переменная cout описана в файле iostream.h как переменная класса ostream, реализующего выходной поток информации, а переменная cin - как переменная класса istream, реализующего поток входной информации. Для переменных встроенных типов операции побитового сдвига влево и вправо переопределены для вывода и ввода информации из потоков соответственно.
Пример
В этом примере приводятся описания классов вектор и матрица, содержащие расширения операторов звездочка, квадратные скобки и побитовый сдвиг влево. Оператор "*" доопределен на умножение матрицы на вектор и наоборот, "[]" - на доступ к элементам вектора и матрицы, а "<<" - на помещение информации из определенных пользователем классов в выходной поток.
Матрица рассматривается как столбец векторов, поэтому запись b[i][j] означает взятие j элемента из i вектора.
В этом примере опущены описания функций ERR и операторы include.
class vect {
float* p;
int size;
public:
vect ( int );
vect( vect& );
float& operator [] ( int );
~vect();
friend ostream& operator << ( ostream&, vect& );
friend vect operator* ( matrix& m, vect& v );
friend vect operator* ( vect& v, matrix& m );
};
vect :: vect( int n )
{
if ( n < 0 ) ERR("Ошибка в размере вектора",n);
p = new float [ size = n ];
}
vect :: vect( vect& v )
{
p = new float [ size = v.size ];
for( int i=0; i<v.size; i++) p[i] = v.p[i];
}
float& vect :: operator [] ( int n )
{
if ( n < 1 || n > size )
ERR("Ошибка в номере элемента вектора",n);
return p[n-1];
}
vect :: ~vect()
{
delete p;
}
ostream& operator << ( ostream& s, vect& v )
{
s << "\t";
for ( int i=0; i<v.size; i++ ) s << v.p[i] << " ";
return s << "\n";