Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
шпоры по ооп.doc
Скачиваний:
2
Добавлен:
01.05.2025
Размер:
301 Кб
Скачать
☆

Операторы new и delete

Для создания объектов и рационального использования памяти используют функции и операторы динамического выделения памяти. Более безопасным и удобным способом выделения памяти является выделение и освобождение памяти с помощью операторов new и delete.

Формат записи следующий:

P_var=new type;

delete p_var;

где type – это спецификатор типа объект, для которого выделяется память;

P_var – указатель на этот тип;

new – это оператор, который возвращает указатель на динамически выделенную память, достаточную для хранения объекта типа type.

Оператор delete освобождает эту память, в которой отпадает необходимость.

14 Наследование. Определение, основные понятия и спецификаторы доступа.

Наследование

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

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

Для создания наследуемого класса используется следующий формат:

Class имя_производного_класса: сп_доступа имя_базового_класса {тело класса};

где сп_доступа – это одно из трех ключевых слов public, private или protected. Спецификатор доступа определяет то, как элементы базового класса наследуются производным классом.

Если спецификатор доступа public, то все открытые члены базового класса являются открытыми и в производном, что отображено в таблице 1

Таблица 1

Спецификатор доступа

Доступ из самого класса

Доступ из производного класса

Доступ из внешних классов и функций

public

есть

есть

есть

protected

есть

есть

нет

private

есть

нет

нет

По умолчанию производный класс наследуется как закрытый с ключевым словом class и открытый с ключевым словом struct.

Спецификатор доступа protected эквивалентен спецификатору доступа private с отличием: защищенные члены базового класса доступны для членов всех производных классов этого базового класса. Вне базового или производного классово защищенные члены не доступны.

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

15 Статические переменные класса. Определение, основные понятия.

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

class MyClass{

public:

static int n;

static int addn(int arg){

return n+arg;}

};

// инициализация статического члена данных

int MyClass::n=30;

int main() {

// обращение к статическим членам

// через имя класса

MyClass::n=3;

cout<<MyClass::addn(2)<<endl;

// обращение к статическим членам

// через объект

MyClass cls;

cls.n=5;

cout<<cls.addn(2)<<endl;

return 0;

}

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

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

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

#include <iostream.h> class Base { public: Base () {cout << "\nBase created\n";} }; class D_class1(): public Base { public: D_class1() {cout << "D_class1 created\n";} }; int main() { D_class1 d1; // ничего не делается, но выполняются конструкторы return 0; }

Эта программа создает объект типа D_class1. Она выводит на экран следующий текст: Base created D_class1 created

Здесь d1 является объектом типа D_class1, производным класса Base. Таким образом, при созда­нии объекта d1 сначала вызывается конструктор Base(), а затем вызывается конструктор D_class1().

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

В противоположность этому деструктор производного класса вызывается перед деструктором базового класса. Причину этого также легко понять. Поскольку уничтожение объекта базового класса влечет за собой уничтожение и объекта производного класса, то деструктор производно­го объекта должен выполняться перед деструктором базового объекта. Следующая программа иллюстрирует порядок, в котором выполняются конструкторы и деструкторы: #include <iostream.h> class Base { public: Base() {cout << "\nBase created\n";} ~Base() {cout << "Base destroyed\n\n,"; } }; class D_class1(): public Base { public: D_class1() {cout << "D_class1 created\n";} ~D_class1() {cout << "D_class1 destroyed\n "; } }; int main() { D_class1 d1; cout << " \n"; return 0; }

Эта программа выдаст следующий текст на экран: Base created D_class1 created D_class1 destroyed Base destroyed

Производный класс может служить базовым классом для создания следующего производного класса. Когда такое происходит, конструкторы исполняются в порядке наследования, а деструк­торы — в обратном порядке. В качестве примера рассмотрим следующую программу, использую­щую класс D_class2, производный от класса D_ciass1: #include <iostream.h> class Base { public: Base() {cout << "\nBase created\n";} ~Base() {cout << "Base destroyed\n\n";} }; class D_class1() : public Base { public: D_class1() {cout << "D_class1 created\n";} ~D_class1() {cout << "D_class1 destroyed\n ";} }; class D_class2: public D_class1 { public: D_class2() {cout << "D_class2 created\n";} ~D_class2() {cout << "D_class2 destroyed\n ";} }; int main() { D_class1 d1; D_class2 d2; cout << "\n"; return 0; }

Эта программа выдаст следующий результат: Base created D_class1 created Base created D_class1 created D_class2 created D_class2 destroyed D_class1 destroyed Base destroyed D_class1 destroyed Base destroyed

17 Конструкторы с параметрами.

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

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

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

class Alarm: public Time { // Класс сообщений таймера.

char *msg;

public:

Alarm(char*);

Alarmfchar*, int, int); // Новый конструктор.

~Alarm() { delete[] msg; }

void SetMsg(char*) ;

void Show(); // Переопределяет Time:: Show ().

//. . .

Alarm::Alarm(char *str, int h, int m): Time(h, m) {

msg = new char[strlen(str) + 1];

strcpy(msg, str);

}

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

Конструктором по умолчанию для класса Х называется такой конструктор, который не принимает никаких аргументов: Х::Х(). Если для класса не существует конструкторов, определяемых пользователем, то Turbo C++ генерирует конструктор по умолчанию. При таких объявлениях, как Х х, конструктор по умолчанию создает объект х.

Важное примечание:

Как и все функции, конструкторы могут иметь аргументы по умолчанию. Например, конструктор

X::X(int, int = 0)

может принимать один или два аргумента. Если данный конструктор будет представлен только с одним аргументом, недостающий второй аргумент будет принят как int 0. Аналогичным образом, конструктор

X::X(int = 5, int = 6)

может принимать два аргумента, один аргумент, либо не принимать аргументов вообще, причем в каждом случае принимаются соответствующие умолчания. Однако, конструктор по умолчанию Х::Х() не принимает аргументов вообще, и его не следует путать с конструктором, например, X::X(int = 0), который может либо принимать один аргумент, либо не принимать аргументов.

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

class X

{

public:

X();

X(int i = 0);

};

main()

{

X one(10); // так можно: используется X::X(int)

X two; // так нельзя; неоднозначно задано, используется

// ли X::X() или X::X(int = 0)

...

return 0;

}

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

Класс может иметь несколько непосредственных базовых классов

class A1 {...};

class A2 {...};

class A3 {...};

class B : public A1, public A2, public A3 {...};

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

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

class Y : public X {...};

class Z : public X {...};

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

Имеем следующую иерархию классов (и объектов):

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

Y::X::f() или Z::X::f()

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

Виртуальные функции позволяют производным классам обеспечивать разные версии функции базового класса. Вы можете объявить виртуальную функцию в базовом классе и затем переопределить ее в любом производном классе, даже если число и тип ее аргументов остается без изменений. Вы также можете объявить функции int Base::Fun (int) и int Derived::Fun (int), даже если они не являются виртуальными. Версия базового класса доступна объектам производного класса через переопределение контекста. Если они являются виртуальными, то доступна только функция, связанная с фактическим типом объекта.

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

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

Если базовый класс В содержит виртуальную функцию vf, и класс D, являющийся производным от класса B, содержит функцию vf того же типа, то если функция vf вызывается для объекта d или D, выполняется вызов D::vf, даже если доступ определен через указатель или ссылку на B. Например,

struct B {

virtual void vf1();

virtual void vf2();

virtual void vf3();

void f();

}

class D : public B {

virtual void vf(); // спецификатор virtual допустим, но

// избыточен

void vf2(int); // не virtual, поскольку здесь

// используется другой список аргументов

char f(); // так нельзя: изменяется только тип

// возврата

void f();

};

void extf()

{

D d; // объявление объекта D

B* bp = &d; // стандартное преобразование из D* в B*

bp->vf1(); // вызов D::vf1

bp->vf2(); // вызов B::vf2, так как vf2 из D имеет

// отличные аргументы

bp->f(); // вызов B::f (не виртуальной)

}

Переопределяющая функция vf1 в D автоматически становится виртуальной. Спецификатор virtual может быть использован в определении переопределяющей функции в производном классе, но на самом деле он является в данном случае избыточным.

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

При наследовании часто бывает необходимо, чтобы поведение некоторых методов базового класса и классов-наследников отличались. Решение, на первый взгляд, очевидное: переопределить соответствующие методы в производном классе. Однако тут возникает одна проблема, которую лучше рассмотреть на простом примере (листинг 9.1).

//Листинг 9.1. Необходимость виртуальных функций

#include <iostream>

using namespace std;

class Base // базовый класс

{ public:

int f(const int &d) // метод базового класса

{ return 2*d; }

int CallFunction(const int &d) // предполагается

{ return f(d)+1; // вызов метода базового класса

}

};

class Derived: public Base // производный класс

{ public: // CallFunction наследуется

int f(const int &d) // метод f переопределяется

{ return d*d; }

};

int main()

{ Base a; // объект базового класса

cout << a.CallFunction(5)<< endl; // получаем 11

Derived b; // объект производного власса

cout << b.CallFunction(5)<< endl; // какой метод f вызывается?

return 0;

}

В базовом классе определены два метода — f() и CallFunction(), — причем во втором методе вызывается первый. В классе-наследнике метод f() переопределен, а метод CallFunction()унаследован. Очевидно, метод f() переопределяется для того, чтобы объекты базового класса и класса-наследника вели себя по-разному. Объявляя объект b типа Derived, программист, естественно, ожидает получить результат 5 * 5 + 1 = 26 — для этого и переопределялся метод f(). Однако на экран, как и для объекта а типа Base, выводится число 11, которое очевидно вычисляется как 2 * 5 + 1 = 11. Несмотря на переопределение метода f() в классе-наследнике, в унаследованной функции CallFunction() вызывается «родная» функция f(), определенная в базовом классе! Аналогичная проблема возникает и в несколько другом контексте: при подстановке ссылки или указателя на объект производного класса вместо ссылки или указателя на объект базового. Рассмотрим опять пример с часами и будильником (листинг 9.2).

//Листинг 9.2. Неожиданная работа принципа подстановки

class Clock // базовый класс — часы

{ public:

void print() const { cout << "Clock!" << endl; }

};

class Alarm: public Clock // производный класс — будильник

{ public:

void print() const // переопределенный метод

{ cout << "Alarm!" << endl; }

};

void settime(Clock &d) // функция установки времени

{ d.print(); } // предполагается вызов метода базового класса

//...

Clock W; // объект базового класса

settime(W); // выводится "Clock"

Alarm U; // объект производного класса

settime(U); // ссылка на производный вместо базового

Clock *c1 = &W; // адрес объекта базового класса

c1->print(); // вызов базового метода

c1 = &U; // адрес объекта производного типа вместо базового

c1->print(); // какой метод вызываетя, базовый или производный?

Опять в классе-наследнике переопределен метод для того, чтобы обеспечить различное поведение объектов базового и производного классов. Однако и при передаче параметра по ссылке базового класса в функцию settime(), и при явном вызове метода print() через указатель базового класса наблюдается одна и та же картина: всегда вызывается метод базового класса, хотя намерения программиста состоят в том, чтобы вызвать метод производного. Для того чтобы разобраться в ситуации, необходимо уяснить, что такое связывание. Связывание — это сопоставление вызова функции с телом. В приведенных ранее примерах связывание выполняется на этапе трансляции (до запуска) программы. Такое связывание обычно называют ранним, или статическим. При трансляции класса Base (см. листинг 9.1) компилятор ничего не знает о классах-наследниках , поэтому он не может предполагать, что метод f() будет переопределен в классе Derived. Его естественное поведение — «прочно» связать вызов f() с телом метода класса Base. Аналогично при трансляции функции settime() компилятору ничего не известно о типе реально передаваемого объекта во время выполнения программы. Поэтому вызов метода print() связывается с телом метода базового класса Clock, как и определено в заголовке функции settime(). Точно так же указатель на базовый класс «прочно» связывается с методом базового класса во время трансляции. Конечно, при вызове метода по указателю в данном конкретном случае мы можем вызвать метод производного класса, задав явное преобразование указателя:

static_cast<Alarm*>(c1)->print();

Или так:

((Alarm *)c1)->print(); // "лишние" скобки нужны!

Однако для функции settime() и метода CallFunction() это сделать невозможно — нам необходимо именно разное поведение в зависимости от типа объекта. Да и с указателем не все так просто: если такой вызов прописан внутри функции, которая принимает этот указатель как параметр (например, settime(Clock *c1)), то мы имеем те же проблемы. Определение виртуальных функций Получается, что в С++ должен существовать механизм, с помощью которого можно узнать тип объекта во время выполнения программы. Такой механизм в С++ есть и он, как уже отмечалось, называется динамической идентификацией типов (RTTI). Однако в ситуациях, подобных описанным, применяется другой, более «сильный» и элегантный механизм С++ — механизм виртуальных функций (см. п. 10.3 в Стандарте). Чтобы добиться разного поведения в зависимости от типа, необходимо объявить функцию-метод виртуальной; в С++ это делается с помощью ключевого слова virtual. Таким образом, в листинге 9.1 объявление метода f() в базовом и производном классе должно быть таким:

virtual int f(const int &d) // в базовом классе

{ return 2*d; }

virtual int f(const int &d) // в производном классе

{ return d*d; }

После этого для объектов базового и производного классов мы получаем разные результаты: 11 и 26. Аналогично в листинге 9.2 объявление метода print() тоже должно начинаться со слова virtual:

irtual void print() const // в базовом классе

{ cout << "Clock!" << endl; }

virtual void print() const // в производном классе

{ cout << "Alarm!" << endl; }

После этого вызов settime() с параметром базового класса обеспечит нам вывод на экран слова «Clock», а с параметром производного класса — слова «Alarm». И при вызове по указателю наблюдается та же картина. Вообще-то ключевое слово virtual достаточно написать только один раз — в объявлении функции базового класса. Определение можно писать без слова virtual — все равно функция будет считаться виртуальной. Однако лучше всегда это делать явным образом, чтобы всегда по тексту было видно, что функция является виртуальной. Для виртуальных функций обеспечивается не статическое, а динамическое (позднее, отложенное) связывание, которое реализуется во время выполнения программы. Естественно, это влечет за собой некоторые накладные расходы, однако на них можно не обращать внимания, так как обеспечивается динамический полиморфизм. Александреску указывает, что в С++ реализованы два типа полиморфизма:

  • статический полиморфизм, или полиморфизм времени компиляции (compile-time polymorphism), осуществляется за счет перегрузки и шаблонов функций;

  • динамический полиморфизм, или полиморфизм времени выполнения (run-time polymorphism), реализуется виртуальными функциями.

С перегрузкой функций «разбирается» компилятор, правильно подбирая вариант функции в той или иной ситуации. И полиморфизм шаблонных функций тоже реализуется на этапе компиляции. Естественно, выбор осуществляется статически. Выбор же виртуальной функции происходит динамически — при выполнении программы. Класс, включающий виртуальные функции, называется полиморфным. Правила описания и использования виртуальных функций-методов следующие: 1. Виртуальная функция может быть только методом класса. 2. Любую перегружаемую операцию-метод класса можно сделать виртуальной, например, операцию присваивания или операцию преобразования типа. 3. Виртуальная функция, как и сама виртуальность, наследуется. 4. Виртуальная функция может быть константной. 5. Если в базовом классе определена виртуальная функция, то метод производного класса с такими же именем и прототипом (включая тип возвращаемого значения и константность метода) автоматически является виртуальным (слово virtual указывать необязательно) и замещает функцию-метод базового класса. 6. Конструкторы не могут быть виртуальными. 7. Статические методы не могут быть виртуальными. 8. Деструкторы могут (чаще — должны) быть виртуальными — это гарантирует корректный возврат памяти через указатель базового класса.

20 Определение методов класса.

Другими словами- “методы класса были расписаны внутри класса”. Такой подход не всегда удобен, поэтому в C++ существует альтернативный вариант. В альтернативном варианте вовнутрь класса размещается прототип функции, а вне класса пишется сама функция. 

21 Чисто виртуальные функции и абстрактные типы.

Чисто виртуальной функцией называется виртуальная функция-член, которая объявлена со спецификатором = 0:

class Figure {

...

virtual void Draw() const = 0;

);

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

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

Абстрактным называется класс с как минимум одной чистой виртуальной функцией. Виртуальная функция задается как "чистая" при помощи соответствующего спецификатора.

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

class shape { // абстрактный класс

point center;

...

public:

where() { return center; }

move(point p) { center = p; draw(); }

virtual void rotate(int) = 0; // чистая виртуальная функция

virtual void draw() = 0; // чистая виртуальная функция

virtual void hilite() = 0; // чистая виртуальная функция

...

}

shape x; // ошибка: попытка создания объекта абстрактного

// класса

shape* sptr; // указатель на абстрактный класс допустим

shape f(); // ошибка: абстрактный класс не может являться

// типом возврата

int q(shape s); // ошибка: абстрактный класс не может являться

// типом аргумента функции

shape& h(shape&); // ссылка на абстрактный класс в качестве типа

// возврата или аргумента функции допустим

Предположим, что D является производным классом непосредственно от абстрактного базового класса B. Тогда для каждой чистой виртуальной функции pvf в B либо D должен обеспечивать определение pvf, либо D должен объявлять pvf как чистую функцию.

Например, с использованием показанного выше класса shape:

class circle : public shape { // circle является производным

// от абстрактного класса

int radius; // private

public:

void rotate(int) { } // определяется виртуальная

// функция:

// действия по вращению окруж-

// ности отсутствуют

void draw(); // circle::draw должна быть

// где-либо определена

void hilite() = 0; // переопределение функции

// как "чистой"

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

Когда виртуальная функция не переопределена в производном классе, то при вызове ее в объекте производного класса вызывается версия из базового класса. Однако во многих случаях невоз­можно ввести содержательное определение виртуальной функции в базовом классе. Например, при объявлении базового класса 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). Важной особенностью абстрактных классов является то, что не существует ни одного объекта данного класса. Вместо этого абстрактный класс служит в качестве базового для других производных классов. Причина, по которой абстрактный класс не может быть ис­пользован для объявления объекта, заключается в том, что одна или несколько его функций-членов не имеют определения. Тем не менее, даже если базовый класс является абстрактным, все равно можно объявлять указатели или ссылки на него, с помощью которых затем поддерживает­ся полиморфизм времени исполнения.

22 Шаблоны классов.

Аналогично шаблонам функций. определяется шаблон семейства классов:

template<список_параметров_шаблона> определение_класса

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

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

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

Следующий шаблон автоматически формирует классы векторов с указанными свойствами:

// vector.h - шаблон векторов

template<class T> // T - параметр шаблона

class Vector

{

public:

Vector(int); // Конструктор класса vector

~Vector() // Деструктор

{

delete [] data;

}

// Расширение действия (перегрузка) операции "[]":

T &operator [](int i)

{

return data [i];

}

protected:

T *data; // Начало одномерного массива

int size; // Количество элементов в массиве

};

// vector.cpp

// Внешнее определение конструктора класса:

template<class T> Vector <T>::Vector(int n)

{

data = new T[n];

size = n;

};

Когда шаблон введен, у программиста появляется возможность определять конкретные объекты конкретных классов, каждый из которых параметрически порожден из шаблона. Формат определения объекта одного из классов, порождаемых шаблоном классов:

имя_параметризованного_класса <фактические_параметры_шаблона>

имя_объекта (параметры_конструктора);

В нашем случае определить вектор, имеющий восемь вещественных координат типа double, можно следующим образом:

Vector<double> z(8);

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

// формирование классов с помощью шаблона

#include <iostream.h>

#include "vector.h" // Шаблон класса "вектор"

int main(void)

{

Vector<int> X(5); //Создаем объект класса "целочисленный вектор"

Vector<char> C(5); // Создаем объект класса "символьный вектор"

for (int i = 0; i < 5; i++) // Определяем компоненты векторов

{

X[i] = i;

C[i] = 'A' + i;

}

for (i = 0; i < 5; i++)

{

cout << X[i] << C[i]; // 0 A 1 B 2 C 3 D 4 E

}

return 0;

}

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

В списке параметров шаблона могут присутствовать формальные параметры, не определяющие тип, точнее - это параметры, для которых тип фиксирован:

#include <iostream.h>

template<class T, int size = 64> class Row

{

public:

Row()

{

length = size;

data = new T [size];

}

~Row()

{

delete [] data;

}

T &operator [](int i)

{

return data [i];

}

protected:

T *data;

int length;

};

int main(void)

{

Row<float, 8> rf;

Row<int, 8> ri;

for (int i = 0; i < 8; i++)

{

rf[i] = i;

ri[i] = i * i;

}

for (i = 0; i < 8, i++)

{

cout << rf[i] << ri[i]; //0 0 1 1 2 4 3 9 4 16 5 25 6 36 7 49

}

return 0;

}

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

Основные свойства шаблонов классов.

Компонентные функции параметризованного класса автоматически являются параметризованными. Их не обязательно объявлять как параметризованные с помощью template.

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

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

В рамках параметризованного класса нельзя определить friend-шаблоны (дружественные параметризованные классы).

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

Шаблоны функций, которые являются членами классов, нельзя описывать как virtual.

Локальные классы не могут содержать шаблоны в качестве своих элементов.

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

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

За именем_класса, предшествующим операции области видимости (::), должен следовать список_имен_параметров шаблона.

template<список_типов>

тип_возвр_значения имя_класса<список_имен_параметров>::имя_функции(список_параметров)

{ ... }

23 Полиморфизм и использование виртуальных функций.

Полиморфизмом в программировании называется переопределение наследником функций-членов базового класса, например:

class Figure {

...

void Draw() const;

...

};

class Square : public Figure {

...

void Draw() const;

...

};

class Circle : public Figure {

...

void Draw() const;

...

};

Какая именно из функций будет вызвана — Figure::Draw(), Square::Draw() или Circle::Draw() — определяется во время компиляции. Например:

Circle *c = new Circle(0,0,5);

Figure *f = c; // Всё ok: Figure — базовый класс для Circle

c->Draw();

f->Draw(); // Указатели друг другу равны, но для f будет вызвана другая функция, чем для c

Несмотря на то, что оба указателя указывают на один и тот же объект класса Circle, для c будет вызвана Circle::Draw(), а для f — Figure::Draw(), поскольку f — указатель на объект класса Figure. Такой полиморфизм называется статическим.

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

class Figure {

...

virtual void Draw() const;

...

};

class Square : public Figure {

...

void Draw() const;

...

};

class Circle : public Figure {

...

void Draw() const;

...

};

Figure *figures[10];

figures[0] = new Square(1, 2, 10);

figures[1] = new Circle(3, 5, 8);

...

for (int i = 0; i < 10; i++)

figures[i]->Draw();

В этом случае для каждого элемента массива будет вызвана Square::Draw() или Circle::Draw() в зависимости от вида фигуры.

Чисто виртуальной функцией называется виртуальная функция-член, которая объявлена со спецификатором = 0:

class Figure {

...

virtual void Draw() const = 0;

);

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

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

24 Дружественные функции.

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

Дружба не наследуется, передается или даруется:

1. Допустим, класс Bicycle - друг класса Cycle. Тогда все потомки Bicycle не будут друзьями Cycle.

2. Если класс Bicycle - друг класса Cycle, а Cycle - друг класса Auto, то Bicycle не будет другом Auto.

3. Дружба односторонняя: Bicycle - друг Cycle, но Cycle не друг Bicycle.

Такая функция (или класс) имеет доступ ко всем нестатическим полям и методам класса в котором находится через параметр-объект класса, которому она дружественна.

25 Исключительные ситуации. Определение и назначение.

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

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

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

  3. Установить состояние ошибки в специальной глобальной переменной. Например, математические функции записывают код ошибки в errno. Недостатки: проверка этой переменной после каждого оператора с вызовом функции приводит к существенному увеличению размера программы, поэтому часто проверка не выполняется.

  4. Вызвать специальную функцию обработки ошибки. Например, некоторые реализации математических функций при ошибке вызывают функцию matherr. Недостатки: при отсутствии исключений эта функция должна выполнить одно из трех рассмотренных ранее действий.

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

  • bad_alloc, генерируемое операцией new;

  • bad_cast, генерируемое операцией dynamic_cast;

  • bad_typeid, генерируемое операцией typeid;

  • bad_exception, генерируемое для обработки исключения, непредусмотренном в заголовке функции;

  • исключения, порождаемые классами и алгоритмами STL.

26 Статические функции.

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

class MyClass{

public:

static int n;

static int addn(int arg){

return n+arg;}

};

// инициализация статического члена данных

int MyClass::n=30;

int main() {

// обращение к статическим членам

// через имя класса

MyClass::n=3;

cout<<MyClass::addn(2)<<endl;

// обращение к статическим членам

// через объект

MyClass cls;

cls.n=5;

cout<<cls.addn(2)<<endl;

return 0;

}

27 Исключительные ситуации и особенности работы с ними.

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

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

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

  3. Установить состояние ошибки в специальной глобальной переменной. Например, математические функции записывают код ошибки в errno. Недостатки: проверка этой переменной после каждого оператора с вызовом функции приводит к существенному увеличению размера программы, поэтому часто проверка не выполняется.

  4. Вызвать специальную функцию обработки ошибки. Например, некоторые реализации математических функций при ошибке вызывают функцию matherr. Недостатки: при отсутствии исключений эта функция должна выполнить одно из трех рассмотренных ранее действий.

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

  • bad_alloc, генерируемое операцией new;

  • bad_cast, генерируемое операцией dynamic_cast;

  • bad_typeid, генерируемое операцией typeid;

  • bad_exception, генерируемое для обработки исключения, непредусмотренном в заголовке функции;

  • исключения, порождаемые классами и алгоритмами STL.

28 Указатель this.

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

имя_класса *const this = адрес_объекта

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

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

void Complex add(Complex ob)

{

this->re = this->re + ob.re;

// или (*this).re = (*this).re + ob.re;

this->im = this->im + ob.im;

}

Если функция возвращает объект, который ее вызвал, используется указатель this.

Например, пусть функция add возвращает ссылку на объект. Тогда

Complex& Complex add(Complex& ob)

{

re = re + ob.re;

im = im + ob.im;

return *this;

}

Примером широко распространенного использования this являются операции со связанными списками.

Пример. Связанный список.

#include <iostream.h>

//Определение класса

class Item

{

public:

Item(char ch) // конструктор

{

symbol = ch;

}

void add(void); // добавить в начало

static void print(void);

protected:

static Item *begin;

Item *next;

char symbol;

};

// Реализация класса

void Item::add(void)

{

this–>next = begin;

begin = this;

}

void Item::print(void)

{

Item *p;

p = begin;

while (p)

{

cout << p–>symbol << "\t";

p = p–>next;

}

}

//Создание и просмотр списка

Item *Item::begin = NULL; // инициализация статического компонента

int main(void)

{

Item A('a');

Item B('b');

Item C('c');

// включение объектов в список

A.add();

B.add();

C.add();

// просмотр списка в обратном порядке

Item::print();

return 0;

}

29 Исключения с аргументами.

В языке F# существует две категории исключений: типы исключений .NET и типы исключений F#. В этом разделе описывается определение и использование типов исключений F#.

exception exception-type of argument-type

Заметки

В приведенной выше синтаксической конструкции exception-type — это имя нового типа исключения F#, а argument-type представляет собой тип аргумента, который может быть передан при создании исключения данного типа. Можно задавать несколько аргументов, используя в качествеargument-type кортежный тип.

Типичное определение исключения F# будет выглядеть примерно так.

F#

exception MyError of string

Сгенерировать исключение этого типа можно с помощью функции raise следующим образом.

F#

raise (MyError("Error message"))

Тип исключения F# можно использовать непосредственно в фильтрах в выражении try...with, как показано в следующем примере.

F#

exception Error1 of string

// Using a tuple type as the argument type.

exception Error2 of string * int

let function1 x y =

try

if x = y then raise (Error1("x"))

else raise (Error2("x", 10))

with

| Error1(str) -> printfn "Error1 %s" str

| Error2(str, i) -> printfn "Error2 %s %d" str i

function1 10 10

function1 9 2

Тип исключения, определяемый с помощью ключевого слова exception в F#, является новым типом, наследуемым от класса Exception.

30 Потоки и файлы. Определение и особенности работы с потоками.

Перед тем как начать изучение файловой системы языка С, необходимо уяснить, в чем разница между потоками и файлами. В системе ввода/вывода С для программ поддерживается единый интерфейс, не зависящий от того, к какому конкретному устройству осуществляется доступ. То есть в этой системе между программой и устройством находится нечто более общее, чем само устройство. Такое обобщенное устройство ввода или вывода (устройство более высокого уровня абстракции) называется потоком, в то время как конкретное устройство называется файлом. (Впрочем, файл — тоже понятие абстрактное.) Очень важно понимать, каким образом происходит взаимодействие потоков и файлов.

Потоки

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

Текстовые потоки

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

Двоичные потоки

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

Файлы

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

Но не у всех файлов одинаковые возможности. Например, к дисковому файлу прямой доступ возможен, в то время как к некоторым принтерам — нет. Таким образом, мы пришли к одному важному принципу, относящемуся к системе ввода/вывода языка С: все потоки одинаковы, а файлы — нет.

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

Файл отсоединяется от определенного потока (т.е. разрывается связь между файлом и потоком) с помощью операции закрытия. При закрытии файла, открытого с целью вывода, содержимое (если оно есть) связанного с ним потока записывается на внешнее устройство. Этот процесс, который обычно называют дозаписью[1] потока, гарантирует, что никакая информация случайно не останется в буфере диска. Если программа завершает работу нормально, т.е. либо main() возвращает управление операционной системе, либо вызывается exit(), то все файлы закрываются автоматически. В случае аварийного завершения работы программы, например, в случае краха или завершения путем вызова abort(), файлы не закрываются.

У каждого потока, связанного с файлом, имеется управляющая структура, содержащая информацию о файле; она имеет тип FILE. В этом блоке управления файлом[2] никогда ничего не меняйте[3].

Если вы новичок в программировании, то разграничение потоков и файлов может показаться излишним или даже "заумным". Однако надо помнить, что основная цель такого разграничения — это обеспечить единый интерфейс. Для выполнения всех операций ввода/вывода следует использовать только понятия потоков и применять всего лишь одну файловую систему. Ввод или вывод от каждого устройства автоматически преобразуется системой ввода/вывода в легко управляемый поток.