Наследование, полиморфизм, инкапсуляция, исключения 2 страница

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

Деструктор класса-наследника всегда вызывает деструктор базового класса.

Уровни доступа

Любой элемент класса имеет один из трех уровней доступа:

private (закрытый) – доступен только из функций-членов данного класса и дружественных функций;

protected (защищенный) – доступен как private, а также из функций-членов производных классов;

public (открытый) – доступен всюду, где виден класс.

 

В объявлении класса обычно сначала перечисляются все открытые элементы, потом защищенные, потом закрытые.

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

 

class Derived: сп_доступа Base1 [, сп_доступа Base2,...] {...};

 

Замечание. Базовых классов больше одного, если наследование множественное.

Спецификаторы определяют уровень доступа к элементам базового класса внутри производного. Уровень доступа к элементу определяется как меньший из двух - того, что элемент имеет внутри базового класса и того, что указан в производном классе (private < protected < public).

 

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

 

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

 

Пример. Возврат утраченного статуса.

ß

class Base {

public:

void f1();

};

 

class Derived: private Base {

public:

Base::f1; // делает f1() снова публичным

};

 

Если элемент описан в базовом классе как private, его никак нельзя сделать public в производном классе.

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

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

 

Пример. Класс с виртуальными функциями.

Определим базовый класс date

ß


 

class date {

protected:

int day, month, year;

public:

date(int,int,int);

void set_year(int y);

void print();

};

 

date::date(int d, int m, int y)

{

day = d; month = m; year = y;

}

 

void date::set_year(int y)

{

year = y;

print();

}

 

void date::print()

{

printf("%d-%d-%d\n", day, month, year);

}

 

и производный от него класс birthday:

ß

class birthday: public date {

public:

char name[80];

birthday(int d, int m, int y, char* n);

void print();

};

 

birthday::birthday(int d, int m, int y, char* n) : date(d, m, y)

{

strcpy(name, n);

}

 

void birthday::print()

{

printf("%d-%d-%d, dear %s\n", day, month, year, name);

}

 

Следующий код

ß

date* d = new date(19, 10, 1951);

birthday* b = new birthday(19, 10, 1951, "Bond");

d->set_year(2001);

b->set_year(2001);

 

напечатает: "19-10-1951" два раза.

 

Если же сделать метод date::print() виртуальным,

ß

virtual void print(int y);

 

то тот же код напечатает:

 

"19-10-1951"

"19-10-1951, dear Bond".

 

Чтобы сделать метод виртуальным, достаточно объявить его с описателем virtual.

 

Для виртуальных функций действуют следующие правила:

 

виртуальную функцию нельзя объявлять как static; .

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

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

virtual void print() = 0;

 

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

Механизм работы виртуальных функций

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

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

Вызов виртуальной функции происходит через vtable, на которую указывает vptr объекта, делающего вызов. Адрес вызываемой функции определяется не во время трансляции (это было бы раннее связывание), а во время выполнения программы (позднее связывание). Это и позволяет из функции set_year ( ), унаследованной от базового класса, обратиться к функции print( ), определенной в производном классе.

Полиморфизм

На объект класса Birthday может указывать как переменная типа Birthday*, так и переменная типа Date*. Независимо от типа указателя виртуальные методы будут вызываться в соответствии с истинным классом объекта.

 

Пример.

ß

Date *pd;

pd = new date(10, 10, 2000);

pd->print(); // Выводится объект класса date

delete pd;

 

pd = new birthday(10, 10, 2000, “Peter”);

pd->print(); // Выводится объект класса birthday

 

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

 

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

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

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

 

class AB: public A, public B {...};

 

Класс AB наследует все компоненты классов A и B. Если компоненты базовых классов имеют одинаковые имена, неоднозначность устраняется операцией разрешения видимости:

 

A::имя или B::имя.

 

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

 

Возможен случай, когда оба класса A и B имеют в числе предков класс R (рисунок а).

 

R R R

| | / \

A B A B

\ / \ /

AB AB

 

а) б)

 

В этом случае класс AB косвенно наследует два разных экземпляра класса R и при обращении к компонентам R из функций AB потребуется уточнить, какой R имеется в виду:

A::имя или B::имя.

Чтобы класс AB наследовал не два, а один экземпляр класса R, его надо объявить виртуальным при определении классов A и B.

ß

class A: virtual public R {...

class B: virtual public R {...

 

Тогда возникнет ситуация, изображенная на рис.1-б, уточнение не понадобится и к тому же сэкономятся ресурсы.

 

Пример. Множественное наследование с общим предком.

ß

class C0 {

public:

void f() { cout << "f from C0" << endl; };

};

 

class C1: public C0 {

public:

void f() { cout << "f from C1" << endl; };

};

 

class C2: public C0 {

public:

void f() { cout << "f from C2" << endl; };

};

 

class C3: public C1, public C2 {

public:

void f() { cout << "f from C3" << endl; };

};

 

///////////////////////// использование ///////////////

C3 c;

c.C1::f(); // uses vtbl for C1

c.C2::f(); // uses vtbl for C2

// ((C1)c).f(); - ОШИБКА!

((C0)(C1)c).f();

 

Списки инициализации

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

 

Пример. Конструктор со списком инициализации.

ß

// Базовый класс

 

class Complex {

public:

Complex( float re, float im )

{real = re; imag = im;};

}

 

// Производный класс

 

class Triplex : public Complex {

public:

Triplex(float re, float im, int co):

Complex(re, im) { color = co;};

}

 

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

ß

Triplex(float re, float im, int co):Complex(re, im), color(co) {};

 

Список инициализации является единственным средством инициализации элементов-констант, элементов-ссылок и элементов-объектов, конструкторы которых имеют параметры.

 

 

ЗАДАНИЯ НА ЛАБОРАТОРНУЮ РАБОТУ

 

 

Необходимо решить задание из предыдущей лабораторной работы в следующем виде:

Операции 1-3 необходимо реализовать в базовом классе;

Операции 4-7 необходимо реализовать в классе-наследнике;

Примечание: поскольку операция присваивания не наследуется, ее необходимо определить также в классе-наследнике.


2 Работа с векторами стандартной библиотеки с++

 

3.1. Векторы стандартной библиотеки

 

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

vector<тип> (например, vector<int> или vector<string>)

Для использования векторов нужно подключить заголовочный файл <vector> и пространство имен std:

using std::vector;

Вектор можно использовать практически так же, как и массив. Вот так задается определение вектора заданной длины:

vector<int> v(n);

Начальное значение числа элементов n может быть как константой, так и переменной.

Доступ к элементам вектора аналогичен доступу к элементам массива: v[3] = 100;

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

for (int i=0; i < v.size(); i++)

cout << v[i];

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

vector<int> v;

и в цикле добавляют в него элементы с конца с помощью функции-элемента

push_back(), которая принимает аргумент того же типа, что и элементы вектора:

int elem;

for (int i=0; i<n; i++) {

cin >> elem;

v.push_back(elem);

}

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

// n и m - переменные

vector<vector<int> > ma(m); // пробел здесь обязателен

for (int i=0; i<m; i++) {

vector<int> line(n);

ma[i] = line;

// можно записать и короче: ma[i] = vector<int>(n);

// доступ аналогичен двумерному массиву

for (int j=0; j<n; j++)

cin >> ma[i][j];

}

Обход такого вектора производится так:

for (int i=0; i<ma.size(); i++)

for (int j=0; j<ma[i].size(); j++)

cout << ma[i][j];

 

Вектора можно использовать вместе с глобальными служебными функциями типа sort() или reverse(). Такие функции (их еще называют алгоритмами) работают с произвольными последовательностями значений вне зависимости от их природы (даже и с обычными массивами). В них передаются данные о начале и конце обрабатываемой последовательности. Начало вектора выдает функция-элемент begin(), а конец - end(). Для использования алгоритмов нужно подключать заголовочный файл <algorithm>:

#include <algorithm> // для sort() и reverse()

using std::sort;

using std::reverse;

vector v(10);

// заполнить v

sort (v.begin(), v.end()); // сортировать вектор

reverse (v.begin(), v.end()); // перевернуть вектор

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

int max_from_vector = *max_element(v.begin(), v.end());

Есть аналогичная функция min_element().

 

 

ЗАДАНИЯ НА ЛАБОРАТОРНУЮ РАБОТУ

Создать программу соответствующего варианта лабораторной работы с использованием векторов стандартной библиотеки.

 

 

ВАРИАНТ 1

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

a[1,1] a[2,1] a[3,1]

a[1,2] a[2,2] ...

 

 

ВАРИАНТ 2

 

 

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

a[3,1] a[2,1] a[1,1]

a[3,2] a[2,2] ...

 

 

ВАРИАНТ 3

 

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

 

 

ВАРИАНТ 4

 

 

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

 

 

ВАРИАНТ 5

 

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

 

 

ВАРИАНТ 6

 

 

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

- суммами отрицательных элементов строк a, если отрицательных элементов в строке больше, чем положительных;

- минимальным элементом строки, если отрицательных элементов меньше;

- числом нулевых элементов в строке, если их поровну

и выводит массив b на экран

 

 

ВАРИАНТ 7

 

 

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

 

 

ВАРИАНТ 8

 

 

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

 

 

ВАРИАНТ 9

 

 

Создать программу, которая вводит c клавиатуры (с обработкой ошибок) массив целых чисел, находит в нем все повторяющиеся последовательности
(1 1, 2 2 и т.д.) и заменяет каждую из них на два элемента: повторяющееся число и длину последовательности. Выдать результирующую последовательность и пары "длина - число последовательностей данной длины"

Пример:

вход: 1 1 1 2 2 2 2 3 4 4 4 5 5 5

выход: 1 3 2 4 3 4 3 5 3

1 - 1

2 - 0

3 - 3

4 - 1

 

 

ВАРИАНТ 10

 

 

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

Пример:

вход: 1 2 3 4 1 3 5 2 1

выход: 1 1 3 3 3 3 1 1 1 5 5

1 - 0

2 - 2

3 - 1

4 - 1

 


4 Работа со строками стандартной библиотеки с++

 

 

4.1. Строки стандартной библиотеки.

 

Другой подход к работе со строками - использовать строки стандартной библиотеки (string-строки). Такие строки значительно удобнее в работе, чем символьные массивы.

Для использования таких строк нужно подключать заголовочный файл string и пространство имен std:

#include <string>

using std::string;

Их можно инициализировать строковыми константами и другими строками, а можно определять без инициализации:

string s = "test";

string s2 = s;

string s5; // пустая строка

Доступ к отдельным символам в строке аналогичен доступу к элементам массива:

s[2] = 'k'; // s == "tekt"

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

(конкатенируются):

string s3 = s+s2;

s += " end of string";

Строки можно копировать, при этом копируется содержимое:

s2 = s;

s = "test2";

Строки можно выводить в поток:

cout << s << endl;

Каждая строка содержит в себе свою длину, к которой можно получить доступ с помощью функции-элемента length():

cout << s3.length();

Строки можно сравнивать с помощью обычных операций отношения:

if (s == s2) { }

Для строк существует множество служебных функций-элементов. Среди них:

1) substr(), которая возвращает подстроку:

s = "this is a string";

s2 = s.substr(0, 4); // s2 == "this"

2) insert(), которая вставляет строку в заданную позицию

s.insert(10, "new "); // s == "this is a new string"

3) find(), которая возвращает позицию найденного символа или подстроки

int i = s.find ('a'); // вернет 8

i = s.find ("is"); // вернет 2

i = s.find ('w'); // символа в строке нет - вернет -1

Можно искать, начиная с заданной позиции в строке

i = s.find ("is",3); // вернет 5 - поиск, начиная с позиции 3

4) rfind() - то же самое, но начиная с конца строки

i = s.rfind ("is"); // вернет 5

5) replace() - замена подстроки на другую подстроку

s.replace(0, 4, "that"); // "that is a new string"

6) erase() - удаление подстроки

s.erase(0, 5); // "is a new string"

 

4.2. Использование строк с векторами стандартной библиотеки.

 

Вектора стандартной библиотеки могут содержать строки как свои элементы:

vector<string> vs;

Использование аналогично:

vs.push_back("string 1");

vs.push_back("string 2");

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

 

В Visual C++ 6.0 при использовании векторов строк компилятор выдает большое количество предупреждений, связанных с тем, что он генерирует слишком длинные имена. Эти предупреждения не свидетельствуют о какой-то ошибке, но мешают восприятию результатов компиляции. Можно подавить эти предупреждения, используя директиву компилятора, которая задается командой препроцессора #pragma

#pragma warning (disable: 4786)

где 4786 - это код сообщения (в данном случае - о слишком длинных

именах). Данная директива должна находиться в самом начале файла.

 

4.3. Некоторые дополнительные сведения о вводе-выводе.

 

При вводе строк стандартной библиотеки (объектов типа string) эти строки расширяются автоматически, так что места для введенных символов всегда достаточно (если хватает ресурсов компьютера). Ввод таких строк с помощью обычного оператора >> аналогичен вводу объектов простого типа (например, целочисленных):

string s;

cin >> s;

введет строку до первого пробельного символа.

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

string s;

getline(cin, s, '\n');

После ввода легко узнать число реально введенных символов - это длина получившейся строки - s.length().

В Visual C++ 6.0 функция getline() содержит серьезную ошибку, которая делает ее практически неработоспособной. На ВЦ кафедры АСУ эта ошибка исправлена (как исправлять, описано в файле lab-3.1bug.txt).

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

Для того, чтобы считать этот символ, можно использовать операцию

cin.get().

char ch = cin.get();

Здесь с клавиатуры считывается один символ (в том числе это может быть пробел или символ новой строки) и помещается в переменную ch.

Если написать просто

cin.get();

то символ будет считан, но в программу не попадет.

 

ЗАДАНИЯ НА ЛАБОРАТОРНУЮ РАБОТУ

 

ВАРИАНТ 1

 

Цель работы:

a) Изучение работы со строками стандартной библиотеки.

 

Создать программу, которая вводит c клавиатуры массив строк и еще две

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

соответствующие символы второй строки (транслирует строки). Выдать

оттранслированные строки и массив пар "символ"-"число замен".

 

ВАРИАНТ 2

 

Цель работы:

a) Изучение работы со строками стандартной библиотеки.

 

Создать программу, которая вводит c клавиатуры массив строк,

подсчитывает реальную длину каждой строки без учета пробелов и выводит

на экран (вместе с самой строкой). Программа должна выдавать также

число пробелов в каждой строке.

 

ВАРИАНТ 3

 

Цель работы:

a) Изучение работы со строками стандартной библиотеки.

 

Создать программу, которая вводит c клавиатуры массив строк, и выводит

список комбинаций "слово" - "строки, в которых оно встречается":

слово1 4 12

слово2 5 14

отсортированный по алфавиту слов

 

 

ВАРИАНТ 4

 

Цель работы:

a) Изучение работы со строками стандартной библиотеки.

 

Создать программу, которая вводит c клавиатуры массив строк,

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

строками. Выдать также список пар "число слов" - "число строк с таким

количеством слов". Слова разделяются пробелами.

 

ВАРИАНТ 5

 

Цель работы:

a) Изучение работы со строками стандартной библиотеки.

 

Создать программу, которая вводит c клавиатуры массив строк, находит в

нем все повторяющиеся последовательности (a a, b b b и т.д.) и заменяет

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

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

пар "длина последовательности" - "число последовательностей с заданной

длиной".

 

ВАРИАНТ 6

 

Цель работы:

a) Изучение работы со строками стандартной библиотеки.

 

Создать программу, которая вводит c клавиатуры массив строк,

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

"слово-число повторений". Слова разделяются пробелами.

 

ВАРИАНТ 7

 

Цель работы:

a) Изучение работы со строками стандартной библиотеки.

 

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

строки длиннее указанного числа (например, 70) и выдает такие строки на

экран, разбитые на части на ближайшей границе слова (делает "перенос на

границе слова" (word wrapping). Остальные строки выводятся без

изменений. Слова разделяются пробелами.

 

ВАРИАНТ 8

 

Цель работы:

a) Изучение работы со строками стандартной библиотеки.

 

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

строки короче указанного числа (например, 70) и выдает такие строки на

экран, дополненные пробелами между словами до нужной длины (text

justification). Остальные строки обрезаются до той же нужной длины.

Слова разделяются пробелами.

 

ВАРИАНТ 9

 

Цель работы:

a) Изучение работы со строками стандартной библиотеки.

 

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

к нему алгоритм "диссоциированного текста" (dissociated text), т.е.

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

начинается с одной и той же последовательности символов (экран-раненый,

короче-очень и т.д.) и заменяет первое слово пары на первое

слово+окончание второго (экраненый, корочень и т.д.). Слова разделяются

пробелами. Процедуру можно повторять, сокращая длину

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

 

 

ВАРИАНТ 10

 

Цель работы:

a) Изучение работы со строками стандартной библиотеки.

 

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

все вхождения указанного слова (аналог программы grep). На экран

выводятся строки, содержащие слово, с номерами этих строк и позициями, в

которых найдено это слово. В выводимой строке каждое найденное слово

окружается символами '*'.

 

 


5 Алгоритмы стандартной библиотеки С++

 

ЧАСТЬ 1. ПРЕДВАРИТЕЛЬНЫЕ СВЕДЕНИЯ

 

1. Общие сведения об алгоритмах

 

Алгоритм - это шаблонная функция, объявленная в пространстве имен std и в заголовочном файле <algorithm>, которая работает с элементами произвольных последовательностей, заданных итераторами. Последовательность задается двумя итераторами. Первый из них указывает на начало последовательности, а второй - на элемент, следующий за ее концом. Это сделано для того, чтобы не выделять специально последовательность, которая не содержит ни одного элемента (для нее просто второй итератор указывает туда же, куда и первый). Для контейнеров стандартной библиотеки такие итераторы возвращаются функциями-элементами begin() и end(), для массивов это будут указатели на первый элемент и за последний.