17.3. Доступ к членам базового класса

17.3. Доступ к членам базового класса

Объект производного класса фактически построен из нескольких частей. Каждый базовый класс вносит свою долю в виде подобъекта, составленного из нестатических данных-членов этого класса. Объект производного класса построен из подобъектов, соответствующих каждому из его базовых, а также из части, включающей нестатические члены самого производного класса. Так, наш объект NameQuery состоит из подобъекта Query, содержащего члены _loc и _solution, и части, принадлежащей NameQuery, - она содержит только член _name.

Внутри производного класса к членам, унаследованным из базового, можно обращаться напрямую, как к его собственным. (Глубина цепочки наследования не увеличивает затраты времени и не лимитирует доступ к ним.) Например:

void

NameQuery::

display_partial_solution( ostream &os )

{

os _name

" is found in "

(_solution ? _solution-size() : 0)

" lines of text ";

}

Это касается и доступа к унаследованным функциям-членам базового класса: мы вызываем их так, как если бы они были членами производного - либо через его объект:

NameQuery nq( "Frost" );

// вызывается NameQuery::eval()

nq.eval();

// вызывается Query::display()

nq.display();

либо непосредственно из тела другой (или той же самой) функции-члена:

void

NameQuery::

match_count()

{

if ( ! _solution )

// вызывается Query::_vec2set()

_solution = _vec2set( &_loc );

return _solution-size();

}

Однако прямой доступ из производного класса к членам базового запрещен, если имя последнего скрыто в производном классе:

class Diffident {

public: // ...

protected:

int _mumble;

// ...

};

class Shy : public Diffident {

public: // ...

protected:

// имя Diffident::_mumble скрыто

string _mumble;

// ...

};

В области видимости Shy употребление неквалифицированного имени _mumble разрешается в пользу члена _mumble класса Shy (объекта string), даже если такое использование в данном контексте недопустимо:

void

Shy::

turn_eyes_down()

{

// ...

_mumble = "excuse me"; // правильно

// ошибка: int Diffident::_mumble скрыто

_mumble = -1;

}

Некоторые компиляторы помечают это как ошибку типизации. Для доступа к члену базового класса, имя которого скрыто в производном, необходимо квалифицировать имя члена базового класса именем самого этого класса с помощью оператора разрешения области видимости. Так выглядит правильная реализация функции-члена turn_eyes_down():

void

Shy::

turn_eyes_down()

{

// ...

_mumble = "excuse me"; // правильно

// правильно: имя члена базового класса квалифицировано

Diffident::_mumble = -1;

}

Функции-члены базового и производного классов не составляют множество перегруженных функций:

class Diffident {

public:

void mumble( int softness );

// ...

};

class Shy : public Diffident {

public:

// скрывает видимость функции-члена Diffident::_mumble,

// а не перегружает ее

void mumble( string whatYaSay );

void print( int soft, string words );

// ...

};

Вызов функции-члена базового класса из производного в этом случае приводит к ошибке компиляции:

Shy simon;

// правильно: Shy::mumble( string )

simon.mumble( "pardon me" );

// ошибка: ожидался первый аргумент типа string

// Diffident::mumble( int ) невидима

simon.mumble( 2 );

Хотя к членам базового класса можно обращаться напрямую, они сохраняют область видимости класса, в котором определены. А чтобы функции перегружали друг друга, они должны находиться в одной и той же области видимости. Если бы это было не так, следующие два экземпляра невиртуальной функции-члена turn_aside()

class Diffident {

public:

void turn_aside( );

// ...

};

class Shy : public Diffident {

public:

// скрывает видимость

// Diffident::turn_aside()

void turn_aside();

// ...

};

привели бы к ошибке повторного определения, так как их сигнатуры одинаковы. Однако запись правильна, поскольку каждая функция находится в области видимости того класса, в котором определена.

А если нам действительно нужен набор перегруженных функций-членов базового и производного классов? Написать в производном классе небольшую встроенную заглушку для вызова экземпляра из базового? Это возможно:

class Shy : public Diffident {

public:

// один из способов реализовать множество перегруженных

// членов базового и производного классов

void mumble( string whatYaSay );

void mumble( int softness ) {

Diffident::mumble( softness ); }

// ...

};

Но в стандартном C++ тот же результат достигается посредством using-объявления:

class Shy : public Diffident {

public:

// в стандартном C++ using-объявление

// создает множество перегруженных

// членов базового и производного классов

void mumble( string whatYaSay );

using Diffident::mumble;

// ...

};

По сути дела, using-объявление вводит каждый именованный член базового класса в область видимости производного. Поэтому такой член теперь входит в множество перегруженных функций, ассоциированных с именем функции-члена производного класса. (В ее using-объявлении нельзя указать список параметров, только имя. Это означает, что если некоторая функция уже перегружена в базовом классе, то в область видимости производного класса попадут все перегруженные экземпляры и, следовательно, добавить только одну из них невозможно.)

Обратим внимание на степень доступности защищенных членов базового класса. Когда мы пишем:

class Query {

public:

const vectorlocation* locations() { return &_loc; }

// ...

protected:

vectorlocation _loc;

// ...

};

то имеем в виду, что класс, производный от Query, может напрямую обратиться к члену _loc, тогда как во всей остальной программе для этого необходимо пользоваться открытой функцией доступа. Однако объект производного класса имеет доступ только к защищенному члену _loc входящего в него подобъекта, относящегося к базовому классу. Объект производного класса неспособен обратиться к защищенным членам другого независимого объекта базового класса:

bool

NameQuery::

compare( const Query *pquery )

{

// правильно: защищенный член подобъекта Query

int myMatches = _loc.size();

// ошибка: нет прав доступа к защищенному члену

// независимого объекта Query

int itsMatches = pquery-_loc.size();

return myMatches == itsMatches;

}

У объекта NameQuery есть доступ к защищенным членам только одного объекта Query - подобъекта самого себя. Прямое обращение к ним из производного класса осуществляется через неявный указатель this (см. раздел 13.4). Первая реакция на ошибку компиляции - переписать функцию compare() с использованием открытой функции-члена location():

bool

NameQuery::

compare( const Query *pquery )

{

// правильно: защищенный член подобъекта Query

int myMatches = _loc.size();

// правильно: используется открытый метод доступа

int itsMatches = pquery-locations()-size();

return myMatches == itsMatches;

}

Однако проблема заключается в неправильном проектировании. Поскольку _loc - это член базового класса Query, то место compare() среди членов базового, а не производного класса. Во многих случаях подобные проблемы могут быть решены путем переноса некоторой операции в тот класс, где находится недоступный член, как в приведенном примере.

Этот вид ограничения доступа не распространяется на доступ изнутри класса к другим объектам того же класса:

bool

NameQuery::

compare( const NameQuery *pname )

{

int myMatches = _loc.size(); // правильно

int itsMatches = name-_loc.size(); // тоже правильно

return myMatches == itsMatches;

}

Производный класс может напрямую обращаться к защищенным членам базового в других объектах того же класса, что и он сам, равно как и к защищенным и закрытым членам других объектов своего класса.

Рассмотрим инициализацию указателя на базовый Query адресом объекта производного NameQuery:

Query *pb = new NameQuery( "sprite" );

При вызове виртуальной функции, определенной в базовом классе Query, например:

pb-eval(); // вызывается NameQuery::eval()

вызывается функция из NameQuery. За исключением вызова виртуальной функции, объявленной в Query и переопределенной в NameQuery, другого способа напрямую добраться до членов класса NameQuery через указатель pb не существует:

если в Query и NameQuery объявлены некоторые невиртуальные функции-члены с одинаковым именем, то через pb всегда вызывается экземпляр из Query;

если в Query и NameQuery объявлены одноименные члены, то через pb обращение происходит к члену класса Query;

если в NameQuery имеется виртуальная функция, отсутствующая в Query, скажем suffix(), то попытка вызвать ее через pb приводит к ошибке компиляции:

// ошибка: suffix() - не член класса Query

pb-suffix();

Обращение к члену или невиртуальной функции-члену класса NameQuery через pb тоже вызывает ошибку компиляции:

// ошибка: _name - не член класса Query

pb-_name;

Квалификация имени члена в этом случае не помогает:

// ошибка: у класса Query нет базового класса NameQuery

pb-NameQuery::_name;

В C++ с помощью указателя на базовый класс можно работать только с данными и функциями-членами, включая виртуальные, которые объявлены (или унаследованы) в самом этом классе, независимо от того, какой фактический объект адресуется указателем. Объявление функции-члена виртуальной откладывает решение вопроса о том, какой экземпляр функции вызвать, до выяснения (во время выполнения программы) фактического типа объекта, адресуемого pb.

Такой подход может показаться недостаточно гибким, но у него есть два весомых преимущества:

поиск виртуальной функции-члена во время выполнения никогда не закончится неудачно из-за того, что фактический тип класса не существует. В таком случае программа просто не смогла бы откомпилироваться;

механизм виртуализации можно оптимизировать. Часто вызов такой функции оказывается не дороже, чем косвенный вызов функции по указателю (детально этот вопрос рассмотрен в [LIPPMAN96a]).

В базовом классе Query определен статический член _text_file:

static vectorstring *_text_file;

Создается ли при порождении класса NameQuery второй экземпляр _text_file, уникальный именно для него? Нет. Все объекты производного класса ссылаются на тот же самый, единственный разделяемый статический член. Сколько бы ни было производных классов, существует лишь один экземпляр _text_file. Можно обратиться к нему через объект производного класса с помощью синтаксиса доступа:

nameQueryObject._text_file; // правильно

Наконец, если производный класс хочет получить доступ к закрытым членам своего базового класса напрямую, то он должен быть объявлен другом базового:

class Query { friend class NameQuery; public: // ... };

Теперь объект NameQuery может обращаться не только к закрытым членам своего подобъекта, соответствующего базовому классу, но и к закрытым и защищенным членам любых объектов Query.

А если мы произведем от NameQuery класс StringQuery? Он будет поддерживать сокращенную форму запроса AndQuery, и вместо

beautiful && fiery && bird

можно будет написать:

"beautiful fiery bird"

Унаследует ли StringQuery от класса NameQuery дружественные отношения с Query? Нет. Отношение дружественности не наследуется. Производный класс не становится другом класса, который объявил своим другом один из базовых. Если производному классу требуется стать другом одного или более классов, то эти классы должны предоставить ему соответствующие права явно. Например, у класса StringQuery нет никаких специальных прав доступа по отношению к Query. Если расширенный доступ необходим, то Query должен разрешить его явно.

Упражнение 17.6

Даны следующие определения базового и производных классов:

class Base {

public:

foo( int );

// ...

protected:

int _bar;

double _foo_bar;

};

class Derived : public Base {

public:

foo( string );

bool bar( Base *pb );

void foobar();

// ...

protected:

string _bar;

};

Исправьте ошибки в каждом из следующих фрагментов кода:

Derived d; d.foo( 1024 );

(b) void Derived::foobar() { _bar = 1024; }

(c) bool Derived::bar( Base *pb )

{ return _foo_bar == pb-_foo_bar; }