Использование спецификатора protected для наследования базового класса
Спецификатор protected можно использовать не только для придания членам класса статуса "защищенности", но и для наследования базового класса. Если базовый класс наследуется как защищенный, все его открытые и закрытые члены становятся защищенными членами производного класса. Рассмотрим пример.
// Демонстрация наследования защищенного базового класса.
#include <iostream>
using namespace std;
class base {
int i;
protected:
int j;
public:
int k;
void seti(int a) { i = a; }
int geti() { return i; }
};
// Наследуем класс base как protected-класс.
class derived : protected base {
public:
void setj(int a) { j = a; } // j — здесь protected-член
void setk(int a) { k = a; } // k — здесь protected-член
int getj() { return j; }
int getk() { return k; }
};
Int main()
{
derived ob;
/* Следующая строка неправомочна, поскольку функция seti() является protected-членом класса derived, что делает ее недоступной за его пределами. */
// ob.seti (10);
// cout << ob.geti(); // Неверно, поскольку функция geti() — protected-член.
//ob.k=10; // Неверно, поскольку переменная k — protected-член.
// Следующие инструкции правомочны.
ob.setk(10);
cout << ob.getk() << ' ';
ob.setj(12);
cout << ob.getj() << ' ';
return 0;
}
Как отмечено в комментариях к этой программе, члены (класса base) k, j, seti() и geti() становятся protected-членами класса derived. Это означает, что к ним нельзя получить доступ из кода, "прописанного" вне класса derived. Поэтому ссылки на эти члены в функции main() (через объект ob) неправомочны.
Об использовании спецификаторов public, protected и private
Поскольку права доступа, определяемые спецификаторами public, protected и private, принципиальны для программирования на C++, имеет смысл обобщить все, что мы уже знаем об этих ключевых словах.
При объявлении члена класса открытым (с использованием ключевого слова public) к нему можно получить доступ из любой другой части программы. Если член класса объявляется закрытым (с помощью спецификатора private), к нему могут получать доступ только члены того же класса. Более того, к закрытым членам базового класса не имеют доступа даже производные классы. Если же член класса объявляется защищенным (protected-членом), к нему могут получать доступ только члены того же или производных классов. Таким образом, спецификатор protected позволяет наследовать члены, но оставляет их закрытыми в рамках иерархии классов.
Если базовый класс наследуется с использованием ключевого слова public, его public-члены становятся public-членами производного класса, а его protected-члены — protected-членами производного класса.
Если базовый класс наследуется с использованием спецификатора protected, его public- и protected-члены становятся protected-членами производного класса.
Если базовый класс наследуется с использованием ключевого слова private, его public- и protected-члены становятся private-членами производного класса.
Во всех случаях private-члены базового класса остаются закрытыми в рамках этого класса и не наследуются.
По мере увеличения вашего опыта в программировании на C++ применение спецификаторов public, protected и private не будет доставлять вам хлопот. А пока, если вы еще не уверены в правильности использования того или иного спецификатора доступа, напишите простую экспериментальную программу и проанализируйте полученные результаты.
Наследование нескольких базовых классов
Производный класс может наследовать два или больше базовых классов. Например, в этой короткой программе класс derived наследует оба класса base1 и base2.
// Пример использования нескольких базовых классов.
#include <iostream>
using namespace std;
class base1 {
protected:
int x;
public:
void showx() { cout << x << ""; }
};
class base2 {
protected:
int y;
public:
void showy() { cout << у << ""; }
};
// Наследование двух базовых классов.
class derived: public base1, public base2 {
public:
void set(int i, int j) { x = i; у = j; }
};
Int main()
{
derived ob;
ob.set (10, 20); // член класса derived
ob.showx(); // функция из класса base1
ob.showy(); // функция из класса base2
return 0;
}
Как видно из этого примера, чтобы обеспечить наследование нескольких базовых классов, необходимо через запятую перечислить их имена в виде списка. При этом нужно указать спецификатор доступа для каждого наследуемого базового класса.
Конструкторы, деструкторы и наследование
При использовании механизма наследования обычно возникает два важных вопроса, связанных с конструкторами и деструкторами. Первый: когда вызываются конструкторы и деструкторы базового и производного классов? Второй: как можно передать параметры конструктору базового класса? Ответы на эти вопросы изложены в следующем разделе.
Когда выполняются конструкторы и деструкторы
Базовый и/или производный класс может содержать конструктор и/или деструктор. Важно понимать порядок, в котором выполняются эти функции при создании объекта производного класса и его (объекта) разрушении.
Рассмотрим короткую программу.
#include <iostream>
using namespace std;
class base {
public:
base() { cout <<"Создание basе-объекта."; }
~base() { cout <<"Разрушение bаsе-объекта."; }
};
class derived: public base {
public:
derived() { cout <<"Создание derived-объекта."; }
~derived() { cout <<"Разрушение derived-объекта."; }
};
Int main()
{
derived ob;
// Никаких действий, кроме создания и разрушения объекта ob.
return 0;
}
Как отмечено в комментариях для функции main(), эта программа лишь создает и тут же разрушает объект ob, который имеет тип derived. При выполнении программа отображает такие результаты.
Создание base-объекта.
Создание derived-объекта.
Разрушение derived-объекта.
Разрушение base-объекта.
Судя по результатам, сначала выполняется конструктор класса base, а за ним — конструктор класса derived. Затем (по причине немедленного разрушения объекта ob в этой программе) вызывается деструктор класса derived, а за ним — деструктор класса base.
Конструкторы вызываются в порядке происхождения классов, а деструкторы — в обратном порядке.
Результаты вышеописанного эксперимента можно обобщить следующим образом. При создании объекта производного класса сначала вызывается конструктор базового класса, а за ним — конструктор производного класса. При разрушении объекта производного класса сначала вызывается его "родной" конструктор, а за ним — конструктор базового класса. Другими словами, конструкторы вызываются в порядке происхождения классов, а деструкторы — в обратном порядке.
Вполне логично, что функции конструкторов выполняются в порядке происхождения их классов. Поскольку базовый класс "ничего не знает" ни о каком производном классе, операции по инициализации, которые ему нужно выполнить, не зависят от операций инициализации, выполняемых производным классом, но, возможно, создают предварительные условия для последующей работы. Поэтому конструктор базового класса должен выполняться первым.
Аналогичная логика присутствует и в том, что деструкторы выполняются в порядке, обратном порядку происхождения классов. Поскольку базовый класс лежит в основе производного класса, разрушение базового класса подразумевает разрушение производного. Следовательно, деструктор производного класса имеет смысл вызвать до того, как объект будет полностью разрушен.
При расширенной иерархии классов (т.е. в ситуации, когда производный класс становится базовым классом для еще одного производного) применяется следующее общее правило: конструкторы вызываются в порядке происхождения классов, а деструкторы — в обратном порядке. Например, при выполнении этой программы
#include <iostream>
using namespace std;
class base {
public:
base() { cout <<"Создание base-объекта."; }
~base(){ cout <<"Разрушение base-объекта."; }
};
class derived1 : public base {
public:
derived1() { cout <<"Создание derived1-объекта."; }
~derived1(){ cout <<"Разрушение derived1-объекта."; }
};
class derived2: public derived1 {
public:
derived2() { cout <<"Создание derived2-oбъeктa."; }
~derived2(){ cout <<"Разрушение derived2-oбъeктa."; }
};
Int main()
{
derived2 ob;
// Создание и разрушение объекта ob.
return 0;
}
отображаются такие результаты:
Создание base-объекта.
Создание derived1-объекта.
Создание derived2-oбъeктa.