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

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

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

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

1012

Аргумент baloo имеет тип класса Bear. Кандидатами для вызова display() будут не только функции, объявления которых видимы в точке ее вызова, но также и те, что объявлены в пространствах имен, в которых объявлены класс Bear и его базовый класс ZooAnimal. Поэтому в множество кандидатов добавляется функция display(const ZooAnimal&), объявленная в пространстве имен NS.

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

namespace NS { class ZooAnimal {

friend void display( const ZooAnimal& );

};

}

// базовый класс Bear объявлен в пространстве имен NS class Bear : public NS::ZooAnimal { };

int main() { Bear baloo;

display( baloo ); return 0;

предыдущем примере display() объявлена как функция-друг ZooAnimal:

}

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

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

функций, видимых в точке вызова;

функций, объявленных в тех пространствах имен, где определен тип класса или любой из его базовых;

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

Наследование влияет также на построение множества кандидатов для вызова функции- члена с помощью операторов точкаили стрелка”. В разделе 18.4 мы говорили, что объявление функции-члена в производном классе не перегружает, а скрывает одноименные функции-члены в базовом, даже если их списки параметров различны:

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

1013

class ZooAnimal { public:

Time feeding_time( string ); // ...

};

class Bear : public ZooAnimal { public:

//скрывает ZooAnimal::feeding_time( string ) Time feeding_time( int );

//...

};

Bear Winnie;

// ошибка: ZooAnimal::feeding_time( string ) скрыта

Winnie.feeding_time( "Winnie" );

Функция-член feeding_time(int), объявленная в классе Bear, скрывает feeding_time(string), объявленную в ZooAnimal, базовом для Bear. Поскольку функция-член вызывается через объект Winnie типа Bear, то при поиске кандидатов для этого вызова просматривается только область видимости класса Bear, и единственным кандидатом будет feeding_time(int). Так как других кандидатов нет, вызов считается ошибочным.

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

class Bear : public ZooAnimal { public:

// feeding_time( int ) перегружает экземпляр из класса ZooAnimal using ZooAnimal::feeding_time;

Time feeding_time( int );

//...

спомощью using-объявлений:

};

Теперь обе функции feeding_time() находятся в области видимости класса Bear и,

// правильно: вызывается ZooAnimal::feeding_time( string )

следовательно, войдут в множество кандидатов:

Winnie.feeding_time( "Winnie" );

Втакой ситуации вызывается функция-член feeding_time( string ).

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

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

1014

class Endangered { public:

ostream& print( ostream& ); // ...

{;

class Bear : public( ZooAnimal ) { public:

void print( );

using ZooAnimal::feeding_time; Time feeding_time( int );

// ...

};

class Panda : public Bear, public Endangered { public:

// ...

};

int main()

{

Panda yin_yang;

//ошибка: неоднозначность: одна из

//Bear::print()

//Endangered::print( ostream& ) yin_yang.print( cout );

//правильно: вызывается Bear::feeding_time() yin_yang.feeding_time( 56 );

}

При поиске объявления функции-члена print() в области видимости класса Panda будут найдены как Bear::print(), так и Endangered::print(). Поскольку они не находятся в одном и том же базовом классе, то даже при разных списках параметров этих функций множество кандидатов оказывается пустым и вызов считается ошибочным. Для исправления ошибки в классе Panda следует определить собственную функцию print(). При поиске объявления функции-члена feeding_time() в области видимости Panda

будут найдены ZooAnimal::feeding_time() и Bear::feeding_time() они расположены в области видимости класса Bear. Так как эти объявления найдены в одном и том же базовом классе, множество кандидатов для данного вызова включает обе функции, а выбирается Bear::feeding_time().

19.3.2. Устоявшие функции и последовательности пользовательских преобразований

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

существуют приведения типа каждого фактического аргумента к типу соответственного формального параметра.

В разделе 15.9 мы показали, как разработчик класса может предоставить пользовательские преобразования для объектов этого класса, которые неявно вызываются

компилятором для трансформации фактического аргумента функции в тип соответственного формального параметра. Пользовательские преобразования бывают двух видов: конвертер или конструктор с одним параметром без ключевого слова

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

1015

explicit. При наследовании на втором шаге разрешения перегрузки рассматривается более широкое множество таких преобразований.

Конвертеры наследуются, как и любые другие функции-члены класса. Например, мы

class ZooAnimal { public:

//конвертер: ZooAnimal ==> const char* operator const char*();

//...

можем написать следующий конвертер для ZooAnimal:

};

Производный класс Bear наследует его от своего базового ZooAnimal. Если значение типа Bear используется в контексте, где ожидается const char*, то неявно вызывается

extern void display( const char* );

Bear yogi;

// правильно: yogi ==> const char*

конвертер для преобразования Bear в const char*: display( yogi );

Конструкторы с одним аргументом без ключевого слова explicit образуют другое множество неявных преобразований: из типа параметра в тип своего класса. Определим

class ZooAnimal { public:

//преобразование: int ==> ZooAnimal ZooAnimal( int );

//...

такой конструктор для ZooAnimal:

};

Его можно использовать для приведения значения типа int к типу ZooAnimal. Однако конструкторы не наследуются. Конструктор ZooAnimal нельзя применять для

const int cageNumber = 8788l

void mumble( const Bear & );

// ошибка: ZooAnimal( int ) не используется

преобразования объекта в случае, когда целевым является тип производного класса: mumble( cageNumber );

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

1016

Поскольку целевым типом является Bear тип параметра функции mumble(), то рассматриваются только его конструкторы.

19.3.3. Наилучшая из устоявших функций

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

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

преобразование аргумента типа производного класса в параметр типа любого из его базовых;

преобразование указателя на тип производного класса в указатель на тип любого из его базовых;

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

Они не являются пользовательскими, так как не зависят от конвертеров и конструкторов,

extern void release( const ZooAnimal& ); Panda yinYang;

// стандартное преобразование: Panda -> ZooAnimal

имеющихся в классе: release( yinYang );

Поскольку аргумент yinYang типа Panda инициализирует ссылку на тип базового класса, то преобразование имеет ранг стандартного.

В разделе 15.10 мы говорили, что стандартные преобразования имеют более высокий

class Panda : public Bear, public Endangered

{

// наследует ZooAnimal::operator const char *()

};

Panda yinYang;

extern void release( const ZooAnimal& ); extern void release( const char * );

//стандартное преобразование: Panda -> ZooAnimal

//выбирается: release( const ZooAnimal& )

ранг, чем пользовательские: release( yinYang );

Как release(const char*), так и release(ZooAnimal&) являются устоявшими функциями: первая потому, что инициализация параметра-ссылки значением аргумента стандартное преобразование, а вторая потому, что аргумент можно привести к типу

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

1017

const char* с помощью конвертера ZooAnimal::operator const char*(), который представляет собой пользовательское преобразование. Так как стандартное преобразование лучше пользовательского, то в качестве наилучшей из устоявших выбирается функция release(const ZooAnimal&).

При ранжировании различных стандартных преобразований из производного класса в базовые лучшим считается приведение к тому базовому классу, который ближе к производному. Так, показанный ниже вызов не будет неоднозначным, хотя в обоих случаях требуется стандартное преобразование. Приведение к базовому классу Bear лучше, чем к ZooAnimal, поскольку Bear ближе к классу Panda. Поэтому лучшей из

extern void release( const ZooAnimal& ); extern void release( const Bear& );

// правильно: release( const Bear& )

устоявших будет функция release(const Bear&): release( yinYang );

Аналогичное правило применимо и к указателям. При ранжировании стандартных

преобразований из указателя на тип производного класса в указатели на типы различных базовых лучшим считается то, для которого базовый класс наименее удален от производного. Это правило распространяется и на тип void*.

Стандартное преобразование в указатель на тип любого базового класса всегда лучше,

void receive( void* );

чем преобразование в void*. Например, если дана пара перегруженных функций: void receive( ZooAnimal* );

то наилучшей из устоявших для вызова с аргументом типа Panda* будет receive(ZooAnimal*).

В случае множественного наследования два стандартных преобразования из типа производного класса в разные типы базовых могут иметь одинаковый ранг, если оба базовых класса равноудалены от производного. Например, Panda наследует классам Bear и Endangered. Поскольку они равноудалены от производного Panda, то преобразования объекта Panda в любой из этих классов одинаково хороши. Но тогда единственной наилучшей из устоявших функции для следующего вызова не существует, и он считается

extern void mumble( const Bear& ); extern void mumble( const Endangered& );

/* ошибка: неоднозначный вызов:

*может быть выбрана любая из двух функций

*void mumble( const Bear& );

*void mumble( const Endangered& );

*/

ошибочным:

mumble( yinYang );

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

1018

Для разрешения неоднозначности программист может применить явное приведение типа:

mumble( static_cast< Bear >( yinYang ) ); // правильно

Инициализация объекта производного класса или ссылки на него объектом типа базового, а также преобразование указателя на тип базового класса в указатель на тип производного никогда не выполняются компилятором неявно. (Однако их можно выполнить с помощью явного применения dynamic_cast, как мы видели в разделе 19.1.) Для данного вызова не существует наилучшей из устоявших функции, так как нет

extern void release( const Bear& ); extern void release( const Panda& );

ZooAnimal za;

// ошибка: нет соответствия

неявного преобразования аргумента типа ZooAnimal в тип производного класса: release( za );

В следующем примере наилучшей из устоявших будет release(const char*). Это может показаться удивительным, так как к аргументу применена последовательность пользовательских преобразований, в которой участвует конвертер const char*(). Но

поскольку неявного приведения от типа базового класса к типу производного не существует, то release(const Bear&) не является устоявшей функцией, так что

Class ZooAnimal { public:

//преобразование: ZooAnimal ==> const char* operator const char*();

//...

};

extern void release( const char* ); extern void release( const Bear& );

ZooAnimal za;

//za ==> const char*

//правильно: release( const char* )

остается только release(const char*): release( za );

Упражнение 19.9

Дана такая иерархия классов:

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

1019

class Base1 { public:

ostream& print(); void debug(); void writeOn();

void log( string ); void reset( void *); // ...

};

class Base2 { public:

void debug(); void readOn();

void log( double ); // ...

};

class MI : public Base1, public Base2 { public:

ostream& print(); using Base1::reset; void reset( char * ); using Base2::log; using Base2::log;

// ...

};

MI *pi = new MI;

(a) pi->print(); (c) pi->readOn(); (e) pi->log( num );

Какие функции входят в множество кандидатов для каждого из следующих вызовов:

(b) pi->debug(); (d) pi->reset(0); (f) pi->writeOn();

Упражнение 19.10

class Base { public:

operator int(); operator const char *(); // ...

};

class Derived : public Base { public:

operator double(); // ...

Дана такая иерархия классов:

};

Удастся ли выбрать наилучшую из устоявших функций для каждого из следующих вызовов? Назовите кандидаты, устоявшие функции и преобразования типов аргументов для каждой из них, наилучшую из устоявших (если она есть):

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

1020

(a)void operate( double ); void operate( string );

void operate( const Base & ); Derived *pd = new Derived;

(b)void calc( int ); void calc( double );

void calc( const Derived & ); Base *pb = new Derived; operate( *pd );

operate( *pb );

20

20. Библиотека iostream

Частью стандартной библиотеки C++ является библиотека iostream объектно- ориентированная иерархия классов, где используется и множественное, и виртуальное наследование. В ней реализована поддержка для файлового ввода/вывода данных встроенных типов. Кроме того, разработчики классов могут расширять эту библиотеку для чтения и записи новых типов данных.

Для использования библиотеки iostream в программе необходимо включить заголовочный файл

#include <iostream>

Операции ввода/вывода выполняются с помощью классов istream (потоковый ввод) и ostream (потоковый вывод). Третий класс, iostream, является производным от них и поддерживает двунаправленный ввод/вывод. Для удобства в библиотеке определены три стандартных объекта-потока:

cin объект класса istream, соответствующий стандартному вводу. В общем случае он позволяет читать данные с терминала пользователя;

cout объект класса ostream, соответствующий стандартному выводу. В общем случае он позволяет выводить данные на терминал пользователя;

cerr объект класса ostream, соответствующий стандартному выводу для ошибок. В этот поток мы направляем сообщения об ошибках программы.

Вывод осуществляется, как правило, с помощью перегруженного оператора сдвига влево (<<), а ввод с помощью оператора сдвига вправо (>>):

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

1021

#include <iostream> #include <string>

int main()

{

string in_string;

//вывести литерал на терминал пользователя cout << "Введите свое имя, пожалуйста: ";

//прочитать ответ пользователя в in_string cin >> in_string;

if ( in_string.empty() )

// вывести сообщение об ошибке на терминал пользователя cerr << "ошибка: введенная строка пуста!\n";

else cout << "Привет, " << in_string << "!\n";

}

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

>> x

перемещает данные в x, а

<< x

перемещает данные из x. (В разделе 20.1 мы покажем, как библиотека iostream поддерживает ввод данных, а в разделе 20.5 – как расширить ее для ввода данных новых типов. Аналогично раздел 20.2 посвящен поддержке вывода, а раздел 20.4 – расширению для вывода данных определенных пользователем типов.)

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

ifstream, производный от istream, связывает ввод программы с файлом;

ofstream, производный от ostream, связывает вывод программы с файлом;

fstream, производный от iostream, связывает как ввод, так и вывод программы с файлом.

Чтобы использовать часть библиотеки iostream, связанную с файловым вводом/выводом, необходимо включить в программу заголовочный файл

#include <fstream>

(Файл fstream уже включает iostream, так что включать оба файла необязательно.) Файловый ввод/вывод поддерживается теми же операторами: