18c0693f

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


Объект производного класса фактически построен из нескольких частей. Каждый базовый класс вносит свою долю в виде подобъекта, составленного из нестатических данных-членов этого класса. Объект производного класса построен из подобъектов, соответствующих каждому из его базовых, а также из части, включающей нестатические члены самого производного класса. Так, наш объект 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\n";

}

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

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 vector<location>* locations() { return &_loc; }

   // ...

protected:

   vector<location> _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 не существует:

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

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

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

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

pb->suffix();

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

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

pb->_name;

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

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

pb->NameQuery::_name;

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

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