Системы программирования.

Курс записан со слов Тамары Васильевны РУДЕНКО студентами 2 потока 2 курса 2005 года запуска. За ошибки дорогая  редакция ответственности не несёт.

План курса:
1. Системы программирования.
2. Элементы теории трансляции.
3. Язык C++.

Рекомендуемая литература: 
C++:
•	Б. Страуструп. ‘Язык C++. Специальное издание’. Бином, 2001.
•	Г. Шилдт. ‘Самоучитель C++’.
•	Ира Пол. ‘ООП на C++’.
•	Стандарт C++. ISO/IEC 14882. (1998)
•	Б. Страуструп. ‘Дизайн и эволюция языка C++’.
•	Г. Буч. ‘Об’ектно-ориентированный анализ и проектирование с примерами приложений на C++’. 
•	Г. Буч, Д. Рэмбо, А. Джакобсон. ‘Язык UML. Руководство пользователя’. 
СП: 
•	А.Ю. Молчанов. ‘Системное программное обеспечение’, 2003.
•	А.В. Гордеев, А.Ю. Молчанов. ‘Системное программное обеспечение’, 2001.
•	А. Ахо, Р. Сети, Дж. Ульман. ‘Компиляторы. Принципы, технологии, инструменты’.
•	А. Ахо, Дж. Ульман. “Green Dragon”. ‘Теория компиляции’.
•	Волкова И.А., Руденко Т.В. ‘Формальные грамматики и языки. Элементы теории трансляции’. 1999.
Материалы в поддержку лекций и практикума доступны на 

Системы программирования. Вводные понятия.
Система программирования – комплекс программных средств, которые поддерживают весь технологический цикл разработки программного продукта.
Программа – текст на языке программирования, который его автор способен запустить на выполнение на ЭВМ (в рамках некоторой конкретной среды!), дать входные данные, получить результаты и понять их. Например, мы пишем программы в процессе обучения на ВМК.
Программный продукт – программа, которую пользователь может инсталлировать, задать ей входные данные, сам анализировать результаты, а так же сопровождать её, то есть программный продукт может жить без присутствия автора (отличительное свойство – отчуждаемость!). Для этого программа должна быть хорошо документирована, тщательно протестирована, должна быть возможность настройки. Расходы при разработке программного продукта возрастают примерно в три раза относительно разработки программы.
Интегрированный программный продукт – программный продукт, который может быть интегрирован с другими программными продуктами в рамках некоторой программной системы. Интегрированные программные продукты одной системы, как правило, имеют похожий пользовательский интерфейс, средства обмена данными друг с другом. Возникает проблема совместного доступа к ресурсам (процессор, память).
Например, Microsoft Office. Приложения имеют похожие кнопочки и менюшки, могут обмениваться данными с помощью технологии OLE.
При разработке интегрированного программного продукта расходы возрастают ещё в три раза.

Технологический цикл программного продукта.
Технологический цикл (жизненный цикл) – последовательность действий, выполняющихся при разработке программного продукта.
По статистике Standish Group, разработка программ не всегда заканчивается успешно. Было исследовано 300 компаний и 24 тысячи проектов. Только 26% из них были завершены в срок, бюджет не был превышен, вся функциональность была реализована. 46% были завершены не вовремя, бюджет был превышен, функциональность реализована не полностью. 28% проектов вообще не были завершены. Поэтому выбор технологии программирования – очень важная и нетривиальная задача. Он зависит от различных факторов: от специфики проекта, от компании-разработчика и т.д.
Технология программирования – набор рекомендаций по разработке программного продукта, включающий в себя разбиение на этапы, выбор инструментальной поддержки и т.д.
Этапы технологического цикла:
1)	анализ требований (требования заказчика уточняются, документируются, хотя бы частично формализуются. Для этого применяется простое текстовое описание, таблица решений, которая содержит условия на входные данные и эффекты на них, а также специальные языки спецификации, например Use case.)
2)	проектирование (как правило, используется стратегия divide & conquer, проектирование в большом и в малом. Сначала выявляется структура, потом уточняются элементы структуры. Применяется алгоритмическая или об’ектная декомпозиция. При первой процесс обработки данных разбивается на модули, в рамках каждого из которых выбираются некоторые алгоритмы. При второй предметная область разбивается на активные части – об’екты, которые могут взаимодействовать с другими об’ектами. Применяются диаграммы взаимодействия об’ектов, а также UML – Universal Markup Language.)
3)	кодирование (по описанной в спецификации функциональности и интерфейсам кодится модуль/класс)
4)	тестирование и отладка (тестирование – запуск программы на некоторых входных данных с целью обнаружить дефект – несоответствие ожидаемого результата и выданного программой. Отладка – обнаружение причины дефекта и её исправление. Различают тестирование методом белого ящика и методом чёрного ящика (когда известен исходный код, или, соответственно, нет). По цели различают функциональное, нагрузочное, стрессовое, регрессионное, адаптивное тестирование.)
5)	внедрение (перенос продукта на целевую систему с инструментальной)
6)	сопровождение (исправление ошибок, перевод в новую среду, добавление функциональности).
Этапы 1 – 4 относятся к непосредственной разработке продукта. Используют различные модели чередования этапов:
•	каскадная (устар.)

•	каскадно-возвратная (появляется возможность возврата в любой момент)

•	спиральная (итерационная) – сейчас наиболее распространена

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

Идеальная система программирования
Идеальная система программирования должна включать в себя некоторые составные части, поддерживающая технологический цикл.
1, 2. Анализ требований и проектирование:
•	средство для создания текстовых материалов и диаграмм
•	контролёр непротиворечивости (например, в таблице решений – неперекрываемость входных данных)
•	браузер, который показывает взаимодействие компонентов системы
•	средства вывода этой информации
•	репозиторий проекта – организованное хранилище всех этих материалов, для него должны быть обеспечены:
o	согласованность изменений (непротиворечивость дополнений по сравнению со старой архитектурой)
o	непротиворечивость
o	санкционированный доступ (разрешение прав группам – проектировщикам, которые могут менять структуру проекта, кодеры могут менять интерфейс об’ектов, которые они описывают, но только в разрешённых пределах)
o	контроль версий (конфигураций) (версии, зависимости, восстановление)
3. Кодирование:
•	частичная автоматизация генерации кода (например, генерация кода пользовательского интерфейса)
•	богатые библиотеки алгоритмов/классов (и программа-библиотекарь)
•	синтаксически-ориентированный редактор (хинты, хоткеи, выделение служебных слов, баланс скобок, дописывание идентификаторов)
•	транслятор (компилятор или интерпретатор) – программа, позволяющая код на языке высокого уровня довести до исполнения (?! - ИМХО, немного косноязычно - я бы дал другое определение – Д.Ч.)
•	редактор связей (линкер) – программа, получающая на вход отдельные файлы с неразрешёнными внешними ссылками и увязывающая отдельные связи
•	макрогенератор
•	загрузчик – программа, обеспечивающая настройку модуля на конкретные адреса оперативной памяти, функции загрузчика в последнее время выполняет не система программирования, а операционная система
4. Тестирование и отладка:
•	отладчик (желательно общение программы с программистом не в терминах адресов, а в терминах программы – переменных, функций, об’ектов – т.е. использовался не только адресно-кодовый, но и символьный отладчик)
•	генератор тестов (по частично формализованной спецификации) 
•	автоматизация прогона (полезно при регрессивном тестировании). По словам ТВ, в Microsoft проги тестируют автоматически по ночам. Я знал, что их пишут не для людей L
•	частичная автоматизация обработки результатов прогона
•	анализ тестового покрытия – проверка на соответствие выборки заявленному требованию (например, при тестировании методом белого ящика с проверкой всех операторов программы аналайзер проверит, прошлись ли мы по всем ветвям алгоритма)
Кроме того, идеальная система программирования должна включать также средства управления проектом (как оказывается, ни программисты, ни менеджеры не могут эффективно управлять проектом; как показывает практика, для этого необходим человек, выросший из этого или похожего проекта). Средства системы программирования для управления проектом должны включать в себя:
•	средства заблаговременного планирования работ (составление списка задач, графика работ, распределение ответственности)
•	управление рисками – оценка степени рисков и решение проблем (менеджеры говорят, что проблемы возникают, как правило, не технические, а организационные: нестабильные требования заказчика, низкая вовлечённость высшего руководства, низкое привлечение пользователей)
•	координация и отслеживание плана работ
Также важна для систем программирования поддержка виртуальных групп разработчиков. Понятно, что взаимодействие программистов можно организовать с помощью электронной почты, мгновенных сообщений, форумов, видеоконференций. Но при этом возникают проблемы секретности, совмещения разных средств обмена информацией, архивирования и ведения логов.
Близки к нашей идеальной системе программирования так называемые CASE-средства (Computer Aided Software Engineering). Там подразумевается:
•	наличие репозитория проекта
•	поддержка визуальных методов проектирования (мощные графические средства, язык моделирования и т.д.)
•	средства ведения документации
•	интеграция всех этих средств.
Реальные CASE-средства:
•	Rational Suite – очень дорогая система. Компания Rational предлагает не только инструментальные средства, но и методику использования – Rational Unified Process
o	Rational Rose (анализ и проектирование)
o	Rational Team Test (тестирование)
o	Rational Robot – запоминание переходов в пользовательском интерфейсе
•	ERuin (у меня записано Erwin – надо ткнуть гугл – Д.Ч.) от фирмы Computer Outsource Internal
•	Paradigm Plus от Platinum
•	Oracle Design от угадайте кого

Система программирования C в UNIX:
•	компиляторы
•	редакторы
•	отладчики
•	координатор make – программа, которая по описанию зависимостей между частями проекта и действий по их сборке (нужен более широкий термин!) обеспечивает перекомпиляцию только тех частей проекта, которые это требуют (которые были изменены); непосредственно компиляцией и сопутствующими действиями make не занимается, используя указанные ему средства
•	Source Code Control System (SCCS) – управление доступом и сохранение версий
•	lint анализирует код лучше, чем компилятор, но не создаёт об’ектного модуля
•	manual (man man)
•	форматёры nroof (делает текст из гипертекста ...скорее визуализирует специально размеченный текст) и tbl (перевод в таблицу)
•	lex – генератор лексических анализаторов и yacc – генератор синтаксических анализаторов

ЧАСТЬ I
Системы программирования, поддерживающие об’ектно-ориентированную разработку. Принципы об’ектно-ориентированного программирования.
Проектирование (декомпозиция) – разбиение системы на подсистемы.
Нас интересует об’ектная декомпозиция, то есть статическая структура системы – об’екты и связи между ними, а динамическое поведение системы – обмен сообщениями между об’ектами. В C++ обмен сообщениями представлен набором методов об’екта, которые можно вызывать из других об’ектов.

Основные принципы об’ектной модели:
•	абстракция и инкапсуляция
•	наследование
•	полиморфизм
Абстракция – приём борьбы со сложностью. В предметной области необходимо выявить об’екты, выделить нужные нам свойства, то есть создать модель предметной области.
Инкапсуляция – средство, позволяющее спрятать реализацию и показывать пользователю только интерфейс абстракции (возможности взаимодействия с другими абстракциями).
Наследование – возможность выявить иерархию об’ектов, в рамках которой мы можем строить абстракции от общего к частному и использовать для частностей общие методы.
Полиморфизм различают статический, динамический и типовой (параметрический).
Статический полиморфизм – возможность иметь несколько функций с одинаковыми именами, но разными профилями (выбор осуществляется по списку параметров во время компиляции) – в C++ это overloading (перегрузка).
Динамический полиморфизм – перегрузка функций в рамках выстроенной иерархии и механизм виртуальных функций. Выбор осуществляется не на этапе компиляции, а во время выполнения.
Типовой (параметрический) полиморфизм (шаблоны функций/классов) – возможность настройки абстракции на конкретный тип данных. Обработка состоит из двух этапов: настройка на типовой параметр и вызов этой функции, уже настроенной на тип. Например, при описании класса, реализующего стек, мы не знаем, какой тип элементов захочет использовать юзер (юзер – это программист! Проще: не хотим привязываться к конкретному типу элементов, чтобы не реализовывать два разных стека для, скажем, целых и вещественных чисел – Д.Ч.).
Следует различать понятия об’ектного языка и об’ектно-ориентированного языка.
Об’ектный язык – язык, который поддерживает все принципы ООП, за исключением наследования. Например, такими были языки Simula ’67, ADA’85.
Об’ектно-ориентированный язык – язык, поддерживающий все принципы ООП. Например, C++, Java, Eiffel, Object Pascal и другие.

Об’ектно-ориентированное проектирование.
Мы должны научиться выделять об’екты и конкретизировать их состояние и поведение. При анализе существительные переходят в об’екты. Например, при описании банкомата об’ектами будут сам банкомат, счёт, банк и т.д. При этом следует следить за отсутствием избыточности (грубо говоря, в списке об’ектов не должно быть синонимов). Свойства об’ектов не должны быть об’ектами, например, деньги – не самостоятельный об’ект, а свойство счёта. Операции с об’ектом также не являются об’ектами. Возникает проблема, на каком этапе надо задумываться об об’ектах, которых нет в предметной области, но которые нужны для реализации? Существуют разные мнения на эту тему: либо на этапе проектирования, либо отложить их до кодирования. Например, таким об’ектом является очередь клиентов при доступе к счетам.
Пример. Моделирование предметной области для банковской сети.
Выделяем следующие абстракции – об’екты предметной области:
•	баксомёт
•	клиент
•	карта
•	счёт
Диаграмма нарисована в стиле UML-карточки. Вверху пишется имя об’екта, во второй части его свойства – атрибуты, в третьей – методы – характеристики его поведения.
Далее нужно проанализировать инкапсуляцию. Обычно прячут всё, что возможно, так как другие об’екты не должны знать, как функционирует об’ект, а только его интерфейс. В большинстве случаев, свойства прячут, а методы открывают. В диаграмме ‘-‘ означает права доступа private, ’+’ – public, а ‘#’ – protected.
Если об’екты получаются из существительных, то их методы возникают из глаголов. Также, у любого об’екта есть конструктор или конструкторы и деструктор. Для скрытых свойств иногда нужны специальные методы – setters и getters, которые читают и записывают скрытые поля.

Некоторые особенности языка C++.
В C++, в отличие от C, в функцию можно передавать параметры по ссылке, примерно как в Pascal. Например, в C мы бы написали
void swap (int *x, int *y){
	int t;  t = *x;  *x = *y;  *y = t;
}
А в теле программы вызывали бы  int a, b; ... swap (&a,&b);
А на плюсах можно написать и так:
void swap (int &x, int &y){
	int t;  t = x;  x = y;  y = t;
}
И тогда вызывать  swap (a,b);
Для работы с динамической памятью в C использовались функции malloc, calloc, realloc и free. В C++ есть операции new и delete, которые призваны заменить их, хотя по-старому писать, конечно, тоже можно. Используют их примерно так:
int *p;
p = new int; // вместо p = (int*) malloc (sizeof(int))
*p = 5;   
// работа с p
delete p;
В случае если надо запросить память под массив, действуют так:
int *p = new int[10]; //вместо p = (int*)malloc(10*sizeof(int))
// работа с p
delete[] p;
 Классы, специальные функции-члены класса. 
Классы бывают плоскими и не плоскими. Плоским принято считать класс, который не содержит указателей.
Конструктор класса – специальная функция-член класса, с помощью которой создаётся об’ект, имя которой совпадает с именем класса, и не возвращающая никакого значения. Любой класс содержит конструкторы, по дефолту есть два конструктора – умолчания и копирования.
Конструктор умолчания – конструктор вида X (); Впрочем, параметры могут присутствовать, но для них всех должны быть заданы значения по умолчанию, поэтому точное определение такое: конструктор, который может быть вызван без параметров. Если в классе явно определён хотя бы один конструктор, конструктор умолчания автоматически не создаётся!
Конструктор копирования – конструктор вида X (const X&); который служит для инициализации об’екта другим об’ектом. Дополнительно могут быть параметры кроме первого, но для них должны быть заданы умолчания. При неиспользовании const в определении типа аргумента могут возникнуть проблемы при инициализации об’екта определяемого класса константным об’ектом. (Было написано все наоборот!! - Д.Ч.)
Обычные конструкторы – конструкторы, у которых есть параметры, не все из них имеют значения по умолчанию, и первый из них не const X&. Такой конструктор вызывается при создании об’екта (что-нить типа X a (3,5);) или при создании об’екта в динамической памяти (X *p; p = new X (3,5); - тут происходит сначала заказ памяти по new, а потом вызывается конструктор об’екта p*), а также в некоторых других случаях, которые будут рассмотрены позднее.
Деструктор класса – специальная функция-член класса, с помощью которой удаляется об’ект, имя которой отличается от имени класса на тильду вначале, не имеющая параметров и не возвращающая никакого значения. Любой класс содержит один деструктор. Если он не описан явно, то действует дефолтный деструктор:
~X () {}
Пример. Описание класса комплексных чисел.
Вывод на консоль обычно осуществляется в C++ через поток cout. Для его использования надо подключить заголовочный файл iostream.
class Complex{
	double re, im;
public:
	void print() const{ // печать числа в алгебраической форме
		cout<<re<<” + i*”<<im<<endl;
}
}
В секцию private попадают имена, которые не видны извне, соответственно, в секцию public – интерфейс класса. По умолчанию, сначала идут скрытые члены класса. Модификатор устанавливает права доступа вниз полям и методам до следующего модификатора или конца класса. В структурах, наоборот – по умолчанию идут общедоступные члены. В остальном, конструкции class и struct совпадают.
Создание об’екта:
Complex a;
Компилятор отведёт автоматическую память под об’ект, вызывается конструктор умолчания.
В об’явлении функции print() содержится слово const. Это значит, что функция не должна менять об’ект, который её вызвал. Только такие функции могут использоваться объектами класса, об’явленными константными.
void change(const double a) { re += a;}
Эту функцию мы не можем об’явить константной, так как она меняет поле класса.
Функции можно определять также и вне класса. Например, в классе мы напишем:
void print() const;
А вне его:
void Complex::print() const{ cout<<re<<” + i*”<<im<<endl;}
Четвероточие – оператор разрешения контекста. Об’явление функций должно в обоих случаях совпадать, то есть тип параметров, тип возвращаемого значения и спецификаторы функции должны быть идентичны.
Зачастую определение функции выносится из класса для удобства. Но есть различия и для компилятора. Если функция описана в классе, то её вызов будет компилироваться не как вызов функции, а просто на место вызова функции подставится её код, настроенный на фактические параметры. В этом случае мы проигрываем по памяти, но выигрываем по скорости. Разумно использовать эту возможность для маленьких функций. Результат будет аналогичным, если дописать к определению функции слово inline, а её тело вынести за класс. Чего-то вроде outline, к сожалению, не существует
Описанный класс содержит требуемые данные, но косяк в том, что мы не можем задать или изменить значения полей re и im. Выход – описать в public-части конструкторы или сеттеры/геттеры. Опишем конструктор:
Complex (double r, double i) {re = r; im = i;}
Теперь мы можем написать:
Complex a (3.5,2.7);
Можно описать ещё один конструктор для задания чисел вещественной оси:
Complex (double r) {re = r; im = 0;}
Он будет вызван, если вызвать конструктор с одним параметром.
Создание хотя бы одного конструктора отменяет действие дефолтного. То есть, мы не можем сейчас написать Complex a ();
Но проще вместо всего этого гемора из трёх конструкторов написать
Complex (double r = 0, double i = 0) {re = r; im = i;}
При вызове конструктора с одним параметром он считается значением первого аргумента, второй определяется по умолчанию.

Конструктор копирования. Оператор присваивания.
1) Complex d = a;
2) Complex e = Complex (1,2);
В описанных примерах работает конструктор копирования. По умолчанию он будет описан так:
Complex (const Complex &a) {re = a.re; im = a.im;}
В первом случае произойдёт копирование полей об’екта a в поля d. Во втором случае сначала создаётся безымянный временный об’ект, проинициализированный требуемыми значениями, а потом его поля копируются в поля e.
В C для структур допустим оператор присваивания: b = c;
Присваивание определено и для классов по умолчанию как простое копирование полей:
Complex& operator= (const Complex &a) 
{re = a.re; im = a.im; return *this;}
Разумеется, оператор присваивания может быть перегружен.
Функция возвращает ссылку на об’ект, чтобы пользователь мог писать цепочки присваиваний типа, как принято в C/C++:
x = y = z;
Возникает логичный вопрос, почему мы возвращаем параметр по ссылке? Рассмотрим, что будет происходить, если об’явить оператор как 
Complex operator= (const Complex &a); 
Тип возвращаемого результата Complex. Значит, вернуть мы должны об’ект, которому мы что-то присвоили. this – определённая в любой функции класса переменная, которая всегда имеет значение ссылки на об’ект, от которого вызвана функция. Поэтому мы возвращаем *this. Допустим, что пользователь написал цепочку x = y = z; Тогда первое присваивание (правое) вернёт копию y. Потом работает конструктор копирования, получаем временный об’ект, который потом присваивается иксу (левое присваивание). Если же написать амперсанд, то мы вернём ссылку на игрек, который присвоится иксу без всяких временных об’ектов. Зачем платить больше?
Однако на этом отличия не заканчиваются. Подумаем, что произойдёт, если написать:
(x = y).change(2);
Компилятор это одобрит. Но в последнем случае функция вызовется от временного об’екта, который потом умрёт, и эффекта никакого мы не увидим. В первом случае мы возвращаем ссылку на об’ект x, который и ‘изменится’.
Рассмотрим применение описанных функций.
int main (){
	Complex z (1.2,3.5); // y = 1.2 + i*3.5
	Complex x (1.5);     // x = 1.5
	Complex y;           // y = 0
	const Complex t (5); // t – константа 5
	Complex z1 = z;      // конструктор копирования
	Complex z2 = Complex (1,2); // создаётся временный об’ект
       //Complex (1,2), потом вызывается конструктор копирования
	  // компиляторы часто оптимизируют и не создают вр. об’ект
       // (стандартом это допускается; в то же время доступность
       //  конструктора копирования все равно проверяется)
	z2 = z;              // оператор присваивания
	z = t;  // для этого пишем const в параметре констр. копир-я
	z2.operator= (t);    // то же самое
	Complex *p;
p = new Complex (1.5); // отводится память в куче
// и вызывается конструктор
	/* работа с p */
	delete p; // вызывается деструктор, память возвр. в кучу
	Complex p[10]; // по адресу p выделяется память для 10
		//об’ектов типа Complex. Работает конструктор умолчания
		// если он не определён, ошибка! 
		//инициализировать массив конструкторами с параметрами,
		// к сожалению, нельзя L
	return 0; 
}

Особенности работы с неплоскими классами.
Пример. Описание класса MFC-строк (откуда дровишки про MFC? - Д.Ч.).
class String {  // С-шная строка, с которой хранится её длина.
	int size;  // длина строки без ‘\0’
	char *p;   // указатель на строку
public:
	String (const char *str){ // конструктор
		p = new char [(size = strlen(str)) + 1];
		strcpy (p, str);
	}  // теперь можно создавать строки: String a (“Hello!”);
	~String () { delete[] p;}
Пусть теперь с классом работает некоторая функция:
void f(){
	String s1 (“Hello!”);
	String s2 = s1;
	String s3 = String (“2007”);
	s3 = s1;
}
Тут происходит поверхностное копирование конструкторами по умолчанию. Это значит, что копируются указатели, но не содержание строк. В этой ситуации s1.p и s2.p ссылаются на одну и ту же область памяти, а при присваивании в последней строчке мы оставляем в памяти мусор. Но самое интересное начнётся при выходе из функции, когда начнёт работать описанный нами деструктор. Он захочет три раза удалить память,  занятую строчкой “Hello!”. Но двум смертям не бывать, получим runtime error.
Напишем специальные функции, поддерживающие глубинное копирование.
String (const String &a){
	p = new char [(size = a.size) + 1];
	strcpy (p, a.p);}
String& operator= (const String &a){
	if (this == &a) return *this;   // если s1 == s2
	delete[] p;   // освобождаем память для старой строки
	p = new char [(size = a.size) + 1];
	strcpy (p, a.p);
	return *this;}

Абстрактные типы данных. Перегрузка операций.
Абстрактный тип данных (АТД) — это такой тип, который предоставляет для работы с элементами этого типа определённый набор функций, а также возможность создавать элементы этого типа при помощи специальных функций. Вся внутренняя структура такого типа спрятана от разработчика программного обеспечения — в этом и заключается суть абстракции. Абстрактный тип данных определяет набор независимых от конкретной реализации типа функций для оперирования его значениями. Например, можно определить абстрактные типы для стека, очереди, списка.
Перегрузка операций – описание своих операций (+, -, new и т.д.) вместо уже определённых.
Нельзя перегружать операции: ., ?:,::, .* , # , ## , sizeof, typeid.
Если мы перегружаем какой-нибудь значок, надо стараться, чтобы он определял похожую на обычный вкладываемый в него смысл сущность. Например, разумно использовать * для перемножения матриц или многочленов или для кон’юнкции.
К сожалению, мы не можем изменить арность операций, то есть бинарная операция всегда будет бинарной, а унарная – унарной. Также операции будут подчиняться законам старшинства, определённым раньше. Часто программисты сталкиваются с проблемой при перегрузке оператора ^ в качестве возведения в степень. От побитового сложения по модулю 2 нам достался приоритет, меньший, чем у сложения или умножения. Очевидно, это противоречит вкладываемому в него смыслу, и приходится использовать скобки.

Перегрузка бинарных операций.
Существует три возможности перегрузки операций:
1) написать функцию-член класса с одним параметром;
2) с помощью глобальной функции с двумя параметрами (в этом случае нам нужен доступ к скрытым полям класса с помощью сеттеров/геттеров, но так получается медленнее и вообще не используется);
3) функция-друг класса с двумя параметрами (если класс об’являет функцию своим другом, она может использовать все поля класса).
Пример. Продолжение описания класса комплексных чисел.
const Complex operator+ (const Complex &a) const{
	Complex t (re + a.re, im + a.im);
	return t;}
При использовании z = x + y,  x – вызывающий об’ект, а  y – параметр. Можно опустить const у параметра, но тогда нельзя будет складывать об’екты нашего класса с константами, то есть z = y + 5.
Почему мы возвращаем значение не по ссылке? Если бы мы действительно сделали так, мы вернули бы адрес локальной переменной, которая лежит в стеке, и её память освобождается после выхода из функции. Конечно, можно рассчитывать на то, что компилятор не заругается (зависит от реализации), и значение t не успеет затереться, просто переместится указатель стека. Но тогда, если мы напишем цепочку сложений, то при следующем вызове функции об’ект t скорее всего будет создан на том же месте, и тогда точно программа полетит. Не возвращайте локальные переменные по ссылке!
l-value – переменная, которой возможно присваивание (она обладает адресом) (Вообще-то, термины lvalue и rvalue пишутся без дефисов... - Д.Ч.).
r-value – кусок памяти, которому нельзя присваивать, например, константа или (не всегда!) временный результат вычисления.
Самый первый const превращает полученный результат из l-value в r-value. Иначе можно будет вызвать, например (a+b).print(), а это не по-C++ному.
int main (){
	double t = 7.5;
	Complex x(1,2), y(5);
	const Complex z(1,3);
	w = x + y;    // равносильно w = x.operator+(y); 
// или w.operator= (x.operator+(y));
	w = x + z;    // можно из-за const у параметра
	w = z + x;    // можно из-за const в конце
	w = x + t;    // преобразование типа double к const Complex
	  // w = x.operator+ (Complex(t)); т.к. есть Complex(double)
	w = t + x;    // ашыпка
	return 0;}
Теперь рассмотрим перегрузку с помощью дружественной функции. Для этого в классе нужно написать:
friend const Complex operator+ (const Complex &a, const Complex &b);
Зона видимости функции совпадает с областью видимости класса. private: на неё не действует! Описание в классе или вне класса (тот же профиль без friend) приводит к одинаковому результату.
const Complex operator+ (const Complex &a, const Complex &b){
	Complex t (a.re + b.re, a.im + b.im);
	return t;}

int main(){
	double t = 7.5;
	Complex x(1,2), y(3), z;
	z = x + y;    // z = operator+ (x, y);
	z = x + t;    // z = operator+ (x, Complex(t));
z = t + x;    // z = operator+ (t, Complex(x));
return 0;}
Так что, если мы хотим смешивать данные, лучше заводить друзей. ;)
Формально можно перегрузить оператор и в классе, и с помощью друга.
z = x + y;    // неоднозначность - ошибка
	z = x + t;    // неоднозначность - ошибка
z = t + x;    // друг работает

Перегрузка унарных операций.
Есть те же три возможности перегрузить унарный оператор  (так мы используем термин «операция» или «оператор» для перевода 'operator'? Выше то же самое - Д.Ч.) (функция-член класса без параметров, обычная глобальная функция или функция-друг с 1 параметром).
const Complex operator- () const{
	Complex t (-re,-im);   return t;}

int main (){
	double t = 7.5;
	Complex x(1,2), y;
	const Complex z (1);
	y = -x;   // y = x.operator-();
y = -z;   // y = z.operator-();
-x = y;   // низя, т.к. –x не l-value
x = -t;   // x = Complex (-t); - здесь нет нашего минуса
return 0;}
Друга пишут аналогично прошлому разу.

Особенности перегрузки операций ++ и --.
Как обычно, есть 3 варианта.
Чтобы отличать описание префиксной формы от описания постфиксной, договорились, что функция для префиксной формы описывается без формальных параметров, а для постфиксной – с одним параметром типа int.
const Complex& operator++ ()   // ++x;
	{ ++re; ++im;  return *this;}

const Complex operator++ (int notused)   // x++;
    {Complex t = *this;
	++re; ++im;
	return t;}

int main () {
	Complex x(1,2);  y;
	y = ++x;       // y = x.operator++();
	y = x++;     //y = x.operator++(0); -обычно там реально ноль
	++ ++x;        // нельзя, т.к. ++x не l-value
	y = (x + y)++; // нельзя, так мы определили сложение
	return 0;}
Еще одно глобальное замечание: совершенно не обязательно при перегрузке, скажем, бинарной операции функцией-членом класса делать тип операнда совпадающим с типом неявного.

Виды отношений между классами.
Выделяют следующие темы межклассовых отношений:
•	ассоциация
•	наследование
•	агрегация
•	использование
•	инстанцирование (выделяют не всегда)
Ассоциация – тот факт, что пара об’ектов связана между собой. Связь не конкретизируется, обычно такая связь появляется в начале планирования, а потом уточняется и переходит в другие формы.
Далее будем приводить UML-подобные примеры, точнее, мы будем рисовать ER-диаграммы (Entity Relations), которые пришли из области баз данных. (к картинке: справа еще один вариант 'N'; да и бесконечность надо поаккуратней нарисовать – Д.Ч.)

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

Наследование – взаимодействие, выражающее связь ‘частное - общее’ (‘is a‘), например, любое млекопитающее есть животное.

Общее считается базовым классом, а частное – производным. В другой терминологии, принятой, например, в Java, это суперкласс и подкласс. На диаграммах стрелочки рисуются от производного класса к базовому. Производный класс может расширить базовый класс, а может переопределить некоторые его члены.
Агрегация – соотношение ‘часть - целое’ (‘has a’) – ситуация, при которой об’ект одного класса внутри себя содержит об’екты другого класса.

Различают строгую и нестрогую агрегацию. Строгая агрегация (композиция) – агрегация, при которой вложенный об’ект не может исчезнуть, пока существует его хозяин. Тогда ромбик на диаграмме закрашивают.
class Triangle { ... Point v1, v2, v3; ... }
Нестрогая агрегация (часто просто агрегация) – часть может исчезнуть, пока существует целое, например, у треугольника можно отрезать вершину. J Тогда ромбик не закрашивают.

class Shareholder { ... Share *asserts; ... }
Возможно, share == NULL. Вот такой у нас безNULL. J
Использование (зависимость) возникает в трёх случаях:
•	ситуация, при которой имя класса используется в профиле функции-члена другого класса (тип параметров или возвращаемого значения);
•	функция-член одного класса для реализации использует локальную переменную типа – другого класса;
•	в теле функции-члена одного класса вызывается функция-член другого класса, то есть, либо у нас есть статическая функция, либо мы поступаем, как в предыдущем пункте.
При этом используемый класс называется сервером, использующий – клиентом.

тип
Инстанцирование – получение из некоторого абстрактного типа конкретного, из которого потом генерируются об’екты, то есть это связь шаблона класса и того, что генерируется по шаблону.

Единичное наследование в C++
Теперь можно рассказать о модификаторе доступа protected. Если имена попали в эту секцию, это значит, что вне класса они не видимы, так же, как private, однако они доступны в производных классах.
Пример. Сотрудники.
class Person { // человек
protected:
	char *name;
	int age;
	char *address;
public:
	Person (char *n, int a, char *addr);
	~Person ();
	char* getName () const {return name}
	void print () const { /* печать данных */ };
};
Person::Person (char *n, int a, char *addr){
	name = new char [strlen(n) + 1];
	strcpy (name, n);   age = a;
	addr = new char [strlen(addr) + 1];
	strcpy (address, addr);
}
Person::~Person () {delete[] name;  delete[] address;}
Опишем теперь класс-сотрудника. Наследование осуществляется с указанием одного из трёх модификаторов доступа, но, как привило, используется public. Конструкторы и деструкторы (кстати говоря, операции присваивания тоже) не наследуются, поэтому в следующем коде мы унаследуем 3 поля и 2 метода.
class Employee: public Person { // работник
protected:
	char *appointment;
	int level;
	double earnings;
public:
	Employee (char *n, int a, char *addr, 
char *app, int l, int ear); // ear – креатив ТВ J
	~Employee();
	void print () const;
};
Возникает вопрос, как повторно использовать код конструктора? А вот так:
Employee::Employee (char *n, int a, char *addr, 
char *app, int l, int ear): Person (n, a, addr){
	appointment = new char [strlen(app) + 1];
	strcpy (appointment, app);
	level = l;   earnings = ear;
}
Сначала работает конструктор Person, а потом наш конструктор. Если не написать двоеточие с «вызовом» конструктора класса Person, будет вызываться конструктор умолчания для Person (а в данном случае мы получим ошибку компиляции). Опишем теперь деструктор, иначе будет работать дефолтный, а он нам не подходит.
Employee::~Employee {delete[] appointment;}
Нам не надо самим освобождать память от наследованных полей, потому что после того, как отработает деструктор производного класса, вызовется деструктор базового.
Оставим getName() такой, какая она была, а print() перегрузим. Можно считать базовый класс об’емлющей областью описания, а производный – внутренней. Таким образом, если описать функцию, которая была об’явлена в базовом классом, то функция из базового класса скрывается.
void Employee::print() const{
	Person::print(); // вызываем print() базового
	/* печать appointment, level и earnings */
}
int main (){
	Person h (“Andrey Ivanov”, 20, “fds7”);
	Employee e (“Boris I. Berezin”, 58, “635”, 
“top-manager”, 12, 65536.00);
	h.print();  // информация об Андрюхе – 3 поля
	e.print();  // информация о БИБ – 6 полей
	cout<<h.getName()<<endl   // Андрей
	    <<e.getName()<<endl;  // Березин
	Person *pp = &h;   // указатель на базовый
	Employee *pe = &e; // указатель на производный
	pp->print();       // Андрей
	pe->print();       // Борис Иванович
}  
Как и в C, в C++ можно присваивать значение одного указателя значению другого указателя без явного преобразования, если:
•	если они указывают на один тип (с точностью до typedef);
•	указателю на void можно присваивать указатель любого типа;
•	ему присваиваем целый 0 (преобразуется к NULL).
В C++ появилась новая тема. Без явного преобразования указателю на базовый тип можно присвоить значение указателя на производный тип. Такое присваивание возможно, если первый (?) класс однозначный, доступный и базовый по отношению ко второму:
pp = pe;
Класс называется доступным, если он наследуется со словом public или protected.
Если используется модификатор public, то все члены класса наследуются так же, как они об’явлены в базовом. Если мы напишем protected, то public-члены будут наследоваться как protected. Если мы напишем private, то доступа к членам базового класса не будет, хотя имена их будут «считаться» используемыми. ХЗ зачем нужно, но так можно…
Например, мы пишем инструкцию:
pp->print();
Возникает дилемма, что вызывать? Существует два типа определения:
•	статическое – по типу указателя,
•	динамическое – по типу об’екта, на который он указывает.
Если не используются виртуальные функции, то функция определяется статически. В нашем случае вызовется Person::print().
При динамическом вызове мы сталкиваемся с динамическим полиморфизмом, а не со статическим, как раньше. В этом случае компилятор анализирует код объект (?), который лежит по указателю. Но для этого нужно определить виртуальную функцию. Для этого при об’явлении функции в базовом классе указывают ключевое слово virtual:
virtual void print () const;
Чтобы работала виртуальность, необходимо, чтобы совпадали списки параметров и тип, возвращаемый функцией (за небольшим исключением). Если изменить список параметров, то код скомпилируется, но виртуальность не сработает. 
Различают раннее связывание, которое происходит на этапе компиляции, и позднее связывание, которое происходит при выполнении. Очевидно, что при использовании виртуальных функций используется позднее связывание, так как нельзя заранее отследить, об’ект какого типа будет записан по данному адресу.

Библия виртуальных функций by ТВ.
Чтобы включился механизм виртуализации, должны быть выполнены следующие требования:
•	присутствует иерархия классов;
•	в базовом классе функция об’явлена как виртуальная;
•	в производном классе описана функция с таким же профилем (совпадают имя, список параметров с точностью до имён и тип результата);
•	используется вызов функции через указатель (без уточнения с помощью оператора разрешения контекста ::).
Однако к этим четырём заповедям нужно прибавить ещё некоторые замечания:
•	Если описывать функцию вне класса, то в её профиле virtual не повторяют. В описании функции в производном классе также можно указать virtual, но это ни на что не повлияет, разве что на наследство производного.
•	Тип результата может слегка отличаться. Если в базовом классе виртуальная функция возвращает указатель на базовый класс, то в производном она имеет право возвращать указатель на производный класс (то же со ссылками).
•	Если списки параметров совпадают, а типы результата существенно отличаются, мы получим синтаксическую ошибку.
•	Если списки параметров различаются, то функция из производного класса скрывает функцию из базового, виртуальность не работает, и при вызове через указатель получаем статическое определение по типу указателя.
•	Если в производном классе вообще нет функции с таким именем, то метод полностью наследуется, то есть мы сможем использовать виртуальность во внуке, а сын (производный класс) просто получит в свое распоряжение эту функцию.
•	Если вызываем метод через об’ект (h.print()), то виртуальность не работает, статическое определение по типу об’екта, от которого его вызываем.
•	При явном указании класса виртуальность не работает, даже если функция вызвана через указатель, то есть pp->Person::print(); – статическое определение.
В заключение, можно дать настоятельный совет: делайте деструкторы виртуальными!
Пусть где-то в программе у нас написано:
Person *pq = new Employee (“Vova Putin”, 54, ”The Kremlin”,
”The President of Russia”, 0, 0);
/* что-то делаем с работником */
delete pq;
Сначала мы должны вызвать деструктор, а потом освободить память, занимаемую об’ектом. В нашем случае вызовется статический деструктор, то есть по типу указателя. А он удалит не всё, что нам нужно. Зато, если в базовом классе об’явить
virtual ~Person();
динамически вызовется деструктор нужного нам класса и освободит всё, что надо.

Абстрактные классы.
Суперклассический пример. Фигуры.
Пример действительно является классическим. Из фигур выстраивается вполне очевидная иерархия, например, от фигуры наследуются многоугольник и круг, от многоугольника - треугольник и квадрат. Все они замкнутые ограниченные плоские фигуры, значит, скорее всего, они обладают площадью и периметром, нужно определить операции их вычисления, которые могут быть разными для разных фигур. 
Возникают ситуации, когда мы не можем определить некоторую общую виртуальную функцию в базовом классе, но знаем, что она будет перегружена в производных. Например, у нас нет общего механизма для подсчёта площади всех фигур. В этом случае, к прототипу функции пишут знак равенства и любое целое число, обычно ноль. Такая функция называется чисто виртуальной.
class Shape { 
public:
	virtual double area () = 0; // чисто виртуальная функция J
	/* ещё какая-то логика */
};
Класс, у которого есть хотя бы одна чисто виртуальная функция, называется абстрактным. Мы не можем создавать об’екты абстрактных классов, но можем использовать указатели на них.
class Rectangle: public Shape {
	double h, w;
public:
	double area () { return h*w; }
	/* ещё какая-то логика */
};
class Circle: public Shape {
	double r;
public:
	double area () { return 3.14*r*r; }
/* ещё какая-то фигня */
};
В таком случае можно написать:
Shape *p[100];      // массив из указателей на Shape
double total = 0;
В массиве можно сохранять указатели и на круги, и на прямоугольники:
for (int i = 0; i<100; i++) total += p[i]->area();
Здесь происходит динамическое определение типа, и вызывается функция, соответствующая об’екту, находящемуся в памяти.
Можно по-другому решать эту проблему. Например, в классе можно хранить свой тип, но это некрасивое решение, хотя зачастую оно работает немного быстрее. При использовании динамического полиморфизма, мы проигрываем немного по времени и по памяти (замечание: с другой стороны, если используются виртуальные функции, то информация о типе и так будет храниться «в» объектах, так что собственный механизм в этом случае можно считать излишеством).

Динамическая идентификация типа
Класс называется полиморфным, если у него есть виртуальные функции.
class Base {
	/* некоторый полиморфный класс */
};
class Derived: public Base { /* производный класс */};
С помощью динамической идентификации типа мы можем узнать тип об’екта, который записан по указателю базового типа. Пусть где-то в программе у нас встречается код:
Base b, *pb;   Derived d, *pd;
pd = &d;
pb = pd;  // так можем, а наоборот - нет
Присваивание pd = pb в нашем случае было бы логично, так как в указателе на базовый тип записан об’ект производного типа. Для этого служит операция 
dynamic_cast<T*> (p);
попытка преобразовать указатель p к типу T*. Либо преобразование работает, либо возвращает NULL. Попытка завершается успешно, если:
•	p содержит адрес об’екта типа T
•	p содержит адрес об’екта типа, для которого T – однозначный доступный базовый класс.
Мы можем написать:
pd = dynamic_cast<Derived*> (pb);  // работает, т.к. pb->Derived
if (pd) /* всё хорошо */; else /* всё плохо */;
pb = &b;
pd = dynamic_cast<Derived*> (pb);  // pd == NULL
pd = &d;
pb = dynamic_cast<Base*> (pd); // а так тоже можно
В C++ рекомендуется не делать статических явных преобразований типа. В нашем случае можно было написать  pd = (Derived*)pb, но это плохо, так как нет контроля, а при динамическом кастинке тип преобразуется, только если это будет разумно. 
Кстати, есть ещё один вариант операции: dynamic_cast<T&> (r);
Если ни одно из двух условий не выполняется, то генерируется исключение bad _cast (только для ссылок!).
Также есть операция typeid (об’ект) или typeid (имя_типа), которая возвращает ссылку на об’ект класса typeinfo. В нём перегружены операции сравнения и определена функция, возвращающая имя типа:
bool operator== (const typeinfo&);
bool operator!= (const typeinfo&);
const char* name ();
А вот так их можно использовать:
if (typeid(*pb) == typeid(Derived))
	pd = dynamic_cast<Derived*> (pb);  
// уже не нужно проверять pd != NULL
cout<<typeid(*pb).name()<<endl; // визуализация – узнать тип *pb
typeid можно применять и к непользовательским типам: int, char и пр.
Есть ещё три оператора, которыми можно не пользоваться, потому что их не было в C. Но при программировании на C++ их лучше писать.
static_cast – отличается от dynamic_cast отсутствием проверок:
pd = static_cast<Derived*> (pb);  
reinterpret_cast – значению одного указателя можно присваивать значение любого другого указателя (обычно используется для «принципиально различных» типов):
p = reinterpret_cast<int*> (q);  // эквивалентно p = (int*)q;
const_cast – преобразование снятия константности.

Множественное наследование.
Множественное наследование - ситуация, когда производный класс создаётся на базе нескольких базовых классов.
Чтобы понять, зачем это нужно, можно привести пример от Бьярна Страуструпа. При программировании некого GUI у нас есть класс ‘окно’. От него есть производные классы: ‘окно с рамкой’ и ‘окно с меню’. А что если мы хотим создать окно с рамкой и с меню? Нам на помощь приходит множественное наследование.
Пример. Далее приведём чисто технический пример, который не несёт никакой смысловой нагрузки, но на котором можно показать синтаксис.
class A { ... };
class B { ... };
class C: public A, public B { ... };
Базовый класс не может появиться несколько раз в этом списке явно (ситуация ‘одна база – два раза’). Однако может возникнуть такая ситуация:
class L { public: int n; ... };
class A: public L { ... };
class B: public L { ... };
class C: public A, public B { ... };

Например, мы напишем: C.c; c.n = 0; Возникает неоднозначность: какую c нам вызвать, ту которая пришла к нам через A, или же ту, которая наследовалась через B. Здесь мы можем уточнить: c.A::n = 5; или c.B::n = 7;. Перед оператором разрешения контекста мы указываем точку, от которой начинается поиск переменной.
Теперь понятно, что мы имели ввиду под однозначным классом ранее, когда говорили о динамическом полиморфизме.
C *pc = new C;
L *pl = pc;      // неверно
C неоднозначен, так как в нём ‘содержатся’ два L.
L *pl = (L*)(A*)pc;   // а так можно!
L *pl = (L*)(B*)pc;   // или так...
Но у нас может возникнуть вполне законное желание, чтобы в C был только один экземпляр L. Для этого придумали виртуальные базовые классы.

Виртуальные базовые классы.
class L { public: int n; ... };
class A: virtual public L { ... };
class B: virtual public L { ... };
class C: public A, public B { ... };
C *pc = new C;
L *pl = pc;      // верно
Теперь мы можем присваивать указателю на базовый класс указатель на производный, поскольку второй доступен и однозначен. Заметим, что при наследовании со словом virtual никакой виртуализации функций не происходит, просто совпадают ключевые слова, видимо, из-за плохой фантазии создателей языка.
Есть ещё одна проблема, возникающая в том случае, когда есть два базовых класса, внутри которых совпадают имена членов. Рассмотрим этот случай. Не важно, совпадают имена методов или полей, или имени поля с именем метода. Неоднозначность может возникнуть на уровне имён.
class A {
public: int a;
	void f (int);
	void g();
	...
};

class B {
	int a;
	void h (char);
public: void f ();
	int g;
	void h ();
	void h (int);
	...
};

class C: public A, public B { ... /* нет плохих имён */};

void blablabla (){
	C c; 
c.a = 1;  // неоднозначность
Используемые для имён классов проверки производятся в таком порядке:
1)	контроль однозначности в разных классах (втупую, считая все члены равноправными и доступными);
2)	попытка разрешить вызов (определение, какая из перегруженных функций вызовется);
3)	контроль доступности того, что выбрали.
c.f(1);   // ошибка, 1 пункт
c.f();    // ошибка, 1 пункт
c.g();    // ошибка, 1 пункт
c.g = 1;  // ошибка, 1 пункт
c.h();    // всё хорошо
c.h(1);   // всё хорошо
c.h(‘a’); // ошибка доступа, пункт 3
c.A::g(); // можно, мы явно сказали, что вызывать
}
Лучше явно указывать, откуда мы хотим вызвать функцию, так как может оказаться, что мы случайно унаследовали что-то лишнее. Особенно это актуально, когда мы пользуемся библиотечными классами из разных библиотек.

Агрегация. Композиция.
При агрегации возникает проблема вызова конструктора.
Пусть нам нужно построить частотный словарь. Для строк есть конструктор
class string {  ...
public: string (const char *);
};
class Word{
	string data;
	int count;
public: Word (const char *s, int count = 1): data(s)
		{ this*.count = count;}
};
Здесь мы инициализируем поле класса аргументом функции. Если нужно несколько инициализаторов, они перечисляются через запятую.
Если членом класса является константа или ссылка, то тоже необходимо использовать инициализаторы.
class X {
	int t;
	const int k;
	int& r;
	...
public: X (int i): k(i), r(t) { t = i; }
};

Статические члены класса.
В языке C статическая локальная переменная инициализируется один раз, при первом входе в функцию, а память, занимаемая ею, не удаляется из стека при выходе из функции. Также можно на файловом уровне определить статическую функцию. Тогда она будет глобальной функцией, на которую нельзя будет сослаться из другого файла. Если всё же попытаемся, получим ошибку линковки.
В C++ также модно использовать ключевое слово static  внутри класса. Если мы описываем статическое поле, то оно создаётся в одном экземпляре и используется всеми об’ектами данного класса. Переменная инициализируется ещё до создания первого об’екта данного класса.
class X {
public: int a;
	static int count; // счётчик созданных об’ектов
	X (int i) { a = i; ++count;}
	...
};
int X::count = 0; // в классе этого сделать нельзя!
По умолчанию все статические члены инициализируются нулями.
int main (){
	cout<<X::count<<endl; // получим ноль. count инициализирован
	X g(1), z(10);
	cout<<g.count<<endl<<z.count<<endl; // две двойки
	return 0;
}
Статические переменные появляются в тот момент, когда инициализируется класс. Как показано в примере, можно обращаться к ним и через об’екты, но это некрасиво.
Даже если сделать статический член класса скрытым, его всё равно можно будет инициализировать. Но тогда мы не сможем её напрямую получить, и в классе нужно описать функцию-селектор или выводящую функцию:
void printCount () { cout<<count; }
Эту функцию мы сможем вызвать, только если есть об’екты данного класса. Однако эту функцию можно тоже сделать статической. Нужно учитывать, что статические функции используют только статические поля класса, то есть она должна уметь работать независимо от об’ектов класса. Естественно, в статической функции не определён указатель this.
static void printCount () { cout<<count; }

Библия специальных функций-членов класса by ТВ.
Обычный конструктор вызывается:
•	при создании об’екта соответствующего класса, в том числе для элементов массива – конструктор умолчания;
•	при создании об’ектов в динамической памяти;
•	если об’ект – член другого класса (агрегация);
•	при создании об’екта производного класса вызываются конструкторы базовых (если ничего специально не напишем, то дефолтный конструктор будет пытаться вызваться);
•	для статических членов, являющихся об’ектами другого класса.
Конструктор копирования вызывается:
•	при инициализации об’екта;
•	при передаче в функцию параметра по значению (без амперсанда); 
•	при возврате результата функции по значению;
•	при выбрасывании исключения (читай дальше).
Деструктор вызывается:
•	при выходе из зоны описания локального об’екта;
•	при выполнении delete для указателя, полученного по new;
•	при явном вызове деструктора;
•	при свёртке стека (читай дальше).
Исключительные ситуации в C++
Существуют различные способы обработки ошибок:
•	прекратить работу программы (вызов exit()); метод плох тем, что глубинные функции не должны иметь таких полномочий;
•	специальный результат функции (так любят делать библиотечные функции C), например, NULL или -1; однако не всегда можно придумать значение, которое не может получиться в результате работы функции;
•	формирование некоторого признака (например, переменная errno); однако программисты не любят постоянно проверять его значение, также метод даёт сбои в некоторых ситуациях при параллельном программировании;
•	вывод на экран диагностики; это не даёт ответа, что делать дальше.
Идея исключений в следующем: отделить код, обрабатывающий входные данные, от кода, реагирующего на проблему.
Общая схема:
try {
	// подверженный ошибкам код
	if (ошибка) throw выражение;
}
catch (тип1 параметр) { ... }
catch (тип2 параметр) { ... }
Сначала мы входим в try, как будто его и нет вовсе. Если вдруг мы попадаем на инструкцию throw, то код ниже неё не выполняется, а управление передаётся. Среди параметров catch ищем подходящий тип исключения. Если находим, то выполняются действия в нём. Там может быть любой код, можно также использовать значение параметра. Когда инструкции выполнятся, переходим на оператор после последнего catch.
Обработчики могут быть трёх видов:
•	catch (тип параметр) { ... }
•	catch (тип) { ... }   // не интересно значение параметра
•	catch (...) { ... }   // обрабатывает любое исключение
Обработчик исключения catch типа T может обработать исключение типа E, если:
•	T и E одного и того же типа (с точностью до typedef)
•	T однозначный и доступный базовый класс к T;
•	T, E – указатели, E может быть преобразован к T с помощью стандартных преобразований (указатель на производный класс приводится к указателю на однозначный доступный базовый класс, любой указатель приводится к void*)
Вместо T также могут появиться const T, T&, const T&.
Важен порядок, в котором перечислены обработчики. 
Вызов throw; в catch-блоке позволяет передать обработку исключения в об’емлющий try-блок с тем же параметром.
Действия при возбуждении исключения throw x:
1.	Создаётся временный об’ект – копия x, это ещё одна ситуация, когда вызывается конструктор копирования.
2.	Для всех других об’ектов из try-блока освобождается память и вызываются деструкторы (на самом деле, то же самое происходит при выходе по goto), для всех созданных подоб’ектов делается то же самое, процесс называется свёрткой стека или раскруткой стека (stack unwinding).
3.	Если найден подходящий catch-обработчик, выполняется его составной оператор, затем выполнение продолжается с оператора после последнего catch.
4.	Если нет подходящего обработчика, то работает принцип динамической ловушки: поиск продолжается в об’емлющем try-блоке. Если и там ничего не нашлось, мы возвращаемся в точку вызова функции и ищем try-блоки дальше.
5.	Если мы находим подходящий обработчик, то, после выполнения его кода, управление передаётся на оператор, следующий после серии catch, в которую входит отработавший обработчик. 
6.	Если и в функции main() не нашлось подходящего обработчика, то вызывается функция terminate(), прекращающая выполнение программы.

Функция terminate()
Она вызывается, если:
•	нет подходящего обработчика исключения;
•	если деструктор в процессе свёртки стека возбудил исключительную ситуацию;
•	при вызове throw; не в catch-блоке и не в функции, вызванной из него;
•	если стек разрушился.
Функцию terminate() можно переопределить:
set_terminate(handler);

Перевозбуждение исключений
try { throw x; }
catch (___) { throw; }
Перевозбуждение исключений позволяет рассредоточить обработку исключения по цепочке вызовов: каждая функция обрабатывает исключения в силу своей компетенции.
Пример.
class Vect {
	int *p, size;
public:
	Vect (int u = 10);
	Vect (const Vect &a);
	~Vect();
	int &operator[] (int i);
};
/* перегрузка операции индексирования возможна только с помощью нестатической функции-члена от целой переменной, возвращающей ссылку, чтобы можно было менять элементы */
Vect::Vect (int n){
	if (n<=0) throw n;
	p = new int [size = n];
/* если оператор new выполняется ошибочно, стандарт предписывает ему возбуждать исключение bad_alloc. Однако старые компиляторы просто возвращают NULL. Поэтому стандарт также рекомендует это делать в целях совместимости. */
	if (!p) throw “Свободная память исчерпана”;
} 
int &Vect::operator[] (int i){
	if (i<0 || i>=size) throw “ошибочный индекс”;
	return p[i];
}
void g (int k){
	try {
		Vect x(k);
		int l, i;
		l = x[10];  x[i] = 100;}
	catch (int n){ cout<<”Неверный размер ”<<n<<endl; throw;}
	catch (const char *str) {cout<<str<<endl; }
}

Спецификация исключений
Мы не всегда пишем код с нуля. В библиотечных классах тоже есть исключения.
void f() throw (X,int) { ... }
Такое об’явление означает, что функция может вызывать throw X, throw int
void f() throw (X,int) { ... }
А это значит, что функция не должна возбуждать исключений совсем.
По умолчанию функция может возбуждать любое исключение. Если функция попытается возбудить исключение, которого не должна, вызывается функция unexpected(), прекращающая выполнение программы. Её также можно переопределить:
set_unexpected(handle);
Спецификация исключений не входит в профиль функции, поэтому она не наследуется. При использовании виртуальных функций мы можем повторить или уменьшить список возбуждаемых исключений.
Перегрузка функций.
Статический полиморфизм позволяет давать одно имя нескольким функциям. Как правило, эти функции имеют схожую семантику, но отличаются списком формальных параметров. Какая функция будет вызвана, определяется на этапе трансляции. О перегрузке функций можно говорить только в пределах одной области видимости. Кстати, когда мы об’являем несколько конструкторов одного класса – это тоже перегрузка функций.
Проблема поиска подходящей перегруженной функции (best matching) – нетривиальная задача. Для начала опишем этот алгоритм для функции одного аргумента.
1.	Поиск функции, точно совпадающей по типу параметра (точное отождествление). Если функция вызывается от параметра типа T, то может быть вызвано описание с прототипом от T, T&, const T, const T&, переопределения этих типов с помощью typedef, T[] эквивалентно T*, функция эквивалентна указателю на функцию.
2.	Если не найдено точное соответствие, то пробуем применить стандартные преобразования. На втором шаге могут сработать безопасные преобразования – целочисленное или вещественное расширение (integral/floating promotion). Тут bool, char, short, enum (знаковые или беззнаковые) преобразуются к int или unsigned, float преобразуется к double.
3.	Если не получилось выполнить шаг 2, пробуем все остальные стандартные преобразования: оставшиеся арифметические преобразования и преобразования указателей и ссылок (указатель на производный класс приводится к указателю на однозначный доступный базовый класс, любой указатель приводится к void*, 0 воспринимается как NULL).
4.	Пользовательские преобразования - рассматриваются конструкторы, которые могут быть вызваны с одним параметром. Также рассматриваются специальные функции преобразования типов.
5.	Если ничего не помогло, придётся вызывать функцию с ‘…’.
Особенности четвёртого шага:
 1. Отсутствие транзитивности пользовательских преобразований. То есть, за один раз не может выполниться более одного преобразования типа.
class X { public: operator int(); ... };
class Y { public: operator X(); ... };
void f(){  Y a;  int b; ...
	b = a;  // нельзя
}
Можно явно указать b = a.operator X().operator int();. 
2. Пользовательские преобразования могут применяться неявно, только если они однозначны.
class B {
public: B (int i);
	operator int();
	B operator+ (int B);
};
void f(){
	B l(1);  ... l+1 ...
}
Возникает неоднозначность: то ли l стоит преобразовать к int с помощью определённого преобразования и складывать числа, то ли вызвать конструктор от int и складывать об’екты типа B. 
3. Конструктор должен быть описан так, чтобы он допускал неявный вызов. То есть, конструктор не может быть описан как explicit.
class X { public: X(int); };
X a(1);  X b  = 2;  // так можно
Теперь изменим об’явление:
class X { public: explicit X(int); };
X a(1);        // так можно
X b = 2;      // так нельзя!
X с = X(2);    // так можно
Зачем же нужна такая конструкция? Вспомним наш класс String.
class String { String (int n); ... };
String s1 = 10;
String s2 = ‘a’;
Этого нам не запрещает так делать. Но, если мы допишем explicit к конструктору, то такая нелогичная запись не прокатит и придётся вызывать через скобочки.
Алгоритм поиска наилучшего соответствия для вызова функции с произвольным числом параметров N:
I.	По каждому из параметров ищется best matching по пятишаговому алгоритму за тем исключением, что если на каком-то шаге несколько кандидатов, способных обслужить вызов, запоминаем все их. В итоге получаем N множеств возможных функций.
II.	Ищем пересечение этих множеств. Если оно пусто, то нет подходящей функции. Если пересечение содержит 2 или более элемента, то неоднозначность. Но если там одна функция, она и обслужит вызов.
Пример.
class X { public: X (int); ... };
class Y { ... };
void f (X, int);  /* 1 */
void f (X, double);  /* 2 */
void f (Y, double);  /* 3 */
Пусть мы вызываем f(1,5);. По первому параметру мы оставляем 1 и 2 (пользовательские преобразования), по второму – 1 (точное соответствие). Пересечение даёт первый вариант.
Теперь попробуем вызвать f(1,5.0);. По первому параметру мы оставляем 1 и 2 (пользовательские преобразования), по второму – 2 и 3 (точное соответствие). Пересечение даёт второй вариант.
Пример на *пятый шаг*.
class R { public: R (double); ... };
void f (int, R);
void f (int, ...);
void g () {
	f (1, 1);                // первый обработчик
	f (1, “preved!”);        // второй обработчик

}

Шаблоны функций. 
Настало время поговорить о типовом полиморфизме. Эта идея в C++ появилась не впервые, она была реализована ещё в языка Ada.
Многие функции делают примерно одно и то же, но работают с разными типами данных. Например, алгоритм сортировки почти не зависит от типа сортируемых элементов. Мы можем описать шаблон, который можно настраивать на типы. Просто надо будет написать соответствующие функции сравнения и т.д.
Пример.
int max (int x, int y) { return x>y? x: y; }
Сделаем теперь функцию независимой от типов.
template<class T> T max (T x, T y) { return x>y? x: y; }
У этой функции только типовые параметры. Ключевое слово class не значит, что T должен быть пользовательским типом. Можно передавать данные встроенных типов также. (Создатели языка опять не придумали нового ключевого слова)
Параметров шаблона может быть несколько, тогда они перечисляются через запятую.
void f() {
	max (1,2);
	/* по параметру автоматически определяется тип шаблона.
	вызовется int max<int> (1,2) */
	max (‘0’,’a’);  // от char
	max (‘a’,100);  // неоднозначность. так нельзя!!
	max (2.5,1);    // неоднозначность. int или double ???
Есть возможности разрешить неоднозначность:
•	явная квалификация: max<int> (‘a’,100);
•	перегрузка шаблонных функций:
int max (char c, int i) { return c>i? c: i; }
Можно перегрузить шаблон и так:
int max (int x, int y) { return x>y? x: y; }
Теперь нам непонятно, что вызывать: шаблонную функцию или явную? Модифицируем наш алгоритм поиска best matching функции, добавив шаг между 1 и 2. На первом шаге, не думая о шаблонах, ищем точное соответствие. На новом, *полуторном* шаге, пытаемся сгенерировать подходящую функцию по шаблону, чтобы получить точное соответствие. Если попытка неудачная, переходим к шагу 2 и далее выполняем алгоритм, как и раньше.
Тип параметра шаблона можно указать и явно. Эта возможность относительно новая в языке. Об этом пишет Бьярн Страуструп в интереснейшей книге ‘Дизайн и эволюция языка C++’. Однако в некоторых ситуациях не обойтись без явного указания.
Пример такой ситуации.
template<class T, class U> T convert (U u) { ... return t; };
void g (int i){
	convert (i);  // U = int; T – непонятно L
}
Надо учитывать, что при поиске соответствия компилятор не обращает внимания на то, чему мы присваиваем результат работы функции. Контекст не рассматривается!
Из этой ситуации можно выйти так:
convert<double> (i);  // U = int; T = double
При спецификации типа указанный тип подставляется на место первого параметра, если указаны два типа – на место первого и второго, и так далее.
Мы можем придать новый смысл функции, например, заставить искать максимальный элемент в массиве. Для этого перегрузим её так:
template<class T > T max (T *p, int size) { ... };
А сможем ли мы искать с помощью нашей шаблонной функции максимум среди об’ектов класса Complex, описанного нами выше? То есть, можно ли написать так:
Complex a(1,2), b(3,4);
max (a,b);  // T = Complex
Проблема, очевидно, в том, что в классе не определены операции сравнения. Однако у ТВ как-то получилось это скомпилировать. Программа даже запустилась и выдала результат! Но результат совершенно не подвластен здравому смыслу. Вы можете покопаться в исходниках и удовлетворить всеобщий интерес рассказом о том, что там происходит.
Вообще, к сожалению, компиляторы далеко не всегда хорошо держат стандарт при работе с шаблонами.

Шаблоны классов.
Шаблоны классов тоже зачастую бывают разумными. Как правило, они применяются при описании контейнеров, то есть типов, которые содержат в себе каким-то образом структурированные об’екты других типов. Например, стек можно реализовать как контейнер. Операции над ним, вообще говоря, не зависят от типа его элементов.
Пример. Контейнер-вектор.
template<class T> class Vector {
	T *p;
	int size;
public: explicit Vector (int);
	T& operator[] (int i);
};
Vector<int> x(20);        // вектор из 20 целых чисел
Vector<Complex> y(100);   // вектор из 20 комплексных чисел
Инстанцирование – процесс генерации класса по шаблону.
Описывать функции вне класса нужно так:
template<class T> T& Vector<T>::operator[] (int i) { ... }
Пример. Класс с нетиповым параметром.
template<class T, int size> class buffer { ... }
buffer <char,1024> x;  
buffer <char,512*2> y; 
buffer <int,1024> z;   
Возникает вопрос, какие переменные будут иметь одинаковые типы? Есть общее правило: инстанцированные классы будут считаться одинаковыми, если типовые параметры совпадают, а нетиповые равны по значению. x и z, очевидно, разного типа, так как типовые параметры не совпадают. Для нетиповых параметров должна быть определена операция сравнения на равенство. Получается, что параметры шаблона не могут быть вещественными.
/* на этом изложение теории языка C++ считается законченным. На лекциях ещё будет рассказ о STL, но это хз когда. */
Удачи всем в субботу! ;)