Шилдт c++_базовый_курс издание 3. Герберт Шилдт С базовый курс
Скачать 9.37 Mb.
|
Наследование — один из трех фундаментальных принципов объектно-ориентированного программирования, поскольку именно благодаря ему возможно создание иерархических классификаций. Используя наследование, можно создать общий класс, который определяет характеристики, присущие множеству связанных элементов. Этот класс затем может быть унаследован другими, узкоспециализированными классами с добавлением в каждый из них своих, уникальных особенностей. В стандартной терминологии языка C++ класс, который наследуется, называется базовым. Класс, который наследует базовый класс, называется производным. Производный класс можно использовать в качестве базового для другого производного класса. Таким путем и строится многоуровневая иерархия классов. Понятие о наследовании Базовый класс наследуется производным классом. Язык C++ поддерживает механизм наследования, позволяя в объявление класса встраивать другой класс. Для этого базовый класс задается при объявлении производного. Лучше всего начать с примера. Рассмотрим класс road_vehicle, который в самых общих чертах определяет дорожное транспортное средство. Его члены данных позволяют хранить количество колес и число пассажиров, которое может перевозить транспортное средство. class road_vehicle { int wheels; int passengers; public: void set_wheels(int num) { wheels = num; } int get_wheels() { return wheels; } void set_pass(int num) { passengers = num; } int get_pass() { return passengers; } }; Это общее определение дорожного транспортного средства можно использовать для определения конкретных типов транспортных средств. Например, в следующем фрагменте путем наследования класса road_vehicle создается класс truck (грузовых автомобилей). class truck : public road_vehicle { int cargo; public: void set_cargo(int size) { cargo = size; } int get_cargo() { return cargo; } void show(); }; Тот факт, что класс truck наследует класс road_vehicle, означает, что класс truck наследует все содержимое класса road_vehicle. К содержимому класса road_vehicle класс truck добавляет член данных cargo, а также функции-члены, необходимые для поддержки члена cargo. Обратите внимание на то, как наследуется класс road_vehicle. Общий формат для обеспечения наследования имеет следующий вид. class имя_производного_класса : доступ имя_базового_класса { тело нового класса } Здесь элемент доступ необязателен. При необходимости он может быть выражен одним из спецификаторов доступа: public, private или protected. Подробнее об этих спецификаторах доступа вы узнаете ниже в этой главе. А пока в определениях всех наследуемых классов мы будем использовать спецификатор public. Это означает, что все public-члены базового класса также будут public-членами производного класса. Следовательно, в предыдущем примере члены класса truck имеют доступ к открытым функциям-членам класса road_vehicle, как будто они (эти функции) были объявлены в теле класса truck. Однако класс truck не имеет доступа к private-членам класса road_vehicle. Например, для класса truck закрыт доступ к члену данных wheels. Рассмотрим программу, которая использует механизм наследования для создания двух подклассов класса road_vehicle: truck и automobile. // Демонстрация наследования. #include using namespace std; // Определяем базовый класс транспортных средств. class road_vehicle { int wheels; int passengers; public: void set_wheels(int num) { wheels = num; } int get_wheels() { return wheels; } void set_pass(int num) { passengers = num; } int get_pass() { return passengers; } }; // Определяем класс грузовиков. class truck : public road_vehicle { int cargo; public: void set_cargo(int size) { cargo = size; } int get_cargo() { return cargo; } void show(); }; enum type {car, van, wagon}; // Определяем класс автомобилей. class automobile : public road_vehicle { enum type car_type; public: void set_type(type t) { car_type = t; } enum type get_type() { return car_type; } void show(); }; void truck::show() { cout << "колес: " << get_wheels() << "\n"; cout << "пассажиров: " << get_pass() << "\n"; cout << "грузовместимость в кубических футах: " << cargo << "\n"; } void automobile::show() { cout << "колес: " << get_wheels() << "\n"; cout << "пассажиров: " << get_pass() << "\n"; cout << "тип: "; switch(get_type()) { case van: cout << "автофургон\n"; break; case car: cout << "легковой\n"; break; case wagon: cout << "фура\n"; } } int main() { truck t1, t2; automobile c; t1.set_wheels(18); t1.set_pass(2); t1.set_cargo(3200); t2.set_wheels(6); t2.set_pass(3); t2.set_cargo(1200); t1.show(); cout << "\n"; t2.show(); cout << "\n"; с.set_wheels(4); с.set_pass(6); с.set_type(van); с.show(); return 0; } При выполнении эта программа генерирует такие результаты. колес: 18 пассажиров: 2 грузовместимость в кубических футах: 3200 колес: 6 пассажиров: 3 грузовместимость в кубических футах: 1200 колес: 4 пассажиров: 6 тип: автофургон Как видно по результатам выполнения этой программы, основное достоинство наследования состоит в том, что оно позволяет создать базовый класс, который затем можно включить в состав более специализированных классов. Таким образом, каждый производный класс может служить определенной цели и при этом оставаться частью общей классификации. И еще. Обратите внимание на то, что оба класса truck и automobile включают функцию- член show(), которая отображает информацию об объекте. Эта функция демонстрирует еще один аспект объектно-ориентированного программирования — полиморфизм. Поскольку каждая функция show() связана с собственным классом, компилятор может легко "понять", какую именно функцию нужно вызвать для данного объекта. После ознакомления с общей процедурой наследования одним классом другого можно перейти и к деталям этой темы. Управление доступом к членам базового класса Если один класс наследует другой, члены базового класса становятся членами производного. Статус доступа членов базового класса в производном классе определяется спецификатором доступа, используемым для наследования базового класса. Спецификатор доступа базового класса выражается одним из ключевых слов: public, private или protected. Если спецификатор доступа не указан, то по умолчанию используется спецификатор private, если речь идет о наследовании типа class. Если же наследуется тип struct, то при отсутствии явно заданного спецификатора доступа по умолчанию используется спецификатор public. Рассмотрим рамификацию (разветвление) использования спецификаторов public или private. (Спецификатор protected описан в следующем разделе.) Если базовый класс наследуется как public-класс, его public-члены становятся public- членами производного класса. Если базовый класс наследуется как publie-класс, все его public-члены становятся public- членами производного класса. Во всех случаях private-члены базового класса остаются закрытыми в рамках этого класса и не доступны для членов производного. Например, в следующей программе public-члены класса base становятся public-членами класса derived. Следовательно, они будут доступны и для других частей программы. #include using namespace std; class base { int i, j; public: void set (int a, int b) { i = a; j = b; } void show() { cout << i << " " << j << "\n"; } }; class derived : public base { int k; public: derived(int x) { k = x; } void showk() { cout << k << "\n"; } }; int main() { derived ob(3); ob.set(1, 2); // доступ к членам класса base ob.show(); // доступ к членам класса base ob.showk(); // доступ к члену класса derived return 0; } Поскольку функции set() и show() (члены класса base) унаследованы классом derived как public-члены, их можно вызывать для объекта типа derived в функции main(). Поскольку члены данных i и j определены как private-члены, они остаются закрытыми в рамках своего класса base. Если базовый класс наследуется как private-класс, его public-члены становятся private- членами производного класса. Противоположностью открытому (public) наследованию является закрытое (private). Если базовый класс наследуется как private-класс, все его public-члены становятся private- членами производного класса. Например, следующая программа не скомпилируется, поскольку обе функции set() и show() теперь стали private-членами класса derived, и поэтому их нельзя вызывать из функции main(). // Эта программа не скомпилируется. #include using namespace std; class base { int i, j; public: void set (int a, int b) { i = a; j = b; } void show() { cout << i << " " << j << "\n"; } }; // Открытые члены класса base теперь становятся // закрытыми членами класса derived. class derived : private base { int k; public: derived(int x) { k = x; } void showk() { cout << k << "\n"; } }; int main() { derived ob (3); ob.set(1, 2); // Ошибка, доступа к функции set() нет. ob.show(); // Ошибка, доступа к функции show() нет. return 0; } Важно запомнить, что в случае, если базовый класс наследуется как private-класс, его открытые члены становятся закрытыми (private) членами производного класса. Это означает, что они доступны для членов производного класса, но не доступны для других частей программы. Использование защищенных членов Член класса может быть объявлен не только открытым (public) или закрытым (private), но и защищенным (protected). Кроме того, базовый класс в целом может быть унаследован с использованием спецификатора protected. Ключевое слово protected добавлено в C++ для предоставления механизму наследования большей гибкости. Если член класса объявлен с использованием спецификатора protected, он не будет доступен для других элементов программы, которые не являются членами данного класса. С одним важным исключением доступ к защищенному члену идентичен доступу к закрытому члену, т.е. к нему могут обращаться только другие члены того же класса. Единственное исключение из этого правила проявляется при наследовании защищенного члена. В этом случае защищенный член существенно отличается от закрытого. Спецификатор доступа protected объявляет защищенные члены или обеспечивает наследование защищенного класса. Как вы знаете, закрытый член базового класса не доступен никаким другим частям программы, включая и производные классы. Однако с защищенными членами все обстоит иначе. Если базовый класс наследуется как public-класс, защищенные члены базового класса становятся защищенными членами производного класса, т.е. доступными для производного класса. Следовательно, используя спецификатор protected, можно создать члены класса, которые закрыты в рамках своего класса, но которые может унаследовать производный класс, причем с получением доступа к ним. Рассмотрим следующий пример программы. #include using namespace std; class base { protected: int i, j; // Эти члены закрыты в классе base, но доступны для класса derived. public: void set(int a, int b) { i = a; j = b; } void show() { cout << i << " " << j << "\n"; } }; class derived : public base { int k; public: // Класс derived имеет доступ к членам класса base i и j. void setk() { k = i*j; } void showk() { cout << k << "\n"; } }; int main() { derived ob; ob.set(2, 3); // OK, классу derived это позволено. ob.show(); // OK, классу derived это позволено. ob.setk(); ob.showk(); return 0; } Поскольку класс base унаследован классом derived открытым способом (т.е. как public- класс), и поскольку члены i и j объявлены защищенными в классе base, функция setk() (член класса derived) может получать к ним доступ. Если бы члены i и j были объявлены в классе base закрытыми, то класс derived не мог бы обращаться к ним, и эта программа не скомпилировалась бы. Узелок на память. Спецификатор protected позволяет создать член класса, который будет доступен в рамках данной иерархии классов, но закрыт для остальных элементов программы. Если некоторый производный класс используется в качестве базового для другого производного класса, то любой защищенный член исходного базового класса, который наследуется (открытым способом) первым производным классом, может быть унаследован еще раз (в качестве защищенного члена) вторым производным классом. Например, в следующей (вполне корректной) программе класс derived2 имеет законный доступ к членам i и j. #include using namespace std; class base { protected: int i, j; public: void set(int a, int b) { i = a; j = b; } void show() { cout << i << " " << j << "\n"; } }; // Члены i и j наследуются как protected-члены. class derived1: public base { int k; public: void setk() { к = i*j; } // правомерный доступ void showk() { cout << к << "\n"; } }; // Члены i и j наследуются косвенно через класс derived1. class derived2 : public derived1 { int m; public : void setm() { m = i-j; } // правомерный доступ void showm() { cout << m << "\n"; } }; int main() { derived1 ob1; derived2 ob2; ob1.set (2, 3); ob1.show(); ob1.setk(); ob1.showk(); ob2.set (3, 4); ob2.show(); ob2.setk(); ob2.setm(); ob2.showk(); ob2.showm(); return 0; } Если базовый класс наследуется закрытым способом (т.е. с использованием спецификатора private), защищенные (derived) члены этого базового класса становятся закрытыми (private) членами производного класса. Следовательно, если бы в предыдущем примере класс base наследовался закрытым способом, то все его члены стали бы private- членами класса derived1, и в этом случае они не были бы доступны для класса derived2. (Однако члены i и j по-прежнему остаются доступными для класса derived1.) Эта ситуация иллюстрируется в следующей программе, которая поэтому некорректна (и не скомпилируется). Все ошибки отмечены в комментариях. // Эта программа не скомпилируется. #include using namespace std; class base { protected: int i, j; public: void set (int a, int b) { i = a; j = b; } void show() { cout << i << " " << j << "\n"; } }; // Теперь все элементы класса base будут закрыты // в рамках класса derived1. class derived1 : private base { int k; public: // Вызовы этих функций вполне законны, поскольку // переменные i и j являются одновременно // private-членами класса derived1. void setk() { k = i*j; } // OK void showk() { cout << k << "\n"; } }; // Доступ к членам i, j, set() и show() не наследуется. class derived2 : public derived1 { int m; public : // Неверно, поскольку члены i и j закрыты в рамках // класса derived1. void setm() { m = i-j; } // ошибка void showm() { cout << m << "\n"; } }; int main() { derived1 ob1; derived2 ob2; ob1.set(1, 2); // Ошибка: нельзя вызывать функцию set(). ob1.show(); // Ошибка: нельзя вызывать функцию show(). ob2.set(3, 4); // Ошибка: нельзя вызывать функцию set(). ob2.show(); // Ошибка: нельзя вызывать функцию show(). return 0; } Несмотря на то что класс base наследуется классом derived1 закрытым способом, класс derived1, тем не менее, имеет доступ к public- и protected-членам класса base. Однако он не может эту привилегию передать дальше, т.е. вниз по иерархии классов. Ключевое слово protected— это часть языка C++. Оно обеспечивает механизм защиты определенных элементов класса от модификации функциями, которые не являются членами этого класса, но позволяет передавать их "по наследству". Спецификатор protected можно также использовать в отношении структур. Но его нельзя применять к объединениям, поскольку объединение не наследуется другим классом. (Некоторые компиляторы допускают использование спецификатора protected в объявлении объединения, но, поскольку объединения не могут участвовать в наследовании, в этом контексте ключевое слово protected будет равносильным ключевому слову private.) Спецификатор защищенного доступа может стоять в любом месте объявления класса, но, как правило, protected-члены объявляются после (объявляемых по умолчанию) private- членов и перед public-членами. Таким образом, самый общий формат объявления класса обычно выглядит так. class имя_класса { private-члены protected: protected-члены public: public-члены }; Напомню, что раздел защищенных членов необязателен. Использование спецификатора protected для наследования базового класса Спецификатор protected можно использовать не только для придания членам класса статуса "защищенности", но и для наследования базового класса. Если базовый класс наследуется как защищенный, все его открытые и закрытые члены становятся защищенными членами производного класса. Рассмотрим пример. // Демонстрация наследования защищенного базового класса. #include 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 using namespace std; class base1 { protected: int x; public: void showx() { cout << x << "\n"; } }; class base2 { protected: int y; public: void showy() { cout << у << "\n"; } }; // Наследование двух базовых классов. 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 using namespace std; class base { public: base() { cout <<"Создание basе-объекта.\n"; } base() { cout <<"Разрушение bаsе-объекта.\n"; } }; class derived: public base { public: derived() { cout <<"Создание derived-объекта.\n"; } derived() { cout <<"Разрушение derived-объекта.\n"; } }; int main() { derived ob; // Никаких действий, кроме создания и разрушения объекта ob. return 0; } Как отмечено в комментариях для функции main(), эта программа лишь создает и тут же разрушает объект ob, который имеет тип derived. При выполнении программа отображает такие результаты. Создание base-объекта. Создание derived-объекта. Разрушение derived-объекта. Разрушение base-объекта. Судя по результатам, сначала выполняется конструктор класса base, а за ним — конструктор класса derived. Затем (по причине немедленного разрушения объекта ob в этой программе) вызывается деструктор класса derived, а за ним — деструктор класса base. Конструкторы вызываются в порядке происхождения классов, а деструкторы — в обратном порядке. Результаты вышеописанного эксперимента можно обобщить следующим образом. При создании объекта производного класса сначала вызывается конструктор базового класса, а за ним — конструктор производного класса. При разрушении объекта производного класса сначала вызывается его "родной" конструктор, а за ним — конструктор базового класса. Другими словами, конструкторы вызываются в порядке происхождения классов, а деструкторы — в обратном порядке. Вполне логично, что функции конструкторов выполняются в порядке происхождения их классов. Поскольку базовый класс "ничего не знает" ни о каком производном классе, операции по инициализации, которые ему нужно выполнить, не зависят от операций инициализации, выполняемых производным классом, но, возможно, создают предварительные условия для последующей работы. Поэтому конструктор базового класса должен выполняться первым. Аналогичная логика присутствует и в том, что деструкторы выполняются в порядке, обратном порядку происхождения классов. Поскольку базовый класс лежит в основе производного класса, разрушение базового класса подразумевает разрушение производного. Следовательно, деструктор производного класса имеет смысл вызвать до того, как объект будет полностью разрушен. При расширенной иерархии классов (т.е. в ситуации, когда производный класс становится базовым классом для еще одного производного) применяется следующее общее правило: конструкторы вызываются в порядке происхождения классов, а деструкторы — в обратном порядке. Например, при выполнении этой программы #include using namespace std; class base { public: base() { cout <<"Создание base-объекта.\n"; } base(){ cout <<"Разрушение base-объекта.\n"; } }; class derived1 : public base { public: derived1() { cout <<"Создание derived1-объекта.\n"; } derived1(){ cout <<"Разрушение derived1-объекта.\n"; } }; class derived2: public derived1 { public: derived2() { cout <<"Создание derived2-oбъeктa.\n"; } derived2(){ cout <<"Разрушение derived2-oбъeктa.\n"; } }; int main() { derived2 ob; // Создание и разрушение объекта ob. return 0; } отображаются такие результаты: Создание base-объекта. Создание derived1-объекта. Создание derived2-oбъeктa. Разрушение derived2-oбъeктa. Разрушение derived1-объекта. Разрушение base-объекта. То же общее правило применяется и в ситуациях, когда производный класс наследует несколько базовых классов. Например, при выполнении этой программы #include using namespace std; class base1 { public: base1() { cout <<"Создание base1-объекта.\n"; } base1(){ cout <<"Разрушение base1-объекта.\n"; } }; class base2 { public: base2() { cout <<"Создание bаsе2-объекта.\n"; } base2(){ cout <<"Разрушение basе2-объекта.\n"; } }; class derived: public base1, public base2 { public: derived() { cout <<"Создание derived-объекта.\n"; } derived(){ cout <<"Разрушение derived-объекта.\n"; } }; int main() { derived ob; // Создание и разрушение объекта ob. return 0; } генерируются такие результаты: Создание base1-объекта. Создание basе2-объекта. Создание derived-объекта. Разрушение derived-объекта. Разрушение basе2-объекта. Разрушение base1-объекта. Как видите, конструкторы вызываются в порядке происхождения их классов, слева направо, в порядке их задания в списке наследования для класса derived. Деструкторы вызываются в обратном порядке, справа налево. Это означает, что если бы класс base2 стоял перед классом base1 в списке класса derived, т.е. в соответствии со следующей инструкцией: class derived: public base2, public base1 {}; то результаты выполнения предыдущей программы были бы такими: Создание basе2-объекта. Создание base1-объекта. Создание derived-объекта. Разрушение derived-объекта. Разрушение base1-объекта. Разрушение base2-объекта. Передача параметров конструкторам базового класса До сих пор ни один из предыдущих примеров не включал конструкторы, для которых требовалось бы передавать аргументы. В случаях, когда конструктор лишь производного класса требует передачи одного или нескольких аргументов, достаточно использовать стандартный синтаксис параметризованного конструктора. Но как передать аргументы конструктору базового класса? В этом случае необходимо использовать расширенную форму объявления конструктора производного класса, в которой предусмотрена возможность передачи аргументов одному или нескольким конструкторам базового класса. Вот как выглядит общий формат такого расширенного объявления. конструктор_производного_класса (список_аргументов) : base1 (список_аргументов), base2 (список_аргументов), baseN {список_аргументов); { тело конструктора производного класса } Здесь элементы base1-baseN означают имена базовых классов, наследуемых производным классом. Обратите внимание на то, что объявление конструктора производного класса отделяется от списка базовых классов двоеточием, а имена базовых классов разделяются запятыми (в случае наследования нескольких базовых классов). Рассмотрим следующую простую программу. #include using namespace std; class base { protected: int i; public: base (int x) { i = x; cout << "Создание bаsе-объекта.\n"; } base() {cout << "Разрушение base1-объекта.\n";} }; class derived: public base { int j; public: // Класс derived использует параметр x, а параметр у передается конструктору класса base. derived(int x, int y): base(y){ j = x; cout << "Создание derived-объекта.\n"; } derived() { cout << "Разрушение derived-объекта.\n"; } void show() { cout << i << " " << j << "\n"; } }; int main() { derived ob(3, 4); ob.show(); // отображает числа 4 3 return 0; } Здесь конструктор класса derived объявляется с двумя параметрами, х и у. Однако конструктор derived() использует только параметр х, а параметр у передается конструктору base(). В общем случае конструктор производного класса должен объявлять параметры, которые принимает его класс, а также те, которые требуются базовому классу. Как показано в предыдущем примере, любые параметры, требуемые базовым классом, передаются ему в списке аргументов базового класса, указываемого после двоеточия. Рассмотрим пример программы, в которой демонстрируется наследование нескольких базовых классов. #include using namespace std; class base1 { protected: int i; public: base1(int x) { i = x; cout << "Создание base1-объекта.\n"; } base1() { cout << "Разрушение base1-объекта.\n"; } }; class base2 { protected: int k; public: base2(int x) { k = x; cout << "Создание basе2-объекта.\n"; } base2() { cout << "Разрушение basе2-объекта.\n"; } }; class derived: public base1, public base2 { int j; public: derived(int x, int y, int z): base1(y), base2(z){ j = x; cout << "Создание derived-объекта.\n"; } derived() { cout << "Разрушение derived-объекта.\n"; } void show() { cout << i << " "<< j << " "<< k << " \ n"; } }; int main() { derived ob(3, 4, 5); ob.show(); // отображает числа 4 3 5 return 0; } Важно понимать, что аргументы для конструктора базового класса передаются через аргументы, принимаемые конструктором производного класса. Поэтому, даже если конструктор производного класса не использует никаких аргументов, он, тем не менее, должен объявить один или несколько аргументов, если базовый класс принимает один или несколько аргументов. В этой ситуации аргументы, передаваемые производному классу, "транзитом" передаются базовому. Например, в следующей программе конструкторы base1() и base2(), в отличие от конструктора класса derived, принимают аргументы. #include using namespace std; class base1 { protected: int i; public: base1(int x) { i=x; cout << "Создание base1-объекта.\n"; } base1() { cout << "Разрушение base1-объекта.\n"; } }; class base2 { protected: int k; public: base2(int x) { k = x; cout << "Создание basе2-объекта.\n"; } base2() { cout << "Разрушение basе2-объекта.\n"; } }; class derived: public base1, public base2 { public: /* Конструктор класса derived не использует параметров, но должен объявить их, чтобы передать конструкторам базовых классов. */ derived(int х, int у): base1(х), base2(у){ cout << "Создание derived-объекта.\n"; } derived() { cout << "Разрушение derived-объекта.\n"; } void show() { cout << i << " " << k << "\n"; } }; int main() { derived ob(3, 4); ob.show(); // отображает числа 3 4 return 0; } Конструктор производного класса может использовать любые (или все) параметры, которые им объявлены для приема, независимо от того, передаются ли они (один или несколько) базовому классу. Другими словами, тот факт, что некоторый аргумент передается базовому классу, не мешает его использованию и самим производным классом. Например, этот фрагмент кода совершенно допустим. class derived: public base { int j; public: // Класс derived использует оба параметра x и у а также передает их классу base. derived(int х, int у): base(х, у){ j = х*у; cout << "Создание derived-объекта.\n"; } // . . . }; При передаче аргументов конструкторам базового класса следует иметь в виду, что передаваемый аргумент может содержать любое (действительное на момент передачи) выражение, включающее вызовы функций и переменные. Это возможно благодаря тому, что C++ позволяет выполнять динамическую инициализацию данных. Предоставление доступа Когда базовый класс наследуется закрытым способом (как private-класс), все его члены (открытые, защищенные и закрытые) становятся private-членами производного класса. Но при определенных обстоятельствах один или несколько унаследованных членов необходимо вернуть к их исходной спецификации доступа. Например, несмотря на то, что базовый класс наследуется как private-класс, определенным его public-членам нужно предоставить public- статус в производном классе. Это можно сделать двумя способами. Во-первых, в производном классе можно использовать объявление using (этот способ рекомендован стандартом C++ для использования в новом коде). Но мы отложили рассмотрение директивы using до темы пространств имен. (Основное назначение директивы using— обеспечить поддержку пространств имен.) Во-вторых, можно настроить доступ к унаследованному члену с помощью объявлений доступа. Объявления доступа все еще поддерживаются стандартом C++, но в последнее время активизировались возражения против их применения, а это значит, что их не следует использовать в новом коде. Поскольку они все еще используются в С++-коде, мы уделим внимание этой теме. Объявление доступа имеет такой формат: имя_базового_класса::член; Объявление доступа восстанавливает уровень доступа унаследованного члена, в результате чего он получает тот уровень доступа, который был у него в базовом классе. Объявление доступа помещается в производном классе под соответствующим спецификатором доступа. Обратите внимание на то, что объявления типа в этом случае указывать не требуется. Чтобы понять, как работает объявление доступа, рассмотрим сначала этот короткий фрагмент кода. class base { public: int j; // public-доступ в классе base }; // Класс base наследуется как private-класс. class derived: private base { public: // Вот использование объявления доступа: base::j; // Теперь член j снова стал открытым. // . . . }; Поскольку класс base наследуется классом derived закрытым способом, его public- переменная j становится private-переменной класса derived. Однако включение этого объявления доступа base::j; в классе derived под спецификатором public восстанавливает public-статус члена j. Объявление доступа можно использовать для восстановления прав доступа public- и protected-членов. Однако для изменения (повышения или понижения) статуса доступа его использовать нельзя. Например, член, объявленный закрытым в базовом классе, нельзя сделать открытым в производном. (Разрешение подобных вещей разрушило бы инкапсуляцию!) Использование объявления доступа иллюстрируется в следующей программе. #include using namespace std; class base { int i; // private-член в классе base public: int j, k; void seti (int x) { i = x; } int geti() { return i; } }; // Класс base наследуется как private-класс. class derived: private base { public: /* Следующие три инструкции переопределяют private- наследование класса base и восстанавливают public-статус доступа для членов j, seti() и geti(). */ base::j; // Переменная j становится снова public-членом, а переменная k остается закрытым членом. base::seti; // Функция seti() становится public-членом. base::geti; // Функция geti() становится public-членом. // base::i; // Неверно: нельзя повышать уровень доступа. int а; // public-член }; int main() { derived ob; //ob.i = 10; // Неверно, поскольку член i закрыт в классе derived. ob.j = 20; // Допустимо, поскольку член j стал открытым в классе derived. //ob.k =30; // Неверно, поскольку член k закрыт в классе derived. ob.a = 40; // Допустимо, поскольку член а открыт в классе derived. ob.seti(10); cout << ob.geti() << " " << ob.j << " " << ob.a; return 0; } Обратите внимание на то, как в этой программе используются объявления доступа для восстановления статуса public у членов j, seti() и geti(). В комментариях отмечены и другие ограничения, связанные со статусом доступа. C++ обеспечивает возможность восстановления уровня доступа для унаследованных членов, чтобы программист мог успешно программировать такие специальные ситуации, когда большая часть наследуемого класса должна стать закрытой, а прежний public-или protected-статус нужно вернуть лишь нескольким членам. И все же к этому средству лучше прибегать только в крайних случаях. Чтение С++-графов наследования Иногда иерархии С++-классов изображаются графически, что облегчает их понимание. Но порой различные способы изображения графов наследования классов вводят новичков в заблуждение. Рассмотрим, например, ситуацию, в которой класс А наследуется классом В, который в свою очередь наследуется классом С. Используя стандартную С++-систему обозначений, эту ситуацию можно отобразить так: Как видите, стрелки на этом рисунке направлены вверх, а не вниз. Многие поначалу считают такое направление стрелок алогичным, но именно этот стиль принят большинством С++-программистов. Согласно стилевой графике C++ стрелка должна указывать на базовый класс. Следовательно, стрелка означает "выведен из", а не "порождает". Рассмотрим другой пример. Можете ли вы описать словами значение следующего изображения? Из этого графа следует, что класс Е выведен из обоих классов С и D. (Другими словами, класс Е имеет два базовых класса С и D.) При этом класс С выведен из класса А, а класс D — из класса В. Несмотря на то что направление стрелок может вас обескураживать на первых порах, все же лучше познакомиться с этим стилем графических обозначений, поскольку он широко используется в книгах, журналах и документации на компиляторы. Виртуальные базовые классы При наследовании нескольких базовых классов в С++-программу может быть внесен элемент неопределенности. Рассмотрим эту некорректную программу. /* Эта программа содержит ошибку и не скомпилируется. */ #include using namespace std; class base { public: int i; }; // Класс derived1 наследует класс base. class derived1 : public base { public: int j;}; // Класс derived2 наследует класс base. class derived2 : public base { public: int k;}; /* Класс derived3 наследует оба класса derived1 и derived2. Это означает, что в классе derived3 существует две копии класса base! */ class derived3 : public derived1, public derived2 { public: int sum; }; int main() { derived3 ob; ob.i = 10; // Это и есть неоднозначность: какой именно член i имеется в виду??? ob.j = 20; ob.k = 30; //И здесь тоже неоднозначность с членом i. ob.sum = ob.i + ob.j + ob.k; // И здесь тоже неоднозначность с членом i. cout << ob.i << " "; cout << ob. j << " " << ob.k << " "; cout << ob.sum; return 0; } Как отмечено в комментариях этой программы, оба класса derived1 и derived2 наследуют класс base. Но класс derived3 наследует как класс derived1, так и класс derived2. В результате в объекте типа derived3 присутствуют две копии класса base, поэтому, например, в таком выражении ob.i = 20; не ясно, на какую именно копию члена i здесь дана ссылка: на член, унаследованный от класса derived1 или от класса derived2? Поскольку в объекте ob присутствуют обе копии класса base, то в нем существуют и два члена ob.is! Потому-то эта инструкция и является наследственно неоднозначной (существенно неопределенной). Есть два способа исправить предыдущую программу. Первый состоит в применении оператора разрешения контекста (разрешения области видимости), с помощью которого можно "вручную" указать нужный член i. Например, следующая версия этой программы успешно скомпилируется и выполнится ожидаемым образом. /* Эта программа использует оператор разрешения контекста для выбора нужного члена i. */ #include using namespace std; class base { public: int i; }; // Класс derived1 наследует класс base. class derived1 : public base { public: int j;}; // Класс derived2 наследует класс base. class derived2 : public base { public: int k;}; /* Класс derived3 наследует оба класса derived1 и derived2. Это означает, что в классе derived3 существует две копии класса base! */ class derived3 : public derived1, public derived2 { public: int sum; }; int main() { derived3 ob; ob.derived1::i = 10; // Контекст разрешен, используется член i класса derived1. ob.j = 20; ob.k = 30; // Контекст разрешен и здесь. ob.sum = ob.derived1::i + ob.j + ob.k; // Неоднозначность ликвидирована и здесь. cout << ob.derived1::i << " "; cout << ob.j << " " << ob.k << " "; cout << ob.sum; return 0; } Виртуальное наследование базового класса гарантирует, что в любом производном классе будет присутствовать только одна его копия. Применение оператора "::" позволяет программе "ручным способом" выбрать версию класса base (унаследованную классом derived1). Но после такого решения возникает интересный вопрос: а что, если в действительности нужна только одна копия класса base? Можно ли каким-то образом предотвратить включение двух копий в класс derived3? Ответ, как, наверное, вы догадались, положителен. Это решение достигается с помощью виртуальных базовых классов. Если два (или больше) класса выведены из общего базового класса, мы можем предотвратить включение нескольких его копий в объекте, выведенном из этих классов, что реализуется путем объявления базового класса при его наследовании виртуальным. Для этого достаточно предварить имя наследуемого базового класса ключевым словом virtual. Для иллюстрации этого процесса приведем еще одну версию предыдущей программы. На этот раз класс derived3 содержит только одну копию класса base. // Эта программа использует виртуальные базовые классы. #include using namespace std; class base { public: int i; }; // Класс derived1 наследует класс base как виртуальный. class derived1 : virtual public base { public: int j;}; // Класс derived2 наследует класс base как виртуальный. class derived2 : virtual public base { public: int k;}; /* Класс derived3 наследует оба класса derived1 и derived2. На этот раз он содержит только одну копию класса base. */ class derived3 : public derived1, public derived2 { public: int sum; }; int main() { derived3 ob; ob.i = 10; // Теперь неоднозначности нет. ob.j = 20; ob.k = 30; // Теперь неоднозначности нет. ob.sum = ob.i + ob.j + ob.k; // Теперь неоднозначности, нет. cout << ob.i << " "; cout << ob.j << " " << ob.k << " "; cout << ob.sum; return 0; } Как видите, ключевое слово virtual предваряет остальную часть спецификации наследуемого класса. Теперь оба класса derived1 и derived2 наследуют класс base как виртуальный, и поэтому при любом множественном их наследовании в производный класс будет включена только одна его копия. Следовательно, в классе derived3 присутствует лишь одна копия класса base, а инструкция ob.i = 10 теперь совершенно допустима и не содержит никакой неоднозначности. И еще. Даже если оба класса derived1 и derived2 задают класс base как virtual-класс, он по-прежнему присутствует в объекте любого типа. Например, следующая последовательность инструкций вполне допустима. // Определяем класс типа derived1. derived1 myclass; myclass.i = 88; Разница между обычным базовым и виртуальным классами становится очевидной только тогда, когда этот базовый класс наследуется более одного раза. Если базовый класс объявляется виртуальным, то только один его экземпляр будет включен в объект наследующего класса. В противном случае в этом объекте будет присутствовать несколько его копий. |