Программирование на C / C++ / Ален И. Голуб. Правила программирования на Си и Си++ [pdf]
.pdfС++ для начинающих |
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, так что включать оба файла необязательно.) Файловый ввод/вывод поддерживается теми же операторами: