CHAP8_2 (1018809), страница 3
Текст из файла (страница 3)
но я видел компиляторы, которые бракуют этот оператор, если в базовом классе не объявлена явно функция operator=(). Первая форма работает независимо от того, объявлена явно operator=(), или нет. (Если не объявлена, то у вас будет по умолчанию реализовано почленное копирование).
132. Конструкторы, не предназначенные для преобразования типов, должны иметь два или более аргумента.
С++ использует конструкторы для преобразования типов. Например, конструктор char* в 9-ой строке листинга 7 на странице 111 также обрабатывает следующую операцию приведения:
char *pchar = "абвг" ;
(string) pchar;
Запомните, что приведение является операцией времени выполнения, которая создает временную переменную нужного типа и инициализирует ее из аргумента. Если приводится класс, то для инициализации используется конструктор. Следующий код работает прекрасно, потому что строковая константа char* беспрепятственно преобразуется в string для передачи в функцию f():
f( const string &s );
// ...
f( "белиберда" );
Проблема состоит в том, что мы иногда не желаем позволить использовать конструктор для неявного преобразования типов. Рассмотрим следующий контейнер массива, которым поддерживается целочисленный конструктор, определяющий размер этого массива:
class array
{
// ...
public:
array( int initial_size );
};
Вероятно вы все же не захотите, чтобы следующий код работал:
f( const array &a );
// ...
f( isupper(*str) );
(Этот вызов передает f() пустой одноэлементный массив, если *str состоит из заглавных букв, или массив без элементов, если *str - из строчных букв).
Единственным способом подавления такого поведения является добавление второго аргумента в конструктор, потому что конструкторы с несколькими аргументами никогда не используются неявно:
class array
{
// ...
public:
enum bogus { set_size_to };
array( bogus, int initial_size );
};
array ar( array::set_size_to, 128 );
Это по настоящему уродливо, но у нас нет выбора. Заметьте, что я не дал аргументу bogus имени, потому что он используется только для выбора функции.
133. Используйте счетчики экземпляров объектов для инициализации на уровне класса.
Несколько разделов назад я рассматривал использование счетчика статических глобальных объектов для управления инициализациями на уровне библиотеки. В С++ у нас есть лучшие варианты, потому что мы может использовать определение класса для ограничения области действия:
class window
{
static int num_windows;
public:
window();
~window();
};
int window::num_windows = 0;
window::window()
{
if( ++num_windows == 1 ) // только что создано первое окно
initialize_video_system();
}
window::~window()
{
if( --num_windows == 0 ) // только что уничтожено последнее окно
shut_down_video_system();
}
Наконец, счетчик экземпляров объектов может быть также использован в качестве счетчика числа вызовов для обеспечения инициализации на уровне подпрограммы:
f()
{
static int have_been_called = 0;
if( !have_been_called )
{
have_been_called = 1;
do_one_time_initializations();
}
}
134. Избегайте инициализации в два приема.
135. Суперобложки на С++ для существующих интерфейсов редко хорошо работают.
Как правило, переменная должна инициализироваться во время объявления. Разделение инициализации и объявления иногда обусловливается плохим проектированием в программе, которая написана не вами, как в следующем фрагменте, написанном для выполнения совместно с библиотекой MFC Microsoft:
f( CWnd *win ) // CWnd - это окно
{
// Следующая строка загружает "буфер" с шапкой окна
// (текстом в строке заголовка)
char buf[80]; /* = */ win->GetWindowText(buf, sizeof(buf));
// ...
}
Так как я должен выполнить инициализацию при помощи явного вызова функции, то умышленно нарушаю свое правило "один оператор в строке" для того, чтобы по крайней мере вместить объявление и инициализацию в одной и той же строке.
Здесь имеется несколько проблем, первая из которых заключается в плохом проектировании класса CWnd (представляющем окно). Так как у окна есть "тестовый" атрибут, хранящий заголовок, то вы должны иметь возможность доступа к этому атрибуту подобным образом:
CString caption = win->caption();
и вы должны иметь возможность модифицировать этот атрибут так:
win->caption() = "новое содержание";
но вы не можете сделать этого в текущей реализации. Главная проблема состоит в том, библиотека MFC не была спро
ектирована в объектно-ориентированном духе - т.е. начать с объектов, затем выбрать, какие сообщения передавать между ними и какими атрибутами их наделить. Вместо этого проектировщики Microsoft начали от существующего процедурного интерфейса (API С - интерфейса прикладного программирования для Windows на С) и добавили к нему суперобложку на С++, тем самым увековечив все проблемы существующего интерфейса. Так как в API С была функция с именем GetWindowText(), то проектировщики беззаботно сымитировали такой вызов при помощи функции-члена в своей оболочке CWnd. Они поставили заплату на интерфейс при помощи следующего вызова:
CString str;
win->GetWindowText( str );
но это - не решение по двум причинам: по-прежнему требуется инициализация в два приема, и аргумент является ссылкой на результат.
Главный урок состоит в том, что проекты, основанные на процедурном подходе, радикально отличаются от объектно-ориентированных проектов. Обычно невозможно использовать код из одного проекта в другом без большой переработки. Простая оболочка из классов С++ вокруг процедурного проекта не сделает его объектно-ориентированным.
Поучительно, я думаю, пошарить вокруг в поисках решения текущей проблемы с помощью С++, но предупреждаю вас - здесь нет хорошего решения (кроме перепроектирования библиотеки классов). Моя первая попытка сделать оболочку вокруг CWnd показана на листинге 11.
Для обеспечения возможности win->text() = "Новый заголовок" необходим вспомогательный класс (window::caption). Вызов text() возвращает объект заголовка, которому затем передается сообщение присваиванием.
Главная проблема на листинге 11 заключается в том, что библиотека MFC имеет много классов, унаследованных от CWnd, и интерфейс, реализованный в классе window, не будет отражен в других потомках CWnd. С++ является компилируемым языком, поэтому нет возможности вставлять класс в средину иерархии классов без изменения исходного кода.
Листинг 12 определяет другое решение для смеси С++ с MFC. Я выделил класс window::caption в отдельный класс, который присоединяется к окну, когда оно инициализируется. Используется подобным образом:
f(CWnd *win)
{
caption cap( win )
CString s = cap; // поддерживается преобразование в CString.
cap = "Новый заголовок"; // использует операцию operator=(CString&)
}
Мне не нравится то, что изменение заголовка caption меняет также окно, к которому этот заголовок присоединен в этом последнем примере. Скрытая связь между двумя объектами может сама по себе быть источником недоразумений, будучи слишком похожей на побочный эффект макроса. Как бы то ни было, листинг 12 решает проблему инициализации.
Листинг 11. Обертка для CWnd: первая попытка.
-
class window : public CWnd
-
{
-
public:
-
class caption
-
{
-
CWnd *target_window;
-
private: friend class window;
-
caption( CWnd *p ) : target_window(p) {}
-
public:
-
operator CString ( void ) const;
-
const caption &operator=( const CString &s );
-
};
-
caption text( void );
-
};
-
//--------------------------------------------------------------
-
caption window::text( void )
-
{
-
return caption( this );
-
}
-
//--------------------------------------------------------------
-
window::caption::operator CString( void ) const
-
{
-
CString output;
-
target_window->GetWindowText( output );
-
return output; // возвращает копию
-
}
-
//--------------------------------------------------------------
-
const caption &window::caption::operation=( const CString &s )
-
{
-
target_window->SetWindowText( s );
-
return *this;
-
}
Листинг 12. Заголовочный объект
-
class caption
-
{
-
CWnd target_window;
-
public:
-
window_text( CWnd *win ) : target_window( win ) {};
-
operator const CString( void );
-
const CString &operator=( const CString &r );
-
};
-
inline caption::operator CString( void );
-
{
-
CString output;
-
target_window->GetWindowText( output );
-
return output;
-
}
-
inline const CString &caption::operator= ( const CString &s )
-
{
-
// возвращает тип CString (вместо типа заголовка "caption"),
-
// поэтому будет срабатывать
-
// a = b = "абв"
-
target_window->SetWindowText( s );
-
return s;
-
}
Глава 8.Д(E). Виртуальные функции
Виртуальные функции дают объекту производного класса способность модифицировать поведение, определенное на уровне базового класса (или предоставить какие-то возможности, в которых базовый класс испытывал потребность, но не мог их реализовать обычно из-за того, что информация, нужная для этой реализации, объявляется на уровне производного класса). Виртуальные функции являются центральными для объектно-ориентированного проектирования, потому что они позволяют вам определить базовый класс общего назначения, не требуя знания особенностей, которые могут быть предусмотрены лишь производным классом. Вы можете писать программу, которая думает, что манипулирует объектами базового класса, но на самом деле во время выполнения воздействует на объекты производного класса. Например, вы можете написать код, помещающий объект в обобщенную структуру данных data_structure, но на самом деле во время выполнения вставляет его в tree или linked_list (классы, производные от data_structure). Это такая фундаментальная объектно-ориентированной операцией, что программа на С++, которая не использует виртуальные функции, вероятно, просто плохо спроектирована.
136. Виртуальные функции - это те функции, которые вы не можете написать на уровне базового класса.
Виртуальные функции существуют ради двух целей. Во-первых, виртуальные функции определяют возможности, которые должны иметь все производные классы, но которые не могут быть реализованы на уровне базового класса. Например, вы можете сказать, что все объекты-фигуры shape должны быть способны себя распечатать. Вы не можете написать функцию print() на уровне базового класса, потому что геометрическая информация хранится в производных классах (круге circle, линии line, многоугольнике polygon и т.д.). Поэтому вы делаете print() виртуальной в базовом классе и фактически определяете эту функцию в производном классе.
Второй целью являются вспомогательные виртуальные функции. Возьмем в качестве примера наш класс storable. Для хранения объекта в сортированной структуре данных сохраняемый объект должен быть способен сравнивать себя с другим сохраненным объектом. То есть эта функция базы данных будет выглядеть примерно так:
add( storable *insert )
{
storable *object_already_in_database;
// ...
if( object_already_in_database->cmp(insert) < 0 )
// вставить объект в базу данных















