Спецификаторы доступа базовых классов



Спецификатор доступа базового класса позволяет при наследовании понижать уровень доступа (или оставлять неизменным) для всех наследуемых полей. Данный механизм действует только в языке С++. Данный механизм не позволяет повышать уровень доступа.

 

  private protected public
private
protected private protected protected
public private protected public

 

Порядок вызова конструкторов

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

Например,

 

class First {...};

class Second: public First {...};

class Third: public Second {...};

При создании экземпляра класса Third конструкторы вызываются в следующем порядке:

First::First()

Second::Second()

Third::Third()

 

Порядок вызова деструктора

Деструкторы для производных классов вызываются в порядке обратном вызову конструкторов. Таким образом, порядок вызовов деструкторов, сгенерированных для разрушения экземпляра класса Third, будет следующим:

 

Third::~Third()

Second::~Second()

First::~First()

Виртуальные методы и ранние и поздние связывания

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

Способность объектно-ориентированных языков автоматически определять тип объекта на этапе выполнения программы называется RTTI (run-time type identification – идентификация во время выполнения).

Виртуальные функции

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

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

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

 

Чистая виртуальная функция

Когда виртуальная функция не переопределена в производном классе, то при вызове ее в объекте производного класса вызывается версия из базового класса. Однако во многих случаях невоз­можно ввести содержательное определение виртуальной функции в базовом классе. Например, при объявлении базового класса figure в предыдущем примере определение функции show_area() не несет никакого содержания. Она не вычисляет и не выводит на экран площадь объекта какого- либо типа. Имеется два способа действий в подобной ситуации. Первый, подобно предыдущему примеру, заключается в выводе какого-нибудь предупреждающего сообщения. Хотя такой подход полезен в некоторых ситуациях, он не является универсальным. Могут быть такие виртуальные функции, которые обязательно должны быть определены в производных классах, без чего эти производные классы не будут иметь никакого значения. Например, класс triangle бесполезен, если не определена функция show_агеа(). В таких случаях необходим метод, гарантирующий, что производные классы действительно определят все необходимые функции. Язык С++ предлагает в качестве решения этой проблемы чистые  виртуальные функции.

Чистая виртуальная функция (pure virtual function) является функцией, которая объявляется в базовом классе, но не имеет в нем определения. Поскольку она не имеет определения, то есть

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

 

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

 

Здесь тип обозначает тип возвращаемого значения, а имя_функции является именем функции. На­пример, следующая версия функции show_area() класса figure является чисто виртуальной функцией.

 

class figure {

double х, у;

public:

void set_dim(double i, double j=0) {

x = i;

y = j;

}

virtual void show_area() = 0; // чисто виртуальная

};

 

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

snow_area() из класса circle, то будет выдано сообщение об ошибке:

 

/* Данная программа не компилируется, поскольку класс circle не переопределил show_агеа() */

#include <iostream.h>

class figure {

protected:

double x, y;

public:

void set_dim(double i, double j) {

x = i;

у = j;

}

virtual void show_area() = 0; // pure

};

class triangle: public figure {

public:

void show_area() {

cout << "Triangle with height ";

cout << x << " and base " << y;

cout << " has an area of ";

cout << x * 0.5 * у << ". \ n";

}

};

class square: public figure {

public:

void show_area() {

cout << "Square with dimensions ";

cout << x << "x" << y;

cout << " has an area of ";

cout << x * у << " . \n";

}

};

class circle: public figure {

// определение show_area() отсутствует и потому выдается ошибка

};

int main ( )

{

figure *р; // создание указателя базового типа

circle с; // попытка создания объекта типа circle - ОШИБКА

triangle t; // создание объектов порожденных типов

square s;

p = &t;

p->set_dim(10.0, 5.0);

p->show_area ();

p = &s;

p->set_dim(10.0, 5.0);

p->show_area();

return 0;

}

 

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

 

Множественное наследование

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

 

class имя_порожденного_класса: список базовых классов
{
...
};

 

В следующем примере класс Z наследует оба класса X и Y:

 

#include <iostream.h>

class X {

protected:

int a;

public:

void make_a(int i) { a = i; }

};

class Y {

protected:

int b;

public:

void make_b(int i) { b = i; }

};

// Z наследует как от X, так и от Y

class Z: public X, public Y {

public:

int make_ab() { return a*b; }

};

int main()

{

Z i;

i.make_a(10);

i.make_b(12);

cout << i .make_ab();

return 0;

}

Поскольку класс Z наследует оба класса X и Y, то он имеет доступ к публичным и защищенным членам обоих классов X и Y.

В предыдущем примере ни один из классов не содержал конструкторов. Однако ситуация ста­новится более сложной, когда базовый класс содержит конструктор. Например, изменим преды­дущий пример таким образом, чтобы классы X, Y и Z содержали конструкторы:

 

 

#include <iostream.h>

class X {

protected:

int a;

public:

X() {

a = 10;

cout << "Initializing X\n";

}

};

class Y {

protected:

int b;

public:

Y() {

cout << "Initializing Y\n";

b = 20;

}

};

// Z наследует как от X, так и от Y

class Z: public X, public Y {

public:

Z() { cout << "Initializing Z\n"; }

int make_ab() { return a*b; }

};

int main()

{

Z i;

cout << i.make_ab();

return 0;

}

Программа выдаст на экран следующий результат:

 

Initializing X

Initializing Y

Initializing Z

200

 

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

В общем случае, когда используется список базовых классов, их конструкторы вызываются сле­ва направо. Деструкторы вызываются в обратном порядке — справа налево.

Перегрузка операций

Перегрузка операций позволяет определить действия для объектов в выражениях.

<имя_класса> operator op(<имя_класса > a, < имя_класса > b) – общий вид перегруженной операции op.

 

В С++ существует 40 операций, которые можно перегрузить.

Нельзя перегрузить следующие операции: «.», «.*»,«?:», «::», «sizeof», «#», «##».

При создании описания класса существует две возможности определения операций:

1) определение операции как функции класса;

2) определение операции как дружественной функции.

Если бинарная перегруженная операция реализована как функция

класса, то ее первым операндом является объект, которому принадлежит сообщение, следовательно, такой функции передается только второй член операции, следовательно, у нее 1 параметр.

Если унарная перегруженная операция реализована как функция класса, то у нее нет параметров.

Если перегруженная операция реализована как дружественная функция, то ей передается 1 или 2 параметра.

С++ «не понимает» семантики перегруженного оператора. С++ не может выводить сложные операторы из простых. Нельзя изменять синтаксис перегруженных операций. Нельзя изобретать новые операторы, а можно использовать только 40.

Пример (работа с комплексными числами):

Вариант 1.

//f1.h

class Complex

{ float real, imag; public:

 

Complex(float aReal, float aImag): real(aReal), imag(aImag) {}; Complex() {real=imag=0.0;}

Complex(float aReal): real(aReal), imag(0.0) {};

float GetReal() {return real;}

float GetImag() {return imag;}

friend Complex operator+(Complex &c1, Complex &c2);

friend Complex operator-(Complex &c1, Complex &c2);

};

Complex operator+(Complex &c1, Complex &c2)

{ return Complex(c1.real+c2.real, c1.imag+c2.imag);}

Complex operator-(Complex &c1, Complex &c2)

{ return Complex(c1.real-c2.real, c1.imag-c2.imag);} //f1.cpp

#include <iostream.h>

#include "f1.h"

void main()

{

Complex a(1.0,2.0), b(3.0,4.0);

a=a+b;

cout<<a.GetReal()<<endl<<a.GetImag()<<endl; }

Вариант 2.

//f1.h

class Complex

{ float real, imag; public:

Complex(float aReal, float aImag): real(aReal), imag(aImag) {}; Complex() {real=imag=0.0;}

Complex(float aReal): real(aReal), imag(0.0) {};

float GetReal() {return real;}

float GetImag() {return imag;}

void operator+(Complex &c1);

void operator-(Complex &c1);

};

void Complex::operator +(Complex &c1) { real+=c1.real;

imag+=c1.imag; }

void Complex::operator -(Complex &c1) { real-=c1.real;

imag-=c1.imag;

}

//f1.cpp

#include <iostream.h> #include "f1.h"

void main()

{

Complex a(1.0,2.0), b(3.0,4.0);

a+b;

cout<<a.GetReal()<<endl<<a.GetImag()<<endl; }

 

Нельзя смешивать два типа объявлений.

 

ПОТОКИ

В языке С++ нет средств для ввода-вывода. Эти средства можно просто создать на самом языке.

В языке C++ производится ввод-вывод потоков (streams) байтов. Поток представляет собой последовательность байтов. В операциях ввода байты пересылаются от устройства (например, от клавиатуры, дисковода или соединения сети) в оперативную память. При выводе байты пересылаются из оперативной памяти на устройства.

Чтение данных из потока называется извлечением, вывод в поток – помещением, или включением.

Основная задача потоковых средств ввода-вывода - это процесс преобразования объектов определенного типа в последовательность символов в битовом их представлении и наоборот. Указанная схема ввода- вывода является основной. Многие схемы двоичного ввода-вывода можно свести к ней.

Обмен с потоком для увеличения скорости передачи данных производится через специальную область оперативной памяти – через буфер. Фактическая передача данных выполняется при выводе после заполнения буфера, а при вводе – если буфер исчерпан.

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

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

Стандартные потоки предназначены для передачи данных от клавиатуры и на экран дисплея, файловые потоки – для обмена информацией с файлами на внешних носителях данных, строковые потоки – для работы с массивами символов в оперативной памяти.

Язык C++ предоставляет возможности для ввода-вывода как на

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

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

 

ЗАГОЛОВОЧНЫЕ ФАЙЛЫ БИБЛИОТЕКИ IOSTREAM

 

Заголовочный файл <ios.h> содержит описание базового класса потоков ввода-вывода.

Заголовочный файл <iosfwd.h> содержит предварительные объявления средств ввода-вывода.

Библиотека <iostream.h> потокового ввода-вывода реализует строгий типовой и эффективный способ символьного ввода и вывода целых, вещественных чисел и символьных строк. Также библиотека является базой для расширения, рассчитанного на работу с пользовательскими типами данных.

Заголовочный файл <iostream.h> определяет интерфейс потоковой библиотеки. В ранних версиях потоковой библиотеки использовался файл

<stream.h>. <iostream.h> определяет полный набор средств. <stream.h> определяет подмножество, которое совместимо с ранними потоковыми библиотеками.

Заголовочный файл <iostream.h> включает объекты cin, cout, cerr и clog. cin соответствует стандартному потоку ввода, cout – стандартному потоку вывода, cerr – небуферизованному стандартному потоку вывода сообщений об ошибках, clog – буферизованному стандартному потоку вывода сообщений об ошибках. Для этих объектов предусмотрены возможности для форматированного и для неформатированного ввода- вывода.

Заголовочный файл <istream.h> содержит описание шаблона потока ввода.

Заголовочный файл <ostream.h> содержит описание шаблона потока вывода.

Заголовочный файл <iomanip.h> содержит информацию, необходимую для обработки форматированного ввода-вывода при помощи параметризованных манипуляторов потока.

Заголовочный файл <fstream.h> содержит информацию для проведения операций с файлами.

Заголовочный файл <sstream.h> содержит описание потоков ввода- вывода в строки.

Заголовочный файл <streamига.h> содержит описание функций буферизации ввода-вывода.

Также есть заголовочные файлы <amstream.h>, <austream.h>,

<ddstream.h> и <mmstream.h>, который содержат информацию, необходимую для управления специализированными устройствами, предназначенными, например, для ввода-вывода аудио- и видеоданных.

 

 

Классы и объекты потоков ввода-вывода

 

Библиотека iostream содержит много классов для обработки операций ввода-вывода. Например, класс istream поддерживает операции по вводу потоков, класс ostream поддерживает операции по выводу объектов, класс iostream поддерживает оба типа операций: и ввод, и вывод потоков.

Класс istream и класс ostream являются прямыми потомками класса ios. Класс iostream является производным классом множественного наследования классов istream и ostream. Иерархию наследования можно представить так:

 

Операции «поместить в поток» и «взять из потока» – это перегруженные операции сдвига влево (<<) и вправо (>>). Эти операции применяются к объектам стандартных потоков cin, cout, cerr и clog.

При обработке файлов C++ используются следующие классы:

– класс ifstream, который выполняет операции ввода из файлов;

– класс ofstream, который выполняет операции вывода в файлы;

– класс fstream, который выполняет операции ввода-вывода файлов.

Класс ifstream наследует класс istream, класс ofstream наследует класс ostream, а класс fstream – класс iostream. Иерархия классов потоков ввода- вывода с ключевыми классами обработки файлов выглядит так:

 

 

 

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

 

cerr << "x = " << x << endl;

 

Здесь cerr обозначает стандартный поток ошибок. Так, если х типа int со значением 123, то приведенный оператор выдаст

x = 123

 

Вывод встроенных типов

Для управления выводом встроенных типов определяется класс ostream с операцией << (вывести):

 

class ostream : public virtual ios

{

// ... public:

ostream& operator<<(const char*); //строки ostream& operator<<(char);

ostream& operator<<(short i)

{ return *this << int(i); } ostream& operator<<(int); ostream& operator<<(long); ostream& operator<<(double);

ostream& operator<<(const void*); // указатели

// ...

};

 

Функция operator<< возвращает ссылку на класс ostream, из которого она вызывалась, чтобы к ней можно было применить еще раз operator<<. Так, если х типа int, то

 

cerr << "x = " <<x

 

понимается как

 

(cerr.operator<<("x = ")).operator<<(x);

 

Это означает, что если несколько объектов выводятся с помощью одного оператора вывода, то они будут выдаваться в естественном порядке: слева - направо.

Функция ostream::operator<<(const void*) напечатает значение указателя в такой записи, которая более подходит для используемой системы адресации. Программа

 

main()

{

int i = 0;

int* p = new int(1);

cout << "local " << &i<< ", free store " << p << '\n';

}

 

выдаст, например,

 

local 0x0066FDF4, free store 0x00790DA0

 

Ввод встроенных типов

 

 

Класс istream определяется :

 

class istream : public virtual ios

{

//... public:

istream& operator>>(char*); // строка istream& operator>>(char&); // символ istream& operator>>(short&);

istream& operator>>(int&); istream& operator>>(long&); istream& operator>>(float&); istream& operator>>(double&);

//...

};

Функции ввода operator>> определяются :

 

istream& istream::operator>>(T& tvar)

{

// пропускаем обобщенные пробелы

// каким-то образом читаем T в`tvar' return *this;

}

 

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

 

class istream : public virtual ios {

//...

istream& get(char& c);       // символ int get();

istream& get(char* p, int n, char ='n'); // строка inline istream& getline( char *,int,char ='\n');

};

 

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

Функция istream::get(char&) вводит один символ в свой параметр.

Поэтому программу посимвольного копирования можно написать так:

 

 int main()

{

char c;

while (cin.get(c)) cout.put(c);

}

 

Функция с тремя параметрами istream::get() вводит в символьный вектор не менее n символов, начиная с адреса p. При всяком обращении к get() все символы, помещенные в буфер (если они были), завершаются 0, поэтому если второй параметр равен n, то введено не более n-1 символов. Третий параметр определяет символ, завершающий ввод. Типичное использование функции get() с тремя параметрами сводится к чтению строки в буфер заданного размера для ее дальнейшего разбора, например так:

 

void f()

{

char buf[100];

cin >> buf;    // подозрительно cin.get(buf,100,'\n'); // надежно

//...

}

 

Операция cin>>buf может работать некорректно, поскольку строка из более чем 99 символов переполнит буфер.

Функция get без аргументов вводит одиночный символ из указанного потока и возвращает это символ в качестве значения вызова функции. Этот вариант функции get возвращает EOF, когда в потоке встречается признак конца файла.

Пример.

Использование функций get, put и eof.

 

int main()

{

char c;

cout<<"Перед вводом cin.eof равен: "<<cin.eof()<<endl; while ((c=cin.get())!=EOF) cout.put(c);

cout<<"После ввода cin.eof равен: "<<cin.eof()<<endl; return 0;

}

 

Функция getline действует подобно функции get, вводящей строку, но, в отличие от функции get, функция getline удаляет символ-ограничитель из потока и не сохраняет его в символьном массиве.

Пример.

Символьный ввод функцией getline.

 

int main()

{

const int SIZE=80; char buff[SIZE];

cout<<"Vvedite predlozhenie\n"; cin.getline(buff,SIZE);

cout<<"Vvedennoye predlozhenie: \n"<<buff<<endl; return 0;

}

 

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

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

Функция peek возвращает очередной символ из входного потока, но не удаляет этот символ из него

 

Неформатированный ввод-вывод.

 

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

Например, вызов

 

char buff[SIZE]="ABCDEFGHIJKLMNOPRSTUVWXYZ";

cout.write(buff,10); или

cout.write("ABCDEFGHIJKLMNOPRSTUVWXYZ",10);

 

выводит первые 10 байтов символьного массива.

Функция read вводит в символьный массив указанное число символов. Если считывается меньшее количество символов, то устанавливается флаг failbit.

Функция gcount сообщает о количестве символов, прочитанных последней операцией ввода.

Пример работы функций неформатированного ввода-вывода.

 

int main()

{

const int SIZE=80; char buff[SIZE];

cout<<"Vvedite predlozhenie:"; cin.read(buff,20); cout<<"Vvedennoye predlozhenie:"; cout.write(buff,cin.gcount());

return 0;

}

 

Форматирование данных

В потоковых классах форматирование выполняется тремя способами – с помощью флагов, манипуляторов и форматирующих методов.

 

Манипуляторы потоков

 

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

 

      Манипуляторы потоков, задающие основание чисел

 

Для указания основания вывода чисел используются следующие манипуляторы без параметров:

dec – устанавливает вывод десятичных чисел;

oct – устанавливает вывод чисел в восьмеричной системе счисления; hex –   устанавливает  вывод чисел в шестнадцатеричной системе

счисления.

Основание выводимых чисел можно также изменить с помощью манипулятора setbase. Этот манипулятор принимает целый параметр со значениями 10, 16 или 8. Так как манипулятор setbase принимает параметр, он называется параметризованным манипулятором. Использование параметризованных манипуляторов требует подключения заголовчного файла iomanip.h.

Основание потока является установленным до тех пор, пока оно не будет изменено явным образом.

Пример. Использование манипуляторов hex, dec, oct и setbase для задания основания выводимых чисел.

 

#include <iostream.h> #include <iomanip.h> int main()

{

int n;

cout<<"Vvedite chislo: "; cin>>n; //60

cout<<"16 format: "<<hex<<n<<endl; //3c cout<<"10 format: "<<dec<<n<<endl; //60 cout<<"8 format: "<<oct<<n<<endl; //74 cout<<"10 format: "<<setbase(10)<<n<<endl; //60 return 0;

}

 


Дата добавления: 2019-09-13; просмотров: 210; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!