close

Вход

Забыли?

вход по аккаунту

?

Kyritcin

код для вставкиСкачать
МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ
Федеральное государственное автономное
образовательное учреждение высшего образования
САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ
АЭРОКОСМИЧЕСКОГО ПРИБОРОСТРОЕНИЯ
К. А. Курицын, А. В. Рабин, К. Н. Рождественская
ТЕХНОЛОГИЯ ПРОГРАММИРОВАНИЯ
ВВЕДЕНИЕ В ООП
Учебное пособие
УДК 004.42
ББК 32.97
К93
Рецензенты:
доктор технических наук, профессор А. И. Водяхо;
кандидат технических наук, доцент А. А. Овчинников
Утверждено
редакционно-издательским советом университета
в качестве учебного пособия
Курицын, К. А.
К93 Технология программирования. Введение в ООП: учеб. пособие / К. А. Курицын, А. В. Рабин, К. Н. Рождественская. –
СПб.: ГУАП, 2018. – 116 с.
ISBN 978-5-8088-1267-3
Содержит основные термины и определения из области объектноориентированного программирования. Рассматриваются парадигмы
и основы объектно-ориентированного программирования на языке C++. Также рассматривается библиотека стандартных шаблонов
(Standard Template Library, STL).
Издается для студентов, обучающихся по направлению «Информатика и вычислительная техника» для использования при выполнении проектов по дисциплине «Технология программирования», первый семестр1.
УДК 004.42
ББК 32.97
Учебное издание
Курицын Константин Александрович
Рабин Алексей Владимирович
Рождественская Ксения Николаевна
ТЕХНОЛОГИЯ ПРОГРАММИРОВАНИЯ
ВВЕДЕНИЕ В ООП
Учебное пособие
Редактор Л. А. Яковлева
Компьютерная верстка В. Н. Костиной
Сдано в набор 14.02.18. Подписано к печати 11.04.18.
Формат 60 × 84 1/16. Усл. печ. л. 6,7. Уч.-изд. л. 7,3.
Тираж 50 экз. Заказ № 156.
Редакционно-издательский центр ГУАП
190000, Санкт-Петербург, Б. Морская ул., 67
ISBN 978-5-8088-1267-3
©
©
Курицын К. А., Рабин А. В.,
Рождественская К. Н., 2018
Санкт-Петербургский государственный
университет аэрокосмического
приборостроения, 2018
1Пособие подготовлено при поддержке Минобрнауки РФ при проведении научноисследовательской работы в рамках базовой части государственного задания в сфере
научной деятельности на 2017–2019 гг., задание № 2.9214.2017/БЧ.
Введение
Реальный мир вокруг нас составляют объекты: люди, животные,
растения, автомобили, здания. Мы мыслим представлениями об
объектах. Каждый объект обладает атрибутами: размером, формой,
цветом, весом и т. д. Все объекты имеют разное поведение: прыгают,
ходят, катятся и т. д.
Объектно-ориентированное программирование (ООП) пришло на
смену процедурному подходу в программировании.
Принципы ООП позволяют представить задачу в виде структурных
частей, взаимосвязанных между собой. Каждая часть задачи – самостоятельный объект, имеющий свои характеристики и поведение.
ООП дает возможность подойти к программированию с точки
зрения объектов и их взаимодействия между собой. Объектно-ориентированное программирование с помощью понятия класса создает объекты, использует понятия наследования, когда новые объекты имеют некоторые параметры от ранее созданных объектов, но и
содержат свои собственные параметры.
ООП инкапсулирует данные и методы в объекты, данные и функции тесно связаны между собой. Объекты обладают возможностью
сокрытия информации. Это значит, что объекты могут знать способы взаимодействия друг с другом с помощью определенного интерфейса, но они ничего не знают о реализации других объектов: подробности реализации скрыты внутри самих объектов.
В учебном пособии рассматриваются принципы ООП на языке С++, основанном на языке программирования C. Он был разработан Б.Страуструпом в 1979 г. и до сих пор является одним из самых
популярных языков программирования. В языке С программной
единицей является функция, а в С++ – класс, на основе которого
создаются представители класса, т. е. экземпляры объектов.
Наиболее важные нововведения в языке С++, о которых необходимо знать перед началом изучения ООП, приведены в приложении.
3
Глава 1
ВВЕДЕНИЕ В C++. ПАРАДИГМЫ ПРОГРАММИРОВАНИЯ
1.1. Введение в ООП
Объект – это ключевой момент в ООП. Простейшие объекты похожи
на структуру, которая содержит члены типа переменных и функции.
Некоторые объекты могут вообще не содержать данных, а определять
процесс и содержать функции для управления этим процессом.
Для типа объекта в ООП есть специальное название – класс. Определения классов позволяют создавать объекты, т. е. отдельные экземпляры класса.
Каждый объект имеет жизненный цикл, в который входят такие этапы, как построение и уничтожение. При создании экземпляра объекта
его нужно проинициализировать. Этот процесс инициализации и называется построением, он выполняется с помощью функции-конструктора.
При уничтожении объекта требуется выполнить операции по зачистке,
освобождению памяти; за эти операции отвечает функция-деструктор.
В целом можно сказать, что объект является переменной определенного пользователем типа класса. И каждый экземпляр такого
типа является составным.
1.2. Основные механизмы ООП
Основные механизмы ООП:
1. Инкапсуляция – механизм, который объединяет данные и код,
манипулирующий этими данными, а также защищает и то, и другое
от внешнего вмешательства. В ООП код и данные могут быть объединены вместе, в этом случае говорят, что создается «черный ящик». Объект, как экземпляр класса, поддерживает инкапсуляцию. Внутри объекта данные могут быть открытыми или закрытыми. Закрытые коды
или данные доступны только для других частей этого объекта и недоступны для тех частей программы, которые существуют вне объекта.
Если код и данные открыты, то они доступны всем частям программы.
2. Наследование – это процесс, посредством которого один объект
может приобретать свойства другого. Объект может наследовать основные свойства другого объекта и добавлять к ним черты, характерные только для него. Наследование поддерживает иерархию классов,
которая делает управляемыми большие потоки информации.
3. Полиморфизм – это свойство, которое позволяет одно и то же имя
использовать для решения двух или более схожих, но технически разных
задач. Целью полиморфизма ООП является использование одного имени
4
для задания общих для класса действий, «один интерфейс, множество
методов». Полиморфизм помогает снижать сложность программ, разрешая использование того же интерфейса для создания единого набора
действий. Выбор конкретного действия зависит от конкретной ситуации.
1.3. Инкапсуляция
Рассмотрим принцип инкапсуляции на примере программы по
работе с многомерным вектором, координаты которого выражены
целыми числами. Такой вектор может быть представлен массивом
целых чисел, где размер массива – размерность вектора.
Определим по шагам, что нужно сделать, чтобы добиться целостности и сохранности данных.
1) Напишем функции для работы с таким вектором:
– функция создания вектора CreateVector, принимающая в качестве аргумента размер size и возвращающая указатель на выделенный в памяти массив целых чисел;
– функция удаления вектора FreeVector, принимающая на вход
указатель на массив;
– функция сложения двух векторов AddVector. Эта функция
должна принимать на вход два вектора и возвращать новый вектор:
int *CreateVector(int size);
void FreeVector(int *data);
int AddVector(int *data1, int size1,
int* data2, int size2,
int &size);
Видно, что при таком подходе в соответствии со стилем языка С функции принимают большое число входных параметров, требуется объявлять большое количество переменных, отсутствует защита и логическая
связанность данных. Например, значение переменной, хранящей размер
массива, может быть изменено независимо от указателя на данные, в результате чего функции по работе с вектором будут работать некорректно.
2) Следующий шаг направлен на устранение большого количества
переменных и параметров функций – объединение данных, являющихся характеристиками моделируемого объекта (многомерного вектора)
в структуру. Такими характеристиками являются размерность вектора
(размер массива, size) и сами данные (указатель на массив, *data):
struct Vector{
int *data;
int size;
};
5
Теперь описанные ранее функции могут использовать объявленную
структуру. Количество входных параметров функций сократилось.
В функции AddVector выходной параметр size исключен, поскольку
содержится в структуре, возвращаемой функцией результата:
Vector CreateVector(int size);
void FreeVector(Vector &V);
Vector AddVector(const Vector& V1,
const Vector& V2);
На втором шаге остается незащищенность данных. Иными словами, к полям структуры имеют доступ любые функции и пользователь. Как и на предыдущем шаге, значение size переменной типа
Vector может быть изменено произвольным образом из любой точки
программы, в которой доступна переменная.
3) Третий шаг – это защита характеристик моделируемого объекта,
которая реализуется через создание класса Vector, который будет объединять характеристики и функции для работы с ним. Переменные,
объявленные внутри класса, называются полями класса, а функции –
методами. Поля и методы класса также называются членами класса.
Кроме того, класс определяет уровень доступа к членам класса. Доступ
к членам класса, объявленным в области public, имеют, как и в случае
со структурой, все функции программы. Эти члены класса называются открытыми или публичными, а сама область – открытой или публичной. Доступ к членам класса, объявленным в private, имеют только методы этого класса или дружественные классы и функции, речь
о которых пойдет дальше. Эти члены класса называются закрытыми
или приватными, а сама область – закрытой или приватной:
class Vector{
private: //это спецификатор доступа
/*data и size недоступны извне класса (пользователь не может
напрямую к ним обращаться), это является защитой данных класса*/
int *data;
int size;
public:
/*все, что находится под идентификатором доступа public,
определяет интерфейс класса, который будет использоваться клиентами
(пользователями) для манипулирования данными экземпляра класса*/
int getSize(); //методы класса
void setSize();
void clear(); // очистка вектора
};
6
При существовании класса Vector пользователь будет взаимодействовать с моделируемым объектом в функции main только через публичные методы Vector:
void main(){
Vector v1; //создание объекта типа Vector
v1.size = 5; //ошибка
v1.setSize(10); //установка размера моделируемого объекта
v1.getSize(); //считываем размер моделируемого вектора
Vector v2; //создание второго объекта типа Vector
v2.setSize(5); //установка размера второго вектора равным 5
v1.clear(); //удаление первого вектора
}
Синтаксис записи реализации методов класса Vector несколько
отличается от записи реализации функций. Перед названием метода добавляется название класса и оператор разрешения области ::.
Метод getSize() возвращает размер вектора – параметр size:
int Vector::getSize(){
return size;
}
Метод clear() обнуляет параметр size и удаляет данные, которые
хранились в объекте.
void Vector::clear(){
if (size == 0)
return;
delete [] data;
data = null;
size = 0;
}
Метод setSize() записывает переданное в метод значение в поле size,
выделяет память под соответствующее число элементов и сохраняет
указатель в поле класса data:
void Vector::setSize(int size){
if (size == this->size)
return;
//надо освободить память, вызываем метод clear();
clear();
if (size < 0) // формируем ошибку
error (“size < 0”); // Error – некоторая функция
if (size == 0)
7
return; // делать нечего
//выделяем память
data = new int [size];
if (data == NULL)
error (“out of memory”);
this -> size = size;
}
Клиенты класса используют класс, не зная внутренних деталей его
реализации. Если изменяется реализация класса, то клиентов класса
изменять не требуется. Это упрощает модификацию исходного кода.
1.4. Особенности написания и использования классов
Класс содержит объявления членов класса – элементов данных и
элементов функций. Элементы данных называют полями класса, а
элементы функций – методами класса. Методы представляют собой
прототипы либо также включают реализацию.
Например, для класса Vector::getSize реализация может быть
выполнена внутри объявления класса:
class Vector {
int size;
int* data;
public:
int getSize() {
return size;
}
void setSize(int size);
void clear();
};
Элементы данных не могут быть инициализированы при их объявлении в теле класса. Элементы данных должны быть инициализированы конструктором, или значения им присваиваются через
специальные функции установки значений (set-функции).
Классы могут включать в качестве элементов объекты других
классов. Такого рода повторное использование может значительно
повысить производительность труда программиста. Создание производных классов на основе существующих называется наследованием.
Класс – это, по сути, объявление нового типа данных. Создание
объектов требуемого класса происходит аналогично объявлению
переменных встроенных типов. При создании объекта выделяется
8
память под все описанные внутри класса переменные. Размер объекта складывается из размеров всех полей класса: sizeof(Vector) =
= sizeof(int) + sizeof(int*):
void main(){
Vector v1;
data
size
v1.size = 5; //ошибка
v1.getSize(); //Vector_getSize_Vector*(*v1)
Vector v2;
data
size
v2.getSize(); // Vector_getSize_Vector*(*v2);
v1.setSize(5); // выделение памяти под 5 элементов
v1.Clear();
}
Переменные и методы класса принадлежат области действия
класса. Внутри области действия класса элементы класса непосредственно доступны для всех методов этого класса и допускают обращение просто по имени. Вне области действия класса на элементы
класса можно ссылаться либо через имя объекта, либо через функции set и get.
Совет: Методы set и get должны проверять корректность вводимых данных и обеспечивать целостность параметров класса.
Операции доступа к членам класса идентичны операциям доступа к элементам структуры. Для доступа применяется операция выбора – точка (.) – в сочетании с именем объекта. Операция выбора
элемента – стрелка (->) – применяется в сочетании с указателем на
объект.
1.4.1. Указатель this
В классах введен специальный указатель this – указатель на текущий объект. Каждый метод класса при обращении получает данный указатель в качестве неявного параметра. Через него методы
класса могут получить доступ к другим членам класса.
Указатель this в методах класса X можно рассматривать как
входной параметр, имеющий тип X* или const X*, если метод отмечен спецификатором const как константный.
9
Рассмотрим пример с вызовом метода setSize класса Vector для
двух векторов:
void Vector::setSize(/* Vector* this, */ int size){
// если не указывать перед size указатель this, то
// компилятор будет использовать входной параметр
if (size == this->size)
return;
clear();
if (size < 0) // проверяем на наличие ошибки
error (“size < 0”); // Error – некоторая функция
if (size == 0)
return; // завершение выполнения функции
// для обращения к полю data указатель this можно
// не добавлять; компилятор это сделает автоматически
data = new int [size];
if (data == NULL)
error (“out of memory”);
this->size = size;
}
int main() {
Vector v1, v2;
v1.setSize(5); // в качестве this в метод передается &v1
v2.setSize(10); // в качестве this в метод передается &v2
return 0;
}
При вызове методов, которые принадлежат классу, им неявно передается тот самый указатель this. Это происходит автоматически и
незаметно для нас, так как он является скрытым первым параметром
любого метода-элемента класса. Указатель this указывает на адрес созданного объекта класса: в первом случае он получает адрес объекта v1
и указывает методу setSize(), в какие элементы объекта надо внести
изменения, во втором случае в метод передается адрес объекта v2.
Важно знать, что у нас есть возможность использовать указатель
this явно. Так, в рассмотренном выше примере указатель this применяется в тех случаях, когда компилятору требуется дать возможность отличить член класса size от одноименной локальной переменной или входного параметра.
Указатель this – это указатель на адрес объекта класса, при этом
он является скрытым первым параметром любого метода класса (кроме статических методов), а типом указателя выступает сам класс.
10
1.5. Отделение интерфейса от реализации
Фундаментальный принцип разработки программного обеспечения – отделение интерфейса от реализации. Это упрощает модификацию программ. Для клиентов класса нет необходимости знать внутреннюю реализацию классов. Реализация и последующие модификации не затрагивают их, пока не будет изменен интерфейс класса.
Важно: помещайте объявление класса в заголовочный файл, который должен быть включен любым клиентом, желающим использовать этот класс. Это открытый интерфейс класса. Помещайте определения методов в отдельный исходный файл. Это – реализация класса.
Пользователям класса нет необходимости видеть исходный код
для того, чтобы пользоваться классом. Однако необходимо предусмотреть возможности взаимодействия и создания объектов класса.
Заголовочные файлы (h-файлы) включаются (с помощью #include)
во все файлы, использующие этот класс, а исходный файл с определениями методов (cpp) компилируется и компонуется с файлом, содержащим главную программу.
Чтобы избежать повторного включения заголовочных файлов,
можно использовать директивы препроцессора #ifndef / #define /
#endif. Если заголовок не был ранее включен, то происходит включение операторов заголовочного файла, иначе – нет.
1.6. Функции доступа и сервисные функции
Не все методы имеет смысл делать общедоступными, т. е. частью
интерфейса класса. Некоторые методы остаются закрытыми и служат в качестве вспомогательных для других функций класса.
Методы доступа могут считывать или отображать данные (set и
get). Методы могут проверять истинность или ложность условий (предикатные методы), например, проверка на пустой класс (isEmpty).
Сервисный метод не является частью интерфейса. Это закрытый метод, который поддерживает работу открытых методов класса. Сервисные функции не предназначены для использования клиентами класса.
1.6.1. Возврат ссылок на члены класса
Метод, возвращающий ссылку на член класса, позволяет использовать этот метод в выражениях слева и справа от оператора
присваивания. Например, метод Data для класса Vector позволяет,
как получать значения элементов, так и изменять их:
int& Vector::Data(int i){
if ((i<0)||(i >= size))
11
error (“…”);
return data[i];
}
result.Data(i) = v1.Data(i) + v2.Data(i);
Ссылка на объект является псевдонимом самого объекта. Другими словами, ссылка приемлема для операций, приводящих к изменению значения по ссылке.
Следует с осторожностью относиться к методам класса, которые
возвращают неконстантную ссылку (указатель) на закрытый элемент класса. Это может нарушать инкапсуляцию данных. В некоторых случаях, например при определении оператора индексирования, возврат неконстантной ссылки допустим.
1.7. Дружественные функции
Для того чтобы функция имела доступ к приватным членам
класса, ее следует объявить дружественной. Для этого в классе
должно быть добавлено описание функции, предваряемое ключевым словом friend. В предыдущем примере операторы > и != были
объявлены дружественными классу Vector.
Понятия доступа не имеют отношения к объявлениям дружественных функций, поэтому они могут определяться в любом месте
этого класса или нескольких классов.
Дружественная функция определяется вне области действия
класса, но имеет доступ к закрытым полям класса.
Совет: Помещайте все объявления дружественных функций и
классов в начале определения класса сразу после его заголовка и не
помещайте перед этими объявлениями никаких спецификаторов
доступа (private, public, proected):
bool operator > (const Vector& l, const Vector& r){
if (&l == &r)
return false; // если адреса объекта одинаковые, значит,
объекты тоже одинаковые
if (l.size <= r.size)
size = r.size;
for (int i=0; i<size; i++){
if (l.data[i] < r.data[i])
return true;
else if (l.data[i] > r.data[i])
return false;
}
12
. . .
if (l.size > r.size)
return true;
return false;
}
Vector v1, v2;
bool b = v1 > v2; // operator > (v1, v2)
// b.operator = (operator > (v1, v2))
bool operator != (const Vector& l, const Vector& r){
// воспользуемся перегрузкой оператора равенства
// operator ! (operator == (l, r))
return !(l == r);
}
bool operator >= (const Vector& l, const Vector&r){
// operator >= (operator == (l, r), operator > (l, r))
return ((l == r) || (l > r));
}
Функция может быть дружественной к более, чем одному классу.
Рассмотрим программу, в которой определяются два класса — line
и box. Класс line содержит все необходимые данные и код для начертания горизонтальной пунктирной линии любой заданной длины,
начиная с указанной точки с координатами х и у и с использованием заданного цвета. Класс box содержит необходимый код и данные для того, чтобы изобразить прямоугольник с заданными левой
верхней и правой нижней точками, причем использовать для этого
указанный цвет. Оба класса используют функцию same_color() для
того, чтобы определить, нарисованы ли линия и прямоугольник одним и тем же цветом:
class box {
int color; // цвет прямоугольника
int upx, upy; // верхний левый угол
int lowx, lowy; // правый нижний угол
public:
friend int same_color (line l, box b);
void set_color(int c);
void define_box (int x1, int y1, int x2, int y2);
void show_box();
};
class line {
int color;
13
int startx, starty;
int len;
public:
friend int same_color (line I, box b);
void set_color(int c);
void define_line (int x, int y, int I);
void show_line();
};
// возвращает истину, если линия и прямоугольник имеют одинаковый
цвет
int same_color (line I, box b){
if (l.color == b.color) return 1;
return 0;
}
Имеется два важных ограничения применительно к дружественным функциям. Первое заключается в том, что производные классы
не наследуют дружественных функций. Второе заключается в том,
что дружественные функции не могут объявляться с ключевыми
словами static или extern.
Следует отметить, что наличие механизма дружественных функций не противоречит принципу инкапсуляции данных: сам класс
определяет перечень его друзей. Для того чтобы объявить функцию
дружественной, необходимо внести изменения в объявление класса.
Это означает, что полный контроль над доступом к данным класса
по-прежнему сосредоточен в самом классе.
1.8. Дружественные классы
Дружественный класс надо объявить ДО того, как мы укажем
в другом классе, что он является дружественным.
Когда класс объявлен дружественным, все его методы автоматически становятся дружественными к тому классу, в котором он
объявлен. При этом методы класса, который разрешил дружбу, не
имеют доступа к элементам дружественного класса.
Значит, используя методы класса Vector, мы можем обращаться
к элементам класса Matrix.
Объявление дружественного класса будет таким:
class Vector; // объявили сначала класс вектор
class Matrix{
friend Vector;
…
};
14
Важно: Не следует злоупотреблять и объявлять множество дружественных классов. Помните о том, что мы должны заботиться о
защите данных. Иногда целесообразней использовать дружественные функции.
Стоит отметить, что существование в языке С++ дружественных
классов и функций не противоречит принципу инкапсуляции, поскольку в самом классе определяется, какие части кода будут иметь
доступ к приватным членам класса.
Рассмотрим следующий код программы. В нем мы создадим два
класса. Один из них объявим дружественным и, используя его методы, внесем изменения в приватные элементы другого класса:
class child; //заранее объявляем класс, который станет дружественным
class schoolchild //определяем следующий класс
{
char name[16];
char surname[16];
int clas;
public:
schoolchild (char*, char*, int);//конструктор
void getData();
friend child;//указываем, что класс дружественный
};
// определяем методы класса schoolchild
schoolchild::schoolchild(char *n, char *s, int c)
{
strcpy(name, n);
strcpy(surname, s);
clas = c;
}
void schoolchild::getData(){
cout << name << “ “ << surname << “\t” << clas << “-й класс”<<
endl;
}
class child //определяем дружественный класс
{
public:
void changeClas(schoolchild &, int );
void getChangeData(schoolchild);
}
// определяем методы класса child
15
//передаем объект класса и вносим изменения в int clas
void child::changeClas(schoolchild &obj, int newCl){ obj.clas = newCl;
}
void child::getChangeData(schoolchild obj)
{
cout << obj.name << “ “ << obj.surname << “\t переведен(а) в
“ << obj.clas << “-й класс\n”;
}
int main()
{
setlocale(LC_ALL, “rus”);
//создаем объекты класса schoolchild
schoolchild visotscaya ( “Маргарита”, “Высоцкая”, 3);
schoolchild semenov ( “Александр”, “Семенов”, 3);
cout << "Список учеников 3-го класса:\n”;
visotscaya.getData();
semenov.getData();
child transfer; //создаем объект transfer - перевод в следующий
класс
transfer.changeClas(visotscaya, 4);
transfer.changeClas(semenov, 4);
cout << "\nПеревод в следующий класс:\n";
transfer.getChangeData(visotscaya);
transfer.getChangeData(semenov);
cout << "\nСписок учеников 4-го класса:\n";
visotscaya.getData();
semenov.getData();
return 0;
}
16
Глава 2
КОНСТРУКТОРЫ И ДЕСТРУКТОРЫ
При объявлении локальной переменной типа класс значения ее
полей не определены. Это ограничивает использование созданного
объекта: часть методов класса может предполагать определенные
значения полей, а также наличие соответствия между значениями
разных полей. После создания объекта необходимо установить должным образом значения всех его полей – выполнить инициализацию.
Например, для класса Vector в качестве инициализации полей
мог бы использоваться метод setSize:
void main() {
Vector v;
// объект v создан, но не инициализирован
v.setSize(5);
// поле объекта v инициализировано
}
В приведенном примере объект создается и инициализируется
отдельно. Поскольку для работы с объектом всегда требуется его
предварительная инициализация, она должна быть выполнена на
этапе создания экземпляра класса. Для этого в языке С++ предусмотрен специальный метод – конструктор класса.
2.1. Конструкторы
Конструктор представляет собой метод класса с тем же именем,
что и класс, но он не возвращает значений.
Конструкторов в классе может быть несколько. Если вы не определили ни одного конструктора, компилятор создаст конструктор
по умолчанию, не имеющий параметров.
Единственной задачей конструктора является инициализация полей. Все необходимые для решения этой задачи данные должны передаваться в качестве входных параметров. Например, для класса Vector
можно создать два конструктора: один – принимающий на вход требуемый размер, и второй – без параметров. При этом второй конструктор,
конечно же, не должен запрашивать размер у пользователя:
class Vector{
private:
int size;
int* data;
public:
void Clear();
void setSize(int size) {...}
17
int getSize() {return size;}
int getData(int i) {return data[i];}
void setData (int i, int value) {data[i] = value;}
Vector(){ // Конструктор по умолчанию
data = NULL; size = 0;
}
Vector (int size); // Перегруженный конструктор
};
Vector::Vector (int size){
data = NULL;
this->size = 0;
setSize(size);
}
void main(){
// вызывается конструктор по умолчанию
Vector v1;
// вызывается перегруженный конструктор Vector::Vector(int)
Vector v2(5);
// выделяется память sizeof(Vector),
// вызывается конструктор по умолчанию
Vector *vptr = new Vector;
Vector *vp2 = new Vector(5);
// для массивов нет возможности
//передать значения в конструктор
Vector *varr = new Vector [10]; // 10 раз вызывается конструктор
по умолчанию
}
Конструктор может быть с аргументами по умолчанию, например:
Vector::Vector (int size, int* data = NULL) { ... }
Компилятор выбирает по сигнатуре необходимый компилятор.
Важно: Если вы объявите какие-либо конструкторы с аргументами, но не объявите конструктора без аргументов, то компилятор
не позволит создавать объекты класса с использованием конструктора без аргументов. В этом случае, например, не удастся создать
массив объектов класса.
Классификация конструкторов приведена в табл. 1.
Совет: Хороший стиль программирования: всегда предусматривайте конструктор, который выполняет соответствующую инициализацию объектов своего класса.
18
Таблица 1
Классификация конструкторов
По наличию параметров
По количеству и типу параметров
Без параметров
Конструктор умолчания
С параметрами
Конструктор преобразования
Конструктор копирования
Конструктор с двумя и более параметрами
Конструктор вызывается всякий раз при выполнении инструкции объявления переменной.
2.1.1. Конструктор копирования и оператор присваивания
При создании объекта его поля могут быть проинициализированы значениями полей другого объекта этого же типа, т. е. создается копия. Для этих целей используется конструктор копирования.
Конструктор копирования принимает на вход ссылку на исходный
объект этого класса:
Vector v1 (5);
Vector v2(v1); // или Vector v2 = v1;
Если в классе нет использования динамической памяти, выделяемой оператором new, то в конструкторе копирования достаточно почленного копирования данных (v1.size = v2.size; v1.data = v2.data). Такой конструктор можно не создавать, он генерируется автоматически.
Если же в классе есть использование динамической памяти, то
использование поверхностного копирования приведет к ошибке,
так как члены – указатели разных объектов будут иметь одинаковые значения и указывать на одну и ту же область памяти.
В Vector есть выделение динамической памяти под параметр
data. Если произвести почленное копирование, то окажется, что
v1.data указывает на ту же область памяти, что и v2.data. Значит,
все изменения, производимые с помощью одного или другого объекта с data, отразятся и на втором. В том числе, если один из объектов
будет уничтожен раньше другого, то это приведет к ошибке.
Необходимо осуществлять копирование не только членов класса, но и динамических структур, т. е. создавать новые динамические структуры и копировать в них значения:
class Vector {
// ...
public:
19
// ...
Vector(const Vector& src) {
size = 0;
data = NULL;
setSize(src.size);
for (int i=0; i<size; ++i) {
data[i] = src.data[i];
}
}
};
void main() {
Vector v1(5);
Vector v2 = v1; // вызывается конструктор копирования
v2 = v1;
// вызывается оператор присваивания
}
Для корректного выполнения последней строки функции main
(v2 = v1;) необходимо перегрузить оператор присваивания. Задача оператора присваивания аналогична задаче конструктора копирования –
сделать объект копией другого. Отличие заключается, во-первых,
в том, что оператор присваивания возвращает объект (для того, чтобы
возможно было выражение вида a = b = c) и, во-вторых, в том, что конструктор копирования вызывается для объекта один раз при его создании, а оператор присваивания для одного объекта может вызываться
множество раз. Эту особенность следует учитывать при реализации
оператора присваивания: в частности, для класса Vector перед выделением новой памяти освободить ранее выделенную. Кроме того, оператор присваивания должен поддерживать корректное присваивание
самому себе: если этого не сделать, то предварительное освобождение
памяти будет применяться и к объекту – входному параметру:
class Vector {
// ...
public:
// ...
Vector& operator=(const Vector& r) {
if (this == &r)
return *this;
setSize(r.size);
for (int i=0; i<size; ++i) {
data[i] = r.data[i];
}
20
}
return *this;
};
Кроме случая объявления переменной с одновременным присвоением значения другого объекта конструктор копирования вызывается автоматически при передаче параметров в функции или возврате
результата по значению.
Рассмотрим функцию, принимающую на вход два вектора:
Vector AddVector(Vector v1, Vector v2){
if (v1.getSize() != v2.getSize()){
throw "Разные размеры векторов";
}
Vector result (v1.getSize());
// ... почленное сложение двух векторов ...
return result;
}
void main(){
// установка значений в v2 и v1
Vector v1(5), v2(5), v3(5);
v3 = AddVector(v2, v3); // передача по значению
}
В указанном примере параметры v1 и v2, по сути, являются локальными переменными функции AddVector. Для их инициализации компилятор использует конструктор копирования: v1 – копия
v2 из функции main, v2 – копия v3 из функции main. При выходе из
функции AddVector объекты v1 и v2 будут разрушены.
Объект result для хранения результата функции объявлен явно
и проинициализирован при помощи конструктора Vector::Vector(int
size). Как и любая другая локальная переменная функции, она
уничтожается при выходе из нее. Чтобы результатом выполнения
функции можно было бы воспользоваться в функции main, он сохраняется в автоматической временной переменной: при выполнении return result в функции main при помощи конструктора копирования создается временная переменная – копия result. Затем при
помощи оператора присваивания устанавливается значение v3, равное значению временной переменной, и она разрушается.
2.1.2. Конструктор преобразования и спецификатор explicit
Рассмотрим объявление объекта Vector вида
Vector ob = 4;
21
Эта инструкция автоматически преобразуется в Vector ob(4). Эти
записи равносильны.
Как правило, всегда, когда у конструктора имеется только один
аргумент, можно использовать любую из представленных выше двух
форм инициализации объекта.
Смысл второй формы инициализации в том, что для конструктора с одним аргументом она позволяет организовать неявное преобразование типа этого аргумента в тип класса, к которому относится
конструктор.
В рассмотренном примере целое число при помощи конструктора
автоматически преобразуется в класс Vector. В некоторых случаях
автоматическое преобразование типов удобно: например, для класса рациональных дробей конструктор, преобразующий целое число
x в дробь вида x/1, полезен. Но в ряде случае такое автоматическое
преобразование неуместно.
Неявное преобразование можно запретить с помощью спецификатора explicit. Спецификатор explicit применим только к конструкторам.
Для конструкторов, заданных со спецификатором explicit, допустим только обычный синтаксис вызова. Автоматического преобразования для таких конструкторов не выполняется.
Например, для приведенного ниже класса недопустимо объявление переменной вида Vector ob = 4:
class Vector{
. . .
explicit Vector(int x);
};
Для такого конструктора допустима только одна форма вызова:
Vector ob(4);
2.2. Деструктор
При выходе объекта из области видимости память, занимаемая
объектом (т. е. данными членами объекта), освобождается. Перед
освобождением памяти необходимо освободить ресурсы, выделенные в течение жизненного цикла объекта. Для решения этой задачи предназначен деструктор. Деструктор – это специальный метод,
который:
– носит имя: ~ + имя класса;
– не возвращает результата;
– не принимает входных параметров.
22
Задача деструктора – освобождение ресурсов, выделенных пользователями в процессе работы, т. е. функция деструктора обратна
функции конструктора:
Vector:: ~Vector(){
Clear();
}
У класса может быть только один деструктор, перегрузка деструкторов не допускается.
Деструктор по умолчанию всегда генерируется компилятором
автоматически, если не был определен программистом деструктор класса.
Деструктор по умолчанию НЕ удаляет объекты или члены объекта, которые были размещены в памяти оператором new. Если память в классе была выделена динамически, вы обязаны написать
свой деструктор, который бы эту память освобождал, т. е. явное использование delete.
Локальные объекты удаляются тогда, когда они выходят из области видимости. Глобальные объекты удаляются при завершении
программы.
Важно: если класс работает с динамической памятью, то необходимо определять: деструктор, конструктор копирования, оператор
присваивания.
2.3. Список инициализации
Конструктор обеспечивает консистентность данных у созданных
объектов. Список инициализации обеспечивает инициализацию
полей до выполнения тела конструктора.
Этот список, в частности, необходим для вызова конструкторов
базового класса, т.е. при наследовании.
Вид списка инициализации для класса Vector:
class Vector{
private:
int size;
int* data;
public:
Vector (): size(0), data(NULL) {};
Vector (int size): size(0), data(NULL) {setSize(size);}
};
Список инициализации выполняется в последовательности объявления полей в классе (но не в списке инициализации).
23
Пример:
class A {
int x;
int y;
public:
A(int a) : y(a), x(y+1){}
};
Очередность выполнения: сначала x(y + 1), затем y(a).
Если A a(2), то a.y = 2, а значение a.x будет не определено.
Правило: Конструктор вызывается последовательно для всех полей
в порядке их объявления, а деструктор – в обратной последовательности.
2.4. Порядок вызова конструкторов и деструкторов
Вызов деструкторов происходит в обратном порядке вызова конструкторов.
Для объектов, объявленных в глобальной области действия, конструкторы вызываются в начале выполнения программы. Соответствующие деструкторы вызываются при ее завершении.
Для автоматических локальных объектов конструкторы вызываются при их объявлении. Соответствующие деструкторы вызываются, когда объекты выходят из области действия (т.е. происходит выход из блока, в котором они объявлены). Обратите внимание,
что конструкторы и деструкторы могут многократно вызываться
для автоматических локальных объектов по мере того, как объекты
входят и выходят из области действия.
Для статических локальных объектов конструкторы вызываются один раз при объявлении, соответствующие деструкторы вызываются при завершении программы:
Пример:
class CD{
public:
CD (int val);
~CD();
private:
int data;
};
CD::CD(int val){
data = val;
cout <<”Object ” << data << “ constructor”;
}
24
CD::~CD(){
cout << “Object ” << data << “ destructor”;
}
void create(void); //внешняя функция
CD first(1); // глобальный объект
main(){
CD second (2);
static CD third(3);
create();
CD fourth(4);
return (0);
}
void create(){
CD fifth(5);
static CD sixth(6);
CD seventh(7);
}
Вывод:
Object 1 constructor
Object 2 constructor
Object 3 constructor
Object 5 constructor
Object 6 constructor
Object 7 constructor
Object 7 destructor
Object 5 destructor
Object 4 constructor
Object 4 destructor
Object 2 destructor
Object 6 destructor
Object 3 destructor
Object 1 destructor
25
Глава 3
ПЕРЕГРУЗКА ОПЕРАТОРОВ
Перегрузка операторов позволяет использовать объекты класса во
многих операциях (логические, математические) наравне со встроенными типами данных. Она происходит при помощи перегрузки методов или при помощи перегрузки функций (как правило дружественных классу):
Vector& Vector:: operator = (const Vector &r){
if (this == &r)
return *this;
setSize(r.getSize);
for (int i=0; i<getSize(); i++)
data(i) = r.data(i);
return this*; // ссылка на самого себя
}
Операторы, которые можно перегружать, указаны в табл. 2.
Таблица 2
Операторы, доступные для перегрузки
Бинарные операторы
Название
Обозначение
Присваивание
=
Метод
= =, !=, >,
<, >=, <=
Метод ИЛИ
Дружественная
функция
Логические
операторы
+, -, *, /,
Арифметические
+=, -=, *=,
операторы
/=
Перегрузка
Примечание
–
Перегрузка операторов
Метод ИЛИ
*, +, -, / не дает автомаДружественная
тическую перегрузку
функция
*=, +=, -=, /=
Унарные операторы
Оператор
отрицания
!
Метод ИЛИ
Дружественная
функция
–
Инкремент
и декремент
++, --
Метод ИЛИ
Дружественная
функция
–
Специальные операторы
[], (), ->, *,
., &
26
Метод
–
Таблица 3
Операторы, не доступные для перегрузки
.
.*
::
?:
sizeof
#
typeid
Совет: Перегружайте операцию для выполнения тех же действий
с объектами класса или близких по смыслу действий, которые выполняет эта операция со встроенными типами. Оператор / не должен
выполнять умножение при перегрузке.
Операторы, которые НЕЛЬЗЯ перегружать указаны в табл. 3.
typeid – это динамическая идентификация типа данных:
Vector v;
if (typeid(v) == typeid(Vector)){ … }
При перегрузке операторов запрещено:
– использовать аргументы по умолчанию;
– изменять число операндов, которое подразумевает оператор.
Унарная операция остается унарной, бинарная – бинарной;
– создавать новые операторы с использованием ключевого слова
operator, только существующие могут быть перегружены;
– перегружать оператор для работы со встроенными типами
только для объектов новых типов. Перегрузка работает только
с объектами, тип которых определен пользователем, или в смешанных ситуациях, когда объект пользовательского типа участвует
в операции вместе с объектом встроенного типа.
В случае перегрузки оператора при помощи метода, ссылка на левый операнд перегружаемой операции передается через указатель
this. Поэтому при перегрузке бинарной операции соответствующий
оператор – член класса имеет лишь один входной аргумент – правый операнд. При перегрузке унарной операции в метод входные
аргументы не передаются.
3.1. Бинарные операторы
Рассмотрим пример реализации операции умножения на число. Если перегружать оператор умножения при помощи метода, то
в класс должен быть добавлен метод operator*, принимающий на
вход один аргумент – целое число. Результатом операции умножения является новый вектор, каждый элемент которого получен умножением соответствующего элемента вектора на число:
class Vector{
…
27
public:
…
Vector operator* (int r);
};
Входной параметр в методе назван именем r чтобы подчеркнуть,
что он является правым операндом:
Vector Vector::operator* (int r){
// результирующий вектор имеет тот же размер
Vector res(size);
for (int i = 0; i < size; ++i)
res.data[i] = data[i] * r;
return res;
}
Перегруженный оператор позволяет использовать класс Vector
в выражениях умножения на число справа:
void main() {
Vector v1(5), v2;
v2 = v1 * 5; // вызывается v2.operator=(v1.operator*(5))
}
Как было отмечено, при перегрузке бинарной операции при помощи метода мы можем указывать тип только правого операнда.
Левый операнд всегда имеет тип класса, для которого переопределяется операция. Это значит, что перегрузить операцию умножения
Vector на число слева при помощи метода класса Vector не получится: нужно использовать перегрузку при помощи функции.
При перегрузке оператора с помощью функции количество входных параметров соответствует числу операндов перегружаемой операции. Поскольку все операнды передаются в функцию явным образом, есть возможность определять тип каждого из них. Объявление
функции умножения Vector на число выглядит следующим образом:
Vector operator*(int l, const Vector& r);
Эту функцию легко реализовать, используя оператор умножения
Vector на число справа:
Vector operator*(int l, const Vector& r) {
return r * l; // вызывается r.operator*(l)
}
При выборе способа перегрузки – через метод или через функцию –
можно руководствоваться следующими соображениями. Если операция затрагивает несколько операндов и экземпляр класса выступает
28
в операции наравне с другими операндами, рекомендуется использовать дружественные функции. Если же можно выделить операнд, по
отношению к которому выполняется операция, то логичнее перегружать операцию при помощи метода класса этого операнда.
Чтобы пояснить вышесказанное, рассмотрим, например, операторы
сложения и вычитания двух векторов. Оба операнда в данных операциях выступают равноценно, результатом операции является новый вектор. Нет объективных причин, по которым следовало бы отнести операцию как к вызову метода левого или правого операнда. Аналогичные
рассуждения можно применить по отношению к операции скалярного
умножения двух векторов. Эти операции разумно перегружать при помощи функций:
int operator* (const Vector& l, const Vector &r);
Vector operator+ (const Vector& l, const Vector &r);
Vector operator- (const Vector& l, const Vector &r);
Реализуем оператор скалярного умножения двух векторов. Функция должна проверить, что размеры векторов совпадают, и вычислить сумму произведений элементов векторов:
int operator* (const Vector& l, const Vector &r) {
int res = 0;
for (int i=0; i < l.size; ++i)
res += l.data[i] * r.data[i];
return res;
}
При компиляции такой функции возникнет ошибка доступа
к приватным членам класса Vector: полям size и data. Соответствующие ошибки можно было бы исправить, если бы в классе Vector
были публичные методы getSize() и getData(), возвращающие size и
data соответственно. Если метод getSize() в классе Vector уже есть,
то спешить добавлять метод getData() не следует. Наличие такого публичного метода существенно ухудшит защиту данных: указатель
с данными будет доступен из любой точки программы.
В тех ситуациях, когда наряду с методами класса (или группы классов)
есть функции, которые в силу специфики нуждаются в доступе к приватным членам класса (или группы классов), такие функции объявляют
дружественными классу (или группе классов). Эти функции являются
в каком-то смысле составной частью класса и расширяют его функциональность. Оператор – наиболее частый представитель таких функций.
Чтобы объявить функцию дружественной классу, внутри объявления класса следует добавить прототип функции, предваренный
29
ключевым словом friend. Если необходимо, чтобы функция была
дружественной нескольким классам, в каждый из классов должен быть добавлен прототип функции с ключевым словом friend.
Указанные инструкции не заменяют объявления функции, а лишь
являются указанием компилятору внутри дружественных классу
функций обращаться к приватным членам класса.
Чтобы было можно реализовать функции-операторы сложения и
вычитания, а также скалярного произведения двух векторов, в классе
Vector указанные функции должны быть объявлены дружественными:
class Vector{
…
public:
…
friend int operator*(const Vector& l, const Vector &r);
friend Vector operator+(const Vector& l, const Vector &r);
friend Vector operator-(const Vector& l, const Vector &r);
};
При реализации операторов сложения и вычитания векторов необходимо выполнить проверку корректности переданных параметров:
складывать и вычитать можно векторы одинакового размера. В случае
вызова оператора для векторов разных размеров необходимо прервать
работу оператора, например создав исключение (описание механизма
исключительных ситуаций приведено в разделе «Исключения»):
Vector operator+(const Vector& l, const Vector &r) {
if (l.size != r.size)
throw "Попытка сложения векторов разных размеров";
Vector res(l.size);
for (int i = 0; i < l.size; ++i)
res.data[i] = l.data[i] + r.data[i];
return res;
}
Аналогичным образом реализуется оператор вычитания векторов.
Логические операции (операции сравнения) также часто перегружают при помощи функций. Результатом операции сравнения
является значение типа bool. Часто достаточно реализовать перегрузку пары операторов, например, == и >. Остальные операторы
могут быть вычислены на основании этих.
Для класса Vector операция сравнения может быть переопределена следующим образом:
class Vector{
30
…
public:
…
friend boolean operator==(const Vector& l,
const Vector &r);
};
boolean operator==(const Vector& l, const Vector &r) {
if (l.size != r.size)
return false;
for (int i = 0; i < l.size; ++i)
if (l.data[i] != r.data[i])
return false;
return true;
}
Операции вида +=, -=, *= и /= отличаются от рассмотренных выше
операций тем, что они используют левый операнд в качестве входного
и выходного параметров. Результатом этих операций является левый
операнд после применения к нему соответствующей операции. Такого
рода операции логично определять при помощи метода класса. Например, для операции += класса Vector перегрузка может выглядеть так:
class Vector{
…
public:
…
Vector& operator+=(const Vector &r);
};
Vector& Vector::operator+=(const Vector &r) {
if (size != r.size)
throw "Попытка сложения векторов разных размеров";
for (int i = 0; i < l.size; ++i)
data[i] += r.data[i];
return *this;
}
В отличие от оператора сложения двух векторов, новый экземпляр Vector не создается, модифицируется левый операнд. Он же и
возвращается в качестве результата операции.
Операторы инкремента и декремента также могут быть переопределены. Чтобы отличить префиксную версию оператора от постфиксной, последняя перегружается как бинарная операция. Это искусственный шаг, позволяющий использовать механизм перегрузки.
31
Для класса Vector операторы инкремента определяются так:
class Vector{
…
public:
…
Vector& operator++(); // префиксная форма
Vector operator++(int dummy); // постфиксная форма
};
Vector& Vector::operator++() {
for (int i = 0; i < size; ++i)
++data[i];
return *this;
}
Vector Vector::operator++(int dummy) {
Vector res(*this);
++(*this); // вызов префиксной формы инкремента
return res;
}
Ниже представлено объявление класса Vector с перегруженными операторами ==, =, !=, >, +, [] как с помощью дружественных
функций, так и методов класса:
class Vector{
…
public:
Vector& operator = (const Vector& r);
bool operator == (const Vector& r);
friend bool operator != (const Vector &l, const Vector& r);
friend bool operator > (const Vector &l, const Vector& r);
Vector operator +(-) (const Vector& r);
int operator * (const Vector& r);
int& operator[](int index);//функция, которая может
обращаться к приватным полям Vector
};
bool Vector::operator == (const Vector& r){
if (this == &r)
return true;
if (size != r.size)
return false;
for (int i=0; i<sie; i++){
if (data[i]!=r.data[i])
32
return false;
}
return false;
}
Бинарные операторы, перегружаемые как методы, принимают
первый входной параметр – правый операнд. Перегруженные как
функции – два параметра – оба операнда.
Перегруженные методы неявно принимают еще указатель this:
Vector Vector::operator +(-) (const Vector& r){
if (size != r.size)
Error(“Wrong size”);
Vector result (size);
for (int i=0; i<size; i++)
result.data[i] = data[i]+r.data[i];
return result;
}
int i = v1 * v2; // i.operator = (v1.operator * (v2)) – возвращаемое
значение ‒ int
v1 = v1*i; // v2.operator = (v1.operator * (i)) – возвращаемое
значение ‒ вектор
Vector Vector::operator * (int x){
Vector result (size);
for (int i=0; i<size; i++){
result.data[i] = data[i]*x;
}
return result;
}
Vector operator * (int x, const Vector& r){
return r*x; // r.operator * (x)
}
Чтобы инструкция (3 * v1) не приводила к вызову operator *
(Vector(3), v1), необходимо запретить автоматическое преобразование. Для этого перед объявлением конструктора Vector необходимо
добавить спецификатор explicit:
explicit Vector (int size);
3.2. Оператор индексирования
Операцию индексирования можно перегрузить при помощи оператора [ ]. Этот оператор принимает на вход в качестве индекса объ33
ект произвольного типа, но только один. Для класса Vector оператор
может возвращать данные элемента вектора с указанным индексом:
class Vector{
…
public:
…
int& operator[](int index);
};
int& Vector::operator[](int index) {
if ((index < 0) || (index >= size))
throw "Некорректный индекс";
return data[index];
}
Указанный метод выполняет проверку, не выходит ли за допустимые границы массива индекс index, и возвращает значение элемента массива data по индексу. Возврат значения осуществляется
по ссылке для того, чтобы оператор [ ] можно было применять слева
от оператора присваивания:
void main() {
Vector v(5);
v[2] = 3;
}
3.3. Операторы по работе с потоком
Перегруженный оператор извлечения из потока ( >> ) принимает
в качестве аргументов ссылку на istream и ссылку на тип, определяемый пользователем, и возвращает ссылку на istream:
friend istream &operator >> (istream &, Vector &);
Оператор передачи в поток ( << ) принимает в качестве аргументов ссылку на ostream и ссылку на пользовательский тип, возвращает ссылку на ostream:
friend ostream &operator << (ostream &os, Vector &v){
for (int i=0; i<v.getSize(); i++){
if (i == 0)
os = os << “(“;
else
os = os << “, “;
os = os << v[i];
}
34
if (v.getSize() > 0)
os = os << “)”;
return os;
};
3.4. Унарные операторы
Vector& operator ++(--)();
Vector operator ++(--) (int);
Что такое постфиксное инкрементирование/декрементирование
и префиксное? В приведенном ниже примере указано отличие префиксного инкрементирования/декрементирования от постфиксного:
int i = 2;
int j = ++i; //i = 3; j = 3
int k = i++; // k = 3; i = 4
// префикс – возвращает после инкремента
Vector& Vector::operator ++(--) (){
for (int i=0; i<size; i++){
++data[i]; // operator ++(--) (int)
return *this;
}
// постфикс – возвращает значение ДО инкремента
// int – фиктивный параметр, для различия между постфиксом и префиксом
Vector Vector::operator ++(--) (int){
Vector result (*this);
++(*this); // operator ++(--) (Vector&)
return result;
}
Если перегружается как дружественная функция, то
// постфикс – возвращает значение ДО инкремента
// int – фиктивный параметр, для различия между постфиксом и префиксом
const Vector operator ++(--) (Vector &v, int){
Vector res(v);
v++;
return res;
}
// префикс – возвращает после инкремента
const Vector& operator++(--)(Vector& v){
v++;
return v;
}
35
Совет: Если нет разницы, как определять оператор, то лучше его
оформить в виде функции класса, чтобы подчеркнуть связь.
3.5. Операторы преобразования
Иногда бывает необходимо преобразовать объект одного типа
в другой тип. Функция преобразования преобразует объект в значение, совместимое с другим типом данных, который может являться
одним из встроенных типов С++.
Одним из способов преобразования типов объектов в заданный
класс, является объявление конструктора преобразования: единственный входной параметр конструктора может автоматически
преобразовываться в новый экземпляр класса.
Другой способ: функция преобразования – автоматически преобразует объект в значение, совместимое с типом выражения, в котором этот объект используется.
Основная форма преобразования:
operator тип() { return значение;}
Тип – это целевой тип преобразования, а значение – это значение
объекта после выполнения преобразования. Функции преобразования возвращают значение типа тип. У функции преобразования не
должно быть параметров, и она должна быть членом класса, для которого выполняется преобразование.
Пример 1: класс coord содержит функцию преобразования, которая преобразует объект в целое. В данном случае функция возвращает произведение двух координат:
class coord {
int x, y;
public:
coord (int i, int j) { x =i; y = j;}
operator int() {return x*y;}
};
int main(){
coord o1(2, 3), o2(4, 3);
int i;
i = o1; // i = 6
i = 100*o2; // i = 1200
}
Пример 2: строка типа str преобразуется в символьный указатель
на str:
class str{
36
char strn[80];
int len;
public:
str(char* s) {strcpy(strn, s); len = strlen(s);}
operator char*() { return strn; }
};
int main(){
strn s(“Проверка”);
char *p, s2[80];
p = s;
strcpy(s2, s);//при вызове функции преобразуется в char*
}
37
Глава 4
НАСЛЕДОВАНИЕ
Наследование – это повторное использование программного
кода, при котором новые классы создаются на основе существующих. Классы наследуют свойства и поведение базовых классов и дополнительно приобретают новые качества.
Возможность повторного использования программного обеспечения сберегает время, затраченное на его разработку, способствует
использованию проверенного и отлаженного кода, уменьшает число
проблем и ошибок.
Наследование классов – очень мощная возможность в объектноориентированном программировании. Оно позволяет создавать производные классы (классы-наследники), взяв за основу все методы и
элементы базового класса (класса-родителя).
Объекты производного класса свободно могут использовать все,
что создано и отлажено в базовом классе. При этом мы можем в производный класс дописать необходимый код для усовершенствования программы: добавить новые элементы, методы и т. д. Базовый
класс останется нетронутым.
Есть два основных механизма – композиция классов и наследование классов.
Композиции соответствует глагол «имеет». Например, класс «Сотрудник» имеет телефон, заработную плату, день рождения и пр.
Наследованию соответствует глагол «является». Нельзя сказать, что сотрудник является днем рождения, заработной платой и
пр. Сотрудник является человеком.
4.1. Композиция классов
Одной из форм повторного использования программного кода является композиция классов, когда класс содержит в качестве элементов объекты других классов.
Рассмотрим класс A, который предоставляет возможность хранить в нем целое число, получать его значение и выводить на экран:
class A{
int a;
public:
int getA() {return a;}
A(int aa): a(aa) {}
void print() {
cout << "a=" << a << endl;
38
}
};
Предположим, есть некоторый набор кода, который работает
с экземплярами класса A:
void foo(A *p) {
if (p->getA() > 0)
p->print();
}
void main() {
A a(2);
foo(&a);
}
Рассмотрим ситуацию, когда потребовалось расширить функциональность и работать не с одним числом, а с двумя. Одним из вариантов реализации является расширение функциональности класса
A путем добавления в него нового поля. В этом случае также потребуется добавить новый метод по получению его значения и изменить
метод print так, чтобы он учитывал наличие нового поля. Следует
также учитывать, что существующий код, работающий с объектами класса A, может быть достаточно большим, а значит, изменения
в классе A могут затрагивать большую часть программы и требовать
трудоемкого тестирования.
Объектно-ориентированный подход предлагает в случае расширения или уточнения функциональности класса создавать новые
классы. В этом случае существующий код по-прежнему использует
старые классы, и тестированию подлежит новый код.
Новый класс B, инкапсулирующий работу с двумя числами, может повторно использовать функциональность, предоставляемую
классом A, за счет объявления в классе B поля с типом A:
class B{
a
getA()
A
getB()
b
getA()
A a;
int b;
public:
B(int aa, int bb) : a(aa), b(bb){}
int getB() { return b; }
int getA() { return a.getA(); }
39
void print() {
a.print();
cout << "b=" << b << endl;
}
A& getAObj() { return a; }
};
void main(){
A a(2);
foo(&a);
B b(3, 4);
// foo(&b); // ошибка компиляции
foo(&b.getAObj());
}
Класс B содержит все методы, которые есть в классе A, как для
экземпляров класса А, так и для экземпляров класса B, является
доступным вызов методов getA и print. Но использовать объекты
класса B в старом коде, где используются объекты класса A, не
получится: с точки зрения компилятора классы A и B не связаны друг с другом, и автоматического преобразования типа B к А
не происходит. Так, например, компилятор не позволит вызвать
foo(&b), несмотря на то, что в классе B есть методы getA и print,
которые используются в функции foo. Для решения подобного
ограничения при использовании декомпозиции классов добавлен
метод getAObj, который возвращает поле типа A. Фактически
этот метод является преобразователем типа B в тип A и позволяет опосредованно использовать объекты класса B в старом коде:
foo(&b.getAObj);
4.2. Наследование
При создании нового класса вместо того, чтобы разрабатывать совершенно новые элементы данных и методы, можно просто указать, что новый класс должен наследовать элементы ранее
определенного базового класса (родительский класс и дочерний)
(рис. 1).
А
а
B
Aa
b
Рис. 1. Пример наследования
40
Пример, приведенный на рисунке, реализуется следующим кодом:
class A{
int x;
public:
void set_x(int xx) { x = xx; }
void print() { cout << “x = ” << x; }
};
class B : public A { }; // пустой класс-наследник
void main (){
A a;
a.set_x(5);
a.print(); // x = 5;
B b;
b.set_x(10);
b.print(); // x = 10;
}
Важно: Любой экземпляр класса наследования является экземпляром базового класса (класса-родителя), но не наоборот.
Зачастую необходимо, чтобы поведение некоторых методов базового класса и классов-наследников отличалось. Решение очевидно:
нужно переопределить необходимые методы в производных классах.
Пример:
class A{
public:
int f(const int &d){ return 2*d; }
int cf (const int &d) { return f(d)+1; }
};
class B : public A{
public:
int f (const int &d) { return d*d; }
};
int main(){
A a;
cout << a.cf(5);
B b;
cout << b.cf(5);
return 0;
}
В базовом классе определены два метода – f и cf, причем во втором методе вызывается f. В классе-наследнике переопределен метод
41
f для того, чтобы выполнялась другая операция при вызове метода
из производного класса. Объявляя объект типа B, ожидается результат 5∙5 + 1 = 26, но на экран будет выведено число 11 (2∙5 + 1),
т. е., несмотря на переопределение метода f, в классе-наследнике,
вызывается «родная» функция f.
Еще пример со ссылкой и указателем на объект:
class A{
public:
void print() const { cout << “A!” << endl; }
};
class B : public A{
public:
void print() const {cout << “B!” << endl; }
};
void set (A &a){
a.print();
}
void main(){
A a;
set(a); // выводится "А!"
B b;
set(b); // ссылка на производный вместо базового
A *a1 = &a; // адрес объекта базового класса
a1->print(); // вызов базового метода
a1 = &b;
// адрес объекта производного типа вместо базового
a1->print(); // какой метод вызовется? Базовый или производный?
}
В классе-наследнике переопределен метод print для того, чтобы
обеспечить различное поведение объектов базового и производного
классов. Однако при передаче параметра по ссылке базового класса
в функцию set() и при явном вызове метода print через указатель базового класса наблюдается одна и та же картина: всегда вызывается
метод базового класса, хотя нужен метод производного.
Причина – раннее связывание, объект и вызов функции связываются между собой на этапе компиляции. Это означает, что вся необходимая информация для того, чтобы определить, какая именно
функция будет вызвана, известна на этапе компиляции программы.
Чтобы связывание метода и адреса соответствующих функций производилось на этапе выполнения программы, необходимо перегруженный
метод объявлять виртуальным (при помощи ключевого слова virtual).
42
Это называется поздним связыванием.
Механизм виртуальных методов заключается в том, то результат вызова виртуального метода не зависит от того, на основе какого
типа создан указатель, а зависит от типа объекта, на который указывает этот указатель.
При вызове виртуального метода через указатель на объект осуществляется динамический выбор (выбор во время исполнения программы) тела метода в зависимости от текущего объекта.
Класс, содержащий хотя бы одну виртуальную функцию, называется полиморфным классом, а объект – полиморфным объектом.
Во всех наследуемых классах виртуальная функция остается
виртуальной.
Пример:
class A{
public:
virtual void f(int x){ cout << “A!”; }
};
class B : public A {
public:
void f(int x) {cout << “B!”; }
};
void main(){
A a1;
A* pa;
B b1;
B* pb;
pb = &b1;
pb->f(1); // B::f
pa = pb;
pa->f(1); // B::f
pa = &a1;
pa->f(1); // A::f
}
При позднем связывании (динамическое связывание) вызов виртуального метода перенаправляется из соответствующего класса.
Для этой цели используется таблица виртуальных функций, называемая vtable, которая реализована в виде массива, содержащего
указатели на функции.
Каждый класс, имеющий виртуальные функции, имеет таблицу
vtable.
43
– Класс A в своей таблице vtable содержит ссылку на свой метод
f (&A_f_A*).
– Класс B в своей таблице vtable содержит ссылку на своей метод
f (&B_f_B*).
Каждый класс содержит ссылку на свою таблицу виртуальных
функций vptr.
На этапе компиляции происходит:
a1.vptr[f](&a) < -- > A_f_A*(&a);
b1.vptr[f][&b) < -- > B_f_B*(&b);
A_f_A*(&a) < -- > A_f*(&a);
A_f_A*(&b) < -- > A_f*(&b);
Важно: Определение в производном классе виртуальной функции,
не совпадающей по типу возвращаемого значения или списку параметров с функцией в базовом классе, приводит к синтаксической ошибке.
Переопределяемая функция должна иметь тот же возвращаемый тип данных и список параметров, что и виртуальная функция
базового класса.
4.3. Область видимости и модификаторы доступа
Имеют доступ:
– private (закрытый) – только методы данного класса и дружественные к классу элементы;
– public (открытый) – все;
– protected (защищенный) – только методы данного класса, классов-наследников и дружественные к классу элементы.
class A {
private:
int a;
protected:
virtual void doPrint();
public:
void print() {doPrint();} // this->vptr[doPrint](this)
A (int aa): a(aa) {}
};
class B : public A{
private:
int b;
protected:
void doPrint();
public:
B(int aa, int bb) : A(aa), b(bb) {}
44
Таблица 4
Зависимость модификатора доступа
от области видимости в базовом классе
Модификатор доступа
Область видимости
в базовом классе
private
protected
pablic
private
private
private
private
protected
private
protected
protected
public
private
protected
public
};
void A::doPrint(){
cout << “a = ” << a;
}
void B::doPrint(){
A::doPrint();
cout << “b = ” << b;
}
void main(){
A a(2);
B b(3, 4);
a.print(); // A_print_A*
b.print(); // A_print_A*
}
В табл. 4 приведены зависимости, возникающие при разных модификаторах доступа.
4.4. Неявное преобразование
объектов производного класса к базовому классу
Несмотря на то, что объект производного класса также является
и объектом базового, типы объектов производного класса и базового
класса различны.
Объекты производного класса можно обрабатывать как объекты
базового класса. Наоборот – неверно. Но это можно добавить при
помощи перегруженной операции присваивания или конструктора
присваивания.
Распространенная ошибка: присваивание объекта производного
класса к соответствующему базовому классу и затем попытка обращения к элементам, наличным только в производном классе.
45
Существует четыре способа для смешивания и согласования
указателей базового и производного классов с объектами базового и
производного классов:
1. Ссылка на объект базового класса с помощью указателя базового класса естественна.
A *a = &aa;
2. Ссылка на объект производного класса с помощью указателя
производного класса тоже естественна.
B *b = &bb;
3. Ссылка на объект производного класса с помощью указателя
базового класса безопасна, так как объект производного класса является также и объектом базового класса. Программа может ссылаться только на объекты базового класса. В случае, если программа
ссылается на элементы, которые присутствуют только в производном классе, программы выдаст ошибку:
*a = &bb;
4. Ссылка на объект базового класса с помощью указателя производного класса является ошибкой:
*b = &aa;
4.5. Динамический полиморфизм
Понятие полиморфизма в С++ означает способность объектов
различных классов, связанных наследованием, различным образом
реагировать на вызов одного и того же метода.
Перегрузка функций и определение одноименных методов в классах-наследниках являются, по сути, проявлением полиморфизма.
До сих пор мы имели дело с ситуацией, когда сопоставление имени метода и адреса соответствующей функции выполняется на этапе
компиляции и линковки – так называемым ранним связыванием.
При вызове метода через указатель адрес вызываемой функции
определяется только типом объявленного указателя и не зависит от
типа объекта, на который он указывает.
Для того, чтобы получить возможность вызывать методы классов-наследников через базовый указатель, необходимо иметь механизм, позволяющий выполнять связывание не на этапе компиляции, а на этапе выполнения программы, когда уже будет известен
тип объекта, через указатель на который производится вызов метода. В этом случае связывание называется поздним, а полиморфизм – динамическим.
46
M1
M2
M3
А
B
D
C
Рис. 2. Пример наследования
Полиморфное поведение обеспечивается механизмом виртуальных
функций, позволяющий одному и тому же методу выполнять различные действия, зависящие от типа объекта, получившего вызов.
В случае, если перед объявлением метода добавлено ключевое
слово virtual, метод называется виртуальным. При вызове виртуальных методов не производится раннего связывания, и адрес
функции, которая будет вызвана при выполнении строчки кода
с вызовом виртуального метода, не определен до момента исполнения. Если вызывается виртуальный метод через указатель базового
класса, и этот метод переопределен в классе-наследнике, то будет
вызван переопределенный метод. В этом заключается функциональное отличие виртуального метода от обычного.
Наследование классов и реализация наследования функций M1,
M2, M3 (рис. 2) приведены в коде ниже:
void F1(A *a){
a->M1(); //A.M1 - не виртуальная в классе А
a->M2(); //a[M2] - виртуальная в классе А
a->M3(); //A.M3 – не виртуальная в классе А
}
void F2(B *b){
b->M1(); //B.M1 – не виртуальная в классе B
b->M2(); //b[M2] – виртуальная в классе А
b->M3(); //b[M3] - виртуальная в классе B
47
}
class A{
public:
void M1() {cout << “A.M1”; }
virtual void M2() {cout << “A.M2”;
void M3 () { cout << ”A.M3”; }
};
class B : public A{
public:
void M2() {cout << “B.M2”; }
virtual void M3() {cout << “B.M3”;
};
class C : public A {
public:
virtual void M1(){ cout << “C.M1”;
void M3() { cout << “C.M3”; }
};
class D : public B{
virtual void M1(){ cout << “D.M1”;
void M2() { cout << “D.M2”; }
void M3() { cout << “D.M3”; }
};
void main(){
A a; B b; C c; D d;
F1(&a);
F1 (&b);
F1 (&c);
F1 (&d);
F2(&b);
F2(&d);
}
}
}
}
}
Табл. 5 демонстрирует, какой класс при вызове какого метода будет выполнять функционал.
Технически позднее связывание осуществляется посредством
обращения к таблице виртуальных функций класса, которая содержит информацию об адресах функций, соответствующих виртуальным методам (табл. 6). При объявлении каждого полиморфного
класса – класса, содержащего виртуальные методы, – формируется
таблица виртуальных функций vtable. Так, для класса A таблица vtable_A будет содержать один элемент для метода M2 – адрес
48
Таблица 5
Соотношение вызовов функций и классов
Класс
A
B
C
D
Метод
F1
M1
M2
M3
M1
M2
M3
M1
M2
M3
M1
M2
M3
A
A
A
A
B
A
A
A
A
A
D
A
F2
A
B
B
A
D
D
Таблица 6
Таблицы виртуальных функций
vtable_A
vtable B
vtable C
vtable D
M2 -> B::M2()
M2 -> A::M2()
M2 -> D::M2()
M2 -> A::M2()
M3 -> B::M3()
M1 -> C::M1()
M3 -> D::M3()
M1 -> D::M1()
A::M2(). В классе B в таблице виртуальных функций vtable_B будет
два элемента: B::M2() для M2 и B::M3() для M3.
Если в базовом классе метод объявлен виртуальным, то он остается виртуальным и в классе-наследнике, и информация о нем также содержится в таблице виртуальных функций класса-наследника. Например, в классе C метод M2() не объявлен, но по наследству
от класса A он является виртуальным, и соответствующая ячейка
в таблице виртуальных функций vtable_C ссылается на A::M2(). Таблица виртуальных функций для класса D содержит три элемента,
причем порядок следования элементов определяется последовательностью появления виртуальных функций в иерархии наследования
A ⇐ B ⇐ D: вначале M2, так как соответствующий метод появился
в базовом классе A, затем M3, поскольку метод был объявлен виртуальным в классе B, и последним – метод M1, как новый виртуальный метод в классе D.
49
4.6. Чистые виртуальные функции
Чистая виртуальная функция – это функция, объявление которой завершается инициализатором =0:
virtual void F(){} =0;
Класс, содержащий одну или несколько чистых виртуальных
функций, называется абстрактным. Объекты такого класса не могут
быть созданы.
Абстрактный базовый класс – это обобщенный базовый класс, на
основе которого строится иерархия наследования.
Например, абстрактный базовый класс Shapes и производные от
него: Cube, Sphere, Cylinder, Pyramid и т. д.:
class A {
protected:
virtual void doF(){} = 0;
public:
void F() { doF();}
};
class B : public A{
protected:
void doF() { … ;}
};
void main(){
A a;
// ОШИБКА
A.F(); // ОШИБКА
B b;
b.F();
}
Если производный класс от базового абстрактного класса не определяет чистую виртуальную функцию, то она остается чистой и в производном классе, т. е. производный класс сам становится абстрактным.
Если в классе две чистых виртуальных функции:
class A2 {
protected:
virtual void F1(){} = 0;
virtual void F2() = 0;
public:
void f(){ F1(); F2(); }
};
class B2 : public A2{
50
protected:
void F1() {cout << “ … ”;}
};
void main(){
A2 a2; // ОШИБКА!
B2 b2; // ОШИБКА!
// должен быть класс, где описана и вторая чистая виртуальная
функция
B3 b3;
}
class B3 : public B2{
protected:
void F2() { cout << “ … ”; }
};
В абстрактном классе определяется интерфейс всех узлов иерархии классов. Абстрактный класс содержит чистые виртуальные
функции, которые будут определяться в производных классах. Благодаря полиморфизму этот интерфейс класса будет доступен всем
функциям в иерархии.
4.7. Виртуальные деструкторы
При использовании полиморфизма в работе с динамически выделяемыми объектами иерархии классов могут возникнуть трудности. Если объекты вызываются при помощи операции delete с указателем на базовый класс, то вызывается деструктор базового класса.
Это происходит независимо от типа объекта, на который ссылается
указатель на базовый класс, и от того факта, что деструктор каждого
класса имеет другое имя.
Если деструктор базового класса объявить виртуальным, то деструкторы всех производных классов будут виртуальными, даже
если их имена отличаются от имени деструктора базового класса.
Теперь, если объект разрушается при помощи операции delete, а
указатель, ссылающийся на объект, является указателем на базовый
класс, вызывается деструктор соответствующего базового класса.
При этом наличие виртуального деструктора базового класса обеспечивает вызовы деструкторов всех классов в ожидаемом порядке, а именно
в порядке, обратном вызовам конструкторов соответствующих классов.
Совет: Если класс имеет виртуальные функции, определяйте
деструктор тоже виртуальным, производные от этого класса могут
иметь свои деструкторы, тогда вызываться будут именно они.
51
Пример:
class Object {
public:
Object() { cout << "Object::ctor()" << endl; }
~Object() { cout << "Object::dtor()" << endl; }
};
// Базовый класс
class Base
{
public:
Base() { cout << "Base::ctor()" << endl; }
virtual ~Base() { cout << "Base::dtor()" << endl; }
virtual void print() = 0;
};
// Производный класс
class Derived: public Base
{
public:
Derived() { cout << "Derived::ctor()" << endl; }
~Derived() { cout << "Derived::dtor()" << endl; } void print() {} Object obj;
};
int main ()
{
Base * p = new Derived;
delete p;
return 0;
}
Вывод при наличии виртуального деструктора:
Base::ctor()
Object::ctor()
Derived::ctor()
Derived::dtor()
Object::dtor()
Base::dtor()
Если деструктор не виртуальный:
Base::ctor()
Object::ctor()
52
Derived::ctor()
Base::dtor()
Тогда будет разрушена только часть объекта, соответствующая
базовому классу.
4.8. Множественное наследование
Множественное наследование, когда у производного класса 2 и
более родительских классов производный класс наследует элементы нескольких базовых классов:
class <DerivedClassName>:[<AccessModification1>]<BaseClass1>
[,<AccessModification2>]<BaseClass2>
...
[,<AccessModificationN>]<BaseClassN>
Пример создания программы с множественным наследованием:
class A{
public:
void M1();
virtual void M2();
void M3();
};
class E{
public:
void M4();
virtual void M5();
};
class C : public A, public E {
virtual void M1();
void M3();
void M5();
};
void F3(E *e){
e->M4();
e->M5();
}
void F1 (A *a){
a->M1();
a->M2();
a->M3();
}
void main(){
53
}
A a; C c; D d;
F1(&a);
F1(&c);
F3(&e);
F3(&c);
4.9. Модификаторы доступа при наследовании
Приведем пример построения архитектуры программы, которая
полностью отражена на рис. 3:
class Parent {
int a;
protected:
int b;
public:
int c;
Parent () { a=1; b=2; c=3; }
virtual void show ( ) {
cout << endl << "Parent: " << a << "," << b << "," << c;
}
};
class Child1 : protected Parent {
public:
virtual void show ( ) {
cout << endl << "Child1: " << b << "," << c;
}
};
Parent
Parent
Child 1
Child 2
Child 12
Child 21
Рис. 3. Архитектура программы
54
Для класса Child1 поля родительского класса Parent, бывшие private,
остались private, наследовались уровни доступа protected и public:
class Child2 : private Parent {
public:
virtual void show ( ) {
cout << endl << "Child2: " << b << "," << c;
}
};
Child2 не сможет «напрямую» использовать поле «a», а его потомки – поля «b» и «c»:
class Child11 : public Child1 {
public:
void show ( ) {
cout << endl << "Child11: " << b << "," << c;
}
};
Поле «a» было в Child1 приватным, прямой доступ к нему больше невозможен:
class Child21 : public Child2 {
public:
void show ( ) {
cout << endl << "Child21: no accessible fields!";
}
};
Этот класс не сможет «увидеть» ни «a», ни «b», ни «c»:
int main ( ) {
Parent A;
A.show ();
Child1 B;
B.show ();
Child2 C;
C.show ();
Child11 B1; B1.show ();
Child21 C1; C1.show ();
return 0;
}
В результате работы программы в консоль будут выведены данные, к которым имеет доступ каждый класс.
4.10. Неоднозначности при наследовании
В двух базовых классах могут быть методы с одинаковым именем, например:
class A{
55
public:
virtual void M1();
};
class B{
public:
virtual void M1();
};
class C : public A,
public B{
. . .
};
void f(C *c){
c->M1(); // ошибка! : неоднозначность
}
Компилятор не знает, какой метод выбрать для исполнения.
Правильно:
void f(C *c){
c->A::M1();
c->B::M1();
}
Это довольно неудобно, лучше решить эту проблему, определив
новую функцию в производном классе C:
class C : public A,
public B{
. . .
void M1(){
A::M1();
B::M1();
}
};
Присутствует еще один тип неоднозначности, он связан с перегрузкой функций в различных базовых классах. Пример:
class A{
public:
void print(double p);
};
class B {
public:
void print (int r);
};
56
class C : public A,
public B{
...
};
void F(C *c){
c->print(1); // ОШИБКА! Неоднозначность
c->A::print(1);
c->B::print(1);
}
Важно заметить, что если разработчик во время проектирования
хотел, чтобы в зависимости от типа аргумента на экран выводилась
разная информация, при этом важно, чтобы имена функций были
именно одинаковыми, то в таком случае необходимо использовать
using-объявления:
class A{
public:
int f(int);
char f(char);
};
class B{
public:
double f (double);
};
class C: public A,
public B{
public:
using A::f;
using B::f;
char f(char); // скрывает A::f(char)
};
void f1(C &c){
c.f(1); // A::f(int);
c.f(‘a’); // C::f(char)
c.f(2.0); //B::f(double)
}
Объявления using позволяют создавать набор перегруженных
функций из базовых и производных классов. Функции, объявленные в производном классе, скрывают функции из базового класса,
которые в противном случае были бы доступны.
57
Важно отметить следующее:
1. Using-объявление в определении класса должно относится к членам базового класса.
2. Using-объявление НЕЛЬЗЯ использовать для члена класса
вне этого класса, его производных классов или их методов.
3. Using-объявление не может использоваться для получения доступа к дополнительной информации, это только механизм предоставления более удобного доступа к информации, доступ к которой
в принципе разрешен.
58
Глава 5
ПРОСТРАНСТВО ИМЕН
Пространство имен – отражение логического группирования.
Если некоторые объявления можно объединить по какому-либо
критерию, их можно поместить в одно пространство имен для отражения этого факта.
namespace имя_пространства_имен{
// объявления и определения глобальных переменных,
функций и классов
}
Пример:
namespace myNameSpace{
int x, y;
void f();
}
Для использования переменных и объектов, определенных
в пространстве имен, необходимо использовать оператор области
видимости:
myNameSpace::x = 10;
Чтобы разрешить использование элементов и объектов в пространстве имен, можно использовать директиву using.
Есть два варианта использования:
– using namespace имя; – все члены, определенные в данном пространстве имен, могут быть использованы без использования оператора области видимости;
– using имя::член; – только указанный член пространства имен
может быть использован.
Существуют следующие особенности применения пространства
имен:
1. Разрешение конфликтов имен. Пространства имен предназначены для отражения логической структуры. Простейшим примером такой структуры является отделение кода, написанного одним
человеком, от кода, написанного другим.
При использовании единой глобальной области видимости сложно сформировать программу из отдельных частей. Проблема в том,
что в разных частях программы могут определяться одинаковые
имена. Поэтому, когда части объединяются в одну программу, возникает конфликт.
59
Очевидно, что помещение каждого набора объявлений в свое собственное пространство имен решит проблему, например:
namespace My{
char f(char);
int f (int);
class String {. . .};
}
namespace Your{
char f(char);
double f(double);
class String {. . .};
}
2. Если целью является локальность кода, то стоит обозначить
пространство имен без имени, чтобы исключить конфликты. Тогда
текущее пространство имен будет доступно только в рамках текущего кода (файла):
namespace {
int a;
void f();
}
3. Существуют псевдонимы для длинного названия пространства имен:
namespace MNS = My_Name_Space;
MNS::x = 15;
Злоупотреблять псевдонимами не стоит, это может привести к путанице.
4. Объединение пространства имен:
Namespace space1 { . . . }
Namespace space2 { . . . }
Namespace SpaceUnion{
Using namespace space1;
Using namespace space2;
. . .
}
Подытожив то, что излагалось в этом подразделе, можно сказать,
что пространство имен должно:
– выражать логически связанный набор средств;
– препятствовать доступу пользователей к ненужным им средствам;
– не требовать значительных дополнительных усилий при использовании.
60
5.1. Стандартное пространство имен
Стандарт C++ определяет целую библиотеку в собственном пространстве имен – std:
using namespace std;
Эта инструкция делает пространство имен std текущим, что
позволяет получить прямой доступ к именам функций и классов,
определенных в библиотеке языка C++.
Стандартная библиотека содержит:
– контейнеры: bitset, deque, list, map, queue, set, stack, vector;
– общие: algorithm, iterator, memory, stdexcept и пр.;
– строки: string;
– потоки ввода-вывода: fstream, ios, iostream, istream и пр.;
– числа: complex, numeric, valarray;
– языковую поддержку: exception, limits, new, typeinfo.
5.2. Повторяющиеся базовые классы
При задании более чем одного базового класса возникает вероятность
того, что какой-либо класс дважды окажется базовым для другого класса:
class A {. . .};
class B: public A{. . .};
class C: public A{. . .};
class D: public B, public C {. . .};
Для B и C используются два разных объекта типа A (рис. 4), они не взаимодействуют друг с другом, поэтому проблем не возникнет. Но вот при
обращении к членам класса D, есть шанс получить неоднозначность.
В тех случаях, когда общий базовый класс не может быть представлен в виде двух отдельных объектов, нужно воспользоваться
абстрактным базовым классом.
А
А
B
C
D
Рис. 4. Пример повторяющихся базовых классов
61
Если к базовому классу (как в нашем случае А) нужен доступ из
места, где видна более чем одна копия базового класса, во избежание неоднозначности ссылка должна быть явно квалифицирована:
void f (D *d){
d-> func(); // ошибка
d->A::funct(); //ошибка: неоднозначно, какой именно А?
d->B::funct();
d->C::funct();
}
Если структура проекта достаточно сложная, и все части объекта
должны использовать единственный объект, то механизмом создания
такого совместного использования является виртуальный базовый
класс. Каждый виртуальный базовый класс в производном классе
представлен одним и тем же (совместно используемым) объектом.
При наследовании все виртуальные базовые классы с одним именем будут представлены одним единственным базовым классом.
5.3. Виртуальный базовый класс
Конструктор виртуального базового класса вызывается (явно
или неявно) из конструктора объекта (конструктора самого «нижнего» производного класса):
class A{}; // нет конструктора
class B{
public:
B();
};
class C{
public:
C(int); // нет конструктора по умолчанию
};
class D: virtual public A, virtual public B,
virtual public C{
D() {. . .}; //ошибка: нет конструктора по умолчанию для C
D(int i) : C(i) {. . .}; };
Конструктор виртуального базового класса вызывается до конструкторов производных классов.
Виртуальный базовый класс в противоположность обыкновенному базовому классу требуется в тех случаях, когда базовый класс не
должен повторяться.
62
Глава 6
ИСКЛЮЧЕНИЯ
6.1. Генерация и обработка исключительных ситуаций
Программные исключения – это условия, которые возникают нечасто и достаточно неожиданно, в общем случае вызывают программную ошибку и требуют определенного ответа от программы.
Исключение – это некое действие, которое вступает в силу при
наступлении нестандартной ситуации:
void main (){
Vector v(3);
int k = v[2];
k += v[4]; // ошибка: некорректный индекс
k = k+v[1];
cout << “k = “ << k;
}
int Vector::operator[] (int i){
if ((i<0)||(i>=size))
Error(“. . . “); // создает исключительную ситуацию
result data[i];
}
bool Vector::getData(int i, int& result){
if ((i<0)||(i>=size))
return false;
result = data[i];
return true;
}
void main(){
Vector v(3);
int k;
if (!v.getData(2, k)){
cout << “Error index”;
}
return;
}
Есть специальные команды C++: throw, try, catch:
– throw – это объект исключения, генерация исключения;
– catch <тип исключения>[<имя объекта исключения>];
– блок, в котором «ловится» исключение:
try{
//некоторые функции
63
throw 10; //генератор исключения типа int
//функции не выполняются
} catch(int i){ cout << “Exception type int ” << i; }
// i=10
Важно: Обработчиков catch может быть несколько! Пример исходного кода с двумя обработчиками:
try{
…
throw “Exception”; // char*
…
throw 10; // int
…
}
catch (int i){
cout << “Exception int : ” << I;
}
catch (char* s){
cout << “Exception char : ” << s;
}
В try-блоке располагается код, который потенциально может вызвать ошибку в работе программы. Если throw сгенерирует код исключения, то программа прекращает свое выполнение и переходит
к catch, выводит сообщение об ошибке. При этом программа продолжает работать и выполнять команды, описанные ниже.
Исключение в примере: деление на 0:
int main {
int num1, num2;
int var = 2;
while (var--){
cout << “ Enter num1 ”;
cin >> num1;
cout << “Enter num2”;
cin >> num2;
cout << “num1 + num2 = ” << num1+num2;
cout << “num1 / num2 = ”;
// на 0 делить нельзя, поэтому вводим try-catch
try{
if (num2 == 0)
throw 123;
cout << num1/num2;
64
}
catch (int i){
cout << “Error # ” << i << “ division by 0 !”;
}
cout << “num1 – num2 = “ << num1 – num2;
}
}
Еще один вариант обработки:
try{
if (num2 == 0){
throw “Error: division by 0!”;
}
cout << num1/num2;
}
catch(char *s){
cout << s;
}
Генерировать исключение можно и в функции:
int division (int n1, int n2){
if (n2 == 0)
throw 444;
}
return n1/n2;
}
void main(){
. . .
try{
cout << division (num1, num2);
}
catch(int i)
cout << “Error # ” << i;
}
Заключительные выводы следующие:
1) try-блок – так называемый блок повторных попыток. В нем
надо располагать код, который может привести к ошибке и аварийному закрытию программы;
2) throw генерирует исключение. То, что остановит работу tryблока и приведет к выполнению кода catch-блока. Тип исключения должен соответствовать типу принимаемого аргумента catchблока;
65
3) catch-блок – улавливающий блок, поймает то, что определил throw и выполнит свой код. Этот блок должен располагаться
непосредственно под try-блоком. Никакой код не должен их разделять;
4) если в try-блоке исключение не генерировалось, catch-блок не
сработает. Программа его обойдет;
5) После того, как исключение обработано, программа производит восстановление и продолжает выполнение.
6.2. Абсолютное завершение
Существует две функции остановки программы: abort и exit.
Они не возвращают управления вызвавшему их коду, и каждая
из них приводит к завершению программы.
Они представляют собой ультимативные обработчики исключений с завершением:
1. Abort. Аномальное завершение программы. По умолчанию
вызов функции abort приводит к выводу диагностики времени выполнения и саморазрушению программы. Такое разрушение может привести, а может и не привести к закрытию открытых файлов или удалению временных файлов в зависимости от прихотей
реализации транслятора.
2. Exit. Цивилизованное завершение программы. В дополнение к закрытию открытых файлов и возврату кода статуса в окружение выполнения программы, функция exit также вызывает любые обработчики,
которые вы зарегистрировали и установили с помощью функции atexit.
Обработчики запускаются в обратном порядке их регистрации.
Пример:
static void atexit_handler_1(){
printf (“atexit_handler_1\n”);
}
static void atexit_handler_2(){
printf (“atexit_handler_2\n”);
}
int main(){
atexit (atexit_handler_1);
atexit (atexit_handler_2);
exit (EXIT_SUCCESS);
cout << “this line should never appear \n”;
return 0;
}
66
Вывод программы:
atexit_handler_2
atexit_handler_1
6.3. Стандартный класс exception
Класс exception содержит стандартные исключения программы
(рис. 5), генерируемые этим классом:
#include <iostream>
// std::cerr;
#include <typeinfo>
// operator typeid;
#include <exception>
// std::exception;
class Polymorphic {virtual void member(){}};
int main () {
try
{
Polymorphic * pb = 0;
typeid(*pb); // throws a bad_typeid exception
}
std:exception
std:bad_alloc
std:domain_error
std:bad_cast
std:invalid_argument
std:bad_typeid
std:length_error
std:bad_exception
std:out_of_range
std:logic_failure
std:overflow_error
std:runtime_error
std:range_error
std:underflow_error
Рис. 5. Структура стандартного класса exception
67
catch (std::exception& e)
{
std::cerr << "exception caught: " << e.what() << '\n';
// e.what() – генерирует строку, описывающую возникшее исключение
}
return 0;
}
Пример работы со стандартным исключением:
#include<iostream>
#include <exception>
using namespace std;
int main()
{
try
{
int * my_array1= new int[100000000];
int * my_array2= new int[100000000];
//int * my_array3= new int[100000000];
//int * my_array4= new int[100000000];
//add more if needed.
}
catch (bad_alloc&)
{
cout << "Error allocating the requested memory." << endl;
}
return 0;
}
При желании можно расширить стандартную библиотеку своими
исключениями:
#include <exception>
using namespace std;
struct MyException : public exception
{
const char * what () const throw ()
{
return "C++ Exception";
}
};
int main()
{
68
}
try
{
throw MyException();
}
catch(MyException& e)
{
std::cout << "MyException caught" << std::endl;
std::cout << e.what() << std::endl;
}
catch(std::exception& e)
{
//Other errors
}
69
Глава 7
ШАБЛОНЫ
7.1. Шаблоны функций
Перегруженные функции обычно используются для выполнения
сходных операций над различными типами данных. Если операция
для всех типов данных идентична, то же самое можно сделать в более сжатой и удобной форме путем определения шаблона функции.
Необходимо написать только одно определение шаблона функции. Исходя из типа аргументов, задаваемых при вызовах этой
функции, C++ автоматически генерирует объектный код для отдельных функций, обрабатывающих вызовы каждого типа.
Шаблоны функций подобно макросам дают компактное решение
проблемы, но при этом обеспечивают полную проверку типов.
Пример: запрограммировать вывод массива на экран.
1. Описываем для каждого типа свою функцию:
void printArray(const int * array, int count)
{
for (int ix = 0; ix < count; ix++)
cout << array[ix] << " ";
cout << endl;
}
void printArray(const double * array, int count)
{
for (int ix = 0; ix < count; ix++)
cout << array[ix] << " ";
cout << endl;
}
void printArray(const float * array, int count)
{
for (int ix = 0; ix < count; ix++)
cout << array[ix] << " ";
cout << endl;
}
void printArray(const char * array, int count)
{
for (int ix = 0; ix < count; ix++)
cout << array[ix] << " ";
cout << endl;
}
70
Таким образом мы имеем четыре перегруженные функции для
разных типов данных. Как видите, они отличаются только заголовком функции, тело у них абсолютно одинаковое.
Кода получилось достаточно много для такой простой операции. А
что, если нам понадобится запрограммировать алгоритм сортировки
в виде функции? Получается, что для каждого типа данных придется
свою функцию создавать, т. е., сами понимаете, что один и тот же код
будет в нескольких экземплярах, нам это ни к чему.
2. Создаем один шаблон, в котором описываем все типы данных.
Таким образом, файл каждого кода не будет захламляться никому
не нужными строками кода:
using namespace std;
// шаблон функции printArray
template <typename T>
void printArray(const T * array, int count)
{
for (int ix = 0; ix < count; ix++)
cout << array[ix] << " ";
cout << endl;
} // конец шаблона функции printArray
int main()
{
// размеры массивов
const int iSize = 10,
dSize = 7,
fSize = 10,
cSize = 5;
// массивы разных типов данных
int iArray[iSize] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
double dArray[dSize] = {1.2345, 2.234, 3.57, 4.67876,
5.346, 6.1545, 7.7682};
float fArray[fSize] = {1.34, 2.37, 3.23, 4.8, 5.879,
6.345, 73.434, 8.82, 9.33, 10.4};
char cArray[cSize] = {"MARS"};
cout << "\t\t Шаблон функции вывода массива на экран\n\n";
// вызов локальной версии функции printArray для типа int
через шаблон
cout << "\nМассив типа int:\n"; printArray(iArray, iSize);
// вызов локальной версии функции printArray для типа
double через шаблон
cout << "\nМассив типа double:\n"; printArray(dArray,
71
dSize);
// вызов локальной версии функции printArray для типа float
через шаблон
cout << "\nМассив типа float:\n"; printArray(fArray,
fSize);
// вызов локальной версии функции printArray для типа char
через шаблон
cout << "\nМассив типа char:\n";printArray(cArray, cSize);
return 0;
}
Код уменьшился в 4 раза, так как в программе объявлен всего
один экземпляр функции – шаблон.
Все шаблоны функций начинаются со слова template, после которого идут угловые скобки, в которых перечисляется список параметров. Каждому параметру должно предшествовать зарезервированное слово class или typename:
– template <class T>;
– template <typename T>;
– template <typename T1, typename T2>.
Ключевое слово typename говорит о том, что в шаблоне будет использоваться встроенный тип данных, такой как: int, double, float, char и т. д.
А ключевое слово class сообщает компилятору, что в шаблоне
функции в качестве параметра будут использоваться пользовательские типы данных, т. е. классы.
У нас в шаблоне функции использовались встроенные типы данных, поэтому в строке 5 мы написали template<typename T>. Вместо
T можно подставить любое другое имя, какое только придумаете.
По сути T – это даже не тип данных, это зарезервированное место
под любой встроенный тип данных, т. е., когда выполняется вызов
этой функции, компилятор анализирует параметр шаблонированной функции и создает экземпляр для соответственного типа данных: int, char и так далее.
Компилятор сам создает локальные копии функции-шаблона, и,
соответственно, памяти потребляется столько, как если бы вы сами
написали все экземпляры функции, как в случае с перегрузкой.
7.2. Шаблоны классов
Если нам надо создать шаблон класса, с одним параметром любого
из встроенных типов данных (Т), шаблон класса будет выглядеть так:
template <typename T>
class Name
72
{
//тело шаблона класса
};
А если параметр шаблона класса должен быть пользовательского
типа, например, типа Array, где Array — это класс, описывающий
массив, шаблон класса будет иметь следующий вид:
template <class T>
class Name
{
//тело шаблона класса
};
Пример: создадим шаблон класса стек, где стек – структура
данных, в которой хранятся однотипные элементы данных. В стек
можно помещать и извлекать из него данные. Добавляемый в стек
элемент помещается в вершину стека. Удаляются элементы стека,
начиная с его вершины:
using namespace std;
template <typename T>
class Stack
{
private:
T *stackPtr; // указатель на стек
int size; // размер стека
T top; // вершина стека
public:
Stack(int = 10);// по умолчанию размер стека равен 10 элементам
~Stack(); // деструктор
bool push(const T ); // поместить элемент в стек
bool pop(); // удалить из стека элемент
void printStack();
};
int main()
{
Stack <int> myStack(5);
// заполняем стек
cout << "Заталкиваем элементы в стек: ";
int ct = 0;
while (ct++ != 5)
{
int temp;
73
cin >> temp;
myStack.push(temp);
}
myStack.printStack(); // вывод стека на экран
cout << "\nУдаляем два элемента из стека:\n";
myStack.pop(); // удаляем элемент из стека
myStack.pop(); // удаляем элемент из стека
myStack.printStack(); // вывод стека на экран
return 0;
}
// конструктор
template <typename T>
Stack<T>::Stack(int s)
{
size = s > 0 ? s: 10; // инициализировать размер стека
stackPtr = new T[size]; // выделить память под стек
top = -1; // значение -1 говорит о том, что стек пуст
}
// деструктор
template <typename T>
Stack<T>::~Stack()
{
delete [] stackPtr; // удаляем стек
}
// элемент функция класса Stack для помещения элемента в стек
// возвращаемое значение - true, операция успешно завершена
//
false, элемент в стек
не добавлен
template <typename T>
bool Stack<T>::push(const T value)
{
if (top == size - 1)
return false; // стек полон
top++;
stackPtr[top] = value; // помещаем элемент в стек
return true; // успешное выполнение операции
}
// элемент функция класса Stack для удаления элемента из стека
// возвращаемое значение - true, операция успешно завершена
74
// false, стек пуст
template <typename T>
bool Stack<T>::pop()
{
if (top == - 1)
return false; // стек пуст
stackPtr[top] = 0; // удаляем элемент из стека
top--;
return true; // успешное выполнение операции
}
// вывод стека на экран
template <typename T>
void Stack<T>::printStack()
{
for (int ix = size -1; ix >= 0; ix--)
cout << "|" << setw(4) << stackPtr[ix] << endl;
}
Перед каждым методом необходимо объявлять шаблон, точно такой же, как и перед классом – template <typename T>.
Элемент-функции шаблона класса объявляются точно так же, как
и обычные шаблоны функций. Если бы мы описали реализацию методов внутри класса, то заголовок шаблона – template <typename T> –
для каждой функции прописывать не надо.
Обратите внимание на объявление объекта myStack шаблона
класса Stack в функции main. В угловых скобках необходимо явно
указывать используемый тип данных, в шаблонах функций этого
было делать не нужно. Далее в main запускаются некоторые функции, которые демонстрируют работу шаблона класса Stack.
75
Глава 8
ДИНАМИЧЕСКАЯ ИДЕНТИФИКАЦИЯ
И ПРИВЕДЕНИЕ ТИПОВ
(RUN-TIME TYPE IDENTIFICATION, RTTI)
8.1. Понятие о динамической идентификации типа
В процессе написания программы возможны ситуации, когда тип
объекта неизвестен, не определена точная природа объекта. Полиморфизм в С++ реализуется через иерархии классов, виртуальные функции и
указатели базовых классов. При таком подходе указатель базового класса
может использоваться либо для указания на объект базового, либо любого производного класса от этого базового класса. Следовательно, не всегда
есть возможность узнать заранее тип объекта, на который будет указывать указатель в любой момент времени. В таких случаях определение
типа объекта должно происходить во время выполнения программы,
для этого используется механизм динамической идентификации типа.
Информацию об объекте получают с помощью оператора typeid
(объект).
Оператор возвращает ссылку на объект типа type_info, который
и описывает тип объекта. В классе type_info описаны:
– == – сравнение типов;
– != – сравнение типов;
– name – возвращает указатель на имя типа.
#include <iostream>
#include <typeinfo>
using namespace std;
class BaseClass{
virtual void f() {};
};
class Derived1: public BaseClass {
...
};
class Derived2: public BaseClass{
...
};
int main(){
int i;
BaseClass *p, baseob;
Derived1 ob1;
76
Derived2 ob2;
cout << “Тип переменной i – это ” << typeid(i).name() <<
endl;
p = &baseob;
cout << “Указатель p указывает на объект типа ” <<
typeid(*p).name() << endl;
p = &ob1;
cout << “Указатель p указывает на объект типа ” <<
typeid(*p).name() << endl;
p = &ob2;
cout << “Указатель p указывает на объект типа ” <<
typeid(*p).name() << endl;
return 0;
}
Вывод программы на экран:
Тип переменной i – это int
Указатель p указывает на объект типа BaseClass
Указатель p указывает на объект типа Derived1
Указатель p указывает на объект типа Derived2
Когда в качестве аргумента оператора typeid задан указатель полиморфного базового класса, реальный тип объекта определяется во время выполнения программы.
Часто оператор typeid используется в ситуациях, когда объекты
передаются функциям по ссылке:
void WhatType(BaseClass &ob){
cout << “ob – это ссылка на объект типа ” <<
typeid(ob).name() << endl;
}
int main(){
BaseClass *p, baseob;
Derived1 ob1;
Derived2 ob2;
WhatType (baseob);
WhatType (ob1);
WhatType (ob2);
}
Пример использования операторов сравнения:
#include <iostream>
#include <typeinfo>
77
using namespace std;
class X{
virtual void f() {};
};
class Y {
virtual void f() {};
};
int main(){
X x1, x2;
Y y1;
if (typeid(x2) == typeid(x2))
cout << “Тип объектов x1 и x2 одинаков\n”;
else
cout << “Тип объектов x1 и x2 не одинаков\n”;
if (typeid(x1) != typeid(y2))
cout << “Тип объектов x1 и y2 не одинаков\n”;
else
cout << “Тип объектов x1 и y2 одинаков\n”;
}
Пример: рисование разных геометрических фигур. Базовый класс
Shape, наследуемые классы – Line, Rectangle, Square и NullShape.
Функция generator() генерирует объект и возвращает указатель на
него. То, какой именно объект создается, определяет генератор случайных чисел rand(). Поскольку объекты возникают случайно, то
заранее неизвестно, какой объект будет создан следующим, т. е. для
определения типов потребуется динамическая идентификация типов:
#include <iostream>
#include <typeinfo>
#include <cstdlib>
using namespace std;
class Shape {
public:
virtual void example() = 0;
};
class Rectangle: public Shape {
public:
void example() { cout << “*****\n* *\n* *\n*****\n”; }
};
class Triangle: public Shape {
public:
void example(){ cout <<”*\n* *\n* *\n*****\n”; }
78
};
class Line: public Shape{
public:
void example() { cout << “*****\n”); }
};
class NullShape: public Shape{
public:
void example();
};
Shape *generator(){
switch (rand() % 4) {
case 0:
return new Line;
case 1:
return new Rectangle;
case 3:
return new Triangle;
case 4:
return new NullShape;
}
return NULL;
}
int main(){
int i;
Shape *p;
for (i=0; i<10; i++){
p = generator();
cout << typeid(*p).name() << endl;
if (typeid(*p) != typeid(NullShape))
p->example();
}
return 0;
}
Typeid может работать с классами-шаблонами. Пример: для хранения значений создается иерархия шаблонов-классов. Виртуальная функция get_val() возвращает определенное в каждом классе
значение. Для класса Num это значение соответствует самому числу.
Для класса Square – это квадрат числа. Для класса Square_root –
квадратный корень числа. Объекты, производные от класса Num,
генерирует функция generator(). С помощью typeid определяется
тип генерируемых объектов.
79
#include <iostream>
#include <typeinfo>
#include <cstdlib>
#include <cmath>
using namespace std;
template <class T> class Num {
public:
T x;
Num (T i) { x = i; };
virtual T get_val() { return x; };
};
template <class T>
class Squary: public Num<T>{
public:
Squary (T i):Num<T>(i) {};
T get_val() {return x*x; };
};
template <class T>
class Sqr_root: public Num<T>{
public:
Sqr_root (T i): Num<T> (i) {};
T get_val() { return sqrt ((double) x); };
};
Num<double> *generator(){
switch(rand()%2){
case 0: return new Sqyary<double> (rand() % 100);
case 1: return new Sqr_root<double> (rand() % 100);
}
return NULL;
}
int main(){
Num<double> ob1 (10), *p1;
Squary<double> ob2 (100.0);
Sqr_root<double> ob3(999.2);
int i;
cout << typeid(ob1).name() << endl;
cout << typeid(ob2).name << endl;
cout << typeid(ob3).name() << endl;
p1 = &ob2;
if (typeid(*p1) == typeid(Squary<double>)
80
}
cout << “is Squary<double>\n”;
cout << “\n\n”;
for (int i=0; i<10; i++){
p1 = generator();
if (typeid(*p1) == typeid(Sqr_root<double>)
cout << “Квадрат объекта \n”;
if (typeid(*p1) == typeid(Sqr_root <double>)
cout << “Квадратный корень объекта \n”;
cout << “Значение равно: ” << p1->get_val() << endl;
return 0;
8.2. Оператор dynamic_cast
Этот оператор реализует приведение типов в динамическом режиме, что позволяет контролировать правильность этой операции
во время работы программы:
dynamic_cast<целевой тип> (выражение)
Целевой тип – это тип, которым должен стать тип параметра выражение после выполнения операции приведения типов.
Пример:
Base *bp, b_ob;
Derived *dp, d_ob;
bp = &d_ob;
dp = dynamic_cast<Derived*>(bp); // успех
if(!dp)
cout << “Приведение типов прошло успешно” << endl;
bp = &b_ob;
dp = dynamic_cast<Derived*> (bp); // ошибка
Указатель bp указывает на объект базового класса Base, а приводить тип объекта базового класса к типу объекта производного
класса нельзя.
Пример:
#include <iostream>
using namespace std;
class Base {
public:
virtual void f() { cout << “Base\n” << endl; }
};
class Derived: public Base{
public:
81
void f() {cout << “Derived\n” << endl; }
};
int main(){
Base *bp, b_ob;
Derived *dp, d_ob;
dp = dynamic_cast<Derived*>(&d_ob);
if(dp){
cout << “Тип Derived* к типу Derived* приведен
успешно \n”;
dp->f();
}
else{
cout << “Ошибка\n”;
}
bp = dynamic_cast<Base*>(&d_ob);
if (bp){
cout << “Тип Derived* к типу Base* приведен
успешно\n”;
bp->f();
}
else
cout << “Ошибка\n”;
bp = dynamic_cast<Base*>(&b_ob);
if (bp){
cout << “Тип Base * к типу Base* приведен успешно\n”;
bp->f();
}
else
cout << “Ошибка\n”;
dp = dynamic_cast<Derived*>(&b_ob);
if (dp){
cout << “Ошибка\n”;
}
else
cout << “Тип Base* к типу Derived* не приведен\n”;
bp = &d_ob;
dp = dynamic_cast<Derived*>(bp);
if(dp){
82
cout << “Указатель bp к типу Derived* приведен успешно,
поскольку bp в действительности указывает на
объект типа Derived \n”
dp->f();
}
else{
cout << “Ошибка\n”;
}
bp = &b_ob;
dp = dynamic_cast<Derived*>(bp);
if(dp)
cout << “Ошибка\n”;
else
cout << “Указатель bp к типу Derived* не приведен,
поскольку bp в действительности указывает на
объект типа Base\n”;
dp = &d_ob;
bp = dynamic_cast<Base*>(dp);
if(bp){
cout << “Указатель dp к типу Base* приведен успешно\n”;
else
cout << “Ошибка\n”;
}
}
return 0;
8.3. Операторы const_cast, reinterpret_cast, static_cast
Оператор const_cast при выполнении операции приведения типов используется для явной подмены атрибутов const и/или volatile.
Целевой тип должен совпадать с исходным типом, за исключением
изменения его атрибутов const или volatile. Обычно с помощью оператора const_cast значение лишают атрибута const:
#include <iostream>
using namespace std;
void f(cons int *p){
int* v;
v = const_cast<int*>(p);
*v= 100;
83
}
int main(){
int x = 99;
f(&x); // х = 100
}
Оператор static_cast предназначен для выполнения операций
приведения типов над объектами неполиморфных классов. Например, его можно использовать для приведения типа указателя базового класса к типу указателя производного класса, но не в динамическом режиме (режиме исполнения программы):
#include <iostream>
using namespace std;
int main(){
int i;
float f = 199.22;
i = static_cast<int>(f); //f к int
}
Оператор reinterpret_cast дает возможность преобразовать указатель одного типа в указатель совершенно другого типа. Он также
позволяет приводить указатель к типу целого и целое к типу указателя. Оператор reinterpret_cast следует использовать для выполнения
операции приведения внутренне несовместимых типов указателей:
#include <iostream>
using namespace std;
int main(){
int i;
char *p = “строка”;
i = reinterpret_cast<int>(p);
cout << i ;
return 0;
}
84
Глава 9
СТАНДАРТНАЯ БИБЛИОТЕКА ШАБЛОНОВ
(STANDARD TEMPLATE LIBRARY, STL)
STL – стандартная библиотека шаблонов. Эта библиотека представляет большой набор данных структур и алгоритмов. STL позволяет работать с любыми типами данных и производить над ними
операции.
Контейнеры – это объекты, предназначенные для хранения других объектов. Контейнеры бывают различных типов (табл. 7).
Алгоритмы выполняют операции над содержимым контейнеров.
Существуют алгоритмы для инициализации, сортировки, поиска
или замены содержимого контейнеров.
Итераторы – это объекты, которые по отношению к контейнерам
играют роль указателей. Они позволяют получать доступ к содержимому контейнера.
Типом итератора является iterator, он определен в различных
контейнерах.
У каждого контейнера есть свой распределитель памяти (allocator),
который управляет процессом выделения памяти для контейнера.
Таблица 7
Список контейнеров
Контейнер
Описание
Заголовок
bitset
deque
list
Множество битов
Двусторонняя очередь
Линейный список
Ассоциативный список для хранения пар
ключ/значение, где с каждым ключом
связано только одно значение
Ассоциативный список для хранения пар
ключ/значение, где с каждым ключом
связано два или более значений
Множество, в котором каждый элемент
не обязательно уникален
Очередь с приоритетом
Очередь
Множество, в котором каждый элемент
уникален
Стек
Динамический массив
<bitset>
<deque>
<list>
map
multimap
miltiset
priority_queue
queue
set
stack
vector
<map>
<map>
<set>
<queue>
<queue>
<set>
<stack>
<vector>
85
По умолчанию распределителем памяти является объект класса
allocator.
В некоторых алгоритмах и контейнерах используется функция
особого типа, называемая предикатом (predicate). Предикат может
быть бинарным или унарным. У унарного предиката один аргумент,
а у бинарного – два. Возвращаемым значением является либо «истина», либо «ложь».
9.1. Вектор (vector)
Динамический массив – это массив, размеры которого могут увеличиваться автоматически по мере необходимости.
Спецификация шаблона для класса vector:
template <class T, class Allocator = allocator<T>> class vector,
Т – тип предназначенных для хранения в контейнере данных.
Allocator задает распределитель памяти, который по умолчанию
является стандартным распределителем памяти.
Хотя синтаксис шаблона выглядит довольно сложно, в объявлении вектора ничего сложного нет:
vector <int> iv;
vector<char> cv(5); // создание вектора из 5 элементов
vector<char> cv(5, ‘x’); // создание и инициализация
5 элементов вектора ‘x’
vector<int> iv2(iv); //создание вектора копии вектора целых
Для класса vector определяются следующие операторы сравнения:
=, <, <=, !=, >, >=.
Также для этого класса определяется оператор индекса [], что
обеспечивает доступ к элементам вектора посредством обычной индексной нотации, которая используется для доступа к элементам
стандартного массива.
Для vector определено достаточно много функций для взаимодействия с элементами, основные это:
– size() – возвращает текущий размер вектора;
– begin() – возвращает итератор начала вектора;
– end() – возвращает итератор конца вектора;
– push_back() – помещает значение в конец вектора;
– insert() – вставляет элемент на указанную позицию;
– erase() – удаляет элементы из вектора.
Пример: основные операции по работе с вектором:
int main(){
vector<int> v;
86
for (int i=0; i<10; i++){
v.push_bask(i);
}
cout << “Размер = ” << v.size();
cout << “В векторе хранится: ”;
for (int i=0; i<10; i++){
cout<<v[i];
}
for (int i=0; i< v.size(); i++){
v.push_bask(i+10);
}
cout << “Размер = ” << v.size();
cout << “В векторе хранится: ”;
for (int i=0; i<v.size(); i++){
cout<<v[i];
}
for (int i=0; i< v.size(); i++){
v[i] = v[i]+v[i];
}
cout << “В векторе хранится: ”;
for (int i=0; i<v.size(); i++){
cout<<v[i];
}
}
В функции main создается вектор v для хранения целых. Поскольку не используется никакой инициализации, то это пустой
вектор с емкостью, равной 0, иными словами, это вектор нулевой
длины. Далее с помощью push_bask() к концу вектора добавляется
10 элементов. Чтобы разместить эти элементы, вектор увеличивается. Его размер становится равным 10. Далее к вектору добавляется
еще 10 элементов, вектор для этого снова автоматически увеличивается. И последняя операция – с помощью стандартного оператора
индекса массива изменяются значения элементов вектора. Функция size() позволяет определить размер вектора во время выполнения программы, что удобно при построении циклов (в данном примере – цикл for).
Пример с использованием итератора:
int main(){
vector<int> v;
int i;
for (int i=0; i<10; i++) v.push_back(i);
87
vector<int>::iterator p = v.begin();
while(p != v.end()){
cout << *p;
p++;
}
}
В этой программе тоже сначала создается вектор, и в него добавляется 10 элементов с помощью push_back(). Тип итератора
определяется с помощью класса-контейнера. С помощью функции
begin() итератор инициализируется, указывая на начало вектора.
Возвращаемым значением этой функции является итератор начала
вектора. Применяя к итератору инкремент, можно получить доступ
к любому элементу вектора. Функция end() определяет факт достижения конца вектора.
Пример insert и erase:
int main(){
vector<int> v(5, 1);
int i;
vector<int>::iterator p = v.begin();
p += 2; // указывает на 3й элемент
v.insert(p, 10, 9); //вставка 10 новых элементов с места куда
указывает p, содержащих значение 9
p = v.begin();
p += 2;
v.erase(p, p+10); // удаление 10 элементов с позиции на которую
указывает p
}
Пример: вектор используется для хранения объектов пользовательского типа данных (класса). В классе определяются конструктор по умолчанию, перегруженные операторы == и <:
class Demo{
double d;
public:
Demo() {d = 0.0;}
Demo (double x) {d = x;}
Demo &operator=(double x){d = x; return *this;}
double getd(){return d;}
};
bool operator<(Demo a, Demo b){ return a.getd() < b.getd();}
bool operator==(Demo a, Demo b) {return a.getd()==b.getd();}
88
int main(){
vector<Demo> v;
int i;
for (i=0; i<10; i++)
v.push_back(Demo(i/3.0));
for (i=0; i<v.size(); i++)
v[i] = v[i].getd()*2.1;
}
9.2. Список (list)
Это двунаправленный линейный список, в отличие от вектора
доступ к элементам может быть только последовательным. Списки
являются двунаправленными, поэтому доступ к элементам списка
возможен с обеих его сторон:
template <class T, class Allocator = allocator<T>> class list
T – это тип данных, предназначенных для хранения в списке.
Allocator – задает распределитель памяти, который по умолчанию является стандартным.
Для класса list определены следующие операторы сравнения: ==,
<, <=, !=, >, >=
Определено много функций для работы со списком. Некоторые
из них:
– size() – возвращает хранящееся на данный момент в списке число элементов;
– push_back() – добавляет в конец списка элемент;
– push_front() – добавляет в начало списка элемент;
– splice() – соединяет два списка;
– merge() – выполняет слияние упорядоченных списков;
– sort() – сортирует список;
– begin() – возвращает итератор на первый элемент списка;
– end() – возвращает итератор конца списка.
Пример:
int main(){
list<char> lst1, lst2;
int i;
for (i=0; i<10; i+=2) lst1.push_back(‘A’+i);//ACEGI
for (i=1; i<11; i+=2) lst2.push_back(‘A’+i);//BDFHJ
lst1.merge(lst2); // lst2 теперь пуст
//В lst1 находится отсортированный общий список: ABCDEFGHIJ
}
89
Пример: список используется для хранения объектов пользовательского типа. Перегружаются операторы <, >, != и ==, так как при
поиске, сортировке или слиянии элементы сравниваются:
class Project{
public:
char name[40];
int days_to_completion;
Project(){strcpy (name, ‘’); days_to_completion = 0;}
Project(char* n, int d) { strcpy (name, n);
days_to_completion = d;}
void add_days (int i){days_to_completion += i;}
void sub_days(int i) { days_to_completion -= i;}
bool completed() {return !days_to_completion;}
void report(){…//вывод инфорамции}
};
bool operator<(const Project &a, const Project &b){
return a.days_to_completion < b.days_to_completion;
}
bool operator>(const Project &a, const Project &b){
return a.days_to_completion > b.days_to_completion;
}
bool operator==(const Project &a, const Project &b){
return a.days_to_completion == b.days_to_completion;
}
bool operator!=(const Project &a, const Project &b){
return a.days_to_completion != b.days_to_completion;
}
int main{
list<Project> proj;
proj.push_back(Project(“Разработка компилятора”, 35));
proj.push_back(Project(“Разработка электронной таблицы”,
190));
proj.push_back(Project(“Разработка STL”, 1000));
list<Project>::iterator p = proj.begin();
//увеличение сроков выполнения первого проекта на 10 дней
p = proj.begin();
p = add_days(10);
//последовательное завершение первого проекта
do{
p->sub_days(5);
90
}
p->report();
}while(!p->completed());
9.3. Ассоциативные списки
Это ассоциативный контейнер, в котором каждому значению соответствует уникальный ключ. После того, как значение помещено
в контейнер, извлечь его оттуда можно с помощью ключа.
Преимущество таких списков состоит в возможности получения
значения по данному ключу.
В ассоциативном списке можно хранить только уникальные
ключи, дублирование ключей не допускается:
– template<class Key, class T, class Comp=less<Key>, class
Allocator = allocator<T>> class map;
– Key – это данные типа ключ;
– Т – тип данных;
– Comp – функция для сравнения двух ключей, по умолчанию
функция less();
– Allocator – распределитель памяти.
Для любого объекта, заданного в качестве ключа, должны быть определены конструктор по умолчанию и несколько операторов сравнения.
Для класса map определяются следующие операторы сравнения:
==, <, <=, !=, >, >=
В ассоциативном списке хранятся пары ключ/значение в виде
объектов типа pair. Шаблон объекта типа pair имеет следующую
спецификацию:
template <class Ktype, class Vtype> struct pair{
typedef Ktype первый_тип; //тип ключа
typedef Vtype второй_тип; // тип значения
Ktype первый; // ключ
Vtype второй; // значение
// конструкторы
pair();
pair(const Ktype &k, const Vtype &v);
template<class A, class B> pair(const<A, B> &объект);
}
Создавать пары ключ/значение можно не только с помощью конструкторов класса pair, но и с помощью функции make_pair():
template<class Ktype, class Vtype>pair<Ktype,
Vtype>make_pair(const Ktype &k, const Vtype &v);
91
Пример: ключ – символ, значение – целое число. Когда пользователь нажимает на клавиатуре ключ (буквы от A до J), программа
выводит на экран соответствующее этому ключу значение:
int main(){
map<char, int> m;
int i;
//размещение пар в map
for (i=0; i<10; i++){}
m.insert(pair<char, int>(‘A’+I, i));
char ch;
cout << “Введите ключ: “;
cin >> ch;
map<char, int>::iterator p;
// поиск значения по заданному ключу
p = m.find(ch); // возвращает итератор соответствующего
ключу элемента или итератор конца
ассоциативного списка, если такого
ключа нет
if (p!=m.en())
cout << p->second;
else
cout << “Такого ключа нет”;
}
Пример 1: использование make_pair:
//размещение пар в map
for (i=0; i<10; i++)
m.insert(make_pair(char)(‘A’+ш, i)).
Пример 2: создается ассоциативный список для хранения слов
со словами-антонимами. Используется два класса: word (слово) и
opposite (антоним). Так как для ассоциативных списков поддерживается отсортированный список ключей, для класса word определяется оператор <. Как правило, оператор < необходимо перегружать
для всех классов, объекты которых предполагается использовать
в качестве ключей:
class word{
char str[20];
public:
word(){strcpy(str, “”);}
word(char *s) {strcpy(str, s);}
char* get(){return str;}
92
};
bool operator<(word a, word b){
return strcmp(a.get(), b.get()) < 0;
}
class opposite{
char str[20];
public:
opposite(){strcpy(str, “”);}
opposite(char* s){strcpy(str, s);}
char* get(){return str;}
};
int main(){
map<word, opposite> m;
//размещение в списке слов и антонимов
m.insert(pair<word, opposite>(word(“yes”, opposite(“no”)));
m.insert(pair<word, opposite>(word(“good”,
opposite(“bad”)));
m.insert(pair<word, opposite>(word(“left”,
opposite(“right”)));
m.insert(pair<word, opposite>(word(“top”,
opposite(“bottom”)));
//поиск антонима по заданному слову
char str[80];
cout << “Введите слово: ”;
cin >> str;
map<word, opposite>::iterator p;
p = m.find(word(str));
if (p!=m.end())
cout << “Антоним: ” << p->second.get();
else
cout << “Такого слова нет”;
}
9.4. Алгоритмы
Алгоритмы предназначены для разнообразной обработки
контейнеров. Для доступа к алгоритмам библиотеки стандартных шаблонов в программу необходимо включить заголовок
<algorithm>.
Все алгоритмы представляют собой шаблоны функции. Это значит, что их можно использовать с контейнерами любых типов.
Алгоритмов очень много, часть представлена в табл. 8.
93
Таблица 8
Алгоритмы
Алгоритм
Назначение
Equal
Определяет идентичность двух диапазонов
Find
Выполняет поиск диапазона для значения и возвращает первый найденный элемент
Includes
Определяет, включает ли одна последовательность все элементы другой последовательности
lexicoghaphical_compare
Сравнивает 2 последовательности в алфавитном
порядке
Max
…
Возвращает максимальное из двух значений
…
Пример: count() и count_if(). Count() возвращает число элементов
в последовательности, начиная с элемента, обозначенного итератором начало, и заканчивая элементом, обозначенным итератором
окончание, значение которых равно параметру значение. Count_if()
возвращает число элементов в последовательности, начиная с элемента начало и заканчивая элементом окончание, для которых
унарный предикат ф_предикат возвращает истину:
/*это унарный предикат, который определяет, является ли значение
четным */
bool even(int x){ return !(x%2); }
int main(){
vector<int> v;
int i;
for (i=0; i<20; i++){
if(i%2) v.push_back(1);
else v.push_back(2);
} // 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1
int n;
n = count(v.begin(), v.end(), 1); // 10
n = count_if(v.begin(), v.end(), even); //10
}
Все унарные предикаты получают в качестве параметра объект,
тип которого тот же, что и тип объектов контейнера, для работы
которого предназначен предикат. В зависимости от значения этого
объекта унарный предикат должен возвращать истину либо ложь.
Пример: remove_copy() – копирует элементы, равные параметру
значение, из заданного итераторами начало и окончание диапазона
94
и размещает результат в последовательности, обозначенный итератором результат:
template<class InIter, class OutIter, class T> OutIter remove_
copy(InIter начало, InIter окончание, OutIter результат, const T
значение)
Алгоритм возвращает итератор конца новой последовательности.
Результирующий контейнер должен быть достаточно велик для хранения новой последовательности:
int main(){
vector<int> v, v(20);
int i;
for (i=0; i<20; i++){
if (i%2) v.push_back(1);
else v.puh_back(2);
} //2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1
//удаление единиц
remove_copy(v.begin(), v.end(), v2.begin(), 1);
// 2 2 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 0 0 0 0
}
Пример: transform() – модифицирует каждый элемент некоторого диапазона в соответствии с заданной функцией. Имеет 2 формы:
– template<class InIter, class OutIter, class Func> OutIter
transform(InIter начало, InIter окончание, OutIter результат, Func
унарная_операция);
– template<class InIter1, class InIter2, class OutIter, class Func>
OutIter transform(InIter1 начало1, InIter1 окончание1, InIter2 начало2, InIter2 окончание2, OutIter результат, Func бинарная функция); transform() применяет функцию к диапазону элементов и сохраняет результат в месте, определенном итератором результат.
В первой форме диапазон задается итераторами начало и окончание, а применяемой является унарная_функция.
Во второй форме модификация осуществляется с помощью бинарной_функции, которая в качестве первого параметра получает
значение элемента из предназначенной для модификации последовательности, а в качестве второго параметра – элемент из второй последовательности.
Обе версии возвращают итератор конца итоговой последовательности:
//xform() – возводит в квадрат элементы списка. Итоговая
последовательность хранится в том же списке, что и исходная.
95
int xform(int i){ return i*i;}
int main(){
list<int> x1;
int i;
//размещение элементов в списке: 0 1 2 3 4 5 6 7 8 9
for (i=0; i<10; i++)
x1.push_back(i);
// модификация элементов списка x1
p = transform(x1.begin(), x1.end(), x1.begin(), xform);
// 0 1 4 9 16 25 36 49 64 81
}
96
Приложение
ОТЛИЧИЕ С++ ОТ С
1. Комментарии в С++
Комментарий в С:
/* Однострочный комментарий в С */
Однострочный комментарий в С++:
// Комментарий
Для многострочных комментариев возможны варианты:
// Это один из способов
// написания
// многострочных комментариев
Или:
/* Это один из способов */
/* написания */
/* многострочных комментариев */
Или:
/* Это один из способов
написания
многострочных комментариев */
Важно помнить о том, что при последнем варианте написания
многострочных комментариев, необходимо ставить */.
2. Стандартная библиотека. Потоковый ввод/вывод C++
С++ предусматривает альтернативу printf и scanf.
Пример: На языке С имеем следующий код:
printf(“Enter number: “);
scanf(“%d”, &num);
printf(“The new number is: %d\n”, num);
На С++ тот же код будет иметь вид:
cout << “Enter number : “;
cin >> num;
cout << “The new number is : ” << num << ‘\n’
В первом операторе используется стандартный выходной поток
cout и операция << (операция передачи в поток), во втором операторе – стандартный входной поток cin и операция >> (операция извлечения из потока).
97
Для организации потокового ввода/вывода программы на С++
должны включать заголовочный файл iosteram.h.
Использование этих операторов делает программы более читаемыми и менее уязвимыми для ошибок, так как они не требуют форматирующих строк и спецификаторов преобразования для указания типа входных и выходных данных.
3. Ключевое слово void и возвращаемые значения
Функция может возвращать значение, может не возвращать значение.
1. Функция, которая не возвращает значение:
В языке C для определения пустого списка параметров в круглые
скобки помещается ключевое слово void : void main(void). В С++ при
задании пустого списка параметров не пишется ничего: void main().
Такое объявление функции сообщает, что функция не принимает параметров и не возвращает значения:
// заголовок функции
void /*имя функции*/(/*параметры функции*/)
{
// тело функции
}
2. Функция, возвращающая значение:
Если функция возвращает значение: int f1();, то она обязана вернуть значение соответствующего типа.
C помощью зарезервированного слова return нужно вернуть
значение, по завершении работы функции. Все, что нужно, так это
указать переменную, содержащую нужное значение, или некоторое
значение, после оператора return:
// заголовок функции
/*возвращаемый тип данных*/ /*имя функции*/
(/*параметры функции*/)
{
// тело функции
return /*возвращаемое значение*/;
}
4. Тип данных bool
Это логический тип данных, используемый для хранения результатов логических выражений, у него может быть один из двух
результатов – true (истина) или false (ложь).
98
True эквивалентно ненулевое значение, false эквивалентен 0.
Пример:
int a=2, b=3;
bool c = a<b; // c = true
int *p = &a; // указатель на целое число
if (a) {…}
if (a!=0) {…}
if (!p) {…}
if (p == NULL) {…}
5. Объявления в C++
В С++ объявления могут размещаться всюду, при условии, что
предшествуют использованию того, что объявляется.
Пример:
cout << “Enter two integers : “;
int x, y;
cin >> x >> y
Кроме того, переменные могут объявляться в разделе инициализации структуры for. Такие переменные остаются в области видимости до конца блока. Например:
for (int i = 0; i < 5; i++){
cout << i << ‘\n’;
}
Тут объявляется переменная i и инициализируется 0 в структуре for.
Область действия и видимости локальной переменной в языке
C++ начинается с ее объявления и распространяется до закрывающей правой фигурной скобки.
Объявления переменных не могут помещаться в условиях структур while, do/while, for или if.
6. Глобальные и локальные переменные в C++
Каждая переменная имеет область видимости – область, в которой можно работать с переменной.
Переменные, объявленные внутри функции, называются локальными, т. е. в разных функциях можно использовать переменные с одинаковыми именами.
Глобальные переменные объявляются вне тела какой-либо
функции и область видимости этих переменных распространяется
99
на всю программу. Обычно глобальные переменные объявляются
перед главной функцией.
Пример:
void example();
int variable = 48; // инициализация глобальной переменной
int main(int argc, char* argv[]) {
int variable = 12; // инициализация локальной переменной
// печать значения содержащегося в локальной переменной
cout << "local variable = " << variable << endl;
example(); // запуск функции
return 0;
}
void example() {
// функция видит только глобальную переменную
cout << "global variable = " << variable << endl;
}
Функция example имеет доступ только к глобальной переменной, в то время как main имеет доступ как к локальной, так и к глобальной переменной. Если в области видимости есть и локальная
и глобальная переменные с одинаковыми именами, то при обращении к ним будет использоваться ближайшая переменная, а это локальная, поэтому вывод программы будет:
local variable = 12
global variable = 48
Чтобы из функции main обратиться к глобальной переменной,
необходимо использовать оператор ::. Он позволяет обратиться к глобальной переменной из любого места программы:
void example();
int variable = 48; // инициализация глобальной переменной
int main(int argc, char* argv[])
{
int variable = 12; // инициализация локальной переменной
// печать значения содержащегося в глобальной переменной
cout << "local variable = " << ::variable << endl;
example(); // запуск функции
system("pause");
return 0;
}
void example()
{
100
// функция видит только глобальную переменную
cout << "global variable = " << variable << endl;
}
Вывод программы будет:
local variable = 48
global variable = 48
7. Встроенные функции (inline)
Функции позволяют уменьшить накладные расходы при вызове
функций. Модификатор inline рекомендует компилятору генерировать в месте вызова функции копию ее кода для того, чтобы избежать вызова функции. Вызовы функций занимают больше времени, чем написание всего кода без функции.
Встроенные функции очень хороши для ускорения программы,
но, если вы используете их слишком часто или с большими функциями, у вас будет чрезвычайно большая программа. Иногда большие
программы менее эффективны и поэтому они будут работать медленнее, чем раньше. Встроенные функции лучше всего подходят
для небольших функций, которые часто вызываются:
inline void hello(){
cout<<"hello";
}
int main() {
hello();
}
Встроенные функции обладают преимуществами перед макросами препроцессора:
a) при обращении к встроенным функциям выполняется надлежащий контроль соответствия типов, макросы же это не поддерживают;
b) встроенные функции исключают непредвиденные побочные
эффекты, связанные с неправильным использованием макросов;
c) встроенные функции можно отлаживать с помощью отладчика. Макросы же не распознаются отладчиком, для него это просто
текстовые замены, выполняемые процессором перед компиляцией:
#include <iosteram>
#define VALIDSQUARE(x) (x)*(x)
#define INVALIDSQUARE(x) x*x
inline int square(int x) {return x*x;}
101
int main(){
cout << “VALIDSQUARE(2+3) = ” << VALIDSQUARE(2+3) << “\nINVALIDSQUARE(2+3) = ” << INVALIDSQUARE(2+3) << “\n square(2+3) = ” <<
square(2+3) << “\n”;
return 0;
}
Вывод:
VALIDSQUARE (2+3) = 25
INVALIDSQUARE (2+3) = 11
Square (2+3) = 25
8. Перегрузка функций
Это определение нескольких функций (двух или больше) с одинаковым именем, но различными параметрами.
Наборы параметров перегруженных функций могут отличаться порядком следования, количеством, типом. Таким образом, перегрузка
функций нужна для того, чтобы избежать дублирования имен функций,
выполняющих сходные действия, но с различной программной логикой.
Пример:
int square(int a) {
return a * a;
}
float square (float a) {
return a * a;
}
Это две одинаковые функции с одинаковыми именами, но разные по входным параметрам и выполняемым действиям.
Вызов перегруженных функций ничем не отличается от вызова
обычных функций, например:
square (32); // будет вызвана функция, int square
square (4.0); // будет вызвана функция float square
Компилятор самостоятельно выберет нужную функцию. Перегруженные функции отличаются своей сигнатурой – комбинацией
имени функции и типов ее параметров. Компилятор специальным
образом кодирует идентификатор каждой функции (декодирование
имен), используя количество и типы ее параметров для обеспечения
безопасного по типу редактирования связей (компоновки).
Перегруженные функции могут иметь разные типы возвращаемых значений, но при этом они обязаны иметь различные списки
параметров.
102
9. Значение по умолчанию
При обращении к функциям может передаваться особое значение аргумента. Можно указать аргумент по умолчанию.
Аргументы по умолчанию должны быть последними аргументами в списке параметров функции. Аргументы по умолчанию задаются при первом появлении имени функции (ее прототипе):
//функция, вычисляющая площадь прямоугольника с двумя параметрами
int rectangle(int a, int b=10)
{
return a * b;
}
Теперь возможные вызовы функции rectangle:
– rectangle (10, 20);
– rectangle (20);
Возможна ошибка при использовании перегруженных функций
с использованием аргументов по умолчанию:
void F();
void F(int);
void F(long);
int F(int); // нельзя, так как отличается только тип
возвращаемого значения:
int F(char*);
void F(int, char*);
void F(char*, int); //можно, аргументы следуют
в другом порядке
void F(char* s, int x, int y = 25); //можно, три аргумента
вместо двух
F (“hello”, 20); // ошибка, совпадают две сигнатуры
10. Передача значений в функцию
по значению, по ссылке, по указателю
В С++ добавляется передача параметра по ссылке. Параметрссылка – это псевдоним для соответствующего ему параметра:
int &count;
Ссылка на локальное имя параметра в теле функции на самом
деле относится к исходной переменной в вызывающей функции, и
исходная переменная может модифицироваться.
Если вы однажды присвоили ссылке значение, вы не можете его
изменить.
103
Пример:
int squareByValue(int);
void squareByPointer(int *);
void squareByReference(int &);
int main(){
int x = 2, y = 3, z = 4;
cout << “x = ” << x << “ before squareByValue\n” << “Value returned by squareByValue : “ << squareByValue(x) << “\nx = ” << x <<
“after squareByValue\n\n ”;
cout << “y = ” << y << “ before squareByPointer \n”;
squareByPointer (y);
cout<< “y = ” << y << “after squareByPointer \n ”;
cout << “z = ” << z << “ before squareByReference \n”;
squareByReference (z);
cout<< “z = ” << z << “after squareByReference \n ”;
return 0;
}
int squareByValue(int a){
return a *= a;
}
void squareByPointer (int *bPtr){
*bPtr *= *bPtr;
}
void squareByReference(int &cRef){
cRef *= cRef;
}
Вывод функций:
X = 2 before squareByValue
Value returned by squareByValue : 4
X = 2 squareByValue
Y = 3 before squareByPointer
Y = 9 after squareByPointer
Z = 4 before squareByReference
Z = 16 after squareByReference
Используйте указатели для передачи параметров, которые могут
быть изменены вызываемой функцией, и ссылки на константы для
передачи параметров большого размера, не подлежащих измене104
нию, это позволит избежать больших накладных расходов, связанных с передачей копии большого объекта.
Переменные ссылки должны инициализироваться при их объявлении. Как только ссылка объявлена в качестве псевдонима другой переменной, все операции, выполняемые якобы над псевдонимом, на самом
деле выполняются непосредственно над самой исходной переменной:
int count = 1;
int &c = count;
++c; // увеличивает значение переменной count,
используя ее псевдоним
При возврате указателя или ссылки из функции необходимо, чтобы
она указывала на статическую переменную, иначе указатель или ссылка будут относиться к переменной, которая разрушается по завершении функции и поведение программы может стать непредсказуемым.
11. Модификатор const
Использование const в списке параметров функции указывает,
что передаваемый параметр не может быть изменен:
void f (const int t)
Он используется:
– для переменных-констант (вместо использования #define). Переменная должна быть инициализирована константным выражением
при ее объявлении и не может быть изменена в дальнейшем:
const float pi = 3.14159;
– для определения константного указателя, адрес изменить нельзя, но можно менять содержимое по этому адресу:
int i =17;
int j = 29;
int* const p; //нельзя, должно быть задано начальное значение
int* const p1 = &i;
*p1 = 29; // можно
p1 = &j; //нельзя;
– как указатель на константный объект. Значение по указателю
не может быть изменено, но самому указателю может быть присвоено
другое значение, указывающее на другую область памяти:
const int *ptr = &i;
– как константный указатель на константные данные. Нельзя
изменять ни значение, ни адрес:
const int *const i
105
12. Динамическое распределение памяти (new и delete)
New заменяет malloc:
int *ptr;
ptr = malloc(sizeof(int)); или ptr = new int;
New выделяет память для объекта из области свободной памяти
программы (памяти, доступной программе во время выполнения).
New автоматически создает объект соответствующего размера и
возвращает указатель правильного типа. Если new не может выделить память, возвращается NULL.
Для освобождения памяти, выделенной для этого объекта, применяется оператор delete:
delete ptr;
Операция delete может быть использована только для освобождения памяти, выделенной операцией new.
Можно инициализировать создаваемый объект:
float *tPtr = new float (3.14159);
Создание массивов с помощью new:
int *arrayPtr;
arrrayPtr = new int [10];
Для освобождения памяти, выделенной под arrayPtr, используется delete:
delete [] arrayPtr;
13. Ключевое слово static
Static может быть использовано в трех основных контекстах:
– внутри функции;
– внутри определения класса;
– перед глобальной переменной внутри файла, составляющего
многофайловую программу.
Существуют три варианта использования ключевого слова static.
1. Первое использование static – внутри функции. Использование static внутри функции является самым простым. Это просто
означает, что после того, как переменная была инициализирована,
она остается в памяти до конца программы.
Например, вы можете использовать статическую переменную для
записи количества раз, когда функция была вызвана, просто добавив
строки static int count = 0; и count++; в функцию. Так как count
является статической переменной, строка static int count = 0; будет
106
выполняться только один раз. Всякий раз, когда функция вызывается, count будет иметь последнее значение, данное ему.
Вы также можете использовать static таким образом, чтобы предотвратить переинициализацию переменной внутри цикла. Например,
в следующем коде переменная number_of_times будет равна 100, несмотря на то, что строка static int number_of_times = 0 или находится внутри цикла, где она, по-видимому, должна исполнятся каждый раз, когда программа доходит до цикла. Хитрость заключается в том, что ключевое слово static препятствует повторной инициализации переменной.
2. Второе использование static – внутри определения класса. Хотя
большинство переменных, объявленных внутри класса, могут иметь
разное значение в каждом экземпляре класса, статические поля класса будут иметь то же значение для всех экземпляров данного класса и
даже не обязательно создавать экземпляр этого класса.
Важно отметить, что хорошим тоном при использовании статических переменных класса является использование class _ name::х;,
а не instance _ of _ class.x;. Это помогает напомнить программисту,
что статические переменные не принадлежат к одному экземпляру класса и что вам не обязательно создавать экземпляр этого класса. Как вы
уже, наверное, заметили, для доступа к static можно использовать оператор области видимости :: , когда вы обращаетесь к нему через имя класса.
У вас также могут быть статические функции класса. Статические
функции – это функции, которые не требуют экземпляра класса и вызываются также по аналогии со статическими переменным с именем
класса, а не с именем объекта. Например, a _ class::static _ function();,
а не an _ instance.function();. Статические функции могут работать
только со статическими членами класса, так как они не относятся
к конкретным экземплярам класса. Статические функции могут быть
использованы для изменения статических переменных, отслеживать
их значения. Например, вы можете использовать статическую функцию, если вы решили использовать счетчик, чтобы дать каждому экземпляру класса уникальный идентификатор.
Последнее использование static – глобальная переменная в файле
кода. В этом случае использование static указывает, что исходный
код в других файлах, которые являются частью проекта, не может
получить доступ к переменной. Только код внутри того же файла может увидеть переменную (ее область видимости ограничена файлом).
14. Ключевое слово extern
Поскольку язык С позволяет выполнять раздельную компиляцию
модулей для большой программы в целях ускорения компиляции
107
и помощи управлению большими проектами, должны существовать
способы передачи информации о глобальных переменных файлам
программы. Решение заключается в объявлении всех глобальных переменных в одном файле и использовании при объявлении в других
файлах слова extern.
Если при объявлении выделяется память под переменную, то
процесс называется определением. Использование extern приводит
к объявлению, но не к определению. Оно просто говорит компилятору, что определение происходит где-то в другом месте программы.
15. Стандартная библиотека. Работа с файлами
Большинство компьютерных программ работают с файлами и
поэтому возникает необходимость создавать, удалять, записывать,
читать, открывать файлы.
Существует такое понятие, как полное имя файлов – это полный
адрес к директории файла с указанием имени файла, например: D:\
docs\file.txt.
Для работы с файлами необходимо подключить заголовочный
файл <fstream>. В <fstream> определены несколько классов и
подключены заголовочные файлы <ifstream> – файловый ввод
и <ofstream> – файловый вывод.
Файловый ввод/вывод аналогичен стандартному вводу/выводу,
единственное отличие – ввод/вывод выполнятся не на экран, а в файл.
Например, необходимо создать текстовый файл и записать в него
строку «Работа с файлами в С++». Для этого необходимо проделать
следующие шаги:
1. создать объект класса ofstream;
2. связать объект класса с файлом, в который будет производиться запись;
3. записать строку в файл;
4. закрыть файл.
// создаем объект для записи в файл
ofstream /*имя объекта*/; // объект класса ofstream
Назовем объект – fout, получится
ofstream fout;
Объект необходим, чтобы можно было выполнять запись в файл.
Объект уже создан, но не связан с файлом, в который нужно записать строку:
fout.open("cppstudio.txt"); // связываем объект с файлом
108
Файл открыт, осталось записать в него нужную строку:
fout << "Работа с файлами в С++"; // запись строки в файл
При использовании операции передачи в поток совместно с объектом fout строка записывается в файл. Так как больше нет необходимости изменять содержимое файла, его нужно закрыть, т. е. отделить объект от файла:
fout.close(); // закрываем файл
Шаги 1 и 2 можно объединить, т. е. в одной строке создать объект
и связать его с файлом. Делается это так:
// создаем объект класса ofstream и связываем его с файлом cppstudio.txt
ofstream fout("cppstudio.txt");
// file.cpp: определяет точку входа для консольного приложения
#include "stdafx.h"
#include <fstream>
using namespace std;
int main(int argc, char* argv[])
{
// создаем объект класса ofstream для записи и
связываем его с файлом cppstudio.txt
ofstream fout("cppstudio.txt");
fout << "Работа с файлами в С++"; // запись строки в файл
fout.close(); // закрываем файл
system("pause");
return 0;
}
Для того чтобы прочитать файл, понадобится выполнить те же
шаги, что и при записи в файл с небольшими изменениями:
1. создать объект класса ifstream и связать его с файлом, из которого будет производиться считывание;
2. прочитать файл;
3. закрыть файл.
// file_read.cpp: определяет точку входа для консольного приложения.
#include "stdafx.h"
#include <fstream>
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
109
setlocale(LC_ALL, "rus");
// буфер промежуточного хранения считываемого
из файла текста
char buff[50];
ifstream fin("cppstudio.txt"); // открыли файл для чтения
fin >> buff; // считали первое слово из файла
cout << buff << endl; // напечатали это слово
fin.getline(buff, 50); // считали строку из файла
fin.close(); // закрываем файл
cout << buff << endl; // напечатали эту строку
system("pause");
return 0;
}
В С++ предусмотрена функция is_open(), которая возвращает целые значения: 1 – если файл был успешно открыт; 0 – если файл
открыт не был.
// file_read.cpp: определяет точку входа для консольного приложения.
#include “stdafx.h”
#include <fstream>
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
setlocale(LC_ALL, "rus");
char buff[50]; // буфер промежуточного хранения считываемого из
файла текста
// (ВВЕЛИ НЕ КОРРЕКТНОЕ ИМЯ ФАЙЛА)
ifstream fin(“cppstudio.doc”);
if (!fin.is_open()) // если файл не открыт
cout << "Файл не может быть открыт!\n";
else
{
fin >> buff; // считали первое слово из файла
cout << buff << endl; // напечатали это слово
fin.getline(buff, 50); // считали строку из файла
fin.close(); // закрываем файл
cout << buff << endl; // напечатали эту строку
}
system("pause");
return 0;
}
110
Таблица 9
Режимы открытия
Константа
Описание
ios_base::in
ios_base::out
ios_base::ate
ios_base::app
ios_base::trunc
ios_base::binary
Открыть файл для чтения
Открыть файл для записи
При открытии переместить указатель в конец файла
Открыть файл для записи в конец файла
Удалить содержимое файла, если он существует
Открытие файла в двоичном режиме
Режимы открытия файлов (табл. 9) устанавливают характер использования файлов. Для установки режима в классе ios_base предусмотрены константы, которые определяют режим открытия файлов.
Режимы открытия файлов можно устанавливать непосредственно при создании объекта или при вызове функции – open():
// открываем файл для добавления информации к концу файла
ofstream fout("cppstudio.txt", ios_base::app);
// открываем файл для добавления информации к концу файла
fout.open("cppstudio.txt", ios_base::app);
Режимы открытия файлов можно комбинировать с помощью
поразрядной логической операции ИЛИ|, например: ios_base::out
| ios_base::trunc — открытие файла для записи, с предварительной
его очисткой.
16. Параметры функции main (argc, argv)
При создании консольного приложения в языке программирования С++ автоматически создается строка, аналогичная
int main(int argc, char* argv[]) // параметры функции main()
Эта строка – заголовок главной функции main(), в скобочках
объявлены параметры argс и argv. Если программу запускать через
командную строку, то существует возможность передать какую-либо информацию этой программе, для этого и существуют параметры argc и argv[].
Параметр argc имеет тип данных int и содержит количество параметров, передаваемых в функцию main. Причем argc всегда не
меньше 1, даже когда мы не передаем никакой информации, так
как первым параметром считается имя функции.
Параметр argv[] – это массив указателей на строки. Через командную строку можно передать только данные строкового типа:
111
// argc_argv.cpp: определяет точку входа для консольного приложения.
#include "stdafx.h"
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
//если передаем аргументы, то argc будет больше 1
(в зависимости от кол-ва аргументов)
if (argc > 1)
{
//вывод второй строки из массива указателей
на строки(нумерация в строках начинается с 0 )
cout << argv[1]<<endl;
} else {
cout << "Not arguments" << endl;
}
system("pause");
return 0;
}
Если просто запустить программу в консоли (запуск – exe).
Например: C:\home\prj\main.exe
Вывод программы будет: Not arguments.
Если же запустить C:\home\prj\main.exe Open, то тут аргументом
является слово Open.
В консоли выведется повторно это слово, как и описано в main.
Если нужно передать параметр, состоящий из нескольких слов,
то их необходимо взять в двойные кавычки, и тогда эти слова будут
считаться как один параметр.
Например: C:\home\prj\main.exe “It work”
17. Стандартная библиотека. Строковый класс string
У класса basic_string имеется два производных класса: класс string,
который поддерживает строки 8-разрядных символов, и wstring, который поддерживает строки широких символов. Чаще всего имеют дело
с 8-разрядными символами, поэтому рассмотрим только string.
Этот класс также можно назвать контейнером.
Класс string очень велик, в нем имеется множество конструкторов и функций, которые в свою очередь имеют перегрузки.
Основные конструкторы класса string:
– string();
112
– string(const char *строка);
– string(const string &строка);
Некоторые операторы, которые доступны при работе с объектами типа string: =, +, +=, ==, !=, <, <=, >, >=, [], <<, >>.
Пример 1:
int main(){
string str1 (“Это проверка”);
string str2 (“АБВГДЕЖ”);
//вставка строки str2 в str1
str1.insert(4, str2); // Это АБВГДЕЖпроверка
//удаление семи символов из строки
str1.erase(4, 7); // Это проверка
//замена восьми символов из str1 символами str2
str1.replace(4, 8, str2); //Это АБВГДЕЖ
}
Пример 2: усовершенствованная версия программы создания ассоциативного списка для хранения слов и антонимов:
int main(){
map<string, string> m;
int i;
m.insert(pair<string, string>(“yes”, “no”);
m.insert(pair<string, string>(“good”, “bad”);
m.insert(pair<string, string>(“left”, “right”);
m.insert(pair<string, string>(“top”, “bottom”);
string s;
cout << “введите слово: ”;
cin >> s;
map<string, string>::iterator p;
p = m.find(s);
if(p!=m.end())
cout << “Антоним: ” << p->second;
else
cout << “Такого слова нет”;
}
113
Библиографический список
1. Шилдт Г. Самоучитель C++. – СПб.: БХВ-Петербург, 2006. –
638 с.
2. Элджер Дж. C++: Библиотека программиста. – СПб.: Питер,
1999. – 320 с.
3. Дейтел Х., Дейтел П. Как программировать на С. – БиномПресс, 2008. – 1456 с.
4. Страуструп Б. Язык программирования С++. – Бином, 2015. –
1136 с.
5. Мейерс С. Эффективное использование STL. Библиотека программиста. – СПб.: Питер, 2003. – 224 с.
6. Мюссер Дэвид Р., Держд Жилмер Дж., Сейни Атул. С++ и STL:
справочное руководство, 2-е изд. (серия C++ in Depth). – М.: Вильямс,
2010. – 432 с.
7. Вандевурд Дэвид, Джосаттис Николай М. Шаблоны С++ и STL:
справочник разработчика. М.: Издательский дом «Вильямс», 2016.
544 с.
114
СОДЕРЖАНИЕ
Введение.................................................................................
3
Глава 1. Введение в C++. Парадигмы программирования...............
1.1. Введение в ООП............................................................ 1.2. Основные механизмы ООП............................................ 1.3. Инкапсуляция............................................................. 1.4. Особенности написания и использования классов............. 1.4.1. Указатель this........................................................... 1.5. Отделение интерфейса от реализации.............................. 1.6. Функции доступа и сервисные функции.......................... 1.7. Дружественные функции.............................................. 1.8. Дружественные классы................................................. 4
4
4
5
8
9
11
11
12
14
Глава 2. Конструкторы и деструкторы........................................
2.1. Конструкторы.............................................................. 2.2. Деструктор.................................................................. 2.3. Список инициализации................................................. 2.4. Порядок вызова конструкторов и деструкторов................ 17
17
22
23
24
Глава 3. Перегрузка операторов.................................................
3.1. Бинарные операторы.................................................... 3.2. Оператор индексирования............................................. 3.3. Операторы по работе с потоком....................................... 3.4. Унарные операторы...................................................... 3.5. Операторы преобразования............................................ 26
27
33
34
35
36
Глава 4. Наследование..............................................................
4.1. Композиция классов..................................................... 4.2. Наследование.............................................................. 4.3. Область видимость и модификаторы доступа.................... 4.4. Неявное преобразование объектов производного класса
к базовому классу............................................................... 4.5. Динамический полиморфизм......................................... 4.6. Чистые виртуальные функции....................................... 4.7. Виртуальные деструкторы............................................. 4.8. Множественное наследование........................................ 4.9. Модификаторы доступа при наследовании....................... 4.10. Неоднозначности при наследовании.............................. 38
38
40
44
45
46
50
51
53
54
55
Глава 5. Пространство имен.......................................................
5.1. Стандартное пространство имен..................................... 5.2. Повторяющиеся базовые классы.................................... 5.3. Виртуальный базовый класс.......................................... 59
61
61
62
Глава 6. Исключения................................................................
6.1. Генерация и обработка исключительных ситуаций........... 6.2. Абсолютное завершение................................................ 6.3. Стандартный класс exception......................................... 63
63
66
67
115
Глава 7. Шаблоны....................................................................
7.1. Шаблоны функций....................................................... 7.2. Шаблоны классов......................................................... 70
70
72
Глава 8. Динамическая идентификация и приведение типов
(Run-Time Type Identification, RTTI)..........................................
8.1. Понятие о динамической идентификации типа................. 8.2. Оператор dynamic_cast.................................................. 8.3. Операторы const_cast, reinterpret_cast, static_cast............ 76
76
81
83
Глава 9. Стандартная библиотека шаблонов
(Standard Template Library, STL)................................................
9.1. Вектор (vector)............................................................. 9.2. Список (list). ............................................................... 9.3. Ассоциативные списки................................................. 9.4. Алгоритмы.................................................................. 85
86
89
91
93
Приложение. Отличие С++ от С.................................................
1. Комментарии в С++........................................................ 2. Стандартная библиотека. Потоковый ввод/вывод C++......... 3. Ключевое слово void и возвращаемые значения................... 4. Тип данных bool............................................................. 5. Объявления в C++........................................................... 6. Глобальные и локальные переменные в C++....................... 7. Встроенные функции (inline)............................................ 8. Перегрузка функций....................................................... 9. Значение по умолчанию................................................... 10. Передача значений в функцию по значению, по ссылке,
по указателю..................................................................... 11. Модификатор const........................................................ 12. Динамическое распределение памяти (new и delete)........... 13. Ключевое слово static.................................................... 14. Ключевое слово extern................................................... 15. Стандартная библиотека. Работа с файлами...................... 16. Параметры функции main (argc, argv).............................. 17. Стандартная библиотека. Строковый класс string.............. 97
97
97
98
98
99
99
101
102
103
103
105
106
106
107
108
111
112
Библиографический список.......................................................
114
Документ
Категория
Без категории
Просмотров
2
Размер файла
4 000 Кб
Теги
kyritcin
1/--страниц
Пожаловаться на содержимое документа