Книга: С++ — это просто. — 3-е изд.: Пер. с англ.
Назад: Для чего применять виртуальные функции?
Дальше: Глава 10. Шаблоны

file.open ( "SAMPLE.TXT", ios::noreplace ) ;

if ( ! file )

{

report ( file ) ;

exit ( 1 ) ;

}

else

{

file << " Если бы автомобили строились как операционные системы," << endl

<< "больниц было бы больше, чем домов" ;

if ( ! file )

{

report ( file ) ;exit ( 2 ) ;

}

}

216

Глава 8

file.close( ) ;

return 0 ;

}

void report ( ofstream &file )

{

cout << endl << "Невозможно открыть SAMPLE.TXT" ; cout << endl << "Статус проверки ошибок = " << file.rdstate( ) ; cout << endl << "good( ) = " << file.good( ) ; cout << endl << "eof( ) = " << file.eof( ) ; cout << endl << "fail( ) = " << file.fail( ) ; cout << endl << "bad( ) = " << file.bad( ) ;

}

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

всякий раз, когда мы открываем файл, следует проверять, было ли открытие файла

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

при чтении из файла — значит утратить связь с реальностью.

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

file.open ( "SAMPLE.TXT", ios::noreplace ) ;

file возвращает ноль (или false), если файл не открывается. Так, мы можем проверить статус file на предмет успешного либо неудачного завершения операции

открытия файла

if ( ! file )

Если это условие выполнено, мы вызываем функцию report( ), отображающую

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

и их значение приведены на рис. 8.5.

Значение переменной состояния можно получить посредством функции ios:: rdstate( ). Другие функции, такие как eof( ), good( ), fail( ), hard( ) и bad( ), возвращают статус остальных битов. Распишем назначение этих функций в табл. 8.5.

Таблица 8.5. Функции выявления ошибок

Функция Назначение

eof( )

Возвращает "верно", если установлен флаг EOF

fail( )

Возвращает "верно", если установлен флаг failbit, badbit или hardfail bad( )

Возвращает "верно", если установлен флаг badbit или hardfail

Система ввода-вывода в C++

217

Таблица 8.5 (окончание)

Функция Назначение

good( )

Возвращает "верно", если все в порядке; ни один флаг не установлен

clear ( int = 0 )

Без аргумента очищает все биты ошибок; в остальных случаях устанавливает указанные флаги, как в clear ( ios::failbit )

Рис. 8.5. Значение битов в переменной состояния

Вот результат работы программы, когда присутствовал SAMPLE.TXT и noreplace не позволял заменить его новым файлом

Невозможно открыть SAMPLE.TXT"

Статус проверки ошибок = 4

good( ) = 0

eof( ) = 0

fail( ) = 4

bad( ) = 4

Статус проверки ошибок, возвращаемый функцией rdstate( ), равен 4. Это означает, что файл невозможно открыть. Функция good( ) возвращает 1 (true) лишь когда

битовые флаги не установлены, и потому в нашем случае она возвращает 0. Мы не

достигли конца файла, поэтому eof( ) возвращает 0. Функции fail( ) и bad( ) возвращают ненулевые значения, поскольку возникла ошибка. В профессионально

написанной программе все или некоторые из этих функций следует использовать

после каждой операции ввода-вывода, чтобы удостовериться в том, что все происходит ожидаемым образом.

Взаимодействие с файловой системой

Для работы с файлами и каталогами программе на языке C++ может потребоваться

доступ к файловой системе машины, на которой она исполняется. Сюда входят такие операции, как создание, переименование, удаление, навигация и т. п. Разные

ОС поддерживают различные файловые системы. К примеру, Windows использует

218

Глава 8

NTFS, а Linux — EXT3. Поскольку каждая файловая система управляет файлами и

каталогами по-разному, работа C++ программ, обращающихся к ее ресурсам, под-чинена требованиям конкретной ОС и файловой системы.

Далее приведена программа на C++, которая иллюстрирует общие операции с дис-ками, файлами и каталогами для ОС Windows с использованием файловой системы

NTFS.

#include <windows.h>

#include <iostream>

using namespace std ;

int main( )

{

char path[ MAX_PATH ] = "" ;

unsigned long int spc, bps, nfc, tnc ;

GetDiskFreeSpaceA ( "C:\\", &spc, &bps, &nfc, &tnc ) ; cout << "Секторов на кластер = " << spc << endl ; cout << "Байтов на сектор =" << bps << endl ; cout << "Кол-во свободных кластеров =" << nfc << endl ; cout << "Общее число кластеров = " << tnc << endl ; float capacity, avlbl, multiplier ;

multiplier = spc * bps / 1024.0f / 1024.0f / 1024.0f ;

capacity = tnc * multiplier ;

avlbl = nfc * multiplier ;

cout << "Емкость диска = " << capacity << " Гб" << endl ; cout << "Доступное свободное место = " << avlbl << " Гб" << endl ; GetModuleFileNameA ( NULL, path, MAX_PATH ) ;

cout << "Исполняемый файл программы = " << path << endl ; GetCurrentDirectoryA ( MAX_PATH, path ) ;

cout << "Текущий каталог = " << path << endl ; cout << "Создание каталога myDir..." << endl ; CreateDirectoryA ( "myDir", NULL ) ;

cout << "Переход к каталогу myDir..." << endl ; SetCurrentDirectoryA ( "myDir" ) ;

GetCurrentDirectoryA ( MAX_PATH, path ) ;

cout << "Текущий каталог = " << path << endl ; cout << "Переход к родительскому каталогу myDir..." << endl ; SetCurrentDirectoryA ( ".." ) ;

GetCurrentDirectoryA ( MAX_PATH, path ) ;

cout << "Текущий каталог = " << path << endl ;

Система ввода-вывода в C++

219

cout << "Удаление каталога myDir..." << endl ; RemoveDirectoryA ( "myDir" ) ;

cout << "Создание файла tempFile..." << endl ; HANDLE hFile ;

hFile = CreateFileA ( "tempFile", GENERIC_WRITE,

FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, 0 , NULL ) ;

FILETIME ftCreate, ftAccess, ftWrite ;

SYSTEMTIME stUTC, stLocal ;

// Получение файловых атрибутов времени для файла

GetFileTime ( hFile, &ftCreate, &ftAccess, &ftWrite ) ;

// Приведение времени создания к местному времени

FileTimeToSystemTime ( &ftCreate, &stUTC ) ;

SystemTimeToTzSpecificLocalTime ( NULL, &stUTC, &stLocal ) ; cout << "Дата создания файла = " ;

cout << stLocal.wDay << "/" << stLocal.wMonth << "/"

<< stLocal.wYear << endl ;

cout << "Время создания файла = " ;

cout << stLocal.wHour << ":" << stLocal.wMinute << endl ; return 0 ;

}

Результат работы программы:

Секторов на кластер = 8

Байтов на сектор = 512

Кол-во свободных кластеров = 6715617

Общее число кластеров = 40650239

Емкость диска = 155.068 Гб

Доступное свободное место = 25.618 Гб

Исполняемый файл программы = C:\Users\Kanetkar\Desktop\Sample\Debug\Sample.exe Текущий каталог = C:\Users\Kanetkar\Desktop\Sample\Sample

Создание каталога myDir...

Переход к каталогу myDir...

Текущий каталог = C:\Users\Kanetkar\Desktop\Sample\Sample\myDir

Переход к родительскому каталогу myDir...

Текущий каталог = C:\Users\Kanetkar\Desktop\Sample\Sample

Удаление каталога myDir...

Создание файла tempFile...

Дата создания файла = 25/3/2019

Время создания файла = 17:13

Обратите внимание на букву «А» в конце имен большинства функций, задейство-ванных в этой программе. Она указывает на кодировку ANSI. Это означает, что

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

220

Глава 8

использовать строки символов Unicode, то следует добавить «L» перед строкой и

заменить «A» на «W», как показано в следующем вызове функции:

CreateDirectoryW ( L"myDir", NULL ) ;

1. Укажите, верны ли следующие утверждения:

Класс ios расположен в корне библиотеки iostream.

Файл ′iomanip.h′ требуется подключать лишь для параметризированных манипуляторов.

При использовании манипулятора в действительности вызывается функция-член.

Можно создавать собственные манипуляторы.

Объекты cout, cin и cprn являются предопределенными потоковыми объектами.

Объекты могут считывать и записывать самих себя.

Классы strstreams используются для ввода-вывода в строку и из строки.

Функция istream::getline( ) не предназначена для работы со строками из нескольких слов.

Решите следующие задачи и ответьте на вопросы:

Составьте диаграмму, иллюстрирующую иерархию различных классов в библиотеке iostream.

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

файла, если infile является объектом ifstream:

while ( infile ) ;

Что обеспечивают флаги nocreate и noreplace, когда они задействованы при открытии файла?

На ваш взгляд, с какой проблемой мы столкнемся, если следующий код будет

выполнен дважды:

file.seekg ( 0L, ios::beg ) ;

или

while ( file.read ( char * ) &p, sizeof ( p ) ) cout << t.name << endl << p.age ; Как бы вы решили эту проблему?

Как получить значение переменной состояния?

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

(A)

#include <iostream>

using namespace std ;

Система ввода-вывода в C++

221

int main( )

{

char str[ ] = "Здесь скучно" ;

char *p = "А вот это интересно" ;

cout << endl << str ;cout << endl << p ;

cout << endl << ( void * ) str ;

cout << endl << ( void * ) p ;

return 0 ;

}

(B)

#include <iostream>

using namespace std ;

int main( )

{

int i = 650 ;

float a = 425.123 ;

cout << setiosflags ( ios::showbase | ios::uppercase ) ;

cout << hex << i << endl ;

cout.precision ( 4 ) ;

cout << setiosflags ( ios::showpoint ) << a ;

return 0 ;

}

(C)

#include <iostream>

using namespace std ;

int main( )

{

char str[ ] = "Просто перечень" ;

cout.width ( 40 ) ;

cout << str << endl ;

cout.setf ( ios::left, ios::adjustfield ) ;

cout.width ( 40 ) ;

cout << str ;

return 0 ;

}

3. Решите следующие задачи:

Какое ограничение у следующей функции перегрузки оператора для отображения объектов класса Complex?

ostream& operator << ( ostream& s, Complex& c )

{

s << "( " << c.real << ", " << c.imag << " )" ;

}

222

Глава 8

Составьте программу, создающую путь к каталогу d1/d2/d3. В d3 создайте файл

'sample.txt'. Запишите в этот файл ваше имя и адрес.

В файле имеется 100 записей, каждая из которых содержит 6-символьный код

товара, 20-символьный номер товара и его количество (целый тип). Напишите

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

Требования к системе ввода-вывода:

• Должна обеспечивать связь с исходными и целевыми устройствами.

• Должна обеспечивать ввод-вывод различных объектов.

• Должна обеспечивать связь различными способами.

• Должна обеспечивать работу с любой файловой системой.

Решение C++ — производить весь ввод-вывод при помощи потоков.

Поток — это последовательность байтов, которые перемещаются от исходного

к целевому устройству по каналу связи.

Потоки реализованы в виде классов библиотеки iostream.

Привязка потоков к физическим устройствам осуществляется системой ввода-вывода C++.

Программа C++ выполняет ввод-вывод, считая/записывая данные из потока /

в поток.

Преимущества применения потоков.

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

• Методы одинаковы, а их реализация меняется в зависимости от устройства.

Предопределенные потоковые объекты:

• cin, cout, cerr, clog — ввод-вывод символов в кодировке ASCII;

cin — объект класса istream;

cout, cerr, clog — объекты класса ostream;

• wcin, wcout, wcerr, wclog — ввод-вывод символов в кодировке Unicode; wcin — объект класса wistream;

wcout, wcerr, wclog — объекты класса wostream.

Существует множество манипуляторов потоков, которые помогают управлять

выводом при его отправке на выводное устройство.

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

′iomanip′.

Система ввода-вывода в C++

223

Возможно создание пользовательских манипуляторов.

При помощи классов, представленных в 'fstream', можно производить три типа

файловых операций ввода-вывода:

• Ввод-вывод символов — чтение/запись файла по символам.

• Ввод-вывод строк — чтение/запись файла по строкам.

• Ввод-вывод записей — чтение/запись файла по записям.

Для прямого доступа к файлу полезны следующие функции:

• seekg( ), seekp( ) — устанавливают в файле позицию указателей считывания

и записи

• tellg( ), tellp( ) — возвращают текущую позицию в файле указателей считывания и записи.

Существует несколько режимов открытия файла.

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

• istrstream — чтение данных из строки;

• ostrstream — запись данных в строку.

Ввод-вывод объектов можно осуществлять, перегружая операторы << и >>.

Сериализация — запись объекта в файл.

Десериализация — чтение объекта из файла.

Если при открытии файла возникает ошибка, то ее причина указывается в битах

переменной состояния.

Взаимодействие с файловой системой зависит от платформы.

Программы на C++ позволяют создавать, изменять, удалять файлы и каталоги, а также перемещаться по ним.

Расширенные возможности C++

Возможно, более специализированные средства языка,

описанные в настоящей главе, и не так часто

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

усовершенствуете ваши навыки программирования на C++

226

Глава 9

Отношения включения

Дружественные (friend) функции и классы

Еще одно применение дружественной функции

Предупреждение

Ключевое слово explicit

Ключевое слово mutable

Пространство имен

Способы применения пространства имен

• Использование оператора разрешения контекста

• Ключевое слово using

Динамическая идентификация типа (RTTI)

Приведение типов в C++

• static_cast

• dynamic_cast

• const_cast

• reinterpret_cast

• Предупреждение

Указатели на члены классов

Упражнения

Важное

Расширенные возможности C++

227

этой главе рассматривается набор тем, с которыми новички обычно не

сталкиваются. Однако по мере того, как вы углубитесь в программирование

В на C++, вы начнете ценить полезность этих продвинутых средств. Я плани-рую осветить здесь такие темы, как отношения включения, дружественные функции, ключевые слова explicit и mutable, пространства имен, динамическая идентификация типа данных (RTTI) и новый синтаксис приведения.

И начнем мы с отношений включения.

Отношения включения

Нам уже известны отношения наследования между классами. При наследовании, если класс Y выведен из класса X, мы говорим, что новый класс Y подобен старому

классу X, или, еще проще, «Y подобен X». Это означает, что класс Y обладает всеми характеристиками класса X и, сверх того, рядом собственных черт. Это все равно, что сказать, что грузовик подобен автомобилю: у грузовика есть свойства, общие для всех автомобилей (наличие колес и двигателя, работает на топливе и т. п.), но имеет некоторые собственные отличительные характеристики (например, ку-зов). По этой причине наследование часто называют «отношениями сходства».

Существует еще один тип отношений между двумя родственными классами. Он

известен как «отношения принадлежности». Допустим, есть два связанных класса — Carburettor (Карбюратор) и Car (Автомобиль). Это родственные классы, поскольку оба связаны с транспортным средством. Тем не менее ни карбюратор не

похож на автомобиль, ни автомобиль не похож на карбюратор. На самом деле отношения между ними могут быть только такими: у автомобиля «есть» карбюратор, или, другими словами, автомобиль содержит в себе карбюратор. Это и называется

«отношениями включения», или «композицией».

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

включения между классом Car и классом Carburettor:

#include <iostream>

#include <string>

using namespace std ;

class Carburettor

{

private :

char type ;

float cost ;

string mfr ;

public :

void setData ( char t, float c, string m )

228

Глава 9

{

type = t ;cost = c ;mfr = m ;

}

void displayData( )

{

cout << type << endl << cost << endl << mfr << endl ;

}

} ;

class Car

{

private :

string model ;

string drivetype ;

Carburettor cc ; // внедренный объект

public :

void setData ( char t, float c, string mf, string m, string d )

{

model = m ;

drivetype = d ;

cc.setData( t, c, mf ) ;

}

void displayData( )

{

cout << model << endl << drivetype << endl ; cc.displayData( ) ;

}

} ;

int main( )

{

Car mycar ;

mycar.setData ( 'A', 18500.00, "Mico", "Спортивный",

"С приводом на 4 колеса" ) ;

mycar.displayData( ) ;

return 0 ;

}

Результат работы программы:

Спортивный

С приводом на 4 колеса

A

18500

Mico

Расширенные возможности C++

229

Дружественные (friend) функции и классы

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

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

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

ключевое слово friend (друг, дружественный). Следующая программа поможет

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

практике:

#include <iostream>

using namespace std ;

class Sample2 ;

class Sample1

{

private :

int data1 ;

public :

Sample1( )

{

data1 = 100 ;

}

friend void accessOne ( Sample1 ) ;

friend void accessBoth ( Sample1, Sample2 ) ;

} ;

class Sample2

{

private :

int data2 ;

public :

Sample2( )

{

data2 = 200 ;

}

friend void accessBoth ( Sample1, Sample2 ) ;

} ;

void accessOne ( Sample1 x )

{

cout << x.data1 << endl ;

}

void accessBoth ( Sample1 x, Sample2 y )

{

cout << x.data1 + y.data2 << endl ;

}

230

Глава 9

int main( )

{

Sample1 a ;

Sample2 b ;

accessOne ( a ) ;

accessBoth ( a, b ) ;

return 0 ;

}

Здесь мы объявляем два класса, Sample1 и Sample2. Конструкторы в этих классах

инициализируют свои закрытые члены данных фиксированным значением

(100 в Sample1 и 200 в Sample2). Мы хотим, чтобы функция accessOne( ) имела

доступ к закрытым данным Sample1, а функция accessBoth( ) — к закрытым данным обоих классов. Для этого мы объявили обе функции дружественными при

помощи ключевого слова friend:

friend void accessOne ( Sample1 ) ;

friend void accessBoth ( Sample1, Sample2 ) ;

Из них первое объявление было помещено в Sample1, поскольку функции

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

Объект Sample1 передается функции accessOne( ), тогда как функции accessBoth( ) передаются оба объекта, Sample1 и Sample2. Будучи «друзьями» классов, accessOne( ) может обращаться к закрытым данным Sample1, а accessBoth( ) —

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

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

никогда не передается. Рассмотрим объявление в начале программы

class Sample2 ;

Такая декларация необходима, поскольку на класс нельзя ссылаться до тех пор, по-ка он не будет объявлен. Объявление функции accessBoth( ) в классе Sample1 ссылается на класс Sample2. Поэтому Sample2 должен быть объявлен перед Sample1.

Эта декларация сообщает компилятору, что класс Sample2 определен далее.

Этот аспект дружественной функции — доступ к закрытым данным класса в глобальной функции — демонстрировался дважды в главе 8, один раз — при определении параметризированного пользовательского манипулятора, а другой — при

перегрузке операторов << и >>. Сейчас вы можете вернуться к этим разделам и внимательно изучить программный код в свете того, что мы здесь узнали.

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

спецификатор friend ко всему классу, как показано ниже:

Расширенные возможности C++

231

class Sample2 ;

class Sample1

{

private :

// поля данных

public :

// функции-члены

friend Sample2 ;

} ;

Поскольку Sample2 помечен как дружественный класс, все его функции-члены

могут обращаться к закрытым данным класса Sample1.

Еще одно применение

дружественной функции

Дружественные функции иногда используются для повышения универсальности

перегруженных операторов. Попытаемся освоить этот способ применения дружественных функций на примере программы, приведенной ниже:

#include <iostream>

using namespace std ;

class Example

{

private :

int i ;

float j ;

public :

Example ( int ii = 0, float jj = 0.0f )

{

i = ii ;

j = jj ;

}

void showData( )

{

cout << i << " " << j << endl ;

}

Example operator * ( Example e )

{

Example temp ;

temp.i = i * e.i ;

temp.j = j * e.j ;

return ( temp ) ;

}

} ;

232

Глава 9

int main( )

{

Example e1 ( 10, 3.14f ), e2 ( 2, 1.5f ), e3, e4, e5 ;

e3 = e1 * e2 ;

// Example * Example работает

e4 = e1 * 2 ; // Example * int работает

e3.showData( ) ;

e4.showData( ) ;

// e5 = 2 * e1 ;

// int * Example не работает

return 0 ;

}

В этой программе мы определили класс Example, имеющий int и float в качестве

закрытых членов данных. У него также есть две функции-члена — конструктор

и перегруженный оператор * для умножения двух объектов класса Example.

Выражение

e3 = e1 * e2 ;

внутри преобразуется в

e3 = e1.operator * ( e2 ) ;

Это вызывает функции перегрузки оператора и возвращает произведение двух объектов Example.

Аналогичным образом выражение

e4 = e1 * 2 ;

внутри преобразуется в

e4 = e1.operator * ( 2 ) ;

Число 2, передаваемое операторной функции, необходимо преобразовать в объект

типа Example. Компилятор задействует конструктор для преобразования этого

целого числа в объект класса Example, а затем выполняет умножение.

Будет ли работать следующее выражение?

e5 = 2 * e1 ;

Нет, поскольку внутри оно преобразуется в

e5 = 2.operator ( e1 ) ;

не имеющее смысла прежде всего потому, что 2 не является объектом. Компилятор

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

образом.

#include <iostream>

using namespace std ;

Расширенные возможности C++

233

class Example

{

private :

int i ;

float j ;

public :

Example ( int ii = 0, float jj = 0.0f )

{

i = ii ;

j = jj ;

}

void showData( )

{

cout << i << "\t" << j << endl ;

}

friend Example operator * ( Example, Example ) ;

} ;

Example operator * ( Example k, Example l )

{

Example temp ;

temp.i = k.i * l.i ;

temp.j = k.j * l.j ;

return ( temp ) ;

}

int main( )

{

Example e1 ( 10, 3.14f ), e2 ( 1, 1.5f ), e3, e4, e5 ;

e3 = e1 * 2 ;

e4 = 2 * e2 ;

e5 = e1 * e2 * 2 ;

e3.showData( ) ;

e4.showData( ) ;

e5.showData( ) ;

return 0 ;

}

Заметьте, что в предыдущей программе функция operator *( ) принимала только

один аргумент, тогда как в этой программе она принимает два аргумента. Причина

в том, что операторная функция больше не является функцией-членом класса.

Теперь это друг класса Example. Поэтому инструкция

e3 = e1 * 2 ;

не преобразуется в вид

e3.operator * ( 2 )

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

позволяет повысить функциональность перегруженных операторов.

234

Глава 9

Предупреждение

Использование дружеских функций порой вызывает споры. Хотя они и добавляют

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

могут получить доступ к закрытым данным класса.

Впрочем, C++ не является стопроцентно объектно-ориентированным языком.

Одной из причин этого и является возможность доступа к закрытым данным за

пределами класса при помощи дружественных функций. В конце концов, C++ был

разработан для решения практических задач программирования, а не для победы на

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

тому и быть.

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

Ключевое слово explicit

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

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

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

применение:

#include <iostream>

using namespace std ;

class Complex

{

private :

float real, imag ;

public :

Complex ( float r = 0.0f, float i = 0.0f )

{

real = r ;

imag = i ;

}

Complex operator + ( Complex c )

{

Complex t ;

Расширенные возможности C++

235

t.real = real + c.real ;

t.imag = imag + c.imag ;

return t ;

}

void display( )

{

cout << real << " " << imag << endl ;

}

} ;

int main( )

{

Complex c1 ( 1.5, 3.5 ), c2 ;

c2 = c1 + 1.25f ;

c2.display( ) ;

return 0 ;

}

В выражении

c2 = c1 + 1.25f ;

должна вызываться функция перегруженного оператора operator +( ). Компилятор

обнаруживает, что эта функция ожидает аргумент типа Complex, тогда как мы

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

Однако инструкция

c2 = 1.25f + c1 ;

не работает.

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

c2 = c1 + 1.25f ;

c2 = 1.25f + c1 ;

Этого можно гарантированно добиться, объявив конструктор с ключевым словом

explicit, как показано ниже:

explicit Complex ( float r = 0.0f, float i = 0.0f )

{

real = r ;i

mag = i ;

}

Теперь при любой попытке неявно преобразовать вещественный тип в Complex компилятор сообщит, что не может выполнить преобразование.

Заметьте, что ключевое слово explicit работает только с конструкторами.

236

Глава 9

Ключевое слово mutable

При создании константного объекта ни один из его членов данных не подлежит

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

#include <iostream>

#include <string>

using namespace std ;

class Car

{

private :

string model ;

mutable string owner ;

int yrOfMfg ;

string regNo ;

public :

Car ( string m, string o, int y, string r )

{

model = m ;owner = o ;yrOfMfg = y ;regNo = r ;

}

void changeOwner ( string o ) const

{

owner = o ;

}

void changeModel ( string m )

{

model = m ;

}

void display( ) const

{

cout << model << endl << owner << endl

<< yrOfMfg << endl << regNo << endl << endl ;

}

} ;

int main( )

{

const Car c1 ( "VX", "Fundu", 2000, "MH31-G6175" ) ; c1.display( ) ;

c1.changeOwner ( "MahaFundu" ) ;

c1.display( ) ;

Расширенные возможности C++

237

// c1.changemodel ( "AX" ) ;

// выдает ошибку

c1.display( ) ;

return 0 ;

}

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

const, ни один из его членов данных не может быть изменен. Однако для поля

«владелец» (owner) делается исключение, поскольку его объявлению предшествует

ключевое слово mutable. Модификация осуществляется посредством функции

changeOwner( ). Попробуйте удалить комментарий в основной функции main( ).

Это приведет к ошибке, поскольку поле «модель» (model) не объявлено как

mutable и, следовательно, не может быть изменено. Если бы c1 не был const-объектом, можно было бы изменить как поле «владелец», так и поле «модель».

Пространство имен

Как ни банально это звучит, но создание имен — одно из самых основных действий

в программировании. Имена переменных, имена массивов, имена функций, имена

структур, имена классов, имена объединений, имена перечислений — все они под-падают под одну общую категорию — имена. При написании больших программ

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

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

// mylib.h char fun1( ) ;

void display( ) ;

class BigNumber { ... } ;

// somelib.h

class BigNumber { ... } ;

void display( ) ;

Если подключить к программе оба файла, возникнет конфликт между двумя классами BigNumber и двумя функциями display( ), что, как следствие, вызовет ошибки.

Одним из решений может стать использование длинных имен для уменьшения ве-роятности случайных совпадений. Но тогда придется оперировать этими длинными

именами. Кроме того, это всего лишь компромиссное решение, придуманное про-граммистами, а не заложенное в концепцию самого языка. Решение подобного рода

вопросов при помощи инструкции namespace, предлагаемое C++, намного лучше.

Язык C++ предоставляет единое глобальное пространство имен. Используя такое

средство, как namespace, можно разбить глобальное пространство имен на более

специализированные декларативные области. Фрагмент кода, приведенный далее, показывает, как осуществить это на практике:

// mylib.h namespace myLib

{

char fun1( ) { ... } ;

238

Глава 9

void display( ) { ... } ;

class BigNumber { ... } ;

}

// somelib.h namespace someLib

{

class BigNumber { ... } ;

void display( ) { ... } ;

}

Теперь имена классов не будут конфликтовать, поскольку они преобразуются

в myLib::BigNumber и someLib::BigNumber соответственно. То же самое произойдет и с именами функций. Они преобразуются в myLib::display( ) и

someLib::display( ), что позволяет избежать нежелательного наложения имен.

Теперь можно без конфликтов использовать одно и то же имя, но в разных контекстах. До тех пор, пока они появляются в разных пространствах имен, каждое имя

будет уникальным благодаря добавлению идентификатора namespace.

Далее приведем ряд моментов о пространствах имен, которые следует помнить: Синтаксис создания пространства имен (namespace) аналогичен синтаксису

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

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

Определение пространства имен может быть разбито на несколько заголовочных файлов, как показано ниже:

// mylib.h namespace myLib

{

char fun1( ) { ... } ;

void display( ) { ... } ;

}

// mylib1.h namespace myLib

{

extern int var1 ;

void display( ) { ... } ;

}

Когда пространство имен разбито таким образом, после его первичного определения продолжение называется определением расширения пространства имен

(extension-namespace-definition).

Пространству имен можно задать альтернативное имя, что предотвращает ввод

громоздких имен. Это псевдоним пространства имен (namespace-alias): namespace hardAndSoftLibrary

{

class hwItem { ... } ;

Расширенные возможности C++

239

class swItem { ... } ;

}

namespace hwsw = hardAndSoftLibrary ;

Имя глобального пространства имен не должно совпадать с любым другим именем глобального объекта в данной программе.

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

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

namespace mine

{

void fun1( ) ;

}

void mine::fun1( )

{

}

Если имя в пространстве имен определяется вне его пределов, то определение

должно стоять после точки объявления.

void mine::fun2( ) // ошибка, fun2( ) еще не член пространства имен mine

{

}

namespace mine

{

void fun2( ) ;

}

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

int main( )

{

namespace local // ошибка: не в глобальном контексте

{

}

return 0 ;

}

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

пространства имен, к примеру:

namespace outer

{

int n = 6 ;

int fun2( ) ;

namespace inner

{

float a = 3.14 ;

}

}

240

Глава 9

Способы применения пространства имен

Обратиться к имени в пространстве имен можно двумя способами:

При помощи оператора разрешения контекста.

Посредством ключевого слова using.

Разберем каждый из них по очереди.

Использование оператора разрешения контекста

Можно указать любое имя в пространстве имен, используя оператор разрешения

контекста, как проиллюстрировано ниже:

#include <iostream>

#include <string>

using namespace std ;

namespace mine

{

class MyClass

{

private :

int yr ;

public :

void changeYear( ) ;

} ;

class YourClass ;

void fun1( ) ;

}

void mine::MyClass::changeYear( )

{

yr = 2000 ;

cout << "Годы не меняются" ;

}

class mine::YourClass

{

public :

YourClass( ) ;

void show( ) ;

} ;

mine::YourClass::YourClass( )

{

cout << "Достигнут непараметризированный конструктор YourClass" << endl ;

}

Расширенные возможности C++

241

void mine::YourClass::show( )

{

cout << "Сделай. И забудь" << endl ;

}

void mine::fun1( )

{

cout << "Будь спонтанным. Соблюдай осторожность" << endl ;

}

int main( )

{

mine::MyClass m ;

m.changeYear( ) ;

mine::fun1( ) ;

mine::YourClass y ;

y.show( ) ;

return 0 ;

}

Получение имени в пространстве имен при помощи оператора разрешения контекста несколько утомительно. Гораздо удобнее воспользоваться ключевым словом

using.

Ключевое слово using

Ключевое слово using позволяет импортировать сразу все пространство имен

вместо того, чтобы использовать оператор разрешения контекста перед каждым

именем. Внесем соответствующую правку в предыдущую программу и оценим

удобство этого метода:

#include <iostream>

#include <string>

using namespace std ;

namespace mine

{

class MyClass

{

private :

int yr ;

public :

void changeYear( ) ;

} ;

class YourClass ;

void fun1( ) ;

}

void mine::MyClass::changeYear( )

{

yr = 2000 ;

242

Глава 9

cout << "Годы не меняются" << endl ;

}

class mine::YourClass

{

public :

YourClass( ) ;

void show( ) ;

} ;

mine::YourClass::YourClass( )

{

cout << "Достигнут непараметризированный конструктор YourClass" << endl ;

}

void mine::YourClass::show( )

{

cout << "Сделай. И забудь" << endl ;

}

void mine::fun1( )

{

cout << "Будь спонтанным. Соблюдай осторожность" << endl ;

}

int main( )

{

using namespace mine ;

MyClass m ;

m.changeYear( ) ;

fun1( ) ;

YourClass y ;

y.show( ) ;

return 0 ;

}

Ключевое слово using объявляет, что все имена в пространстве имен находятся

в текущей области. Таким образом, мы можем использовать имена без каких-либо

спецификаторов.

Динамическая идентификация типа (RTTI)

RTTI означает «идентификация типа во время выполнения» (Run Time Type Identification) или динамическая идентификация типа. Как следует из самого названия, это средство позволяет узнать тип объекта во время выполнения программы. Как мы узнали в главе 7, при использовании виртуальных функций необходимо

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

Расширенные возможности C++

243

Тип объекта можно установить во время выполнения программы посредством оператора typeid. Следующий листинг иллюстрирует его применение:

#include <typeinfo>

#include <iostream>

using namespace std ;

class Base

{

public :

virtual void fun1( ) { }

} ;

class MyClass : public Base { } ;

class YourClass : public Base { } ;

int main( )

{

Base *b1, *b2 ;

MyClass m ;

YourClass y ;

b1 = &m ;

b2 = &y ;

cout << typeid ( b1 ).name( ) << endl ;

cout << typeid ( b2 ).name( ) << endl ;

cout << typeid ( *b1 ).name( ) << endl ;

cout << typeid ( *b2 ).name( ) << endl ;

if ( typeid ( *b1 ) == typeid ( *b2 ) )

cout << "Соответствие" << endl ;

else

cout << "Несоответствие" << endl ;

cout << typeid ( 45 ).name( ) << endl ;

cout << typeid ( '4' ).name( ) << endl ;

cout << typeid ( 4.5 + 2.2 ).name( ) << endl ;

return 0 ;

}

Результат работы программы:

class Base *

class MyClass

class YourClass

Несоответствие

int

char

double

244

Глава 9

В этой программе мы вывели два класса — MyClass и YourClass — из базового

класса Base. Затем мы создали два указателя базового класса b1 и b2 и два объекта

производных классов m и y. Далее мы выполнили преобразование с приведением

с помощью команд:

b2 = &y ;

b1 = &m ;

Затем мы использовали оператор typeid для получения типов указателей, а также

адресуемых ими типов. Оператор typeid находит тип своего операнда, сохраняет

его в const-объекте типа typeinfo и возвращает на него ссылку. При помощи этой

ссылки мы можем вызвать функцию name( ) для получения имени типа в строко-вом формате (string).

Сравнение типов, возвращаемых typeid, также можно осуществлять при помощи

операторов == и !=. Оператор typeid также применим к встроенным типам данных

или даже к целым выражениям.

Приведение типов в C++

C++ — это строго типизированный язык. Это означает, что любое несоответствие

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

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

типа в другой, используя операцию приведения типов.

Фрагмент кода, приведенный ниже, иллюстрирует подобные преобразования: float x = 3.14 ;int y ;

y = ( int ) x ; // приведение типов в стиле C

y = int ( x ) ; // приведение типов в стиле C++

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

типах, то такой программный код может вызвать ошибки во время выполнения или

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

Чтобы надежно обеспечить обнаружение таких ошибок во время компиляции, C++

предоставляет 4 оператора приведения типов. Эти операторы и их синтаксис приведены ниже:

static_cast < тип > ( выражение )

dynamic_cast < тип > ( выражение )

reinterpret_cast < тип > ( выражение )

const_cast < тип > ( выражение )

Давайте подробно рассмотрим каждый из них.

Расширенные возможности C++

245

static_cast

Оператор static_cast применяется в четко определенных преобразованиях: Преобразования ограничивающего типа.

Преобразования к пустому типу (void*).

Их действие продемонстрировано в следующей программе. Не пропустите указания в комментариях.

#include <iostream>

using namespace std ;

class Base { } ;

class Derived : public Base { } ;

class Sample { } ;

int main( )

{

int i = 10 ;

long l ;

float f ;

char str[ ] = "Nagpur" ;

// преобразования ограничивающего типа

i = static_cast < int > ( l ) ;

i = static_cast < int > ( f ) ;

// преобразование к void-указателю для отображения адреса

void *vptr ;

vptr = static_cast < void * > ( str ) ;

cout << vptr << endl ;

// преобразование несвязанных типов

Sample *sptr ;

sptr = ( Sample * ) baseptr ;

// sptr = static_cast < Sample * > ( baseptr ) ;

// ошибка

return 0 ;

}

Большая часть программы не нуждается в пояснениях. Мы сосредоточимся на двух

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

на класс Base в несвязанный указатель на класс Sample, используя традиционный

механизм приведения, это не вызывает сообщение об ошибке. Но стоит нам попытаться сделать то же самое посредством static_cast, ошибка тут как тут. Связано это

с тем, что static_cast не позволяет нам бесконтрольно осуществлять приведение

типов, что делает этот оператор надежнее традиционных средств преобразования.

246

Глава 9

dynamic_cast

Этот оператор приведения типов применяется лишь с указателями и ссылками на

классы либо с пустым указателем (void*). Он называется «динамическим», поскольку проверяет во время выполнения программы, можем ли мы «безопасно»

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

указатель для вызова функций.

Рассмотрим механизм динамической проверки в действии:

#include <iostream>

#include <string>

using namespace std ;

class Employee

{

private :

string name ;

public:

Employee ( string n )

{

name = n ;

}

virtual void showData( )

{

cout << "Имя: " << name << endl ;

}

} ;

class Manager : public Employee

{

private :

double commision ;

public:

Manager ( string n, double comm ) : Employee ( n )

{

commision = comm ;

}

virtual void showData( )

{

cout << "Комиссия :" << commision << endl ;

}

} ;

int main( )

{

Employee e1 ( "Dinesh" ) ;

Manager m1 ( "Suresh", 3000.0 ) ;

Расширенные возможности C++

247

Employee *pemp1, *pemp2 ;

Manager *pmgr ;

pemp1 = &e1 ;

pemp2 = &m1 ;

pmgr = dynamic_cast < Manager * > ( pemp1 ) ;

cout << "pmgr = " << pmgr << endl ;

if ( pmgr )

pmgr -> showData( ) ;

else

cout << "при приведении вниз pmgr содержит null" << endl ; pmgr = dynamic_cast < Manager * > ( pemp2 ) ;

cout << "pmgr = " << pmgr << endl ;

if ( pmgr )

pmgr -> showData( ) ;

else

cout << "при приведении вниз pmgr содержит null" << endl ;

}

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

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

операцию.

Когда мы совершаем приведение вниз при помощи dynamic_cast, этот оператор

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

Если нет, он присваивает указателю значение 0.

Давайте проследим, как реализуется этот механизм в нашей программе. Рассмотрим следующие инструкции:

pmgr = dynamic_cast < Manager * > ( pemp1 ) ;

pmgr = dynamic_cast < Manager * > ( pemp2 ) ;

Здесь pemp1 хранит адрес объекта Employee, а pemp2 — адрес объекта Manager.

Поэтому, когда мы пытаемся преобразовать pemp1 в pmgr (целевой тип указателя), преобразование невозможно, т. к. pemp1 содержит адрес объекта Employee. И потому в этом случае pmgr присваивается null.

Вторая операция приведения работает, т. к. в этом случае pmgr указывает на объект класса Manager.

Позволим себе ряд полезных советов:

Если бы мы использовали в программе static_cast или традиционный механизм

преобразования, то проверка условия во время выполнения не запустилась бы.

Оператор static_cast не видит здесь ошибки, и это может ввести в заблуждение.

248

Глава 9

Хотя преобразования при помощи dynamic_cast безопаснее, этот оператор работает лишь с указателями или ссылками, тогда как проверка типа во время выполнения повышает затраты вычислительных ресурсов.

Поскольку приведение вверх является неявным преобразованием, то не имеет

значения, пользуемся ли мы static_cast, dynamic_cast или вообще совершаем

приведение типов.

Если вместо указателей используются ссылки и не удается осуществить динамическое приведение, то выдается исключение типа bad_cast.

const_cast

Этот оператор приведения позволяет манипулировать const-статусом объекта. С его

помощью можно преобразовать константный объект в неконстантный или наоборот.

#include <iostream>

using namespace std ;

void display ( char * ) ;

int main( )

{

const char *ptr = "В чем дело?" ;

display ( const_cast < char * > ( ptr ) ) ;

return 0 ;

}

void display ( char *p )

{

cout << p << endl ;

}

Поскольку функция display( ) ожидает тип char *, необходимо преобразовать const char * в char * перед тем, как передать ее в display( ).

Удалять const-статус объекта не рекомендуется. Приведенная выше программа работает, поскольку функция display( ) не пытается манипулировать объектом, на

который указывает p. Если мы попытаемся сделать это при помощи инструкции

вроде

*p = ’H’ ;

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

выполнения.

reinterpret_cast

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

Приведение указателя к целому числу и наоборот.

Расширенные возможности C++

249

Первое преобразование просто копирует значение из одного указателя в другой, предоставляя программисту возможность решить, имеет ли это преобразование

смысл или нет. Этот механизм приведения наименее безопасен и нередко служит

причиной программных ошибок.

Предупреждение

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

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

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

Помните, когда вы задействуете приведение типов, вы нарушаете систему типов.

И это чревато опасными последствиями.

Указатели на члены классов

Мы знаем, что для доступа к элементу структуры мы используем оператор '.' или

'->'. Кроме того, для разыменования указателя мы используем оператор *. Давайте

посмотрим, как это работает на практике:

int i ;

int *ptr ;

struct Emp

{

char name ;

int age ;

} ;

Emp e, *eptr ;

ptr = &i ;

cout << *ptr ; // разыменование

cout << e.name ; // доступ

cout << eptr -> name ; // доступ

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

Коль скоро элементы структуры размещаются в смежных участках памяти, адрес

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

требуется оператор '.' или '->', а также оператор '*' для разыменования указателя.

Для одновременного доступа и разыменования C++ располагает двумя новыми

операторами: '.*' и '->*'. Они известны как операторы указателей на члены класса

(pointer to member operator). Следующая программа иллюстрирует их применение:

#include <iostream>

using namespace std ;

struct Sample

250

Глава 9

{

int a ;

float b ;

} ;

int main( )

{

int Sample::*p1 = &Sample::a ;

float Sample::*p2 = &Sample::b ;

Sample so = { 10, 3.14f } ;

cout << so.*p1 << endl << so.*p2 << endl ;

Sample *sp ;sp = &so ;

cout << sp->*p1 << endl << sp->*p2 << endl ;

// можно даже присваивать новые значения

so.*p1 = 20 ;

sp->*p2 = 6.28f ;

cout << so.*p1 << endl << so.*p2 << endl ;

cout << sp->*p1 << endl << sp->*p2 << endl ; return 0 ;

}

Рассмотрим определение указателей p1 и p2:

int Sample::*p1 = &Sample::a ;

float Sample::*p2 = &Sample::b

Обратите внимание на ту часть выражения, что расположена перед оператором

присваивания. Звездочки указывают на то, что p1 и p2 являются указателями.

Sample:: говорит о том, что они адресуют переменные целого и вещественного

типа в Sample.

Мы инициализировали эти указатели при их объявлении с адресами a и b соответственно.

На самом деле нет «адреса» Sample::a, поскольку мы ссылаемся на сам класс, а не

на объект этого класса. Выражение &Sample::a просто создает смещение в классе.

Фактический адрес будет получен, когда мы объединим это смещение с начальным

адресом определенного объекта.

Таким образом, &Sample::a есть не что иное, как синтаксис указателя на член

класса. Если мы используем p1 и p2 с одним объектом, мы получим один набор

значений, если используем его с другим, то получим другой набор значений. Это

наглядно продемонстрировано ближе к концу программы, где мы построили массив объектов и получили доступ ко всем полям объектов при помощи p1 и p2. Суть

в том, что указатели на члены не привязаны ни к какому конкретному объекту.

Расширенные возможности C++

251

Слева от '.*' всегда должна быть структурная переменная (объект) или ссылка, а слева от '->*'’ всегда должен быть указатель на структуру.

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

public-статус данным класса, как это сделано в приведенной выше программе?

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

При помощи указателей на члены классов можно гибко выбирать функцию-член

для вызова во время выполнения. Это позволяет направлять или изменять поведение программы во время выполнения. Звучит не слишком конкретно? Что ж, скоро

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

#include <iostream> using namespace std ;

int main( )

{

void fun ( int, float ) ;

void ( *p ) ( int, float ) ;

p = fun ;

( *p ) ( 10, 3.14f ) ;

return 0 ;

}

void fun ( int a, float b )

{

cout << a << endl << b << endl ;

}

Здесь p — указатель на функцию, которая получает int и float и возвращает void.

Заметьте, что *p нужно обязательно заключить в круглые скобки. Перед нами объявление прототипа функции p, которая получает int и float и возвращает void *.

Мы инициализировали p с адресом функции fun( ). Упоминание имени функции

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

можем воспользоваться оператором & для получения адреса, как в

p = &fun ;

Синтаксис обращения к fun( ) посредством p следующий:

( *p )( 10, 3.14 ) ;

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

каждую функцию по очереди, как показано в следующем фрагменте кода: void ( *p[ 3 ] ) ( int, float ) = { fun1, fun2, fun3 } ;

for ( int i = 0 ;i <= 2 ;i++ )

( *p[ i ] ) ( 14 + i, 5.54 + i ) ;

Одним из основных условий работы этого кода является совпадение прототипов

функций fun1( ), fun2( ) и fun3( ); иначе мы не сможем собрать их адреса в массиве p[ ].

252

Глава 9

Давайте продвинемся еще на шаг и сделаем fun1( ), fun2( ) и fun3( ) открытыми

членами класса Sample, а затем попробуем вызвать их способом, подобным тому, что мы применили выше:

#include <iostream>

using namespace std ;

class Sample

{

public :

void fun1( )

{

cout << this << "В теле fun1" << endl ;

}

void fun2( )

{

cout << this << "В теле fun2" << endl ;

}

void fun3( )

{

cout << this << "В теле fun3" << endl ;

}

} ;

int main( )

{

Sample so[ 4 ] ;

void ( Sample::*p[ 3 ] )( ) = {

&Sample::fun1, &Sample::fun2,

&Sample::fun3

} ;

for ( int j = 0 ;j <= 3 ;j++ )

{

for ( int i = 0 ;i <= 2 ;i++ ) ( so[ j ].*p[ i ] )( ) ;

}

return 0 ;

}

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

вызывается функция-член.

Так выглядит результат работы программы:

002BF994 В теле fun1

002BF994 В теле fun2

002BF994 В теле fun3

002BF995 В теле fun1

Расширенные возможности C++

253

002BF995 В теле fun2

002BF995 В теле fun3

002BF996 В теле fun1

002BF996 В теле fun2

002BF996 В теле fun3

002BF997 В теле fun1

002BF997 В теле fun2

002BF997 В теле fun3

1. Укажите, верны ли следующие утверждения:

Такое явление, как создание класса внутри класса, называется композицией.

«Композиция» и «отношения включения» — это одно и то же.

Как отношения включения, так и наследование являются средствами многократного использования программного кода.

Дружественная функция может получить доступ к закрытым членам данных

класса, но не может их модифицировать.

Если весь класс становится другом другого класса, то все функции-члены этого

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

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

этому классу.

При помощи интеллектуального указателя (smart pointer) можно выполнять

итерации по всем элементам объекта-контейнера.

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

указателем.

Перегруженный оператор -> называется оператором интеллектуального указателя.

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

Указатели на члены классов не привязаны ни к какому конкретному объекту.

Определение пространства имен может быть разбито на несколько заголовочных файлов.

2. Ответьте на вопросы:

Должны ли данные класса иметь public-статус при использовании указателя на

член класса?

В каких случаях в конструкторе следует использовать ключевое слово explicit?

Может ли оно использоваться с какой-либо другой функцией?

254

Глава 9

Каким образом можно изменить член данных const-объекта?

Если два заголовочных файла содержат одинаковые имена, как избежать кон-фликта имен при подключении обоих файлов?

Как присвоить альтернативное имя пространству имен?

3. Решите следующие задачи:

Составьте программу, в которой имеется класс с именем base. Выведите из него

два класса, derived1 и derived2. Занесите адреса объектов производных классов

в указатели базового класса. Задействовав typeid( ) и dynamic_cast, установите

тип объекта во время выполнения программы.

Напишите программу, преобразующую целочисленный указатель в целое число

и наоборот.

«Отношения включения» или «композиция» — это средство многократного использования объектного кода. Оно позволяет создать подобъект внутри объекта.

Функция может быть объявлена «другом» класса.

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

Назад: Для чего применять виртуальные функции?
Дальше: Глава 10. Шаблоны