Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Алгоритмические языки и программирование. Учебное пособие

.pdf
Скачиваний:
0
Добавлен:
12.08.2026
Размер:
997 Кб
Скачать

Абстрактный базовый класс

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

class B{ public:

virtual void Iam(){}

};

class D1 : public B{ public:

virtual void Iam(){

cout << "I am D1";

}

};

class D2 : public B{ public:

virtual void Iam(){

cout << "I am D2";

}

};

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

class B{ public:

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

};

61

class D1 : public B{ public:

virtual void Iam(){cout << "I am D1";}

};

class D2 : public B{ public:

virtual void Iam(){cout << "I am D2";}

};

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

class Person{ public:

virtual void Hello()=0; virtual ~Person(){}

};

class Student : public Person{ public:

virtual void Hello(){

cout << "Hello! I am a Student.";

}

virtual ~Student(){}

};

class Professor : public Person{ public:

virtual void Hello(){

cout << "Hello! I am a Professor.";

}

virtual ~Professor(){}

};

62

class Worker : public Person{ public:

virtual void Hello(){

cout << "Hello! I am a Worker.";

}

virtual ~Worker(){}

};

int main(){

Person* Array[] = {new Student, new Professor, new Worker}; for(int i = 0; i < 3; ++i){

Array[i]->Hello(); cout << endl; delete Array[i];

}

return 0;

}

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

Рассмотрим пример со множественным наследованием: class B{

public:

void Hello(){

cout << "Hello!";

}

};

class D1 : public B{};

class D2 : public B{};

class C : public D1, public D2{};

63

int main(){ C c; c.Hello(); return 0;

}

Как ни странно, при компиляции этой программы мы получим сообщение об ошибке дублирования функции Hello. Дело в том, что каждый из классов D1, D2 получает копию класса B с копией метода Hello, а класс C получает две копии этого метода. При вызове компилятор не может определить, какую копию мы хотим вызвать. Для решения проблемы применяется виртуальное наследование:

class D1 : virtual public B{};

class D2 : virtual public B{};

При этом в классах D1, D2 не происходит копирование класса B, создается только указатель на класс B.

Преобразование классов

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

class B{ public:

virtual void F(){} //нужен для оператора dynamic_cast

};

64

class D : public B{};

int main(){ B b;

D d;

D *PtrD = dynamic_cast<D*>(&b); cout << PtrD << endl;

cout << endl;

B *PtrB = dynamic_cast<B*>(&d); cout << PtrB << endl;

return 0;

}

Информация о типе

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

class B{ public:

virtual void F(){} //нужно для оператора typeid

};

class D : public B{};

int main(){

D *PtrD;

B *PtrB;

65

cout << typeid(PtrD).name() << ' ' << typeid(PtrB).name();

return 0;

}

Стандартная библиотека

Архитектура

Библиотека разработана программистами Александром Степановым и Менгом Ли из фирмы Hewlett Packard (США). Основными сущностями библиотеки являются контейнеры, алгоритмы и итераторы. Все они реализованы при помощи шаблонов, поэтому библиотека называется библиотекой шаблонов.

Контейнер – способ организации хранения данных. Примеры контейнеров – стек, связанный список.

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

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

Контейнеры

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

66

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

Имя

 

 

 

Назначение

 

 

 

 

 

 

 

 

size()

Возвращает

количество

элементов

в

 

контейнере

 

 

 

 

 

 

 

 

empty()

Возвращает true, если контейнер пуст

 

 

 

max_size()

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

 

контейнера

 

 

 

 

 

 

 

begin()

Возвращает итератор на начало контейнера

 

(итерации будут производиться в прямом

 

направлении)

 

 

 

 

 

 

 

end()

Возвращает итератор на последнюю позицию

 

в

контейнере

(итерации

в

прямом

 

направлении будут закончены)

 

 

 

 

 

rbegin()

Возвращает обратный итератор на конец

 

контейнера (итерации будут производиться в

 

обратном направлении)

 

 

 

 

 

rend()

Возвращает обратный итератор на начало

 

контейнера

(итерации

в

обратном

 

направлении будут закончены)

 

 

 

 

 

 

 

 

 

 

 

Последовательные контейнеры

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

Проблема с обычным массивом заключается в том, что его размер необходимо указывать на этапе компиляции. В STL для решения этой проблемы существует контейнер vector, который автоматически меняет свой размер при добавлении новых элементов. Для работы с вектором нужно подключить соответствующую библиотеку: #include <vector>.

67

}
cout << endl; return 0;

int main(){

 

vector<int> v;

// create a vector of ints

v.push_back(10);

// put values at end of array

v.push_back(11);

 

v.push_back(12);

 

v.push_back(13);

 

v[0] = 20;

// replace with new values

v[3] = 23;

 

for(int j=0; j<v.size(); j++) // display vector contents cout << v[j] << ' '; // 20 11 12 23

cout << endl; return 0;

}

Метод push_back добавляет элементы в конец контейнера, метод size возвращает текущее количество элементов. Для обращения к созданным элементам используется перегруженный оператор индексации [].

int main(){

// an array of doubles

double arr[] = { 1.1, 2.2, 3.3, 4.4 };

vector<double> v1(arr, arr+4); // initialize vector to array

vector<double> v2(4);

// empty vector of size 4

v1.swap(v2);

// swap contents of v1 and v2

while (!v2.empty()){

// until vector is empty,

cout << v2.back() << ' '; // display the last element

v2.pop_back();

// remove the last element

// output: 4.4 3.3 2.2 1.1

}

68

Сначала мы инициализируем контейнер обычным массивом, при этом передаем начало массива и конец (указатель на элемент за последним). Метод swap обменивает содержимое двух контейнеров, порядок следования при этом не меняется. Метод empty проверяет пустоту контейнера, метод back возвращает последний элемент, метод pop_back удаляет последний элемент.

int main(){

int arr[] = { 100, 110, 120, 130 }; //an array of ints

vector<int> v (arr, arr+4);

// initialize vector to array

cout << "\nBefore insertion: ";

for (unsigned int j = 0; j < v.size(); ++j){ // display all elements cout << v[j] << ' ';

}

v.insert (v.begin()+2, 115); // insert 115 at element 2

cout << "\nAfter insertion: ";

for(unsigned int j = 0; j < v.size(); ++j){ // display all elements cout << v[j] << ' ';

}

 

 

v.erase (v.begin()+2);

// erase element 2

 

cout << "\nAfter erasure:

";

 

for (unsigned int j = 0; j < v.size(); ++j){

// display all elements

cout << v[j] << ' ';

 

 

}

 

 

return 0;

 

 

}

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

69

Если существует необходимость добавлять элементы в середину контейнера, можно использовать контейнер list, который основан на связном списке. Необходимо подключить библиотеку #include <list>.

int main(){ list<int> List;

List.push_back(30);

// push items on back

List.push_back(40);

 

List.push_front(20);

// push items on front

List.push_front(10);

 

int Size = List.size();

// number of items

for (int j = 0; j < Size; ++j){

cout << List.front() << ' '; // read item from front

List.pop_front();

// pop item off front

}

 

return 0;

 

}

Методы push_back, back, push_front, front добавляют и возвращают элементы в конец и в начало соответственно. Для последовательного доступа к элементам используется метод pop_front, поскольку оператор индексирования для списков не определен.

int main(){

list<int> List1, List2;

int Arr1[] = { 40, 30, 20, 10 }; int Arr2[] = { 15, 20, 25, 30, 35 };

for (unsigned int j = 0; j < 4; ++j){

List1.push_back (Arr1[j]); // List1: 40, 30, 20, 10

}

70

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]