Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]

.pdf
Скачиваний:
138
Добавлен:
02.05.2014
Размер:
5.67 Mб
Скачать

С++ для начинающих

 

862

 

class Library {

 

 

 

 

 

 

public:

// выдать

 

bool check_out( LibMember* );

 

bool check_in ( LibMember* );

// принять назад

 

bool is_late( const Date& today );

// просрочил

 

double apply_fine();

 

// наложить штраф

 

ostream& print( ostream&=cout );

 

 

Date* due_date() const;

 

// ожидаемая дата возврата

 

Date* date_borrowed() const;

 

// дата выдачи

 

string title() const;

 

// название

 

const LibMember* member() const;

// записавшийся

};

Упражнение 17.4

Идентифицируйте члены базового и производных классов для той иерархии, которую вы выбрали в упражнении 17.2 (раздел 17.1). Задайте виртуальные функции, а также открытые и защищенные члены.

Упражнение 17.5

class base { ... };

(a)class Derived : public Derived { ... };

(b)class Derived : Base { ... };

(c)class Derived : private Base { ... };

(d)class Derived : public Base;

Какие из следующих объявлений неправильны:

(e) class Derived inherits Base { ... };

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

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

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

С++ для начинающих

863

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;

//...

последнего скрыто в производном классе:

};

С++ для начинающих

864

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

void Shy::

turn_eyes_down()

{

// ...

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

// ошибка: int Diffident::_mumble скрыто _mumble = -1;

использование в данном контексте недопустимо:

}

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

члена базового класса именем самого этого класса с помощью оператора разрешения области видимости. Так выглядит правильная реализация функции-члена

void Shy::

turn_eyes_down()

{

// ...

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

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

Diffident::_mumble = -1;

turn_eyes_down():

}

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

class Diffident { public:

void mumble( int softness ); // ...

};

class Shy : public Diffident { public:

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

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

void mumble( string whatYaSay ); void print( int soft, string words ); // ...

перегруженных функций:

};

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

С++ для начинающих

865

Shy simon;

//правильно: Shy::mumble( string ) simon.mumble( "pardon me" );

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

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

simon.mumble( 2 );

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

class Diffident { public:

void turn_aside( ); // ...

};

class Shy : public Diffident { public:

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

//Diffident::turn_aside() void turn_aside();

//...

следующие два экземпляра невиртуальной функции-члена turn_aside()

};

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

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

class Shy : public Diffident { public:

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

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

void mumble( string whatYaSay ); void mumble( int softness ) {

Diffident::mumble( softness ); }

// ...

для вызова экземпляра из базового? Это возможно:

};

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

С++ для начинающих

866

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;

независимого объекта базового класса:

}

С++ для начинающих

867

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

bool NameQuery::

compare( const Query *pquery )

{

//правильно: защищенный член подобъекта Query int myMatches = _loc.size();

//правильно: используется открытый метод доступа int itsMatches = pquery->locations()->size();

return myMatches == itsMatches;

location():

}

Однако проблема заключается в неправильном проектировании. Поскольку _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()

С++ для начинающих

868

вызывается функция из 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 через

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

pb тоже вызывает ошибку компиляции: pb->_name;

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

Квалификация имени члена в этом случае не помогает: pb->NameQuery::_name;

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

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

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

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

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

static vector<string> *_text_file;

С++ для начинающих

869

Создается ли при порождении класса 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

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

С++ для начинающих

870

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 );

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

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

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

{return _foo_bar == pb->_foo_bar; }

17.4.Конструирование базового и производного классов

Напомним, что объект производного класса состоит из одного или более подобъектов, соответствующих базовым классам, и части, относящейся к самому производному. Например, NameQuery состоит из подобъекта Query и объекта-члена string. Для

иллюстрации поведения конструктора производного класса введем еще один член

class NameQuery : public Query { public:

// ...

protected:

bool _present; string _name;

встроенного типа:

};

Если _present установлен в false, то слово _name в тексте отсутствует.

С++ для начинающих

871

Рассмотрим случай, когда в NameQuery конструктор не определен. Тогда при

определении объекта этого класса

NameQuery nq;

по очереди вызывается конструктор по умолчанию Query, а затем конструктор по умолчанию класса string (ассоциированный с объектом _name). Член _present остается неинициализированным, что потенциально может служить источником ошибок. Чтобы инициализировать его, можно так определить конструктор по умолчанию для класса

NameQuery:

inline NameQuery::NameQuery() { _present = false; }

Теперь при определении nq вызываются три конструктора по умолчанию: для базового класса Query, для класса string при инициализации члена _name и для класса

NameQuery.

А как передать аргумент конструктору базового класса Query? Ответить на этот вопрос можно, рассуждая по аналогии.

Для передачи одного или более аргументов конструктору объекта-члена мы используем список инициализации членов (здесь можно также задать начальные значения членам, не

inline NameQuery::

NameQuery( const string &name )

: _name( name ), _present( false )

являющимся объектами классов; подробности см. в разделе 14.5):

{}

Для передачи одного или более аргументов конструктору базового класса также разрешается использовать список инициализации членов. В следующем примере мы передаем конструктору string аргумент name, а конструктору базового класса Query

inline NameQuery::

NameQuery( const string &name, vector<location> *ploc )

: _name( name ), Query( *ploc ), _present( true )

объект, адресованный указателем ploc:

{}

Хотя Query помещен в список инициализации вторым, его конструктор всегда вызывается раньше конструктора для _name. Порядок их вызова следующий:

Конструктор базового класса. Если базовых классов несколько, то конструкторы вызываются в порядке их следования в списке базовых классов, а не в порядке появления в списке инициализации. (О множественном наследовании в этой связи мы поговорим в главе 18.)

Конструктор объекта-члена. Если в классе есть несколько таких членов, то конструкторы вызываются в порядке их объявления в классе, а не в порядке появления в списке инициализации (подробнее см. раздел 14.5).