};
class Y1 : public X {
public:
int i;
virtual void print() { cout << "Содержимое поля i класса Y1 -" << i << "\n"; }
void b() { cout << "Функция b() класса Y1\n"; }
virtual void c() { cout << "Виртуальная Функция c() класса Y1\n"; }
};
class Y2 : public X {
public:
int i;
virtual void print() { cout << "Содержимое поля i класса Y2 -" << i << "\n"; }
};
class Z : public Y1, public Y2 {
public:
int i;
virtual void c() { cout << "Виртуальная Функция c() класса Z\n"; }
virtual void print() { cout << "Содержимое поля i класса Z - " << i << "\n"; }
};
void main( void )
{
Z* z = new Z;
Y1* y1 = z;
Y2* y2 = z;
cout << "Вызову ((X*)y1)->b(); соответствует "; ((X*)y1)->b();
cout << "Вызову ((X*)y1)->c(); соответствует "; ((X*)y1)->c();
((Y1*)z)->X::i = 10;// Инициализация поля i класса X,
// наследуемого через класс Y1
((Y2*)z)->X::i = 20;// Инициализация поля i класса X,
// наследуемого через класс Y2
z->Y1::i = 1;// Инициализация поля i,
// наследуемого через класс Y1
z->Y2::i = 2;// Инициализация поля i,
// наследуемого через класс Y2
z->i = 0; // Инициализация поля i, класса Z
y1->X::print();
y2->X::print();
z->Y1::print();
z->Y2::print();
((X*)y1)->print();
}
Результат выполнения этой программы:
Вызову ((X*)y1)->b(); соответствует Функция b() класса X
Вызову ((X*)y1)->c(); соответствует Виртуальная Функция c() класса Z
Содержимое поля i класса X - 10
Содержимое поля i класса X - 20
Содержимое поля i класса Y1 - 1
Содержимое поля i класса Y2 - 2
Содержимое поля i класса Z - 0
Как обычно виртуальные функции вызываются из самого производного класса. Через два базовых класса Y1 и Y2 переменные класса Z наследуют два поля i класса X. Избежать этого возможно, объявив наследование класса X виртуальным. Поскольку при таком наследовании набор элементов класса X в переменных класса Z будет единственным, то класс Z может рассматриваться не только как подтип классов Y1 и Y2, но также и как подтип класса X.
#include <iostream.h>
class X {
public:
int i;
virtual void a() { cout << "Виртуальная Функция a() класса X\n"; }
void b() { cout << "Функция b() класса X\n"; }
virtual void c() { cout << "Виртуальная Функция c() класса X\n"; }
virtual void print() { cout << "Содержимое поля i класса X - " << i << "\n"; }
};
class Y1 : public virtual X {
public:
int i;
virtual void a() { cout << "Виртуальная Функция a() класса Y1\n"; }
void b() { cout << "Функция b() класса Y1\n"; }
virtual void c() { cout << "Виртуальная Функция c() класса Y1\n"; }
virtual void print() { cout << "Содержимое поля i класса Y1 -" << i << "\n"; }
};
class Y2 : public virtual X {
public:
int i;
virtual void a() { cout << "Виртуальная Функция a() класса Y2\n"; }
virtual void print() { cout << "Содержимое поля i класса Y2 -" << i << "\n"; }
};
class Z : public Y1, public Y2 {
public:
int i;
virtual void a() { cout << "Виртуальная Функция a() класса Z\n"; }
virtual void print() { cout << "Содержимое поля i класса Z - " << i << "\n"; }
};
void main( void )
{
Z* z = new Z;
Y1* y1 = z;
Y2* y2 = z;
X* x = z;
cout << "Вызову y1->X::a(); соответствует "; y1->X::a();
cout << "Вызову z->X::a(); соответствует "; z->X::a();
cout << "Вызову y2->a(); соответствует "; y2->a();
cout << "Вызову y2->b(); соответствует "; y2->b();
// Обратите внимание!
cout << "Вызову y2->c(); соответствует "; y2->c();
// Это вызов функции Y1:: c()
cout << "Вызову x->b(); соответствует "; x->b();
cout << "Вызову x->c(); соответствует "; x->c();
((Y1*)z)->X::i = 10;// Инициализация поля i класса X
((Y2*)z)->X::i = 20;
z->X::i = 30;
z->Y1::i = 1;// Инициализация поля i,
// наследуемого через класс Y1
z->Y2::i = 2;// Инициализация поля i,
// наследуемого через класс Y2
z->i = 0;// Инициализация поля i, класса Z
z->X::print();
z->Y1::print();
z->Y2::print();
((X*)y1)->print();
}
X { virtual a(), c() print(); b() }?Y1 { virtual a(), c() print(); b() }
??
Y2{ virtual a(), print() } ? Z { virtual a(), print() }
Вызову y1->X::a(); соответствует Виртуальная Функция a() класса X
Вызову z->X::a(); соответствует Виртуальная Функция a() класса X
Вызову y2->a(); соответствует Виртуальная Функция a() класса Z
Вызову y2->b(); соответствует Функция b() класса X
Вызову y2->c(); соответствует Виртуальная Функция c() класса Y1
Вызову x->b(); соответствует Функция b() класса X
Вызову x->c(); соответствует Виртуальная Функция c() класса Y1
Содержимое поля i класса X - 30
Содержимое поля i класса Y1 - 1
Содержимое поля i класса Y2 - 2
Содержимое поля i класса Z - 0
Организация ввода-вывода
Для организации обменов с внешними устройствами в С++ определены специальные классы. Мы уже сталкивались с описанными в С++ переменными cin, cout и cerr. Входной поток информации со стандартного ввода доступен через переменную cin класса istream. Поток выходной информации на стандартный вывод передается через переменную класса ostream.
В описании этих классов операции побитового сдвига доопределены для обмена информацией с потоками. Например, в класс ostream входят такие описания, как
ostream& operator << ( int );
ostream& operator << ( double );
ostream& operator << ( char* );
ostream& put( char ); ,
реализующие преобразования переменных стандартных типов в выходную информацию, помещаемую на стандартный вывод.
Так же организована и передача потока выходной информации на устройство stderr через cerr.
Вообще говоря, любой из потоков может быть связан пользователем не со стандартными устройствами, а с некоторым файлом.
Например, в следующей программе выходная информация будет помещена в файл c именем out, а не экран терминала. Связь потока с файлом осуществляется через буфер f, переменную класса filebuf:
#include <fstream.h>
int main( void )
{long a = 10;
filebuf f;
if ( f.open("out",ios::out) == 0 ) { cerr << "Error\n"; return 1; }
ostream cout( &f );
cout << " a = " << a << "\n";
}
Первый параметр функции open это имя файла, второй - статус открываемого файла. Этот параметр может принимать одно из следующих значений, которые описаны в классе ios следующим образом:
enum open_mode {
in = 0x01, // open for reading
out = 0x02, // open for writing
ate = 0x04, // seek to eof upon original open
app = 0x08, // append mode: all additions at eof
trunc = 0x10, // truncate file if already exists
nocreate = 0x20, // open fails if file doesn't exist
noreplace= 0x40, // open fails if file already exists
binary = 0x80 // binary (not text) file
};
Для класса istream функции ввода определяются в том же духе. При вводе значения переменной стандартного типа будут опущены все пропуски (пробелы, табуляции, символы новой строки и т.п.), а для считывания текущих символов следует воспользоваться функциями get, которые описаны так:
istream& get( char& );
istream& get( char* c, int n, int k='\n' ); .
Последняя функция предназначена для считывания н более n символов в символьный массив, причем если в считываемой последовательности символов встретится символ, код которого указан в переменной k, считывание прекратится (а указанный символ k останется на буфере вода).
Для буферизованных потоков ввода определена также функция putback, предназначенная для возвращения символа назад в поток ввода. Использование этой функции позволяет "заглянуть вперед" в поток ввода. Простейший пример, демонстрирующий использование этой функции может быть таким:
#include <iostream.h>
void main( void )
{
int a;
char c;
cin.get( c );
if ( c == ' ' ) cout << "\t";
else cin.putback(c);
cin >> a;
cout << " a= " << a << "\n";
}
При вводе числа, начинающегося с пробела, выдача будет начинаться с табуляции, если же вводится число без начального пробела, то с начала строки.
Работа с файлами
Используя классы ifstream и ofstream, пользователь может определить свои потоки, связанные с файлами входных и выходных данных. Работа с такими потоками ничем не отличается от работы с потоками cin или cout.
Например, программа копирования содержимого символьного файла в другой может быть написана с использованием определенных пользователем потоков так:
#include <fstream.h>
#include <stdlib.h>
inline void ERR( char* s1, char* s2="" )
{ cerr << s1 << " " << s2 << "\n"; exit(1); }
void main ( int n, char** a )
{
if ( n != 3 ) ERR("Использование: copy откуда куда");
ifstream in( a[1] );
if ( !in ) ERR("Не могу найти файл с именем",a[1]);
ofstream out( a[2] );
if ( !out ) ERR("Не могу открыть файл с именем",a[2]);
char c;
while( in.get( c ) ) out.put( c );
}
В этой программе после работы конструкторов классов ifstream и ofstream, связывающих потоки с открываемыми файлами, производится проверка состояния потоков. При проверке состояния потока возвращается ненулевое значение, если предыдущая операция завершилась благополучно, и нулевое - в противном случае (не удалось открыть файл, обнаружен конец файла и т.п.).
Шаблоны
Для облегчения создания семейств функций или классов, оперирующих со множеством различных типов данных в языке С++, определено понятие шаблонов, которые освобождают пользователя от необходимости описывать каждую отдельную функцию или класс. При определении шаблона задается параметр типа, который обозначает тип переменной или константы, передаваемой через вызов функции.
Описание шаблона прототипа функции выглядит следующим образом:
template < Список параметров типов >
Тип возвращаемого значения Имя функции ( Список параметров );
Описать шаблон прототипа класса можно еще проще:
template < Список параметров типов > class Имя класса ;
Пояснения требует только Список параметров типов, все остальные параметры выглядят стандартным образом:
Список параметров типов - список мнемонических имен типов, каждый элемент списка начинается с ключевого слова class.
Пример
// Шаблон функции
template <class T> T max(T x, T y) { return (x > y) ? x : y; };
// Шаблон класса
template <class T> class List
{
T *v;
int size;
public:
List(int);
T& operator[](int i) {return v[i];}
virtual ~List( void ) { delete v;}
};
// Описание функции вне класса требует специального вида:
template <class T> List<T>::List(int n)
{
v = new T[ size = n ];
}
class Myclass {
float a;
public:
Myclass(float b=0.) { a=b; }
friend int operator> (Myclass a, Myclass b) { return (a.a>b.a)?1:0; }
};
void main( void )
{
int i=5;
Myclass a(4), b(5);
int j = max(i,0); // аргументы имеют тип int
Myclass m = max(a,b); // аргументы типа Myclass
List<int> x(20);
List<Myclass> y(30);
x[3] = 7;
y[3] = m; // m имеет тип Myclass
int& (List<int>::*pti)(int) = List<int>::operator[];
Myclass& (List<Myclass>::*ptm)(int) = List<Myclass>::operator[];
x[2]=(x.*pti)(3);
(y.*ptm)(4)=y[3];
}
При использовании шаблонов классов следует иметь в виду, что они порождают полный набор возможных функций для каждого из типов, с которыми эти шаблоны встречаются. Именно поэтому в приведенном примере было возможным определить два указателя pti и ptm на функции operator[] - члены классов List<int> и List<Myclass>.
Обработка исключительных ситуаций
Возможность обработки исключительных ситуаций была встроена в язык для обработки ошибок, которые иначе обработаны быть не могут, или обработка таких ситуаций перегрузила бы текст программы лишним кодом.
Простейшим примером является использование в программе функции printf. Известно, что в качестве возвращаемого значения эта функция передает количество выведенных символов. Это количество может быть равно нулю, например, в том случае, когда стандартный вывод переопределен на файл и на диске (что легко может случиться с дискетой) отсутствует свободное место. Но, как правило, никто и никогда не проверяет результат работы функции printf из-за достаточно большого размера непроизводительных затрат, которые возникли бы в таком случае. Другим примером могут служить конструкторы, эти функции вызываются неявно и не могут возвращать код ошибок обычным путем в случае их возникновения. Для обработки такого рода ошибок и предназначены исключительные ситуации в C++. Пример того, как мог бы выглядеть класс List, содержащий обработку исключительных ситуаций, приведен ниже:
template<class T> class List
{
T *v;
int size;
public:
List(int) {
if( n>0 ) v = new T[size=n];
throw "err ind";
}
class out_of_bounds {};
T& operator[](int i) {
if ( 0<=i && i<size ) return v[i];
throw out_of_bounds();
}
virtual ~List( void ) { delete v;}
};
void main( void )
{
try
{
List<int> x(10);
x[22]=3;
}
catch(out_of_bounds)
{
cerr << " Index is out of range\n";
}
catch (char*)
{
cerr << "Error in size of array\n";
}
}
Здесь ключевые слова throw служат для возбуждения исключительных ситуаций и задания их типа, try - для выделения блока контроля, а catch - для описания реакции на ситуацию определенного типа. Реакция будет вызвана только тогда, когда возбуждение исключительной ситуации производится в блоке контроля или в функциях, вызванных из этого блока. Недостатком механизма обработки исключительных ситуаций является то, что управление из программы передается сразу на обработку ошибок, поэтому далеко не всегда понятно, в каком именно месте программы возникла эта исключительная ситуация.