Семестр 2
Практическое занятие 4. Стремительный курс по C++.
На С Вы просто можете делать ошибки, на С++ Вы сможете их наследовать!
Что нужно предварительно знать для выполнения задания:
1. Объявление класса.
Класс – это определяемый пользователем тип. Для того, чтобы создать объект такого пользовательского типа, компилятор должен во всяком случае знать, сколько памяти следует отвести под данный объект. Поэтому использованию класса должно предшествовать его объявление. Объявление класса рекомендуется помещать в заголовочный файл.
Формально простейшее объявление класса выглядит так:
class имя_класса{
список_членов
}; где
class – ключевое слово (вместо него можно использовать struct или union),
список_членов – включает описание типов и имен как данных (Member Variables), так и функций (Member Functions), называемых также методами класса.
Например:
class Point{
int m_nX; //данное – член класса
int m_nY; //данное – член класса
void SetXY(int x, int y); //функция – член класса (метод класса)
};
Замечание: большинство разработчиков Microsoft использует для имен элементов данных С++ - классов префикс m_ (от слова member). Рекомендую придерживаться этого соглашения.
2. Спецификаторы доступа
Существуют три уровня привилегий доступа к членам класса. Каждый уровень доступа определяется своим ключевым словом (спецификатором доступа) public, private или protected. Каждое объявление переменной или метода внутри класса определяет привилегию доступа в зависимости от того, в какой секции оно расположено. Секций может быть сколько угодно и располагаться они могут в любом порядке. По умолчанию объявления являются private.
Например:
class Acces{
int m_iX; // private по умолчанию
public:
int m_iY; //public
protected:
int m_iZ; //protected
char m_cZ; //protected
public:
void Increase(int dx, int dy); //public
private:
int SomeFunc(void); //private
};
Назначение спецификаторов доступа:
public – общедоступный, то есть к нему имеют доступ не только методы класса, но и «внешние» по отношению к классу функции посредством объекта (экземпляра) данного класса (см. Доступ к public-элементам посредством объекта) или указателя на экземпляр класса (см. Указатель на класс. Доступ к элементам класса посредством указателя.). Public – методы и переменные образуют открытый интерфейс для «общения» с классом извне.
private – локальный, то есть данный объект не доступен вне класса; обращаться к нему могут только методы этого класса
protected – защищенный, то есть данный элемент не доступен вне класса, пользоваться им могут методы данного класса и методы производного класса (см. Наследование.).
Замечание: компилятор строго следит за соблюдением привилегий доступа, защищая private и protected переменные и методы от случайного или злонамеренного несанкционированного доступа.
3. Создание объекта (экземпляра) класса
В простейшем случае создание объекта класса выглядит также как и объявление переменной базового типа. Например: один из возможных способов создания объекта типа Point:

Создан объект pt типа Point. То есть компилятор зарезервировал память для данного объекта и проинициализировал (или не проинициализировал) его согласно общим правилам инициализации переменных (то есть для глобальных и статических объектов переменные m_nX и m_nY будут проинициализированы нулем, а для локальных объектов переменные будут содержать случайные значения).
Объекты класса могут быть присвоены другим объектам того же класса, переданы в качестве аргументов функции, возвращены функцией, могут быть скомпонованы в массив... (также как и объекты базовых типов)
4. Определение (реализация) методов класса
Объявление класса всегда содержит объявления всех своих методов, но иногда при объявлении функции можно сразу же привести и ее определение – как это сделано при объявлении метода Dec() в нижеприведенном примере. В приведенном примере тело метода - {} - дано прямо в объявлении класса – такие функции по умолчанию являются встраиваемыми (inline). Это означает следующее: встречая в тексте программы вызов такой функции, компилятор, если это возможно, подставляет в месте вызова ее код, не тратя ресурсов на вызов. Иногда так удобно делать для очень короткого и часто вызываемого кода.
Общепринятая практика – помещать определение метода (реализацию метода) в соответствующий. cpp-файл.
Например:
файл Example. h:
class Point{
public:
int m_nX;
int m_nY;
void Dec(void){m_nX = m_nX-1; m_nY = m_nY-1;} //объявление и реализация встроенного метода класса
void Increase(int dx, int dy); //объявление метода класса
};
файл Example.cpp:
void Point::Increase(int nDeltaX, int nDeltaY) //реализация метода класса Point
{
m_nX+= nDeltaX;
m_nY+= nDeltaX;
}
При определении метода после типа возвращаемого значения всегда нужно указывать класс, членом которого является данный метод (в нашем случае – Point:: ).
Замечание: методы, определенные вне класса также можно сделать встраиваемыми, используя при определении ключевое слово inline:
inline void Point::Increase(int nDeltaX, int nDeltaY)
{
...
}
5. Доступ к public-элементам посредством объекта. Селектор «.»
-осуществляется с помощью имени объекта, оператора–точки и имени элемента класса. Другая возможность (использование указателя на объект) будет обсуждаться в пункте Указатель на класс. Доступ к элементам класса посредством указателя.
Point pt; //создание экземпляра класса Point с именем pt
pt.m_nX = 10; //присвоили переменной m_nX объекта pt значение 10
pt.m_nY = 5; // присвоили переменной m_nY объекта pt значение 5
pt. Increase(1,2); //при вызове функции значения m_nX и m_nY будут увеличены на 1 и 2 соответственно
6. Конструкторы
Обычно при создании экземпляра класса требуется не только выделить для него память, но и проинициализировать его переменные. Способ, которым это было сделано в пункте Доступ к public-элементам посредством объекта, довольно неуклюж. Гораздо лучше предоставить в распоряжение программиста специально предназначенную для инициализации переменных данного экземпляра класса функцию. У всех классов С++ есть одна или несколько таких специальных функций – конструкторов (constructors), автоматически вызываемых при создании экземпляра класса компилятором. Специфика конструктора заключается в следующем: имя конструктора всегда совпадает с именем класса, и конструктор никогда ничего не возвращает (даже void)! В остальном конструктор подобен обычным функциям, в частности как и любая функция, может иметь любое количество параметров.
6.1. Конструктор по умолчанию.
Конструктор, не имеющий аргументов, называется конструктором по умолчанию (default constuctor). Если в классе отсутствует явно определенный пользователем конструктор (например, как в пункте Определение (реализация) методов класса), то компилятор генерирует собственный конструктор по умолчанию. Компилятор, генерируя вызов конструктора по умолчанию, выделяет память для объекта, но явно не инициализирует значения данных-членов класса.
Например:
void main()
{
Point pt; //вызван коструктор по умолчанию: создан локальный объект pt типа Point (на стеке), m_nX и m_nY не инициализированы
}
Ничто не мешает “явно” объявить в классе конструктор по умолчанию (без аргументов), который будет выполнять какие-то инициализирующие действия. Например:
class Point{
public:
Point() { m_nX = 0; m_nY = 0;} //конструктор по умолчанию
int m_nX;
int m_nY;
void Increase(int dx, int dy);
};
Замечание 1 (существенное!): обратите внимание на следующий фрагмент кода и запомните разницу:
void main()
{
Point pt; //вызов конструктора по умолчанию
Point pt1(); //а эту строчку компилятор трактует как объявление функции, которая не принимает параметров и возвращает тип Point!
}
6.2. Конструктор с параметрами.
Так как одно из предназначений конструктора - это инициализация переменных класса, то в основном используются конструкторы с параметрами. Вот новое объявление класса Point с явным встроенным конструктором:
class Point{
int m_nX;
int m_nY;
public:
Point(int x, int y){m_nX = x; m_nY = y;} //объявление и реализация явного конструктора
void Increase(int dx, int dy);
};
Соответственно изменится и код создания объекта типа Point:
Point pt(10,5); //компилятор создаст экземпляр класса Point и инициализирует переменные m_nX и m_nY данного экземпляра значениями 10 и 5 соответственно.
Замечание 1: поскольку Вы ввели явный конструктор с параметрами и явно не переопределили свой конструктор без параметров, компилятор уже не будет генерировать автоматически конструктор по умолчанию и в ответ на попытку сконструировать объект Point по умолчанию выдаст ошибку:
Point pt; //ошибка – в классе не определен конструктор без параметров
Замечание 2: конструктор с параметрами, у которого все параметры имеют значения по умолчанию (смотри пункт Конструктор с параметрами по умолчанию.), эквивалентен конструктору по умолчанию.
class Point{
…
Point(int x = 0, int y = 0){ m_nX = x; m_nY = y;} //конструктор с параметрами по умолчанию
…
};
6.3. Специфика записи при вызове конструктора с одним параметром
Иногда Вы можете встретить несколько странную запись, которая означает неявный вызов конструктора с одним параметром:
class Point{
int m_nX;
int m_nY;
public:
Point (int x){m_nX = x; m_nY = 0;} //объявлен конструктор с одним параметром
};
void main()
{
Point pt = 1; //неявный вызов конструктора с одним параметром. Означает то же самое, что и Point pt(1);
}
Такую запись можно интерпретировать следующим образом: компилятор приводит значение, стоящее справа от знака равенства, к требуемому типу слева от знака равенства (аналогично неявному приведению базовых типов: float f = 1;).
6.4. Конструкторы и модификатор explicit
Для того, чтобы запретить неявное преобразование применяется модификатор explicit:
class Point{
...
explicit Point (int x){m_nX = x; m_nY = 0;} //конструктор объявлен с модификатором explicit
};
void main()
{
Point pt = 1; // неявное преобразование запрещено при объявлении конструктора с одним параметром => ошибка компилятора: «Не могу преобразовать const int в класс Point»
}
6.5. Перегрузка конструкторов
Довольно часто удобно иметь несколько способов инициализации переменных класса. В С++ могут перегружаться любые функции, в том числе и конструкторы. Вы можете объявить любое количество конструкторов, различающихся по числу и типу аргументов, а при создании экземпляра класса компилятор сгенерирует вызов требуемого. Например:
class Point{
int m_nX, m_nY;
double m_dX, m_dY;
public:
Point() { m_nX = 0; m_nY = 0;m_dX = 0; m_dY = 0;} //конструктор по умолчанию
Point(int x, int y){m_nX = x; m_nY = y;} //объявление и реализация явного конструктора, который инициализирует целые переменные m_nX и m_nY
Point(double x, double y){m_dX = x; m_dY = y;} //объявление и реализация явного конструктора, который инициализирует плавающие переменные m_dX и m_dY
};
Теперь экземпляры класса Point можно создать следующими способами:
Point pt1(10,5); //компилятор вызовет конструктор Point(int, int), который проинициализирует переменные m_nX и m_nY данного экземпляра значениями 10 и 5 соответственно.
Point pt2(1.5 , 5.0); // компилятор вызовет конструктор Point(double, double), который проинициализирует переменные m_dX и m_dY данного экземпляра значениями 1.5 и 5.0 соответственно.
Point pt3; // компилятор вызовет конструктор Point(), который по умолчанию переменные m_nX, m_nY, m_dX и m_dY данного экземпляра нулями.
6.6. Конструктор с параметрами по умолчанию.
Как и все функции, конструктор может иметь аргументы, используемые по умолчанию.
Например:
class Point{
public:
Point(int x , int y=1){m_nX = x; m_nY = y;} //объявление и реализация конструктора с одним аргументом по умолчанию
...
};
Объявлен конструктор с одним обязательным аргументом – x, и вторым аргументом, по умолчанию равным 1 – y. То есть экземпляр класса Point можно создать:
Point pt1(10,5); //m_nX примет значение 10, а m_nY – 5
Point pt2(10); //m_nX примет значение 10, а m_nY – 1 по умолчанию
6.7. Возможные конфликты при использовании параметров по умолчанию.
При наличии нескольких конструкторов и использовании конструктора, имеющего аргументы по умолчанию, может возникнуть следующая ситуация:
class Point{
public:
Point(int x=1 , int y=1){m_nX = x; m_nY = y;} //объявление и реализация конструктора с аргуменами по умолчанию
Point() { m_nX = 0; m_nY = 0;}; //объявление и реализация конструктора без аргументов
}
void main()
{
Point pt; //ошибка компилятора, так как нет однозначности – какой именно конструктор должен быть вызван
}
6.8. Конструктор копирования.
Конструктор копирования – особый вид конструктора. Как и конструктор по умолчанию, конструктор копий (copy constructor) – это метод класса, который зачастую генерирует сам компилятор.
6.8.1. Создание нового объекта по существующему объекту.
Основное назначение конструктора копий – создавать новые экземпляры (того же класса) по существующему экземпляру, передаваемому в качестве параметра.
Замечание 1: существенным является передача именно ссылки в качестве источника копирования для того, чтобы избежать бесконечной рекурсии (смотри Передача объекта в качестве параметра). Компилятор отслеживает тип аргумента конструктора копирования, и если Вы попробуете задать в качестве типа параметра не ссылку, а значение, выдаст ошибку.
Пример явного задания конструктора копирования:
class Point{
public:
Point(int x, int y){m_nX = x; m_nY = y;}
Point(const Point& refPt) {m_nX = refPt. m_nX; m_nY = refPt. m_nY;} //объявление и реализация конструктора копирования. Модификатор const указывает, что функция не может менять значение, на которое ссылается параметр refPt.
}
void main()
{
Point pt1(10,5);
//Когда вызывается конструктор копирования
Point pt2(pt1); // создается объект pt2 посредством вызова копирующего конструктора (получаем копию pt1).
Point pt3 = pt1; //то же самое
Point pt4 = Point(5,5); //сначала создается временный объект типа Point (5,5), а затем создается объект pt4 посредством вызова копирующего конструктора
}
Если конструктор копирования не задан программистом явно, компилятор создаст его сам, и такой конструктор копирования будет всего лишь «поэлементно» копировать все данные экземпляра.
Замечание 2: конструктор копирования по умолчанию подходит только для простых классов (типа Point). Для более сложных классов, требующих динамического выделения памяти или иной специализированной инициализации, конструктор копирования по умолчанию не годится.
Замечание 3: инициализация и присваивание являются различными операциями! Копирование для уже существующих объектов осуществляется не с помощью копирующего конструктора, а посредством оператора копирующего присваивания (смотри следующее занятие):
{
Point pt1(1,2), pt2, pt3;
//Когда конструктор копирования не вызывается!
pt2 = pt1; //объект pt2 принимает значение объекта pt1 посредством оператора копирования «=».
pt3 = Point(7,8); //создается временный объект типа Point (вызывается конструктор с двумя параметрами 7,8), а затем объект pt3 принимает значение временного объекта посредством оператора копирования «=».
}
6.8.2. Передача объекта в качестве параметра
Менее очевиден вызов конструктора копирования, например, при передаче экземпляра класса в качестве параметра функции (по значению). Например:
void Func(Point pt); //прототип функции, которая принимает параметр типа Point
void main()
{
Point pt1(10,5);
Func(pt1); //вызывается конструктор копий, чтобы скопировать объект pt1 в стек для передачи в качестве параметра при вызове функции Func
}
6.8.3. Возвращение объекта по значению
Возвращение объекта класса по значению происходит тоже с помощью конструктора копирования:
Point GetPoint()
{
Point pt(3,3);
return pt;
}
void main()
{
Point pt1 = GetPoint();
}
6.9. Проблемы, которые могут возникнуть при использовании конструктора копирования по умолчанию
Как отмечалось в пункте Создание нового объекта по существующему объекту. конструктор копирования по умолчанию подходит только для простых классов. Рассмотрим пример, в котором использование конструктора по умолчанию порождает ошибки:
class A{
public:
int* m_p;
A(int iNum){m_p = new int[iNum];} //конструктор принимает размерность одномерного массива и динамически выделяет под него память
//Копирующий конструктор не переопределен!
};
void main()
{
A a(5); //создали экземпляр класса A (при конструировании объекта а динамически выделили память под 5*sizeof(int) )
A b = a; //вызывается копирующий конструктор по умолчанию, который честно почленно копирует все данные из “а” в “b”, в том числе и указатель m_p на динамически созданный массив
delete[] a. m_p; //уничтожается динамически созданный массив
delete[] b. m_p; //ошибка времени выполнения – попытка уничтожить уже несуществующий объект
}
Замечание: имейте в виду, что точно такая же ситуация возникает при копирующем присваивании по умолчанию (смотри следующее занятие)
7. Деструкторы.
7.1. Деструктор – метод класса.
Деструктор – еще одна специфическая функция класса. Ее имя – это имя класса, перед которым ставят тильду –«~». У каждого класса только одна функция-деструктор. Деструктор ничего не принимает и ничего не возвращает.
Деструктор вызывается для разрушения элементов объекта перед разрушением самого объекта. То есть обычно деструкторы выполняют операции, обратные тем, которые выполняют конструкторы: например, если конструктор выделяет динамическую память, то деструктор, вероятнее всего, ее освобождает и т. д.
Если явно в классе деструктор программистом не определен, то компилятор генерирует его сам. Такой деструктор вызывает деструкторы элементов данного класса, которые тоже являются С++ - объектами. При этом следует отдавать себе отчет в том, что некоторые действия за программиста он автоматически сгенерировать не может (например, освободить выделенную динамически память).
Пример:
#include <string. h>
class String
{
public:
String( char* pString ); // объявление конструктора
~String(); // объявление деструктора
private:
char* m_pString; //строка-член класса
};
// Определение конструктора.
String::String( char *pString )
{
// Динамически выделить требуемое количество памяти.
m_pString = new char[strlen( pString ) + 1];
// Если память выделена, скопировать строку-аргумент в строку-член класса
strcpy( m_pString, pString );
}
// Определение деструктора.
String::~String()
{
// Освобождение памяти, занятой в конструкторе для строки-члена класса
delete[] m_pString;
}
7.2. Когда вызываются деструкторы?
– для локальных объектов деструктор вызывается автоматически при выходе из блока, в котором данный локальный объект объявлен. Для глобальных объектов вызов деструкторов является частью процедуры завершения после выхода из функции main.
Если объект был создан динамически (new), то деструктор вызывается при динамическом уничтожении объекта (delete).
8. Указатель на класс. Доступ к элементам класса посредством указателя. Селектор «->».
Так как понятие класс определяет тип данных, то в соответствии с логикой языка С++ ничто не мешает (а иногда без этого не получить преимуществ ООП) объявить тип данных - «указатель на класс». Как и обычный указатель, он хранит адрес объекта и может использоваться для доступа не только к «самому объекту», но и для доступа к элементам объекта (как к членам-данным, так и для вызова методов).
Пример:
class Point{
public:
Point(int x, int y){m_nX = x; m_nY = y;} //объявление и реализация явного конструктора
int m_nX;
int m_nY;
void Increase(int dx, int dy) ){m_nX+=dx; m_nY+=dy;}
};
void main()
{
Point* pPt = new Point(10,5); //динамическое создание объекта типа Point, на который указывает указатель pPt
int nX = pPt->m_nX; //доступ к public-элементам данных класса посредством указателя
pPt->Increase(1,-1); //вызов public-метода посредством указателя
delete pPt; //динамически созданный объект необходимо уничтожить (при этом будет вызван деструктор класса)
}
9. Указатель this.
В языке С++ предусмотрен синтаксис для доступа объекта «к самому себе» – в нестатической функции-члене класса ключевое слово this является указателем на объект для которого вызвана функция. Однако, это не обычная переменная: невозможно использовать ее вне методов класса или что-нибудь ей присвоить. В большинстве случаев использование this является неявным. Например:
1. При вызове нестатического метода класса для данного объекта адрес объекта передается как скрытый аргумент функции:
pt. Increase(1,1); //можно интерпретировать как
Increase(&pt,1,1);
2. Для доступа к нестатическому члену-данному класса также неявно используется this:
void Point::Increase(int nDeltaX, int nDeltaY)
{
m_nX+= nDeltaX; // (1)
m_nY+= nDeltaX;
//строчки (2) и (3) эквивалентны (1)
this->m_nX+= nDeltaX; // (2) корректно, но компилятор сделает это и без явного указания
(*this).m_nX+= nDeltaX; // (3)
}
3. Выражение (*this) часто используется для возвращения текущего объекта из метода класса:
Point& Point::SomeFunc()
{
...
return *this;
}
4. (*this) или this можно использовать для передачи в качестве параметра, передаваемого функции:
void Point::SomeFunc()
{
...
OtherFunc(this);
}
10. Массивы и классы
10.1. Массив объектов класса.
Пример:
class Point{
public:
Point(){}; //объявление и реализация конструктора по умолчанию
Point(int x, int y){m_nX = x; m_nY = y;} //объявление и реализация конструктора с аргументами
int m_nX;
int m_nY;
};
void main()
{
Point points1[5]; //массив из пяти объектов Point. При создании каждого элемента массива вызывается конструктор по умолчанию
Point points2[2] = { Point(1,1),Point(2,2)}; //массив из двух объектов Point. При создании каждого элемента массива вызывается конструктор с аргументами
Point points3[3] = { Point(1,1),Point(2,2)}; //массив из трех объектов Point. При создании первых двух элементов массива вызывается конструктор с аргументами, при создании третьего – конструктор по умолчанию
}
10.2. Массив указателей на объекты класса.
Пример:
void main()
{
Point* points1[5]; //неинициализированный массив из пяти указателей на объекты Point.
Point* points2[3] = { new Point(1,1), new Point(2,2) }; //массив из трех указателей на объекты Point. Два первых элемента массива проинициализированы, третий – инициализируется нулем (смотри второе занятие).
delete points2[0];
delete points2[1];
}
11. Закрытые и открытые элементы класса. Инкапсуляция.
В некоторых рассмотренных выше примерах элементы данных m_nX и m_nY класса Point были открытыми (public), а поэтому доступными посредством объекта или указателя на объект в любом месте программы. С++ позволяет «скрывать» данные класса. Если объявить элемент данных со спецификатором private или protected, он станет недоступен вне класса, то есть обращаться к нему смогут только методы этого класса. Например:
class Point{
public:
Point(int x, int y){m_nX = x; m_nY = y;}
private:
int m_nX, m_nY;
};
void main()
{
Point pt(1,1);
pt. m_nX = 5; //ошибка компилятора
}
Модифицируем класс Point следующим образом: добавим две public-функции
class Point{
public:
Point(int x, int y){m_nX = x; m_nY = y;}
void SetX(int x) ){m_nX = x;}
void SetY(int y) ){m_nY = y;}
private:
int m_nX, m_nY;
};
void main()
{
Point pt(1,1);
pt. SetX(5); //теперь все корректно!
}
Элементы данных m_nX и m_nY теперь “закрыты” и доступны извне только через функцию типа SetX. Это хорошая иллюстрация такой концепции С++, как инкапсуляция (encapsulation). Инкапсуляция особенно полезна в сложных классах, где необходима защита от «несанкционированных посягательств» на внутренние данные. Набор public-функций и public-данных называется пользовательским интерфейсом класса – это то, что должен знать программист-пользователь при использовании разработанного (возможно кем-то другим) и отлаженного класса.
Замечание 1: методы тоже могут быть protected и private. Закрытый метод (иногда называемый вспомогательной или helper - функцией) нельзя вызвать извне класса, но он доступен другим методам данного класса.
12. Наследование.
Классы чаще всего строятся постепенно, начиная от простых базовых классов с общими для некоторого множества объектов свойствами и заканчивая специализированными производными классами. Каждый раз, когда из предыдущего класса производится последующий, производный класс наследует какие-то или все родительские качества, добавляя к ним новые (или, наоборот, убирая лишние качества).
Замечание: при использовании технологии ООП центр тяжести при программировании перемещается от написания оптимального кода к оптимальному разбиению на классы!
12.1. Объявление производного класса:
class имя_производного_класса : спецификатор_доступа имя_базового_класса {список_членов_класса};
Например:
class First{список членов класса First}; //базовый класс
class Second : public First{ список членов класса Second};
12.2. Спецификаторы доступа базового класса при объявлении производного класса:
Изменение вида доступа к элементу базового класса из производного класса в зависимости от спецификатора доступа, указываемого при объявлении производного класса
Спецификатор доступа, указываемый при объявлении производного класса | Изменение вида доступа | ||
в базовом public | в базовом protected | в базовом private | |
class Second : public First | в производном | в производном protected | в производном недоступен |
class Second : protected First | в производном protected | в производном protected | в производном недоступен |
class Second : private First | в производном private | в производном private | в производном недоступен |
12.3. Порядок вызовов конструкторов при создании экземпляра производного класса.
При создании экземпляра класса вызывается его конструктор (смотри пункт Конструкторы ). Если класс является производным, кроме конструктора данного класса должен быть также вызван конструктор базового класса. Порядок вызовов конструкторов в С++ фиксирован: прежде всего вызывается конструктор базового класса, затем (если существуют) вызываются конструкторы всех промежуточных классов согласно иерархии наследования, и наконец, вызывается конструктор данного класса.
Приведенный порядок имеет смысл, поскольку производные классы имеют более специализированный характер, чем базовый => специализированная часть “накладывается” поверх общей.
Пример:
class First{...};
class Second : public First{...};
class Third : public Second{...};
При создании экземпляра класса Third конструкторы вызываются в следующем порядке: First::First()-> Second::Second()->Third::Third()
12.4. Порядок вызовов деструкторов при разрушении экземпляра производного класса.
Деструкторы при разрушении объекта производного класса вызываются в порядке, обратном вызову конструкторов. Причина та же: сначала разрушаются специализированные части, затем общие. Например, при разрушении экземпляра класса Third порядок вызовов деструкторов будет следующим: Third::~Third()->Second::~Second()->First::~First().
12.5. Аргументы конструктора, передаваемые в базовый класс.
При создании экземпляра производного класса обычно возникает необходимость передать какие-то параметры конструктору базового класса. Так как конструктор базового класса выполняется раньше, чем конструктор производного, то любые аргументы, передаваемые в базовый класс, следует определить до того, как выполнится тело конструктора, поэтому для передачи параметров конструктору базового класса используется специальная запись:
Например:
class A //базовый класс
{
public:
A(int x, int y){m_nX = x; m_nY = y;}
protected: //спецификатор доступа делает следующие данные защищенными, то есть «видимыми» только внутри данного класса и внутри классов, производных от данного
int m_nX, m_nY;
};
class B : public A
{
public:
B(int x, int y, int dx, int dy):A(x, y){m_nDx = dx; m_nDy = dy;}
protected:
int m_nDx, m_nDy;
};
13. Полиморфизм. Виртуальные функции.
Полиморфизм – свойство одного и того же кода С++ вести себя по-разному в зависимости от текущих условий выполнения программы (не путайте с перегрузкой функций – какая функция будет вызвана, решает компилятор!). Свойством полиморфизма могут обладать только методы класса (а обычные функции – не могут!).
13.1. Раннее и позднее связывание.
В случае полиморфной функции одному имени функции соответствует несколько вариантов кода, а то, какой именно вариант будет вызван, определяется лишь на этапе исполнения программы (а не на этапе компиляции) Такой процесс известен как позднее связывание.
При вызове неполиморфных функций (в том числе перегруженных) идентификаторы функций ассоциируются с конкретным исполняемым кодом до выполнения – на стадии компиляции и компоновки. Такой процесс называется ранним связыванием. При раннем связывании программист сам определяет: какая функция (какого класса) будет вызвана.
13.2. Виртуальные функции.
В С++ позднее связывание для функции определяется при ее объявлении с помощью ключевого слова virtual. Позднее связывание имеет смысл только для классов, связанных наследованием:
class A{
...
public:
A(){} //конструктор класса A
virtual void DoSomething() {printf(“Это виртуальная функция класса A”);}
}
class B : public A{
...
public:
B(){} //конструктор класса B
virtual void DoSomething(){printf(“Это виртуальная функция класса B”);}
}
class C : public B{
...
public:
C(){} //конструктор класса C
virtual void DoSomething(){printf(“Это виртуальная функция класса C”);}
}
void main()
{
A* ar[3] = { new A() , new B(), new C()}; //массив из трех объектов классов A, B и С (важно! – имеют один и тот же базовый класс)
for (int i =0; i<3; i++) ar[i]->DoSomething(); //для каждого объекта вызывается функция с одним и тем же именем, но какой именно код вызовется, определяется во время выполнения
}
13.3. Как происходит вызов виртуальной функции.
Рассмотрим, как реализован механизм «позднего связывания» в случае простого (немножественного) наследования:
class A{
public:
virtual void VFunc1() {...};
virtual void VFunc2() {...};
int a;
};
class B : public A{
public:
virtual void VFunc1() {...};
virtual void VFunc2() {...};
int b;
};
class C : public B{
public:
virtual void VFunc1() {...};
virtual void VFunc2() {...};
int c;
};
Как только в объявлении класса появляется виртуальная функция, так сразу же для создания каждого экземпляра такого класса компилятор расплачивается выделением дополнительной памяти:



Обычно компиляторы реализуют «виртуальность» следующим образом:
1) для каждого класса (содержащего виртуальные функции):создается своя таблица vtab, содержащая указатели на виртуальные функции
2) каждой виртуальной функции соответствует свой индекс в таблице vtab
3) в каждом экземпляре класса в фиксированном месте отводится место для указателя vptr на свою таблицу vtab
4) вызов виртуальной функции осуществляется поэтапно: по указателю vptr находится требуемая таблица vtab, имя функции преобразуется в индекс относительно начала vtab, по адресу (vtab + индекс) вызывается функция.
13.4. Виртуальные деструкторы.
Кроме конструктора, все методы класса могут быть виртуальными, в том числе и деструктор.
Например:
class A{
...
virtual ~A();
};
class B : public At{
virtual ~B(); //объявление деструктора виртуальным в производном классе необязательно. Это имеет значение в том случае, если Вы собираетесь «порождать» классы от данного
};
Point* pAr[2] = {new A(0,0), new B(0,0,5,5)};
for(int i=0; i<2; i++)
{
delete pAr[i]; //при i=0 вызовется деструктор Point, при i=1 вызовется сначала деструктор Line, а потом деструктор базового класса – деструктор Point
}
13.5. Разрешение области видимости отменяет полиморфизм
Если в базовом и производном от него классах объявлены переменные или методы с одинаковыми именами, то «увидеть» соответствующие элементы базового класса «из производного» можно с помощью оператора разрешения области видимости «имя_класса ::». Например:
class A{
...
public:
int m_iN;
};
class B : public A{
...
public:
int m_iN;
};
void main()
{
A a;
B b;
int iTmp = a. m_iN; //получили значение переменной класса А
iTmp = b. m_iN; // получили значение переменной класса В
iTmp = b.A::m_iN; // получили значение переменной класса А
}
Замечание: в случае виртуальных функций явное указание области видимости предписывает компилятору на этапе компиляции сгенерировать вызов метода указанного класса, тем самым отменяя в данном конкретном случае позднее связывание.
14. Множественное наследование.
В С++ производный класс вовсе не обречен иметь только одного непосредственного «родителя». Теоретически класс может иметь неограниченное число родителей (базовых классов), наследуя свойства каждого из них, однако и сложность такой системы при этом катастрофически нарастает, что (на мой взгляд) очень быстро сводит на нет преимущества множественного наследования.
Замечание: тему множественного наследования мы рассмотрим в данном пункте кратко, так как в MFC множественное наследование не принято из-за возникающих при этом проблем, но все же иметь представление об этом следует, так как множественное наследование – это тот фундамент, на котором стоит COM-технология.
Например:
class Circle{
double m_dRadius;
public:
Circle(double r){ m_dRadius = r;}
double Area (){return 3.14* m_dRadius * m_dRadius;}
};
class Table{
double m_dHeight;
public:
Table(double h){ m_dHeight = h;}
double GetHeight(){return m_dHeight; }
};
class RoundTable : public Table, public Circle{
int m_nColor;
public:
RoundTable(double h, double r, int c) : Table(h), Circle(r) {m_nColor = c;}
int GetColor(){return m_nColor ;}
}
void main()
{
RoundTable rt (1.5, 1.0, 2);
double height = rt. GetHeight();
double area = rt. Area();
int color = rt. GetColor();
}


