Алгоритмические языки и программирование. Учебное пособие
.pdfАбстрактный базовый класс
Наследование в совокупности с виртуальными функциями часто используется для выделения общего интерфейса. Такой подход принят в ситуации, когда требуется отделить интерфейс от реализации (например, есть несколько возможных реализаций интерфейса).
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
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
