Методичка: Введение в объектно-ориентированное программирование на языке С++

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

Пример

#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";