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

Анализ прямых расширений языка С. Проведение исследования ассоциированных типов идентификатора в программе. Семантика использования макросов и семантика применения функций. Использование динамической памяти. Характеристика конструкторов и деструкторов.

Рубрика Программирование, компьютеры и кибернетика
Вид методичка
Язык русский
Дата добавления 31.01.2019
Размер файла 50,6 K

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.

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

}

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

};

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 - для описания реакции на ситуацию определенного типа. Реакция будет вызвана только тогда, когда возбуждение исключительной ситуации производится в блоке контроля или в функциях, вызванных из этого блока. Недостатком механизма обработки исключительных ситуаций является то, что управление из программы передается сразу на обработку ошибок, поэтому далеко не всегда понятно, в каком именно месте программы возникла эта исключительная ситуация.

Размещено на Allbest.ru


Подобные документы

  • Использование скриптового языка программирования для разработки web-приложений (сценариев). Изучение основ объектно-ориентированного программирования в языке PHP. Ознакомление со специальными методами для работы с классами. Назначение интерфейсов.

    контрольная работа [25,1 K], добавлен 14.03.2015

  • Объектно-ориентированное программирование как методология программирования, опирающаяся на инкапсуляции, полиморфизме и наследовании. Общая форма класса. Наследование как процесс, посредством которого один объект получает свойства другого объекта.

    презентация [214,9 K], добавлен 26.10.2013

  • Приемы и правила объектно-ориентированного программирования с использованием языка С++. Общие принципы разработки объектно-ориентированных программ. Основные конструкции языка С++. Разработка различных программ для Windows с использованием WIN32 API.

    учебное пособие [1,6 M], добавлен 28.12.2013

  • Свойства объектно-ориентированного языка программирования. Понятия инкапсуляции и наследования. Виртуальные функции и полиморфизм. Инициализация экземпляра объекта с помощью конструктора. Динамическое создание объектов. Совместимость объектных типов.

    реферат [17,0 K], добавлен 15.04.2015

  • Разработка программы с использованием принципов объектно-ориентированного программирования на языке высокого уровня С средствами Microsoft Visual Studio 2010. Построение алгоритма реализации. Класс программы, инструкция по использованию программы.

    курсовая работа [1,0 M], добавлен 26.12.2013

  • Почему C++. Возникновение и эволюция языка C++. Сравнение языков С++ и С. Эффективность и структура. Процедурное программирование. Модульное программирование. Абстракция данных. Объектно-ориентированное программирование. Улучшенный С.

    реферат [26,4 K], добавлен 03.06.2004

  • Введение в объектно-ориентированное программирование. Постановка задачи. Описание алгоритма решения в псевдокоде (команды в виде текста на русском языке). Исходный текст программы на С. Тестирование программы. Модификация программы. Полиморфизм.

    курсовая работа [294,0 K], добавлен 08.09.2008

  • Описание предметной области. Контроль и методы доступа. Работа с графикой в С++ Builder. Программирование игры "Воздушный бой" с использованием основных принципов объектно-ориентированного программирования. Принципы работы конструкторов и деструкторов.

    курсовая работа [901,0 K], добавлен 31.05.2015

  • Вычисление выражений, использование стандартных функций; работа с графикой. Порядок действий при вычислении, способы ввода данных с клавиатуры. Построение таблиц функций. Организация циклов в программе, итерационные процессы. Работа с массивами чисел.

    контрольная работа [614,7 K], добавлен 16.09.2012

  • Принципы объектного подхода. UML как воплощение идеи визуального моделирования. Синтаксис и семантика основных объектов UML, различные отношения между классами. Диаграммы использования, их назначение. Разработка модели бизнес-прецедентов данных.

    презентация [1,0 M], добавлен 19.09.2016

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.