straustrup2 (852740), страница 86
Текст из файла (страница 86)
Поэтому, несмотря на простотупреобразования обычного движущегося средства в аварийное, второе решение представляетсянепрактичным. Пожарные машины и машины скорой помощи – это движущиеся средства специальногоназначения со специально подготовленным персоналом, они действуют под управлением команддиспетчера, требующих специального оборудования для связи. Такое положение означает, чтопринадлежность к аварийным движущимся средствам - это базовое понятие, которое для улучшенияконтроля типов и применения различных программных средств должно быть прямо представлено впрограмме. Если бы мы моделировали ситуацию, в которой назначение движущихся средств не стольопределенно, скажем, ситуацию, в которой частный транспорт периодически используется для доставкиспециального персонала к месту происшествия, а связь обеспечивается с помощью портативныхприемников, тогда мог бы оказаться подходящим и другой способ моделирования системы.Для тех, кто считает пример моделирования движения транспорта экзотичным, имеет смысл сказать,что в процессе проектирования почти постоянно возникает подобный выбор между наследованием ипринадлежностью.
Аналогичный пример есть в $$12.2.5, где описывается свиток (scrollbar) прокручивание информации в окне.12.2.3 Зависимости в рамках иерархии классов.Естественно, производный класс зависит от своих базовых классов. Гораздо реже учитывают, чтообратное также может быть справедливо.Эту мысль можно выразить таким способом: "Сумасшествие наследуется, вы можете получить его отсвоих детей."Если класс содержит виртуальную функцию, производные классы могут по своему усмотрению решать,реализовывать ли часть операций этой функции каждый раз, когда она переопределяется впроизводном классе. Если член базового класса сам вызывает одну из виртуальных функцийпроизводного класса, тогда реализация базового класса зависит от реализаций его производныхклассов.
Точно так же, если класс использует защищенный член, его реализация будет зависеть отпроизводных классов. Рассмотрим определения:class B {//...protected:int a;public:virtual int f();int g() { int x = f(); return x-a; }};Каков результат работы g()? Ответ существенно зависит от определения f() в некотором производномклассе. Ниже приводится вариант, при котором g() будет возвращать 1:class D1 : public B {int f() { return a+1; }};а при нижеследующем определении g() напечатает "Hello, World" и вернет 0:class D1 : public {int f() { cout<<"Hello, World\n"; return a; }};Этот пример демонстрирует один из важнейших моментов, связанных с виртуальными функциями. Хотявы можете сказать, что это глупость, и программист никогда не напишет ничего подобного.
Дело здесь втом, что виртуальная функция является частью интерфейса с базовым классом, и что этот класс будет,по всей видимости, использоваться без информации о его производных классах. Следовательно, можнотак описать поведение объекта базового класса, чтобы в дальнейшем писать программы, ничего незная о его производных классах.318Бьерн Страуструп.Язык программирования С++Всякий класс, который переопределяет производную функцию, должен реализовать вариант этойфункции. Например, виртуальная функция rotate() из класса Shape вращает геометрическую фигуру, афункции rotate() для производных классов, таких, как Circle и Triangle, должны вращать объектысоответствующих типов, иначе будет нарушено основное положение о классе Shape.
Но о поведениикласса B или его производных классов D1 и D2 не сформулировано никаких положений, поэтомуприведенный пример и кажется неразумным. При построении класса главное внимание следуетуделять описанию ожидаемых действий виртуальных функций.Следует ли считать нормальной зависимость от неизвестных (возможно еще неопределенных)производных классов? Ответ, естественно, зависит от целей программиста.
Если цель состоит в том,чтобы изолировать класс от всяких внешних влияний и, тем самым, доказать, что он ведет себяопределенным образом, то лучше избегать виртуальных функций и защищенных членов. Если цельсостоит в том, чтобы разработать структуру, в которую последующие программисты (или вы сами черезнеделю) смогут встраивать свои программы, то именно виртуальные функции и предлагают элегантныйспособ решения, а защищенные члены могут быть полезны при его реализации.В качестве примера рассмотрим простой шаблон типа, определяющий буфер:template<class T>class buffer {// ...void put(T);T get();};Если реакция на переполнение и обращение к пустому буферу, "запаяна" в сам класс, его применениебудет ограничено.
Но если функции put() и get() обращаются к виртуальным функциям overflow() иunderflow() соответственно, то пользователь может, удовлетворяя своим нуждам, создать буфераразличных типов:template<class T>class buffer {//...virtual int overflow(T);virtual int underflow();void put(T);// вызвать overflow(T), когда буфер полонT get();// вызвать underflow(T), когда буфер пуст};template<class T>class circular_buffer : public buffer<T> {//...int overflow(T);// перейти на начало буфера, если он полонint underflow();};template<class T>class expanding_buffer : public buffer<T> {//...int overflow(T);// увеличить размер буфера, если он полонint underflow();};Этот метод использовался в библиотеках потокового ввода-вывода ($$10.5.3).12.2.4 Отношения принадлежностиЕсли используется отношение принадлежности, то существует два основных способа представленияобъекта класса X:[1]Описать член типа X.319Бьерн Страуструп.[2]Язык программирования С++Описать член типа X* или X&.Если значение указателя не будет меняться и вопросы эффективности не волнуют, эти способыэквивалентны:class X {//...public:X(int);//...};class C {X a;X* p;public:C(int i, int j) : a(i), p(new X(j)) { }~C() { delete p; }};В таких ситуациях предпочтительнее непосредственное членство объекта, как X::a в примере выше,потому что оно дает экономию времени, памяти и количества вводимых символов.
Обратитесь также к$$12.4 и $$13.9.Способ, использующий указатель, следует применять в тех случаях, когда приходится перестраиватьуказатель на "объект-элемент" в течении жизни "объекта-владельца". Например:class C2 {X* p;public:C(int i) : p(new X(i))~C() { delete p; }X* change(X* q){X* t = p;p = q;return t;}};{ }Член типа указатель может также использоваться, чтобы дать возможность передавать "объектэлемент" в качестве параметра:class C3 {X* p;public:C(X* q) : p(q) {// ...}}Разрешая объектам содержать указатели на другие объекты, мы создаем то, что обычно называется"иерархия объектов".
Это альтернативный и вспомогательный способ структурирования по отношению киерархии классов. Как было показано на примере аварийного движущегося средства в $$12.2.2, частоэто довольно тонкий вопрос проектирования: представлять ли свойство класса как еще один базовыйкласс или как член класса. Потребность в переопределении следует считать указанием, что первыйвариант лучше.
Но если надо иметь возможность представлять некоторое свойство с помощьюразличных типов, то лучше остановиться на втором варианте. Например:class XX : public X { /*...*/ };class XXX : public X { /*...*/ };void f(){320Бьерн Страуструп.Язык программирования С++C3* p1 = new C3(new X);C3* p2 = new C3(new XX);C3* p3 = new C3(new XXX);//...// C3 "содержит"// C3 "содержит"// C3 "содержит"XXXXXX}Приведенные определения нельзя смоделировать ни с помощью производного класса C3 от X, ни спомощью C3, имеющего член типа X, поскольку необходимо указывать точный тип члена. Это важнодля классов с виртуальными функциями, таких, например,как класс Shape ($$1.1.2.5), и для классаабстрактного множества ($$13.3).Заметим, что ссылки можно применять для упрощения классов, использующих члены-указатели, если втечение жизни объекта-владельца ссылка настроена только на один объект, например:class C4 {X& r;public:C(X& q) : r(q) { }// ...};12.2.5 Принадлежность и наследованиеУчитывая сложность важность отношений наследования, нет ничего удивительного в том, что часто ихнеправильно понимают и используют сверх меры.
Если класс D описан как общий производный откласса B, то часто говорят, что D есть B:class B { /* ... */ ;class D : public B /* ... */ };// D сорта BИначе это можно сформулировать так: наследование – это отношение "есть", или, более точно дляклассов D и B, наследование - это отношение D сорта B. В отличие от этого, если класс D содержит вкачестве члена другой класс B, то говорят, что D "имеет" B:class D { // D имеет B// ...public:B b;// ...};Иными словами, принадлежность - это отношение "иметь" или для классов D и B просто: D содержит B.Имея два класса B и D, как выбирать между наследованием и принадлежностью? Рассмотрим классысамолет и мотор.Новички обычно спрашивают: будет ли хорошим решением сделать класс самолетпроизводным от класса мотор.
Это плохое решение, поскольку самолет не "есть" мотор, самолет"имеет" мотор. Следует подойти к этому вопросу, рассмотрев, может ли самолет "иметь" два илибольше моторов. Поскольку это представляется вполне возможным (даже если мы имеем дело спрограммой, в которой все самолеты будут с одним мотором), следует использовать принадлежность, ане наследование. Вопрос "Может ли он иметь два..?" оказывается удивительно полезным во многихсомнительных случаях. Как всегда, наше изложение затрагивает неуловимую сущностьпрограммирования.
Если бы все классы было так же легко представить, как самолет и мотор, то былобы просто избежать и тривиальных ошибок типа той, когда самолет определяется как производное откласса мотор. Однако, такие ошибки достаточно часты, особенно у тех, кто считает наследование ещеодним механизмом для сочетания конструкций языка программирования. Несмотря на удобство илаконичность записи, которую предоставляет наследование, его надо использовать только длявыражения тех отношений, которые четко определены в проекте. Рассмотрим определения:class B {public:virtual void f();void g();};321Бьерн Страуструп.class D1 {public:B b;void f();};void h1(D1* pd){B* pb = pd;pb = &pd->b;pb->q();pd->q();pd->b.q();pb->f();pd->f();}Язык программирования С++// D1 содержит B// не переопределяет b.f()// ошибка: невозможно преобразование D1* в B*// вызов B::q// ошибка: D1 не имеет член q()// вызов B::f (здесь D1::f не переопределяет)// вызов D1::fОбратите внимание, что в этом примере нет неявного преобразования класса к одному из егоэлементов, и что класс, содержащий в качестве члена другой класс, не переопределяет виртуальныефункции этого члена.
Здесь явное отличие от примера, приведенного ниже:class D2 : public B {public:void f();};void h2(D2* pd){B* pb = pd;pb->q();pd->q();pb->f();pd->f();}// D2 есть B// переопределение B::f()//////////нормально: D2* неявно преобразуется в B*вызов B::qвызов B::qвызов виртуальной функции: обращение к D2::fвызов D2::fУдобство записи, продемонстрированное в примере с классом D2, по сравнению с записью в примере склассом D1, является причиной, по которой таким наследованием злоупотребляют.
Но следуетпомнить, что существует определенная плата за удобство записи в виде возросшей зависимости междуB и D2 (см. $$12.2.3). В частности, легко забыть о неявном преобразовании D2 в B. Если только такиепреобразования не относятся к семантике ваших классов, следует избегать описания производногокласса в общей части. Если класс представляет определенное понятие, а наследование используетсякак отношение "есть", то такие преобразования обычно как раз то, что нужно.Однако, бывают такие ситуации, когда желательно иметь наследование, но нельзя допускатьпреобразования.