Производные классы.(3 час.)

Наследование классов и производные классы. Конструкторы, деструкторы и наследование. Множественное наследование. Виртуальные базовые классы. Иерархия классов. Виртуальные функции. Полиморфизм. Абстрактные классы и чистые виртуальные функции.

Наследование классов и производные классы. Начиная рассматривать вопросы наследования, нужно отметить, что обоснованно введенный в программу объект призван моделировать свойства и поведение некоторого фрагмента решаемой задачи, связывая в единое целое данные и методы, относящиеся к этому фрагменту. В терминах объектно - ориентированной методологии объекты взаимодействуют между собой и с другими частями программы с помощью сообщений. В каждом сообщении объекту передается некоторая информация. В ответ на сообщение объект выполняет некоторое действие, предусмотренное набором компонентных функций того класса, которому он принадлежит. Таким действием может быть изменение внутреннего состояния (изменение данных) объекта либо передача сообщения другому объекту.

Каждый объект является конкретным представителем класса. Объекты одного класса имеют разные имена, но одинаковые по типам и внутренним именам данные. Объектам одного класса для обработки своих данных доступны одинаковые компонентные функции класса и одинаковые операции, настроенные на работу с объектами класса. Таким образом, объект выступает в роли типа, позволяющего вводить нужное количество объектов, имена (названия) которых программист выбирает по своему усмотрению.

Объекты разных классов и сами классы могут находиться в отношении наследования, при котором формируется иерархия объектов, соответствующая заранее предусмотренной иерархии классов.

Иерархия классов позволяет определять новые классы на основе уже имеющихся. Имеющиеся классы обычно называют базовыми (иногда порождающими), а новые классы, формируемые на основе базовых, - производными (порожденными), иногда классами-потомками или наследниками. Производные классы «получают наследство» - данные и методы своих классов - и, кроме того, могут пополняться собственными компонентами (данными и собственными методами). Наследуемые компоненты не перемещаются в производный класс, а остаются в базовых классах. Сообщение, обработку которого не могут выполнить методы производного класса, автоматически передается в базовый класс. Если для обработки сообщения нужны данные, отсутствующие в производном классе, то их пытаются отыскать автоматически и незаметно для программиста в базовом классе.

При наследовании некоторые имена методов (компонентных функций) и (или) компонентных данных базового класса могут быть по-новому определены в производном классе. В этом случае соответствующие компоненты базового класса становятся недоступными из производственного класса. Для доступа из производственного класса к компонентам базового класса, имена которых повторно определены в производном, используется операция ‘::’ указания (уточнения) области видимости.

Любой производный класс может, в свою очередь, становиться базовым для других классов, и таким образом формируется направленный граф иерархии классов и объектов. В иерархии производный объект наследует разрешенные для наследования компоненты всех базовых объектов. Другими словами, у объекта имеется возможность доступа к данным и методам всех своих базовых классов.

Наследование в иерархии классов может отображаться и в виде дерева, и в виде более общего направленного ациклического графа. Допускается множественное наследование - возможность для некоторого класса наследовать компоненты нескольких никак не связанных между собой базовых классов. Например, класс «окно на экране» и класс «сообщение» совместно могут формировать новый класс объектов «сообщение в окне».

При наследовании классов важную роль играет статус доступа (статус внешней видимости) компонентов. Для любого класса все его компоненты лежат в области его действия. Тем самым любая принадлежащая классу функция может использовать любые компонентные данные и вызывать любые принадлежащие классу функции. Вне класса в общем случае доступны только те его компоненты, которые имеют статус public.

В иерархии классов соглашение относительно доступности компонентов класса следующее.

· Собственные (private) методы и данные доступны только внутри того класса, где они определены.

· Защищенные (protected) компоненты доступны внутри класса, в котором они определены, и дополнительно доступны во всех производных классах.

· Общедоступные (public) компоненты класса видимы из любой точки программы, т.е. являются глобальными.

Если считать, что объекты, т.е. конкретные представители классов, обмениваются сообщениями и обрабатывают их, используя методы и данные классов, то при обработке сообщения используются, во-первых, общедоступные члены всех классов программы; во-вторых, защищенные компоненты базовых и рассматриваемого классов и, наконец, собственные компоненты рассматриваемого класса. Собственные компоненты базовых и производных классов, а также защищенные компоненты производных классов не доступны для сообщения и не могут участвовать в его обработке.

Еще раз отметим, что на доступность компонентов класса влияет не только явное использование спецификаторов доступа (служебных слов) - private ( собственный ), protected(защищенный), public ( общедоступный), но и выбор ключевого слова class, struct, union, с помощью которого объявлен класс.

Определение производного класса. В определении и описании производного класса приводится список базовых классов, из которых он непосредственно наследует данные и методы. Между именем вводимого (нового) класса и списком базовых классов помещается двоеточие. Например, при таком определении

class S: X, Y, Z {...};

класс S порожден классами X, Y, Z, откуда он наследует компоненты. Наследование компонента не выполняется, если его имя будет использовано в качестве имени компонента в определении производного класса S. Как уже говорилось, по умолчанию из базовых классов наследуются методы и данные со спецификаторами доступа - public (общедоступные) и protected (защищенные).

В порожденном классе эти унаследованные компоненты получают статус доступа private, если новый класс определен с помощью ключевого слова class, и статус доступа public, если новый класс определен как структура, т.е. с помощью ключевого слова struct. Таким образом при определении класса struct J: X, Z {... }; любые наследуемые компоненты классов X, Z будут иметь в классе J статус общедоступных (public). Пример:

class B { protected: int t;

public: char u;

};

class E: B {... }; // t, и наследуются как private

class S: B {... }; // t, и наследуются как public

Явно изменить умалчиваемый статус доступа при наследовании можно с помощью спецификаторов доступа - private, protected и public. Эти спецификаторы доступа указываются в описании производного класса непосредственно перед нужными именами базовых классов. Если класс B определен так, как показано выше, то можно ввести следующие производные классы:

class M: protected B {... }; // t, и наследуется как protected

class P: public B {... }; // t - protected, и - public

class D: private B {... }; // t, и наследуется как private

struct F: private B {... }; // t, и наследуется как private

struct G: public B {... }; t - protected, и - public

Конструктор базового класса всегда вызывается и выполняется до конструктора производного класса.

Особенности деструкторов. Итак, конструктор вызывается при создании каждого объекта класса и выполняет все необходимые операции как для выделения памяти объекта, так и для ее инициализации. Когда объект уничтожается при завершении программы или при выходе из области действия определения соответствующего класса, необходимы противоположные операции, самая важная из которых - освобождение памяти. Эти операции могут и должны выполняться по-разному в зависимости от особенностей конкретного класса. Поэтому в определении класса явно или по умолчанию включают специальную принадлежащую классу функцию - деструктор. Деструктор имеет строго фиксированное имя вида:

имя_класса

У деструктора не может быть параметров (даже типа void), и деструктор не имеет возможности возвращать какой-либо результат, даже типа void. Статус доступа деструктора по умолчанию public (т.е. деструктор доступен во всей области действия определения класса). В несложных классах деструктор обычно определяется по умолчанию.

Деструкторы не наследуются, поэтому даже при отсутствии в производном классе деструктора он не передается из базового, а формируется компилятором как умалчиваемый со статусом доступа public. Этот деструктор вызывает деструкторы базовых классов.

В любом классе могут быть в качестве компонентов определены другие классы. В этих классах будут свои деструкторы, которые при уничтожении объекта охватывающего (внешнего) класса выполняются после деструктора охватывающего класса.

Деструкторы базовых классов выполняются в порядке, обратном перечислению классов в определении производного класса. Таким образом порядок уничтожения объекта противоположен по отношению к порядку его конструирования.

Вызовы деструкторов для объектов класса и для базовых классов выполняются неявно и не требуют никаких действий программиста. Однако вызов деструктора того класса, объект которого уничтожается в соответствии с логикой выполнения программы, может быть явным. Это может быть, например, случай, когда при создании объекта для него явно выделялась память.

Множественное наследование и виртуальные базовые классы. Класс называют непосредственным (прямым) базовым классом (прямой базой), если он входит в список базовых при определении класса. В то же время для производного класса могут существовать косвенные или непрямые предшественники, которые служат базовыми для классов, входящих в список базовых. Если некоторый класс А является базовым для В и В есть база для С, то класс В является непосредственным базовым классом для С, а класс А - непрямой базовый класс для С. Обращение к компоненту ха, входящему в А и унаследованному последовательно классами В и С, можно обозначить в классе С либо как А::ха, либо как В::ха. Обе конструкции обеспечивают обращение к элементу ха класса А.

Производные классы принято изображать ниже базовых. Именно в таком порядке их объявления рассматривает компилятор и их тексты размещаются в листинге программы. Класс может иметь несколько непосредственных базовых классов, т.е. может быть порожден из любого числа базовых классов, например,

class X1 {... };

class X2 {... };

class X3 {... };

class Y1: public X1, public X2, public X3 {... };

Наличие нескольких прямых базовых классов называют множественным наследованием.

Определения базовых классов должны предшествовать их использованию в качестве базовых. При множественном наследовании никакой класс не может больше одного раза использоваться в качестве непосредственного базового. Однако класс может больше одного раза быть непрямым базовым классом:

class X {...; f ();... };

class Y: public X {... };

class Z: public X {... };

class D: public Y, public Z {... };

В данном примере класс Х дважды опосредовано наследуется классом D.

Проиллюстрированное дублирование класса соответствует включению в производный объект нескольких объектов базового класса. В нашем примере существуют два объекта класса Х, и поэтому для устранения возможных неоднозначностей вне объектов класса D нужно обращаться к конкретному компоненту класса Х, используя полную квалификацию: D::Y::X::f() или D::Z::X::f(). Внутри объекта класса D обращения упрощаются Y::X::f() или Z::X::f(), но тоже содержат квалификацию.

Чтобы устранить дублирование объектов непрямого базового класса при множественном наследовании, этот базовый класс объявляют виртуальным. Для этого в списке базовых классов перед именем класса необходимо поместить ключевое слово virtual. Например, класс Х будет виртуальным базовым классом при таком описании:

class X {... f();... };

class Y: virtual public X {... };

class Z: virtual public X {... };

class D: public Y, public Z {... };

Теперь класс D будет включать только один экземпляр Х, доступ к которому равноправно имеют классы Y и Z.

Обратите внимание, что размеры производных классов при отсутствии виртуальных базовых равны сумме длин их компонентов и длин унаследованных базовых классов. «Накладные расходы» памяти здесь отсутствуют.

При множественном наследовании один и тот же базовый класс может быть включен в производный класс одновременно несколько раз, причем и как виртуальный, и как не виртуальный.

class X {... };

class Y: virtual public X {... };

class Z: virtual public X {... };

class B: virtual public X {... };

class C: virtual public X {... };

class E: public X {... };

class D: public X {... };

class A: public D, public B, public Y, public Z, public C, public E {... };

В данном примере объект класса А включает три экземпляра объектов класса Х: один виртуальный, совместно используемый классами B, Y, C, Z, и два не виртуальных относящихся соответственно к классам D и E. Таким образом, можно констатировать, что виртуальность класса в иерархии производных классов является не свойством класса как такового, а результатом особенностей процедуры наследования.

Возможны и другие комбинации виртуальных и невиртуальных базовых классов. Например:

class BB {... };

class AA: virtual public BB {... };

class CC: virtual public BB {... };

class DD: public AA, public CC, public virtual BB {... };

При использовании наследования и множественного наследования могут возникать неоднозначности при доступе к одноименным компонентам разных базовых классов. Простейший и самый надежный способ устранения неоднозначностей - использование квалифицированных имен компонентов. Как обычно, для квалификации имени компонента используется имя класса. Следующий пример иллюстрирует упомянутую неоднозначность и ее разрешение с помощью квалификационных имен компонентов:

class X { public: int d;... };

class Y { public: int d;... };

class Z: public X, public Y,

{ public:

int d;

...

d=X::d + Y::d;

...

};

Виртуальные функции и абстрактные классы. К механизму виртуальных функций обращаются в тех случаях, когда в базовый класс необходимо поместить функцию, которая должна по-разному выполняться в производных классах. Точнее, по-разному должна выполняться не единственная функция из базового класса, а в каждом производственном классе требуется свой вариант этой функции.

Например, базовый класс может описывать фигуру на экране без конкретизации ее вида, а производные классы (треугольник, эллипс и т.п.) однозначно определяют ее формы и размеры. Если в базовом классе ввести функцию для изображения фигуры на экране, то выполнение этой функции будет возможно только для объектов каждого из производных классов, определяющих конкретные изображения.

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

Рассмотрим теперь, как ведут себя при наследовании невиртуальные компонентные функции с одинаковыми именами, типами и сигнатурами параметров.

Если в базовом классе определена некоторая компонентная функция, то такая же функция (с тем же именем, того же типа и с тем же набором и типами параметров) может быть введена в производном классе. Рассмотрим следующее определение классов:

// BASE.DIR - определения базового и производного классов

class base { public:

void fun (int i)

{ printf("\nbase::i =",i); }

};

class dir: public base

{ public: void fun (int i)

{ printf("\nbase::i =",i); }

};

В данном случае внешне одинаковые функции void fun (int)определены в базовом классе baseи в производном классе dir.

В теле класса dirобращение к функции fun(), принадлежащей классу base, может быть выполнено с помощью полного квалифицированного имени, явно включающего имя базового класса: base::fun(). При обращении в классе dirк такой же (по внешнему виду) функции, принадлежащей классу dir, достаточно использовать имя fun() без предшествующего квалификатора.

В программе, где определены и доступы оба класса baseи dir, обращения к функциям fun()могут быть выполнены с помощью указателей на объекты соответствующих классов:

// одинаковые функции в базовом и производном классах

# inclube <stdio.h>

# inclube «base.dir» // Определения классов

void main (void)

{ base B, *bp = &B;

dir D, *dp = &D;

base *pbd = &D;

bp->fun (1); // Печатает: base::i = 1

dp->fun (5); // Печатает: dir::i = 5

pbd->fun (4); // Печатает: base::i = 4

}

В программе введены три указателя на объекты разных классов. Следует обратить внимание на инициализацию указателя pbd. В ней адрес объекта производного класса (объекта D) присваивается указателю на объект его прямого базового класса (base *). При этом выполняется стандартное преобразование указателей, предусмотренное синтаксисом языка Си++. Обратное образование, т.е. преобразование указателя на объект базового класса в указатель на объект производного класса, невозможно (запрещено синтаксисом). Обращения к функциям классов baseи dir с помощью указателей bpи dp не представляют особого интереса. Вызов pbd->fun() требуется прокомментировать. Указатель pbd имеет тип base*, однако его значение - адрес объекта D класса dir.

Какая же из функций base::fun()или dir::fun() вызывает при обращении pbd->fun()? Результат выполнения программы показывает, что вызывается функция из базового класса. Именно такой вызов предусмотрен синтаксисом языка Си++, т.е. выбор функции (не виртуальной) зависит только от типа указателя, но не от его значения. «Настроив» указатель базового класса на объект производного класса, не удается с помощью этого указателя вызвать функцию из производного класса.

Пусть в этом классе определена компонентная функция void show(). Доступ к функции show() производного класса возможен только с помощью явного указания области видимости:

имя_производного_класса:: show()

либо с использованием имени конкретного объекта:

имя_объекта_производного_класса. show()

В обоих случаях выбор нужной функции выполняется при написании исходного текста программы и не изменяется после компиляции. Такой режим называется ранним или статическим связыванием.

Большую гибкость (особенно при использовании уже готовых библиотек классов) обеспечивает позднее (отложенное), или динамическое связывание, которое предоставляется механизмом виртуальных функций. Любая нестатическая функция базового класса может быть сделана виртуальной, если в ее объявлении использовать спецификатор virtual. Прежде чем объяснить преимущества динамического связывания, приведем пример. Опишем в базовом классе виртуальную функцию и введем два производных класса, где определим функции с такими же прототипами, но без спецификатора virtual.

// виртуальные функции в базовом и производном классах

# inclube <stdio.h>

class base { public: virtual void vfun (int i)

{ printf("\nbase::i =",i); }

};

class dir1: public base { public: void vfun (int i)

{ printf("\ndir1::i =",i); }

};

class dir2: public base { public: void vfun (int i)

{ printf("\nbase::i =",i); }

};

void main (void)

{ base B, *bp = &B;

dir1 D1, *dp1 = &D1;

dir2 D2, *dp2 = &D2;

base *pbd = &D;

bp->vfun (1); // Печатает: base::i = 1

dp1->vfun (2); // Печатает: dir1::i = 2

dp1->vfun (3); // Печатает: dir2::i = 3

bp =&D1; bp->vfun (4); // Печатает: dir1::i = 4

bp =&D2; bp->vfun (5); // Печатает: dir1::i = 5

}

Заметим, что доступ к функциям vfun() организован через указатель bp на базовый класс. Когда он принимает значение адреса объекта базового класса, то вызывается функция из базового класса. Когда указателю присваиваются значения ссылок на объекты производных классов &D1, &D2, выбор соответствующего экземпляра функции определяется именно объектом. Таким образом, интерпретация каждого вызова виртуальной функции через указатель на базовый класс зависит от значения этого указателя, то есть от типа объекта, для которого выполняется вызов. Для невиртуальной функции ее вызов через указатель интерпретируется взависимости от типа указателя.

Виртуальными могут быть не любые функции, а только нестатические компонентные функции какого-либо класса. После того как функция определена как виртуальная, ее повторное определение в производном классе (с тем же самым прототипом) создает в этом классе новую виртуальную функцию, причем спецификатор virtual может не использоваться.

В производном классе нельзя определять функцию с тем же именем и с тем же набором параметров, но с другим типом возвращаемого значения, чем у виртуальной функции базового класса. Это приводит к ошибке на этапе компиляции.

Если в производном классе ввести функцию с тем же именем и типом возвращаемого значения, что и виртуальная функция базового класса, но с другим набором параметров, то эта функция производного класса не будет виртуальной. В этом случае с помощью указателя на базовый класс при любом значении этого указателя выполняется обращение к функции базового класса (несмотря на спецификатор virtualи присутствие в производном классе похожей функции). Рассмотрим это на примере:

// особенности виртуальных функций

# inclube <stdio.h>

class base { public:

virtual void f1 (void){ printf("\nbase::f1"); }

virtual void f2 (void){ printf("\nbase::f2"); }

virtual void f3 (void){ printf("\nbase::f3"); }

};

class dir: public base { public:

void f1 (void){ printf("\ndir::f1"); } // виртуальная

//int f2 (void){ printf("\ndir::f2"); } // ошибка в типе

void f3(int i){printf("\ndir::f3:%d",i);} //невиртуальная

};

void main (void)

{ base B, *bp = &B; dir D, *dp = &D;

bp->f1(); // Печатает: base::f1

bp->f2(); // Печатает: base::f2

bp->f3(); // Печатает: base::f3

dp->f1(); // Печатает: dir::f1

dp->f2(); // Печатает: base::f2

//dp->f3(); // Не печатает - вызов без параметра

dp->f3(3); // Печатает: dir::f3::3

dp = &D;

bp->f1(); // Печатает: dir::f1

bp->f2(); // Печатает: base::f2

bp->f3(); // Печатает: base::f3

bp->f3(3); // Не печатает - лишний параметр

}

Как уже было упомянуто, виртуальной функцией может быть только нестатическая компонентная функция. Виртуальной не может быть глобальная функция. Функция, подменяющая виртуальную, в производном классе может быть описана как со спецификатором virtual, так и без него. В обоих случаях она будет виртуальной, т.е. ее вызов возможен только для конкретного объекта. Виртуальная функция может быть объявлена дружественной (friend) в другом классе.

Механизм виртуального вызова может быть подавлен с помощью явного использования полного квалифицированного имени. Таким образом, при необходимости вызова из производного класса виртуального метода (компонентной функции) базового класса употребляется полное имя. Например,

class base { public:

virtual int f (int j) { return j * j; }

};

class dir: public base { public:

int f (int i){return base::f (i * 2); }

};

Абстрактные классы. Абстрактным классом называется класс, в котором есть хотя бы одна чистая (пустая) виртуальная функция. Чистой виртуальной называется компонентная функция, которая имеет следующее определение:

virtual тип имя_функции (список_формальных_параметров) = 0;

В этой записи конструкция «= 0» называется «чистый спецификатор». Пример описания чистой виртуальной функции:

virtual void fpure (void) = 0;

Чистая виртуальная функция «ничего не делает» и недоступна для вызовов. Ее назначение - служить основой для подменяющих ее функций в производных классах. Исходя из этого становится понятной невозможность создания самостоятельных объектов абстрактного класса. Абстрактный класс может использоваться только в качестве базового для производных классов. При создании объектов такого производного класса в качестве подобъектов создаются объекты базового абстрактного класса. Пример:

class B { protected:

virtual void f (int) = 0;

void s(int);

};

class D: public B {

...

void f (int);

};

class E: public B { void s (int);

};

Здесь B - абстрактный, D - нет, поскольку f - переопределена, а s - наследуется, E - абстрактный, так как s - переопределена, а f - наследуется.

Как всякий класс, абстрактный класс может иметь явно определенный конструктор. Из конструктора возможен вызов методов класса, но любые прямые или опосредованные обращения из конструктора к чистым виртуальным функциям приведут к ошибкам во время выполнения программы.

По сравнению с обычными классами абстрактные классы пользуются «ограниченными правами». Абстрактный класс нельзя употреблять для задания типа параметра функции или в качестве типа возвращаемого функцией значения. Абстрактный класс нельзя использовать при явном приведении типов. В то же время можно определять указатели и ссылки на абстрактные классы. Объект абстрактного класса не может быть формальным параметром функции, однако формальным параметром может быть указатель абстрактного класса. В этом случае появляется возможность передавать в вызываемую функцию в качестве фактического параметра значение указателя на производный объект, заменяя им указатель на абстрактный базовый класс.

6. Классы потоков С++.(4 час.)

Заголовочные файлы. Предопределенные объекты и потоки. Операции помещения и извлечения. Форматирование. Флаги форматирования. Манипуляторы. Ошибки потоков. Файловый ввод-вывод с применением потоков С++. Конструкторы файловых потоков. Открытие файлов в разных режимах. Ввод-вывод в файлы.

Заголовочные файлы. В Си имеется общая технология, которая касается как организации модульных программ, так и библиотек. Любой модуль, который претендует на дальнейшее использование через обращение к собственным внешним переменным и вызов собственных внешних функций, должен иметь некоторое описание своего интерфейса. Оно заключается в составлении заголовочного файла (файла с расширением - ".h"), который используется другими модулями. Заголовочный файл должен включать в себя:

· определение используемых типов данных в формальных параметрах и результатах функций с использованием оператора typedef;

· объявления внешних переменных и функций модуля, к которым возможно обращение.

При помощи директивы #include текст заголовочного файла включается в текст транслируемого модуля, таким образом транслятор получает необходимые определения типов и объявления переменных и функций модуля, к которым будут производиться обращения. Директива #include возможна в двух вариантах:

#include <alloc.h> - заголовочный файл из системного каталога

#include "myhead.h" - заголовочный файл из текущего (явно указанного) каталога

Процесс подготовки библиотеки включает в себя следующие шаги

· создание заголовочного файла, содержащего определения используемых типов данных и объявления внешних функций и переменных библиотеки;

· создание модуля, включающего определение функций и переменных библиотеки и трансляция его в объектный модуль;

· включение объектного модуля в библиотеку.

Предопределенные объекты и потоки. В стандартной библиотеке ввода/вывода стандартного Си (заголовочный файл библиотеки - <stdio.h>) имеются внешние переменные-указатели на дескрипторы файлов - стандартных устройств ввода-вывода.

extern FILE *stdin, *stdout, *stderr, *stdaux, *stdprn;

стандартный ввод

стандартный вывод

регистрация ошибок

дополнительное устройство

устройство печати

Эти файлы открываются библиотекой автоматически перед выполнением функции main и по умолчанию назначаются на терминал (stdin - клавиатура, stdout, stderr - экран), последовательный порт (stdaux) и принтер (stdprn). stdin и stdout могут быть переназначены в командой строке запуска программы на любые другие файлы

>test.exe <a.dat >c:\xxx\b.dat

файл stdout

файл stdin

В Си++ существуют классы потоков ввода-вывода, которые являются объектно-ориентированным эквивалентом (stream.h) стандартной библиотеки ввода-вывода (stdio.h):

ios базовый потоковый класс

streambuf буферизация потоков

istream потоки ввода

ostream потоки вывода

iostream двунаправленные потоки

iostream_withassign поток с переопределенной операцией присваивания

istrstream строковые потоки ввода

ostrstream строковые потоки вывода

strstream двунаправленные строковые потоки

ifstream файловые потоки ввода

ofstream файловые потоки вывода

fstream двунаправленные файловые потоки

Стандартные потоки (istream,ostream,iostream) служат для работы с терминалом.

Строковые потоки (istrstream, ostrstream, strstream) служат для ввода-вывода из строковых буферов, размещенных в памяти.

Файловые потоки (ifstream, ofstream, fstream) служат для работы с файлами.

Следующие объекты-потоки заранее определены и открыты в программе перед вызовом функции main:

extern istream cin; // Стандартный поток ввода с клавиатуры

extern ostream cout; // Стандартный поток вывода на экран

extern ostream cerr; // Стандартный поток вывода сообщений об ошибках (экран)

extern ostream cerr;// Стандартный буферизованный поток вывода сообщений об ошибках (экран).

Операции помещения и извлечения. Для начала рассмотрим пример:

#include <stream.h>

main()

{

cout << "Hello, world\n";

}

Строка #include <stream.h> сообщает компилятору, чтобы он включил стандартные возможности потока ввода и вывода, находящиеся в файле stream.h. Без этих описаний выражение cout << "Hello, world\n" не имело бы смысла. Операция << ("поместить в") пишет свой первый аргумент во второй (в данном случае, строку "Hello, world\n" в стандартный поток вывода cout). Программирующим на C << известно как операция сдвига влево для целых. Такое использование << не утеряно, просто в дальнейшем << было определено для случая, когда его левый операнд является потоком вывода.

Ввод производится с помощью операции >> ("извлечь из") над стандартным потоком ввода cin. Описания cin и >>, конечно же, находятся в <stream.h>. Операцию вывода << можно применять к ее собственному результату, так что несколько команд вывода можно записать одним оператором:

cout << inch << " in = " << inch*2.54 << " cm\n";

Операция вывода используется, чтобы избежать той многословности, которую дало бы использование функции вывода. Но почему <<? Возможности изобрести новый лексический символ нет. Операция присваивания была кандидатом одновременно и на ввод, и на

вывод, но оказывается, большинство людей предпочитают, чтобы операция ввода отличалась от операции вывода. Кроме того, = не в ту сторону связывается (ассоциируется), то есть cout=a=b означает cout=(a=b).

Делались попытки использовать операции < и >, но значения "меньше" и "больше" настолько прочно вросли в сознание людей, что новые операции ввода/вывода во всех реальных случаях оказались нечитаемыми. Помимо этого, "<" находится на большинстве клавиатур как раз на ",", и у людей получаются операторы вроде такого:

cout < x, y, z;

Для таких операторов непросто выдавать хорошие сообщения об ошибках.

Операции << и >> к такого рода проблемам не приводят, они асимметричны в том смысле, что их можно проассоциировать с "в" и "из", а приоритет << достаточно низок, чтобы можно было не использовать скобки для арифметических выражений в роли операндов.

Например:

cout << "a*b+c=" << a*b+c << "\n";

Естественно, при написании выражений, которые содержат операции с более низкими приоритетами, скобки использовать надо. Например:

cout << "a^b|c=" << (a^b|c) << "\n";

Операцию левого сдвига тоже можно применять в операторе вывода:

cout << "a<<b=" << (a<<b) << "\n";

В С++ нет выражений с символьными значениями, в частности, '\n' является целым (со значением 10, если используется набор символов ASCII), поэтому

cout << "x = " << x << '\n';

напечатает число 10, а не ожидаемый символ новой строки. Эту и аналогичные проблемы можно сгладить, определив несколько макросов (в которых используются стандартные имена символов ASCII):

#define sp << " "

#define ht << "\t"

#define nl << "\n"

Теперь предыдущий пример запишется в виде:

cout << "x = " << x nl;

Для печати символов предоставляются функции ostream::put(char) и chr(int). Рассмотрим примеры:

cout << x << " " << y << " " << z << "\n";

cout << "x = " << x << ", y = " << y << "\n";

Люди находят их трудно читаемыми из-за большого числа кавычек и того, что операция вывода внешне выглядит слишком непривычно. Здесь могут помочь приведенные выше макросы и несколько отступов:

cout << x sp << y sp << z nl;

cout << "x = " << x

<< ", y = " << y nl;

Форматирование. Флаги форматирования. Пока << применялась только для неформатированного вывода, и в реальных программах она именно для этого главным образом и применяется. В то же время << легко справляется с тремя стандартными типами данных: char, int, float.В резудьтате переопределения операция помещения в поток определяет тип посланных данных и сама выбирает подходящий формат. То же самое происходит и с операцией извлечения из потока >>, которая вводит данные. Сравним пример на Си:

scanf("%d%f%c", &int_data, &float_data, &char_data);

с его эквивалентом на Си++:

cin >> int_data >> float_data >> char_data;

В Си++ не нужно заботиться о том, чтобы ставить перед переменными знак адреса &. Операция извлечения из потока сама вычисляет адреса переменных, определяет их типы и формат ввода.

Помимо этого существует также несколько форматирующих функций, создающих представление своего параметра в виде строки, которая используется для вывода. Их второй (необязательный) параметр указывает, сколько символьных позиций должно использоваться.

char* oct(long, int=0); // восьмеричное представление

char* dec(long, int=0); // десятичное представление

char* hex(long, int=0); // шестнадцатеричное представление

char* chr(int, int=0); // символ

char* str(char*, int=0); // строка

Если не задано поле нулевой длины, то будет производиться усечение или дополнение; иначе будет использоваться столько символов (ровно), сколько нужно. Например:

cout << "dec(" << x

<< ") = oct(" << oct(x,6)

<< ") = hex(" << hex(x,4)

<< ")";

Если x==15, то в результате получится:

dec(15) = oct(17) = hex(f);

Можно также использовать строку в общем формате:

char* form(char* format...);

cout<<form() эквивалентно применению стандартной функции вывода языка C printf()*. form() возвращает строку, получаемую в результате преобразования и форматирования ее параметров, которые стоят после первого управляющего параметра - строки формата format. Строка формата состоит из объектов двух типов: обычных символов, которые просто копируются в поток вывода, и спецификаций преобразования, каждая из которых влечет преобразование и печать следующего из параметров. Каждая спецификация преобразования начинается с символа %. Например:

cout<<form("there were %d members present",no_of_members);

Здесь %d указывает, что no_of_members должно рассматриваться как int и печататься в виде соответствующей последовательности десятичных цифр. Если no_of_members==127, вывод будет такой:

there were 127 members present

Множество спецификаций преобразования довольно велико и обеспечивает высокую степень гибкости. После % может стоять:

· необязательный знак минус, который задает выравнивание преобразованного значения влево в указанном поле;

· d необязательная строка цифр, задающая ширину поля. Если преобразованное значение имеет меньше цифр, чем ширина поля, оно будет дополняться пробелами слева (или справа, если был задан индикатор выравнивания влево) до заполнения всей ширины поля; если ширина поля начинается с нуля, то вместо дополнения пробелами будет делаться дополнение нулями;

· необязательная точка, для отделения ширины поля от следующей строки цифр;

· d необязательная строка цифр, специфицирующая точность, которая задает число цифр после десятичной точки для преобразований e и f или печатаемых символов для строки;

· * в ширине поля или точности вместо строки цифр может стоять *. В этом случае ширина поля и точность задается целым параметром;

· h необязательный символ h; указывает на то, что идущие за ним d, o, x или y соответствуют параметру короткое целое;

· l необязательный символ l; указывает на то, что идущие за ним d, o, x или y соответствуют параметру длинное целое;

· % указывает, что должен быть напечатан символ %, никакие параметры при этом не затрагиваются;

· c символ, указывающий, какой тип преобразования должен применяться. Символы преобразования и их значения таковы:

§ d целый параметр преобразуется в десятичную запись;

§ o целый параметр преобразуется в восьмеричную запись;

§ x целый параметр преобразуется в шестнадцатиричную запись;

§ f параметр float или double преобразуется в десятичную запись вида [-]ddd.ddd, где число, задаваемое цифрами d после десятичной точки, эквивалентно спецификации точности для параметра. Если точность опущена, дается шесть цифр;

§ если точность явно задана как 0, то не печатается десятичная точка и не печатается ни одной цифры;

§ e параметр float или double преобразуется в десятичную запись вида [-]d.ddde+dd, где перед десятичной точкой стоит одна цифра, а число, задаваемое цифрами после десятичной точки, эквивалентно спецификации точности для параметра;

§ когда точность опущена, выдается шесть цифр;

§ g параметр float или double печатается в том из видов d,f или e, который обеспечивает полную точность при минимальной затрате места;

§ c печатается символьный параметр, пустые символы игнорируются;

§ s параметр воспринимается как строка (указатель на символ), и печатаются символы из строки до пустого символа или до тех пор, пока не будет достигнуто число символов, указанное спецификацией точности; но если точность равна нулю, печатаются все символы до пустого;

§ u беззнаковый целый параметр преобразуется в десятичную запись.

Несуществующая или недостаточная ширина поля никогда не приводит к обрезанию поля; дополнение поля записи имеет место только в том случае, если указанная ширина поля превышает фактическую ширину.

Вот более сложный пример:

char* src_file_name;

int line;

char* line_format = "\n#line %d \"%s\"\n";

//...

cout << "int a;\n";

cout << form(line_format,line,src_file_name);

cout << "int b;\n";

который печатает

int a;

#line 13 "С++/main.c"

int b;

Применение form() небезопасно в смысле того, что не выполняется проверка типа. Вот, например, хорошо хорошо известный способ получить непредсказуемый вывод и/или дамп (core dump):

char x;

//...

cout<<form("bad input char: %s",x);

Правда, она дает большую гибкость в том виде, который хорошо знаком программистам на C. Потоковый вывод можно смешивать с выводом в стиле printf().

В настоящее время нет полностью удовлетворительных средств, обеспечивающих форматированный вывод типов, определяемых пользователем. Вполне осуществимый, но не идеальный подход состоит в том, чтобы снабжать определяемый пользователем тип функциями, которые порождают соответствующее строковое представление объекта, для которого они вызываются, аналогично форматирующим функциям oct(), hex() и т.д. Например:

class complex {

float re,im;

public:

//...

char* string(char* format)

{ return form(format,re,im); }

};

//...

cout << z.string("(%.3f,%.3f)");

Память для хранения строк, которые возвращают form(), hex() и т.п., берется из одного статически размещаемого циклического буфера, поэтому не имеет смысла сохранять указатель, возвращаемый любой из этих функций, для дальнейшего использования. Указываемые символы будут меняться.

Манипуляторы. Манипуляторы - функции потока, которые можно включать в операции помещения и извлечения в потоки (<<, >>). Имеются следующие манипуляторы:

endl // Помещение в выходной поток символа конца строки '\n' и вызов функции flush

ends // Помещение в выходной поток символа '\0'

flush // Вызов функции вывода буферизованных данных в выходной поток

dec // Установка основания 10 системы счисления

hex // Установка основания 16 системы счисления

oct // Установка основания 8 системы счисления

ws // Установка игнорирования при вводе пробелов

setbase(int) // Установка основания системы счисления (0 - 10 - по умолчанию, также 8,10,16)

resetiosflasg(long) // Сброс флагов форматирования по маске

setiosflags(long) // Установка флагов форматирования по маске

setfill(int) // Установка заполняющего символа

setprecision(int) // Установка точности вывода вещественных чисел

setw(int) // Установка ширины поля ввода-вывода

Пример вызова манипулятора:

cout << 15 << hex << 15 << setbase(8) << 15;

Ошибки потоков. Каждый поток (istream или ostream) имеет ассоциированное с ним состояние, и обработка ошибок и нестандартных условий осуществляется с помощью соответствующей установки и проверки этого состояния.

Поток может находиться в одном из следующих состояний:

enum stream_state { _good, _eof, _fail, _bad };

Если состояние _good или _eof, значит последняя операция ввода прошла успешно. Если состояние _good, то следующая операция ввода может пройти успешно, в противном случае она закончится неудачей. Другими словами, применение операции ввода к потоку, который не находится в состоянии _good, является пустой операцией. Если делается попытка читать в переменную v, и операция оканчивается неудачей, значение v должно остаться неизменным (оно будет неизменным, если v имеет один из тех типов, которые обрабатываются функциями членами istream или ostream). Отличие между состояниями _fail и _bad очень незначительно и представляет интерес только для разработчиков операций ввода. В состоянии _fail предполагается, что поток не испорчен и никакие символы не потеряны. В состоянии _bad может быть все что угодно.

Состояние потока можно проверять например так:

switch (cin.rdstate()) {

case _good: // последняя операция над cin прошла успешно

break;

case _eof: // конец файла

break;

case _fail: // некоего рода ошибка форматирования, возможно, не слишком плохая

break;

case _bad: // возможно, символы cin потеряны

break;

}

Для любой переменной z типа, для которого определены операции << и >>, копирующий цикл можно написать так:

while (cin>>z) cout << z << "\n";

Например, если z - вектор символов, этот цикл будет брать стандартный ввод и помещать его в стандартный вывод по одному слову (то есть, последовательности символов без пробела) на строку.

Когда в качестве условия используется поток, происходит проверка состояния потока, и эта проверка проходит успешно (то есть, значение условия не ноль) только если состояние _good. В частности, в предыдущем цикле проверялось состояние istream, которое возвращает cin>>z. Чтобы обнаружить, почему цикл или проверка закончились неудачно, можно исследовать состояние.

Делать проверку на наличие ошибок после каждого ввода или вывода действительно не очень удобно, и обычно источником ошибок служит программист, не сделавший этого в том месте, где это существенно. Например, операции вывода обычно не проверяются, но они могут случайно не сработать. Парадигма потока ввода/вывода построена так, чтобы когда в С++ появится (если это произойдет) механизм обработки исключительных ситуаций (как средство языка или как стандартная библиотека), его будет легко применить для упрощения и стандартизации обработки ошибок в потоках ввода/вывода.

Файловый ввод-вывод с применением потоков С++. Конструкторы файловых потоков. Потоки обычно связаны с файлами. Библиотека потоков создает стандартный поток ввода cin, стандартный поток вывода cout и стандартный поток ошибок cerr. Программист может открывать другие файлы и создавать для них потоки.

Для инициализации потоков вывода ostream имеет конструкторы:

class ostream {

//...

ostream(streambuf* s); // связывает с буфером потока

ostream(int fd); // связывание для файла

ostream(int size, char* p); // связывает с вектором

};

Главная работа этих конструкторов - связывать с потоком буфер. streambuf - класс, управляющий буферами, как и класс filebuf, управляющий streambuf для файла. Класс filebuf является производным от класса streambuf.

Естественно, тип istream, так же как и ostream, снабжен конструктором:

class istream {

//...

istream(streambuf* s, int sk =1, ostream* t =0);

istream(int size, char* p, int sk =1);

istream(int fd, int sk =1, ostream* t =0);

};

Параметр sk задает, должны пропускаться пропуски или нет. Параметр t (необязательный) задает указатель на ostream, к которому прикреплен istream. Например, cin прикреплен к cout; это значит, что перед тем, как попытаться читать символы из своего файла, cin выполняет cout.flush(); - пишет буфер вывода

С помощью функции istream::tie() можно прикрепить (или открепить, с помощью tie(0)) любой ostream к любому istream.

Например:

int y_or_n(ostream& to, istream& from)

/*"to", получает отклик из "from" */

{

ostream* old = from.tie(&to);

for (;;) {

cout << "наберите Y или N: ";

char ch = 0;

if (!cin.get(ch)) return 0;

if (ch!= '\n') { // пропускает остаток строки

char ch2 = 0;

while (cin.get(ch2) && ch2!= '\n');

}

switch (ch) {

case 'Y':

case 'y':

case '\n':

from.tie(old); // восстанавливает старый tie

return 1;

case 'N':

case 'n':

from.tie(old); // восстанавливает старый tie

return 0;

default:

cout << "извините, попробуйте еще раз: ";

}

}

}

Когда используется буферизованный ввод (как это происходит по умолчанию), пользователь не может набрав только одну букву ожидать отклика. Система ждет появления символа новой строки. y_or_n() смотрит на первый символ строки, а остальные игнорирует.

Символ можно вернуть в поток с помощью функции istream::putback(char). Это позволяет программе "заглядыватьвперед" в поток ввода.

Деструктор для ostream сбрасывает буфер с помощью открытого члена функции ostream::flush():

ostream::~ostream()

{

flush(); // сброс

}

Сбросить буфер можно также и явно. Например:

cout.flush();

Открытие файлов в разных режимах. Точные детали того, как открываются и закрываются файлы, различаются в разных операционных системах. Поскольку после включения <stream.h> становятся доступны cin, cout и cerr, во многих (если не во всех) программах не нужно держать код для открытия файлов. Вот, однако, программа, которая открывает два файла, заданные как параметры командной строки, и копирует первый во второй:

#include <stream.h>

void error(char* s, char* s2)

{

cerr << s << " " << s2 << "\n";

exit(1);

}

main(int argc, char* argv[])

{

if (argc!= 3) error("неверное число параметров","");

filebuf f1;

if (f1.open(argv[1],input) == 0)

error("не могу открыть входной файл",argv[1]);

istream from(&f1);

filebuf f2;

if (f2.open(argv[2],output) == 0)

error("не могу создать выходной файл",argv[2]);

ostream to(&f2);

char ch;

while (from.get(ch)) to.put(ch);

if (!from.eof()!! to.bad())

error("случилось нечто странное","");

}

Последовательность действий при создании ostream для именованного файла та же, что используется для стандартных потоков:

(1) сначала создается буфер (здесь это делается посредством описания filebuf);

(2) затем к нему подсоединяется файл (здесь это делается посредством открытия файла с помощью функции filebuf::open());

(3) создается сам ostream с filebuf в качестве параметра.

Потоки ввода обрабатываются аналогично.

Файл может открываться в одной из двух мод:

enum open_mode { input, output };

Действие filebuf::open() возвращает 0, если не может открыть файл в соответствие с требованием. Если пользователь пытается открыть файл, которого не существует для output, он будет создан.

Используя классы ifstream и ofstream - производные от istream и ostreasm, описанные в fstream.h, можно открывать файловые потоки в разных модах с помощью флагов конструктора потока:

ofstream object (filename, flag)

где flag может иметь следующие значения:

ios::app запись в конец существующего файла

ios::ate после открытия файла перейти в его конец

ios::binary открыть файл в двоичном режиме (по умолчанию - текстовый)

ios::in открыть для чтения

ios::nocreate сообщать о невозможности открытия, если файл не существует

ios::noreplace сообщать о невозможности открытия, если файл существует

ios::out открыть для вывода

ios::trunc если файл существует, стереть содержимое.

При необходимости изменить способ открытия или применения файла можно при создании файлового потока использовать два флага или более флага: ios::app|ios::noreplace

Для открытия файла одновременно на чтение и запись можно использовать объекты класса fstream:

fstream object(filename, ios::in|ios::app);

Перед завершением программа проверяет, находятся ли потоки в приемлемом состоянии. При завершении программы открытые файлы неявно закрываются. Для явного закрытия объектов файловых потоков применяется метод close():

object.close();

Ввод-вывод в файлы. Для ввода/вывода в потоковые объекты можно применять методы put(), get(), для связывания объекта с различными файлами служат методы open(), close(), для позиционирования в файле имеются методы seekg(), seekp(), tellp(). При этом seekg() назначает или возвращает текущую позицию указателя чтения, а seekp() назначает или возвращает текущую позицию указателя записи. Обе функции могут иметь один или два аргумента. При вызове с одним аргументом функции перемещают указатель на заданное место, а при вызове с двумя аргументами вычисляется относительная позиция от начала файла (ios::beg), текущей позиции (ios::cur) или от конца файла (ios::end). Текущщая позиция определяется методом tellp().

Для объектов файловых потоков контроль состояния также производится с помощью методов, манипулирующих флагами ошибок:

bad() возвращает ненулевое значение, если обнаружена ошибка

clear() сбрасывает сообщения об ошибках

eof() возвращает ненулевое значение, если обнаружен конец файла

fail() возвращает ненулевое значение, если операция завершилась неудачно

good() возвращает ненулевое значение, если флаги ошибок не выставлены

rdstate() возвращает текущее состояние флагов ошибок.

Если флаги показывают наличие ошибки, все попытки поместить в поток новые объекты будут игнорироваться, то есть состояние потока не изменится.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: