Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Ответы на ООП.doc
Скачиваний:
5
Добавлен:
09.11.2019
Размер:
1.47 Mб
Скачать

Объектно-ориентированное программирование

  1. Раскройте смысл таких понятий объектно-ориентированного программирования, как «наследование», «инкапсуляция» и «полиморфизм» с примерами на языке С++.

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

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

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

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

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

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

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

Для дополнения класса shape (фигура) классом circle (круг), достаточно лишь объявления его в классе shape (без изменения функций элементов класса) в модуле shape.h и скомпилировать исходный текст в shape.obj. Таким образом нет необходимости изменять исходный текст shape.c. Успешное создание потомков позволяет увеличивать программу за счет накопления уже готовых текстов программ.

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

Объекты класса конструируются снизу вверх: сначала базовый, потом компоненты-объекты (если они имеются), а потом сам производный класс. Уничтожаются объекты в обратном порядке: сначала производный, потом его компоненты-объекты, а потом базовый объект.

Пример.

class Basiс {

int i; // сокрытие переменной

public:

Basis(int x){ i=x; }

int GetI(void) {return I;} // получить значение скрытой переменной

void SetI(int k) {i=k;} // установить значение скрытой переменной

};

class Inherit: public Basis { // класс-наследник

int count;

public:

Inherit(int x,int c): Basis(x){ count=c; }

};

  1. Специальные функции-члены класса – конструкторы, деструкторы, конструкторы копирования. Привести примеры на С++.

Как отмечалось выше объекты создаются как переменные соответствующих типов (в качастве типа указывается класс объекта):

Light lt;

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

Конструктор – выделяет память для объекта и инициализирует данные-члены класса.

  • имя конструктора совпадает с именем класса;

  • конструктор не имеет возвращаемого значения (даже void);

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

  • при явно описанном конструкторе конструктор по умолчанию не генерируется;

  • конструкторы могут быть перегружены;

  • не могут быть описаны с ключевыми словами virtual, static, const, mutable, volatile;

  • не могут явно вызываться в программе;

  • частным случаем конструктора является конструктор копирования, у которого в аргументах передается ссылка на другой объект того же класса.

Деструктор – вызывается автоматически при разрушении объекта, его задача - освободить память и корректно уничтожить объект.

  • имя деструктора также совпадает с именем класса но предваряется символом тильда "~" (~имя_класса);

  • деструктор не имеет ни какого возвращаемого значения;

  • не может быть описан как static, const, mutable, volatile;

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

  • могут быть описаны как virtual - у всех производных классов деструкторы автоматически будут виртуальными;

  • могут явно вызываться;

Ниже приведем объявление конструктора:

ComplexType();

Определение пустого конструктора:

ComplexType() { }

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

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

ComplexType(double rePar, double imPar); /* Объявление… */

ComplexType(double rePar, double imPar) { /*…*/ } /*Определение…*/

Конструктор вызывается не для объекта класса, как другие функции-члены, а для области памяти. Для её преобразования ("превращения") в объект класса.

Конструктор копирования (пример):

class demo {

char *p;

public:

demo(void) { p="NULL"; }

demo(char* s){ p=s; }

demo(demo& demo_o){ p=o.p; }

void out(void){ puts(p); }

}

void main(){

demo a("Ok"), b(a);

a.out(); b.out();

}

Вместе с конструктором умолчания, конструктор копирования входит в обязательный набор конструкторов для любого класса. Реализация механизма копирования значений для транслятора не является неразрешимой задачей. Конструктор копирования всего лишь создаёт копии объектов. Этот процесс реализуется при помощи стандартного программного кода. И построить такой код транслятор способен самостоятельно. Здесь и далее, в примерах нами будет применяться операция присвоения = . В определённом смысле эта операция подобна конструктору. Реализующий эту операцию код автоматически создаётся на этапе трансляции для любого класса. Как и генерация кода стандартных конструкторов, это не самая сложная задача.

Чтобы восстановить структуру заголовка конструктора копирования, мы должны лишь определить тип его параметра. На первый взгляд, здесь всё просто. В качестве значения параметра конструктору передаётся имя объекта, значит можно предположить, что тип параметра конструктора копирования соответствует данному классу. Однако это не так. И вот почему. В C++ конструктор копирования является единственным средством создания копий объекта. На самом деле, в конструкторе копирования класса X в качестве параметра используется ссылка на объект этого класса. Причём эта ссылка объявляется со спецификатором const. Как известно, выражение вызова функции с параметром типа X ничем не отличается от выражения вызова функции, у которой параметром является ссылка на объект типа X. При вызове такой функции не приходится копировать объекты как параметры. Передача адреса не требует копирования объекта, а значит, при этом не будет и рекурсии.

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

Операторы new и delete предназначены для динамического распределения памяти и могут использоваться в С++ - программах вместо функций malloc() и free().

Оператор new имеет два допустимых формата вызова:

[::] new [список_аргументов] тип [инициализатор]

[::] new [список_аргументов] (тип) [инициализатор]

тип - это тип данных, для которых необходимо зарезервировать память.

инициализатор - значение, используемое для инициализации данных в памяти.

Пример использования оператора new:

double *p,*p2,*p3,*p4;

p=new double;

p2=new double(1.2);

p3=new double[1];

p4=new double[1](3.14); // некорректно, массивы не могут

//инициализироваться по new

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

delete p;

delete[] p3;

Для удаления элементов классов вызывается деструктор. В некоторых реализациях языка вызов оператора delete для несуществующего или пустого указателя приводит к возникновению исключения.

  1. Перегрузка и переопределение функций и операторов в С++. Привести примеры кода.

Одним из подходов реализации принципа полиморфизма в языке С++ является использование переопределения функций. В С++ две и более функций могут иметь одно и то же имя. Компилятор С++ оперирует не исходными именами функций, а их внутренними представлениями, которые существенно отличаются от используемых в программе. Эти имена содержат в себе скрытое описание типов аргументов. С этими же именами работают программы компоновщика и библиотекаря. По этой причине мы можем использовать функции с одинаковыми именами, только типы аргументов у них должны быть разными. Именно на этом и основана реализация одной из особенностей полиморфизма. Заметим, что компилятор не различает функции по типу возвращаемого значения. Поэтому для компилятора функции с различным списком аргументов – это разные функции, а с одинаковым списком аргументов, но с разными типами возвращаемого значения - одинаковые. Для корректной работы программ последнего следует избегать. Рассмотрим простой пример переопределения функции sum, выполняющей сложение нескольких чисел различного типа.

#include "iostream.h"

class cls

{ int n;

double f;

public:

cls(int N,float F) : n(N),f(F) {}

int sum(int); // функция sum с целочисленнным аргументом

double sum(double); // функция sum с дробным аргументом

void see(); // вывод содержимого объекта

};

int cls:: sum(int k)

{ n+=k;

return n;

}

double cls:: sum(double k)

{ f+=k;

return f;

}

void cls:: see()

{cout <<n<<' '<<f<<endl;}

void main()

{ cls obj(1,2.3);

obj.see(); // вывод содержимого объекта

cout <<obj.sum(1)<<endl; // вызов функции sum с целочисл. аргументом

cout <<obj.sum(1.)<<endl; // вызов функции sum с дробным аргументом

}

Результат работы программы:

1 2.3

2

3.3

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

class A

{ . . .

public:

void fun(int i,long j) {cout<<i+j<<endl;}

void fun(long i,int j) { cout<<i+j<<endl;}

};

main()

{ A a;

a.fun(2,2); // ошибка неизвестно какая из 2 функций вызывается

}

В этом случае возникает неоднозначность вызова функции fun объекта a.

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

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

тип_возвр_значения operator знак_операции (специф_параметров)

{операторы_тела_функции}

Перегрузка унарных операций. Любая унарная операция  может быть определена двумя способами: либо как компонентная функция без параметров, либо как глобальная функция с одним параметром. В первом случае выражение Z означает вызов Z.operator (), во втором  вызов operator(Z). Вызываемый объект класса автоматически воспринимается как операнд.

Пример.

class audio_player {

int records_count;

int current_record;

...

public:

play(int i);

void init(void){ current_record = 0; }

void operator++ (){ //++a

current_record++;

if(current_record>records_count) current_record = 0;}

void operator++ (int i){ //a++

current_record++;

if(current_record>records_count) current_record = 0;}

void operator—(){ //--a

current_record--;

if(current_record<0) current_record = records_count - 1;}

} ;

void main(){

audio_plajer a;

a.init();

++a;

a++;

}

Перегрузка бинарных операций. Любая бинарная операция  может быть определена двумя способами: либо как функция-член класса с одним параметром, либо как глобальная функция с двумя параметрами. В первом случае xy означает вызов x.operator (y), во втором – вызов operator (x,y).

class Car{

int model; int year; int color;

public:

bool __fastcall operator== (Car& c){

return (model==c.model) && (year==c.year) && (color==c.color);}

} ;

  1. Раскройте суть понятий «раннее связывание» и «позднее связывание». Виртуальные функции. Абстрактные классы в С++. Примеры кода на С++.

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

имя_класса :: имя_компонента

Однако так можно обращаться только к public членам. Для обращения к private статической компоненте извне можно с помощью статических методов. Пример.

#include <iostream.h>

class Car {

int speed;

static int CarCount; // статический атрибут – количество машин

public:

Car(int s) {

speed = s;

CarCount++;

}

static int& count() {

return CarCount;

}

};

int Car::CarCount = 0; //инициализация статического атрибута

void main(void) {

Car c1(20); Car c2(30);

cout << ” Количество машин = “ << Car::count();

}

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

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

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

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

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

virtual int funct1(void);       // определение виртуальной функции

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

Абстрактные классы и интерфейсы.

Концептуально понятия класса и интерфейса отличаются в ООП. Интерфейс – это декларация методов (событий) объекта без конкретизации реакций на эти события. С++ не декларирует интерфейс явно, как это делает, например, Java. Но, в качестве альтернативы явному описанию интерфейса, в С++ введены так называемые абстрактные классы.

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

virtual void rotate(int) = 0; // pure virtual method

virtual void rotate(int) = 0 { /* code of pure method */ }; // pure virtual method

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

Пример абстрактного класса:

class shape {       // abstract class

   point center;

. . .

public:

   where() { return center; }

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

   virtual void rotate(int) = 0;  // pure virtual function

   virtual void draw() = 0;       // pure virtual function

   virtual void hilite() = 0;     // pure virtual function

. . .

};

shape x; // ERROR: attempt to create an object of an abstract class

shape* sptr; // pointer to abstract class is OK

shape f(); // ERROR: abstract class cannot be a return type

int g(shape s); // ERROR: abstract class cannot be a function argument type

shape& h(shape&); // reference to abstract class as return

  // value or function argument is OK

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

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

// abstract class circle derived from abstract class shape

class circle : public shape {

    int radius; // private

public:

    void rotate(int) { } // virtual function defined: no action

             //  to rotate a circle

    void draw();       // circle::draw must be defined somewhere

}

  1. Шаблоны функций и классов в С++. Привести примеры кода.

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

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

Шаблоны, в некотором смысле, напоминают макросы, однако они более разнообразны и надежны. Тем не менее, достаточно часто шаблоны реализуются как обычные С-макросы.

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

Нужно отметить, что подавляющее большинство компонентов библиотек С++ базируется на шаблонах.

Шаблоны функций.

Определение шаблона функции имеет вид:

template <список_параметров_типов>

тип_возвращаемого_значения имя_функции (список параметров){ /* реализация */ }

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

Дальнейшее определение шаблона соответствует синтаксису описаний функций, но вместо определенных типов в нем могут быть задействованы элементы списка_параметров_типов.

Пример определения шаблона функции:

template <class T1> T1 add (T1 a,T1 b){

T1 result;

result=a+b;

return result;

}

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

Пример явной инсталляции функции:

float add (float a, float b);

Во втором случае инсталляция осуществляется в момент вызова функции с конкретными аргументами. Напрмер:

int a = 10, b = 20, c;

c = add (a, b);

Необходимо только, чтобы для типа-параметра был определен оператор +.

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

Определение шаблона функции имеет вид:

template <список_параметров_типов> class имя_класса { /* тело класса */ }

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

Пример определения шаблона класса:

template <class T> class Vector {

T *data;

int size;

public:

Vector(int);

~Vector( ) { delete[ ] data; }

T& operator[ ] (int i) { return data[i]; }

};

// Note the syntax for out-of-line definitions.

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

data = new T[n];

size = n;

};

void main() {

Vector<int> x(5); // Generate a vector to store five integers

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

x[i] = i; // Initialize the vector.

}

В списке_параметров_типов собственно параметры типов указываются с ключевым словом class, а параметры констант - с какими-либо другими ключевыми словами, применяемыми для идентификации типа.

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

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

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

Пример определение функции-члена шаблона класса:

template <class T, class S> class demo {

T var1;

S var2;

public:

T func(void);

};

template <class T, class S> T demo<T,S>::func(void) {

return var1;

}

  1. Обработка исключений в С++. Привести примеры кода.

С++ обеспечивает встроенный механизм обработки ошибок, называемый обработкой исключительных ситуаций. Благодаря обработке исключительных ситуаций можно упростить управление и реакцию на ошибки времени исполнения. Обработка исключительных ситуаций в С++ строится с помощью трех ключевых слов: try, catch, throw. Операторы программы, во время выполнения которых вы хотите обеспечить обработку исключительных ситуаций, располагаются в блоке try. Если исключительная ситуация (т.е. ошибка) имеет место внутри блока try, искусственно она генерируется (с помощью throw). Перехватывается и обрабатывается исключительная ситуация с помощью ключевого слова catch. Любой оператор, который генерирует исключительную ситуацию, должен выполняться внутри блока try. (Функции, которые вызываются внутри блока try также могут генерировать исключительную ситуацию). Любая исключительная ситуация должна перехватываться оператором catch, который следует непосредственно за блоком try, генерирующим исключительную ситуацию.

try { // блок try

}

catch (type1 arg1) { // блок catch

}

catch (type2 arg2) { // блок catch

}

...

Блок try должен содержать ту часть программы, в которой вы хотите отслеживать ошибки. Это могут быть несколько операторов внутри одной функции, так и все операторы функции main() (что естественно вызывает отслеживания ошибок во всей программе).

Когда исключительная ситуация возникает, она перехватывается соответствующим ей оператором catсh, который ее обрабатывает. С блоком try может быть связано более одного оператора catch. То, какой конкретно оператор catch используется, зависит от типа исключительной ситуации. Т.е., если тип данных, указанный в операторе catch, соответствует типу исключительной ситуации, то выполняется данный оператор catch. А все другие операторы блока try пропускаются. Если исключительная ситуация перехвачена, то аргумент arg получает ее значение. Можно перехватить любые типы данных, включая и создаваемые вами типы.

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

throw; или throw выражение;

Выражение throw устанавливает исключение. Самый внутренний блок try, в котором устанавливается исключение, используется для выбора оператора catch, который обрабатывает исключение. Выражение throw без аргумента повторно устанавливает текущее исключение. Обычно оно используется для дальнейшей обработки исключения вторым обработчиком, вызываемый из первого.

Рассмотрим пример работы исключительной ситуации.

void main(){

try{ throw 10; }

catch(int i) { cout << " error " << i << endl; }

return;

}

На экран выведется сообщение: error 10. Значение целого числа, выданное через throw i, хранится до завершения работы обработчика с целочисленной сигнатурой catch(int). Это значение доступно для использования внутри обработчика в виде аргумента.

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

catch(int n) { . . .

throw; // переустановка

}

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

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

catch (char *message) { cerr << message << endl; exit(1); }

catch (...) { // действие, которое нужно приянть по умолчанию.

cerr << “ That’s all folks” << andl; abort();

}

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

Синтаксически Спецификация исключения представляет собой части объявления функции и имеет форму:

заголовок_функции throw (список типов)

Список типов - это список типов, которые может иметь выражение throw внутри функции. Если этот список пуст, компилятор может предположить, что throw не будет выполняться функцией.

void foo() throw(int,over_flow);

void noex(int i) throw();

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

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

Восстановление при возникновении ошибок - в основном имеет отношение к правильности написания программы. Восстановление при возникновении ошибок основывается на передачи управления. Недисциплинированная передача управления ведет к хаосу. При восстановлении отказов предполагается, что исключение нарушило вычисления. Продолжать вычисления становится опасно. Обработка исключений влечет за собой упорядоченное восстановление при появлении отказа. В большинстве случаев программирование, которое вызывает исключения, должно выводить диагностическое сообщение и элегантно завершать работу. При специальных формах обработки, типа работы в режиме реального времени и при отказоустойчивом вычислении существует необходимость в том, чтобы система не приостанавливалась. В таких случаях героические попытки восстановления узаконены. С++ использует модель завершения, которая вынуждает завершать текущий блок try. При этом режиме пользователь или повторяет код, игнорируя исключение, или подставляет результат по умолчанию и продолжает. При повторении кода более вероятно получить правильный результат. Опыт показывает, что обычно код слабо комментируется. Хорошо спланированный набор ошибочных условий, обнаруживаемых пользователями абстрактных типов данных – важная часть проекта. Если при нормальном программировании слишком часто обнаруживаются ошибки и происходит прерывание - это признак того, что программа плохо обдумана.

  1. Стандартная библиотека шаблонов для С++ STL. Использование контейнеров и итераторов. Привести примеры кода.

Библиотека STL содержит пять основных видов компонентов:

- алгоритм (algorithm): определяет вычислительную процедуру.

- контейнер (container): управляет набором объектов в памяти.

- итератор (iterator): обеспечивает для алгоритма средство доступа к содержимому контейнера.

- функциональный объект (function object): инкапсулирует функцию в объекте для использования другими компонентами.

- адаптер (adaptor): адаптирует компонент для обеспечения различного интерфейса.

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

В STL определены два типа контейнеров: последовательности и ассоциативные. Ключевая идея для стандартных контейнеров заключается в том, что когда это представляется разумным, они должны быть логически взаимозаменяемыми. Пользователь может выбирать между ними, основываясь на соображениях эффективности и потребности в специализированных операциях. Например, если часто требуется поиск по ключу, можно воспользоваться map (ассоциативным массивом). С другой стороны, если преобладают операции, характерные для списков, можно воспользоваться контейнером list. Если добавление и удаление элементов часто производится в концы контейнера, следует подумать об использовании очереди queue, очереди с двумя концами deque, стека stack. По умолчанию пользователь должен использовать vector; он реализован, чтобы хорошо работать для самого широкого диапазона задач.

В STL определены следующие классы-контейнеры (в угловых скобках указаны заголовочные файлы, где определены эти классы):

bitset

множество битов <bitset.h>

vector

динамический массив <vector.h>

list

линейный список <list.h>

deque

двусторонняя очередь <deque.h>

stack

стек <stack.h>

queue

очередь <queue.h>

priority_queue

очередь с приоритетом <queue.h>

map

ассоциативный список для хранения пар ключ/значение <map.h>

multimap

с каждым ключом связано два или более значений <map.h>

set

множество <set.h>

multiset

множество, в котором элемент не обязательно уникален <set.h>

Контейнер vector. Вектор vector в STL определен как динамический массив с доступом к его элементам по индексу.

vector<int> a; vector<double> x(5); vector<char> c(5,’*’); vector<int> b(a);

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

Для класса вектор определены следующие операторы сравнения: ==, <, <=, !=, >, >=. Кроме этого, для класса vector определяется оператор индекса []. Новые элементы могут включаться с помощью функций insert(), push_back(), resize(), assign(). Существующие элементы могут удаляться с помощью функций erase(), pop_back(), resize(), clear(). Доступ к элементам осуществляется с помощью итераторов begin(), end(), rbegin(), rend(). Манипулирование контейнером, сортировка, поиск в нем и тому подобное возможно с помощью глобальных функций <algorithm.h>. Пример:

#include<iostream.h>

#include<vector.h>

using namespace std;

void main(){

vector<int> v; int i;

for(i=0;i<10;i++) v.push_back(i);

for(i=0;i<10;i++)v[i]=v[i]+v[i];

for(i=0;i<v.size();i++) cout<<v[i]<<“ ”; cout<<endl; }

Пример – Доступ к вектору через итератор:

#include<iostream.h>

#include<vector.h>

using namespace std;

void main(){

vector<int> v; int i;

for(i=0;i<10;i++)v.push_back(i); cout<<“size=”<<v.size()<<“\n”;

vector<int>::iterator p=v.begin();

while(p!=v.end()) {cout<<*p<<” “;p++;}

}

Пример – Вектор содержит объекты пользовательского класса:

#include<iostream.h>

#include<vector.h>

#include”student.h”

using namespace std;

void main() {

vector<STUDENT> v(3); int i;

v[0]=STUDENT(“Иванов”,45.9);

v[1]=STUDENT(“Петров”,30.4);

v[0]=STUDENT(“Сидоров”,55.6);

for(i=0;i<3;i++) cout<<v[i]<<“ ”; cout<<endl; //вывод

}

Ассоциативные контейнеры (массивы). Ассоциативный массив содержит пары значений. Зная одно значение, называемое ключом (key), мы можем получить доступ к другому, называемому отображенным значением (mapped value). Ассоциативный массив можно представить как массив, для которого индекс не обязательно должен иметь целочисленный тип: V& operator[](const K&) возвращает ссылку на V, соответствующий K.

Ассоциативные контейнеры – это обобщение понятия ассоциативного массива. Ассоциативный контейнер map  это последовательность пар (ключ, значение), которая обеспечивает быстрое получение значения по ключу. Типичная операция для ассоциативного контейнера – это ассоциативный поиск при помощи операции индексации ([]).

Множества set можно рассматривать как ассоциативные массивы, в которых значения не играют роли, так что мы отслеживаем только ключи.

  1. Стандартная библиотека шаблонов для С++ STL. Использование алгоритмов. Привести примеры кода.

Библиотека STL содержит пять основных видов компонентов:

- алгоритм (algorithm): определяет вычислительную процедуру.

- контейнер (container): управляет набором объектов в памяти.

- итератор (iterator): обеспечивает для алгоритма средство доступа к содержимому контейнера.

- функциональный объект (function object): инкапсулирует функцию в объекте для использования другими компонентами.

- адаптер (adaptor): адаптирует компонент для обеспечения различного интерфейса.

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

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

Алгоритмы определены в <algorithm.h>. Ниже приведены имена некоторых наиболее часто используемых функций-алгоритмов STL.

Немодифицирующие операции.

for_earch()

выполняет операции для каждого элемента последовательности

find()

находит первое вхождение значения в последовательность

find_if()

находит первое соответствие предикату в последовательности

count()

подсчитывает количество вхождений значения в последовательность

count_if()

подсчитывает количество выполнений предиката в последовательности

search()

находит первое вхождение последовательности как подпоследовательности

search_n()

находит n-е вхождение значения в последовательность

Модифицирующие операции.

copy()

копирует последовательность, начиная с первого элемента

swap()

меняет местами два элемента

replace()

заменяет элементы с указанным значением

replace_if()

заменяет элементы при выполнении предиката

replace_copy()

копирует последовательность, заменяя элементы с указанным значением

replace_copy_if()

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

fill()

заменяет все элементы данным значением

remove()

удаляет элементы с данным значением

remove_if()

удаляет элементы при выполнении предиката

remove_copy()

копирует последовательность, удаляя элементы с указанным значением

remove_copy_if()

копирует последовательность, удаляя элементы при выполнении предиката

reverse()

меняет порядок следования элементов на обратный

random_shuffle()

перемещает элементы согласно случайному равномерному распределению (“тасует” последовательность)

transform()

выполняет заданную операцию над каждым элементом последовательности

unique()

удаляет равные соседние элементы

unique_copy()

копирует последовательность, удаляя равные соседние элементы

Сортировка.

sort()

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

эффективностью

partial_sort()

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

stable_sort()

сортирует последовательность, сохраняя порядок

следования равных элементов

lower_bound()

находит первое вхождение значения в отсортированной последовательности

upper_bound()

находит первый элемент, больший чем заданное значение

binary_search()

определяет, есть ли данный элемент в отсортированной последовательности

merge()

сливает две отсортированные последовательности

Работа с множествами.

includes()

проверка на вхождение

set_union()

объединение множеств

set_intersection()

пересечение множеств

set_difference()

разность множеств

Минимумы и максимумы.

min()

меньшее из двух

max()

большее из двух

min_element()

наименьшее значение в последовательности

max_element()

наибольшее значение в последовательности

Перестановки.

next_permutation()

следующая перестановка в лексикографическом порядке

pred_permutation()

предыдущая перестановка в лексикографическом порядке

Пример 1:

#include <vector>

#include <iostream>

#include <algorithm>

using namespace std;

void Print(int x)

{ cout << x <<' '; }

int main()

{ vector<int> v(4);

v[0] = 3;

v[1] = 1;

v[2] = 5;

v[3] = 2;

sort(v.begin(), v.end() );

for_each(v.begin(), v.end(), Print);

return 0;

}

Результатом работы программы будет массив

1 2 3 5

}

Пример 2:

#include < algorithm>

#include <iostream.h>

bool odd (int a_){

return a_ % 2;

}

int numbers[6] = { 2, 4, 8, 15, 32, 64 };

int main (){

int* location = find_if (numbers, numbers + 6, odd);

if (location != numbers + 6)

cout

<< "Value "

<< *location

<< " at offset "

<< (location - numbers)

<< " is odd"

<< endl;

return 0;

}