« Поставить закладку » « Сделать стартовой »

« Форумы » « Блоги » « Статьи » « Новости » « Файлы » « Realcoding IRC » « Site map » « Поиск »


Главная Главная
Анонсы Анонсы
Форумы Форумы
Каталог Каталог
Поиск Поиск
Опросы Опросы
Книжный магазин Книжный магазин
Реклама на сайте
Публикации Публикации
Партнеры Партнеры
Карта Карта сайта
Рассылки Рассылки
RSS экспорт
Настройки Настройки
О нас пишут О нас пишут
Контакты Контакты
Гостевая книга Гостевая книга

Тестирование 64-битных приложений

ПнВтСрЧтПтСбВс
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30          
    Популярное
PHP против ASP - делайте ставки, господа!

Работа с библиотеками динамической компоновки (DLL)

Добавляем внешний HTML-редактор

SQL Server 2005 Reporting Services (SSRS)

MyODBC 3.51.03: тонкости и советы

Работа с графиками и диаграммами (TChart, Series)

Описание функций C (Си) / C++ - sbrk

Конфигурирование GRE туннелей

Комплексные числа в .NET

Функция GetProfileString




    Архив файлов



    Сообщества



    Документация

Статьи:: C/С++ :: Правила программирования на С и С++ :: Глава 8. Ссылки


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

Глава 8. Ссылки





120. Ссылочные аргументы всегда должны быть константами.

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

Использование ссылочных аргументов в языке программирования вызвано четырьмя причинами:

• Они нужны вам для определения конструктора копии.
• Они нужны вам для определения перегруженных операций. Если вы определили:

some_class *operator+( some_class *left, some_class *right );

то вы должны сделать такое дополнение:

some_class x, y;
x = *(&x + &y)

Использование ссылок для аргумента и возвращаемого значения позволяет вам написать:

x = x + 1;

• Вы часто хотите передать объекты по значению, исходя из логики. Например, вы обычно в функцию передаете тип double, а не указатель на double. Тем не менее, тип double представляет собой 8-байтовую упакованную структуру с тремя полями: знаковым битом, мантиссой и порядком. Передавайте в этой ситуации ссылку на константный объект.

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

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

Проблема ссылочных аргументов - сопровождение. В прошлом году один из наших сотрудников написал следующую подпрограмму:

void copy_word( char *target, char *&src ) // src является ссылкой на char*
{
while( isspace(*src) )
++src; // Инкрементировать указатель,
// на который ссылается src.
while( *src && !isspace(*src) )
*target++ = *src++; // Передвинуть указатель,
// на который ссылается src,
// за текущее слово.
}

Автор полагал, что вы будете вызывать copy_word() многократно. Каждый раз подпрограмма копировала бы следующее слово в буфер target и продвигала бы указатель в источнике.

Вчера вы написали следующий код:

f( const char *p )
{
char *p = new char[1024];
load( p );

char word[64];
copy_word( word, p );
delete( p ); // Сюрприз! p был модифицирован, поэтому
} // весь этот участок памяти обращается в кучу мусора!

Главная проблема состоит в том, что, глядя на вызов copy_word( word, p ), вы не получаете подсказки о возможном изменении p в подпрограмме. Чтобы добраться до этой информации, вы должны взглянуть на прототип этой функции (который, вероятно, скрыт на 6-ом уровне вложенности в заголовочном файле). Огромные проблемы при сопровождении.

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

copy_word( word, &p );

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

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

f( const some_class &obj );

этот код вполне законен:

some_class an_object;
f( an_object );

Он похож на вызов по значению и при этом, что более важно, действует подобно вызову по значению - модификатор const предотвращает модификацию an_object в функции f(). Вы получили эффективность вызова по ссылке без его проблем.

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

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

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

CDocument *doc;
CView *view;

POSITION pos = doc->GetFirstViewPosition();
while( view = GetNextView(pos) )
view->Invalidate();

Здесь есть две проблемы. Во-первых, у функции GetNextView() неудачное имя. Она должна быть названа GetCurrentViewAndAdvancePosition(), потому что она на самом деле возвращает текущий элемент и затем продвигает указатель положения (который является ссылочным аргументом результата) на следующий элемент. Что приводит нас к второй проблеме: средний читатель смотрит на предыдущий код и задумывается над тем, как завершается этот цикл. Другими словами, здесь скрывается сюрприз. Операция итерации цикла скрыта в GetNextView(pos), поэтому неясно, где она происходит. Ситуация могла быть хуже, если бы цикл был больше и содержал бы несколько функций, использующих pos в качестве аргумента - вы бы не имели никакого представления о том, какая из них вызывает перемещение.

Есть множество лучших способов решения этой проблемы. Простейший заключается в использовании в качестве аргумента GetNextView() указателя вместо ссылки:

POSITION pos = doc->GetFirstViewPosition();
while( p = GetNextView( &pos ) )
view->Invalidate();

Таким способом &pos сообщает вам, что pos будет модифицироваться; иначе зачем передавать указатель? Тем не менее, существуют и лучшие решения. Вот первое:

for( CView *p = doc->GetFirstView(); p ; p = p->NextView() )
p->Invalidate();

Вот второе:

POSITION pos = doc->GetFirstViewPosition();
for( ; pos ; pos = doc->GetNextView(pos) )
(pos->current())->Invalidate();

Вот третье:

CPosition pos = doc->GetFirstViewPosition();
for( ; pos; pos.Advance() )
( pos->CurrentView() )->Invalidate();

Вот четвертый:

ViewListIterator cur_view = doc->View_list(); // Просмотреть весь
// список отображений
// этого документа.
for( ; cur_view ; ++cur_view ) // ++ переходит к следующему отображению.
cur_view->Invalidate(); // -> возвращает указатель View*.

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

122. Не возвращайте ссылки (или указатели) на локальные переменные.

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

some_class &f()
{
some_class x;
// ...
return x;
}

Действительной проблемой здесь является синтаксис С++. Оператор return может располагаться на отдалении от определения возвращаемой величины. Единственный способ узнать, что на самом деле делает return x, - это взглянуть на заголовок функции и посмотреть, возвращает она ссылку,или нет.

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

Каждый вызов new должен сопровождаться delete - подобно malloc() и free(). Я иногда видел людей, старающихся избежать накладных расходов от конструкторам копии перегруженной бинарной операции подобным образом:

const some_class &some_class::operator+( const some_class &r ) const
{
some_class *p = new some_class;
// ...
return *p;
}

Этот код не работает, потому что вы не можете вернуться к этой памяти, чтобы освободить ее. Когда вы пишите:

some_class a, b, c;

c = a + b;

то a + b возвращает объект, а не указатель. Единственным способом получить указатель, который вы можете передать в оператор delete, является:

some_class *p;
c = *(p = &(a + b));

Это даже страшно выговорить. Функция operator+() не может прямо возвратить указатель. Если она выглядит подобным образом:

const some_class *some_class::operator+( const some_class &r ) const
{
some_class *p = new some_class;
// ...
return p;
}

то вы должны записать:

c = *(p = a + b);

что не так страшно, как в предыдущем примере, но все еще довольно плохо. Единственное решение этой задачи состоит в том, чтобы стиснуть зубы и вернуть объект:

const some_class *some_class::operator+( const some_class &r ) const
{
some_class obj;
// ...
return obj;
}

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


Глава 8.Г(D). Конструкторы, деструкторы и operator=( )

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

Генерируемый компилятором конструктор копии (чьим аргументом является ссылка на текущий класс) нужен еще по двум причинам, кроме таблицы виртуальных функций. Во-первых, код на С++, который выглядит как на С, должен и работать, как С. Так как правила копирования, которые относятся к классу, относятся также и к структуре, поэтому компилятор будет вынужден обычно генерировать конструктор копии в структуре, чтобы обрабатывать копирование структур в стиле С. Этот конструктор копии используется явно подобным образом:

some_class x; // конструктор по умолчанию
some_class y = x; // конструктор копии

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

some_class x;
f( some_class x ); // передается по значению, а не по ссылке.

// ... // вызывается конструктор копии для передачи x
f( x ); // по значению. Оно должно скопироваться в стек.

Второй является возврат по значению:

g() // Помните, что x - локальная, автоматическая переменная.
{ // Она исчезает после возвращения функцией значения.
some_class x; // Оператор return после этого должен скопировать x куда-нибудь
return x; // в надежное место (обычно в стек после аргументов).
} // Он использует для этой цели конструктор копии.

Генерируемая компилятором функция-операция operator=() нужна лишь для поддержки копирования структур в стиле С там, где не определена операция присваивания.

124. Операция operator=( ) должна возвращать ссылку на константу.

125. Присваивание самому себе должно работать.

Определение operator=( ) должно всегда иметь следующую форму:

class class_name
{
const class_name &operator=( const class_name &r );
};

const class_name &class_name::operator=( const class_name &r )
{
if( this != &r )
{
// здесь скопировать
}
return *this;
}

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

Эта функция возвращает ссылку, потому что она может это сделать. То есть вы могли бы удалить & из объявления возвращаемой величины, и все бы работало прекрасно, но вы бы получили ненужный вызов конструктора копии, вынужденный возвратом по значению. Так как у нас уже есть объект, инициализированный по типу правой части (*this), то мы просто можем его вернуть. Даже если возврат объекта вместо ссылки в действительности является ошибкой для функции operator=(), компилятор просто выполнит то, что вы ему приказали. Здесь не будет сообщения об ошибке; и на самом деле все будет работать. Код просто будет выполняться более медленно, чем нужно.

Наконец, operator=() должен возвращать ссылку на константу просто потому, что не хотите, чтобы кто-нибудь имел возможность модифицировать возвращенный объект после того, как произошло присваивание. Следующее будет недопустимым в случае возврата ссылки на константу:

(x =y) = z;

Причина состоит в том, что (x=y) расценивается как возвращаемое значение функции operator=(), т.е. константная ссылка. Получателем сообщения =z является объект, только что возвращенный от x=y. Тем не менее, вы не можете послать сообщение operator=() константному объекту, потому что его объявление не имеет в конце const:

// НЕ ДЕЛАЙТЕ ЭТОГО
// В ФУНКЦИИ С ИСПОЛЬЗОВАНИЕМ
// operator=().
// |
// V
const class_name &operator=( const class_name &r ) const;

Компилятор должен выдать вам ошибку типа "не могу преобразовать ссылку на переменную в ссылку на константу", если вы попробуете (x=y)=z.

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

if( this != &r )

в функции operator=(). Выражение:

class_name x;
// ...
x = x;

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

class_name array[10];
class_name *p = array;
// ...
*p = array[0];






126. Классы, имеющие члены-указатели, должны всегда определять конструктор копии и функцию operator=().

Если класс не определяет методы копирования - конструктор копии и функцию operator=(), то это делает компилятор. Созданный компилятором конструктор должен выполнять "почленное" копирование, которое осуществляется таким образом, как будто вы написали this->field = src.field для каждого члена. Это означает, что теоретически должны вызываться конструкторы копий и функции operator=() вложенных объектов и базовых классов. Даже если все работает правильно, все же указатели копируются как указатели. То есть, строка string, представленная как char*, - не строка, а указатель, и будет скопирован лишь указатель. Представьте, что определение string на листинге 7 со страницы 111 не имеет конструктора копии или функции operator=(). Если вы запишите:

string s1 = "фу", s2;
// ...
s2 = s1;

то это присваивание вместо поля указателя s2 запишет указатель от s1. Та память, которая была адресована посредством s1->buf, теперь потеряна, то есть у вас утечка памяти. Хуже того, если вы меняете s1, то s2 меняется также, потому что они обе указывают на один и тот же буфер. Наконец, когда строки выходят из области действия, они обе передают buf для освобождения, по сути очищая его область памяти дважды, и, вероятно, разрушают структуру динамической памяти. Решайте эту проблему путем добавления конструктора копии и функции operator=(), как было сделано на листинге 7 со страницы 111. Теперь копия будет иметь свой собственный буфер с тем же содержанием, что и у буфера строки-источника.

Последнее замечание: Я выше написал "должен выполнять" и "теоретически" в первом абзаце, потому что встречал компиляторы, которые фактически выполняли функцию memcpy() в качестве операции копирования по умолчанию, просто как это бы сделал компилятор С. В этом случае конструктор копии и функция operator=() вложенных объектов не будут вызваны, и вы всегда будете должны обеспечивать конструктор копии и функцию operator=() для копирования вложенных объектов. Если вы желаете достигнуть здесь абсолютной надежности, то будете должны проделать это для всех классов, чьи члены не являются основными числовыми типами С.

127. Если у вас есть доступ к объекту, то он должен быть инициализирован.

128. Используйте списки инициализации членов.

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

Многие неопытные программисты на С++ избегают списков инициализации членов, как я полагаю, потому, что они выглядят так причудливо. Фактом является то, что большинство программ, которые их не используют, попросту некорректны. Возьмите, например, следующий код (определение строкового класса из листинга 7 со страницы 111):

class base
{
string s;
public:
base( const char *init_value );
}
//------------------------------
base::base( const char *init_value )
{
s = init_value;
}

Основной принцип такой: если у вас есть доступ к объекту, то он должен быть инициализирован. Так как поле s видимо для конструктора base, то С++ гарантирует, что оно инициализировано до окончания выполнения тела конструктора. Список инициализации членов является механизмом выбора выполняемого конструктора. Если вы его опускаете, то получите конструктор по умолчанию, у которого нет аргументов, или, как в случае рассматриваемого нами класса string, такой, аргументы получили значения по умолчанию. Следовательно, компилятор вначале проинициализирует s пустой строкой, разместив односимвольную строку при помощи new и поместив в нее . Затем выполняется тело конструктора и вызывается функция string::operator=(). Эта функция освобождает только что размещенный буфер, размещает буфер большей длины и инициализирует его значением init_value. Ужасно много работы. Лучше сразу проинициализировать объект корректным начальным значением. Используйте:

base( const char *init_value ) : s(init_value)
{}

Теперь строка s будет инициализирована правильно, и не нужен вызов operator=() для ее повторной инициализации.

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

• Базовые классы в порядке объявления.
• Поля данных в порядке объявления.
Лишь затем выполняется конструктор производного класса. Одно последнее предостережение. Заметьте, что порядок объявления управляет порядком инициализации. Порядок, в котором элементы появляются в списке инициализации членов, является несущественным. Более того, порядок объявления не должен рассматриваться как неизменный. Например, вы можете изменить порядок, в котором объявлены поля данных. Рассмотрим следующее определение класса где-нибудь в заголовочном файле:

class wilma
{
int y;
int x;
public:
wilma( int ix );
};

Вот определение конструктора в файле .c:

wilma::wilma( int ix ) : y(ix * 10), x(y + 1)
{}

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

Исправьте код, исключив расчет на определенный порядок инициализации:

wilma::wilma( int ix ) : y(ix * 10), x((ix *10) + 1)
{}



130. Конструкторы копий должны использовать списки инициализации членов.

У наследования тоже есть свои проблемы с копированием. Конструктор копии все же остается конструктором, поэтому здесь также применимы результаты обсуждения предыдущего правила. Если у конструктора копии нет списка инициализации членов, то для базовых классов и вложенных объектов используется конструктор по умолчанию. Так как список инициализации членов отсутствует в следующем определении конструктора копии, то компонент базового класса в объекте производного класса инициализируется с использованием base(void), а поле s инициализируется с использованием string::string(void):

class base
{
public:
base( void ); // конструктор по умолчанию
base( const base &r ); // конструктор копии
const base &operator=( const base &r );
};

class derived
{
string s; // класс имеет конструктор копии
public:
derived( const derived &r )
};

derived::derived( const derived &r )
{}

Чтобы гарантировать копирование также поля string и компонента базового класса в объекте производного класса, используйте следующее:

derived::derived( const derived &r ) : base(r), s(r.s) {}


131. Производные классы должны обычно определять конструктор копии и функцию operator=( ).

При наследовании есть и другая связанная с копированием проблема. В одном месте руководства10 по языку С++ недвусмысленно заявлено: "конструкторы и функция operator=() не наследуются". Однако далее в этом же документе говорится, что существуют ситуации, в которых компилятор не может создать конструктор копии или функцию operator=(), которые бы корректно вызывались вслед за функциями базового класса. Так как нет практической разницы между унаследованной и сгенерированной функциями operator=(), которые ничего не делают, кроме вызова функции базового класса, то эта неопределенность вызвала много бед.

Я наблюдал два полностью несовместимых поведения компиляторов, столкнувшихся с этой дилеммой. Некоторые компиляторы считали правильным, чтобы сгенерированные компилятором конструкторы копий и функции operator=() вызывались автоматически после конструкторов и функций operator=() базового класса (и вложенного объекта).11 Это как раз тот способ, который, по мнению большинства, реализуется языком программирования. Другими словами, со следующим кодом проблем не будет:

class base
{
public:
base( const base &r );
const base &operator=( const base &r );
};

class derived : public base
{
string s;
// нет операции operator=() или конструктора копии
};

derived x;
derived y = x; // вызывает конструктор копии базового класса
// для копирования базового класса. Также вызывает
// конструктор копии строки для копирования поля s.
x = y; // вызывает функцию базового класса operator=() для
// копирования базового класса. Также вызывает строковую
// функцию operator=() для копирования поля s.

Если бы все компиляторы работали таким образом, то проблемы бы не было. К несчастью, некоторые компиляторы принимают ту самую директиву "не наследуются" за чистую монету. Только что представленный код не будет работать с этими компиляторами. В них сгенерированные компилятором конструктор копии и функция operator=() производного класса действуют так, как будто бы их эквиваленты в базовом классе (и вложенном объекте) просто не существуют. Другими словами, конструктор по умолчанию - без аргументов - вызывается для копирования компонента базового класса, а почленное копирование - которое может выполняться просто функцией memcpy() - используется для поля. Мое понимание пересмотренного проекта стандарта С++ ISO/ANSI позволяет сделать вывод, что такое поведение некорректно, но в течение некоторого времени вам придется рассчитывать на худшее, чтобы обеспечивать переносимость. Следовательно, это, вероятно, хорошая мысль - всегда помещать в производный класс конструктор копии и функцию operator=(), которые явно вызывают своих двойников из базового класса. Вот реализация предыдущего производного класса для самого худшего случая:

class derived : public base
{
string s;
public:
derived( const derived &r );
const derived &operator=( const derived &r );
};
//--------------------------------------------------------------
derived::derived( const derived &r ) : base(r), s(r.s)
{}
//--------------------------------------------------------------
const derived &derived::operator=( const derived &r )
{
(* (base*)this) = r;
s = r.s;
}

Список инициализации членов в конструкторе копии описан ранее. Следующий отрывок из функции operator=() нуждается в некотором пояснении:

(* (base*)this) = r;

Указатель this указывает на весь текущий объект; добавление оператора приведения преобразует его в указатель на компонент базового класса в текущем объекте - (base*)this. (* (base*)this) является самим объектом, а выражение (* (base*)this) = r передает этому объекту сообщение, вызывая функцию operator=() базового класса для перезаписи информации из правого операнда в текущий объект. Вы могли бы заменить этот код таким образом:

base::operator=( r );

но я видел компиляторы, которые бракуют этот оператор, если в базовом классе не объявлена явно функция 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: первая попытка.
1 class window : public CWnd
2 {
3 public:
4 class caption
5 {
6 CWnd *target_window;
7
8 private: friend class window;
9 caption( CWnd *p ) : target_window(p) {}
10
11 public:
12 operator CString ( void ) const;
13 const caption &operator=( const CString &s );
14 };
15
16 caption text( void );
17 };
18 //--------------------------------------------------------------
19 caption window::text( void )
20 {
21 return caption( this );
22 }
23 //--------------------------------------------------------------
24 window::caption::operator CString( void ) const
25 {
26 CString output;
27 target_window->GetWindowText( output );
28 return output; // возвращает копию
29 }
30 //--------------------------------------------------------------
31 const caption &window::caption::operation=( const CString &s )
32 {
33 target_window->SetWindowText( s );
34 return *this;
35 }

Листинг 12. Заголовочный объект
1 class caption
2 {
3 CWnd target_window;
4 public:
5 window_text( CWnd *win ) : target_window( win ) {};
6
7 operator const CString( void );
8 const CString &operator=( const CString &r );
9 };
10
11 inline caption::operator CString( void );
12 {
13 CString output;
14 target_window->GetWindowText( output );
15 return output;
16 }
17
18 inline const CString &caption::operator= ( const CString &s )
19 {
20 // возвращает тип CString (вместо типа заголовка "caption"),
21 // поэтому будет срабатывать
22 // a = b = "абв"
23
24 target_window->SetWindowText( s );
25 return s;
26 }



Глава 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 )
// вставить объект в базу данных
}

Объект storable вновь не может определить функцию cmp(), потому что информация, необходимая для сравнения (ключ), находится в объекте производного класса, а не в базовом классе storable. Поэтому вы делаете функцию виртуальной в классе storable и предусматриваете ее в производном классе. Кстати, эти вспомогательные функции никогда не будут открытыми (public).

137. Виртуальная функция не является виртуальной, если вызывается из конструктора или деструктора.

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

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

class storable
{
int stuff;
public:
storable( void );

virtual void print( void );
virtual void virtf ( void );
virtual int cmp ( const storable &r ) = 0;

int nonvirtual( void );
};

storable::storable ( void ) { stuff = 0; }
void storable::print ( void ) { /* материал для отладки print */ }
void storable::virtf ( void ) { /* делай что-нибудь */ }
int storable::nonvirtual( void ) { }

Лежащее в основе определение класса (сгенерированное компилятором) может выглядеть подобно этому:

int _storable__print ( storable *this ) { /* ... */ }
int _storable__virtf ( storable *this ) { /* ... */ }
int _storable__nonvirtual( storable *this ) { /* ... */ }

typedef void (*_vtab[])(...); // массив указателей на функции

_vtab _storable__vtab
{
_storable__print,
_storable__virtf,
NULL // метка-заполнитель для функции сравнения
};

typedef struct storable
{
_storable__vtab *_vtable;
int stuff;
}
storable;

_storable__ctor( void ) // конструктор
{
_vtable = _storable__vtable; // Эту строку добавляет компилятор
stuff = 0; // Эта строка из исходного кода.
}

Когда вы вызываете невиртуальную функцию, используя такой код как:

storable *p;
p->nonvirtual();

то компилятор в действительности генерирует:

_storable__nonvirtual( p )

Если вы вызываете виртуальную функцию, подобную этой:

p->print();

то получаете нечто совершенно отличное:

( p->_vtable[0] )( p );

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

class employee : public storable
{
int derived_stuff;
// ...
public:
virtual int cmp( const storable &r );
};

/* виртуальный */ int employee::print( const storable &r ) { }
/* виртуальный */ int employee::cmp ( const storable &r ) { }

А вот что сделает с ним компилятор:

int _employee__print( employee *this ) { /* ... */ }
int _employee__cmp ( employee *this, const storable *ref_r ) { /* ... */ }

_vtab _employee_vtable =
{
_employee__print,
_storable_virtf, // Тут нет замещения в производном классе, поэтому
// используется указатель на функцию базового класса.
_employee_cmp
};

typedef struct employee
{
_vtab *_vtable; // Генерируемое компилятором поле данных.
int stuff; // Поле базового класса.
int derived_stuff; // Поле, добавленное в объявлении производного класса.
}
employee;

_employee__ctor( employee *this ) // Конструктор по умолчанию, генерируемый
{ // компилятором.
_storable_ctor(); // Базовые классы инициализируются
// в первую очередь.
_vtable = _employee_vtable; // Создается таблица виртуальных функций.
}

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

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

storable *p = new employee();

то компилятор на самом деле генерирует:

storable *p;
p = (storable *)malloc( sizeof(employee) );
_employee_ctor( p );

Вызов _employee_ctor() сначала инициализирует компонент базового класса посредством вызова _sortable_ctor(), которая добавляет таблицу этой виртуальной функции к своей таблице и выполняется. Затем управление передается обратно к _employee_ctor() и указатель в таблице виртуальной функции переписывается так, чтобы он указывал на таблицу производного класса.

Отметьте, что, хотя p теперь указывает на employee, код p->print() генерирует точно такой же код, как и раньше:

( p->_vtable[0] )( p );

Несмотря на это, теперь p указывает на объект производного класса, поэтому вызывается версия print() из производного класса (так как _vtable в объекте производного класса указывает на таблицу производного класса). Крайне необходимо, чтобы эти две функции print() располагались в одной и той же ячейке своих таблиц смешений, но это обеспечивается компилятором.

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

( this->_vtable[0] )( p );

но _vtable указывает на таблицу базового класса и _vtable[0] указывает на функцию базового класса. Тот же самый вызов в конструкторе производного класса даст версию print() производного класса, потому что _vtable будет перекрыта указателем на таблицу производного класса к тому времени, когда была вызвана print().

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

138. Не вызывайте чисто виртуальные функции из конструкторов.

Это правило вытекает из только что рассмотренной картины. Определение "чисто" виртуальной функции (у которой =0 вместо тела) приводит к тому, что в таблицу виртуальных функций базового класса помещается NULL вместо обычного указателя на функцию. (В случае "чисто" виртуальной функции нет функции, на которую необходимо указывать). Если вы вызываете чисто виртуальную функцию из конструктора, то используете таблицу базового класса и на самом деле вызываете функцию при помощи указателя NULL. Вы получите дамп оперативной памяти на машине с UNIX и "Общая ошибка защиты" в системе Windows, но MS-DOS просто исполнит то, что вы просили, и попытается выполнить код по адресу 0, считая его правильным.

139. Деструкторы всегда должны быть виртуальными.

Рассмотрим этот код:

class base
{
char *p;

~base() { p = new char[SOME_SIZE]; }
base() { delete p; }
};

class derived : public base
{
char *dp;

~derived() { dp = new char[[SOME_SIZE]; }
derived() { delete dp; }
};

Теперь рассмотрим этот вызов:

base *p = new derived;
// ...
delete p;

Запомните, что компилятор не знает, что p на самом деле указывает на объект производного класса. Он исходит из того, что p указывает на объявленный тип base. Следовательно, delete p в действительности превращается в:

_base__destructor(p);
free(p);

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

virtual ~base() { /* ... */ }

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

( p->_vtable[DESTRUCTOR_SLOT] ) (p);

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

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

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

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

141. Не делайте функцию виртуальной, если вы не желаете, чтобы производный класс получил контроль над ней.

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

142. Защищенные функции обычно должны быть виртуальными.

Одним из смягчающих факторов в ранее описанной ситуации со сцеплением базового и производного классов является то, что объекту производного класса С++ едва когда-либо нужно посылать сообщение компоненту своего базового класса. Производный класс наследует назначение (и члены) от базового класса и обычно добавляет к нему назначение (и члены), но производный класс часто не вызывает функции базового класса. (Естественно, производный класс никогда не должен получать доступ к данным базового класса). Одним исключением являются виртуальные функции, которые можно рассматривать как средство изменения поведения базового класса. Сообщения часто передаются замещающей функцией производного класса в эквивалентную функцию базового класса. То есть, виртуальное замещение производного класса часто образует цепь с функцией базового класса, которую оно заместило. Например, класс CDialog из MFC реализует диалоговое окно Windows (тип окна для ввода данных). Этот класс располагает виртуальной функцией OnOk(), которая закрывает диалоговое окно, если пользователь щелкнул по кнопке с меткой "OK". Вы определяете свое собственное диалоговое окно путем наследования от CDialog и можете создать замещение OnOk(), которое будет выполнять проверку правильности данных перед тем, как позволить закрыть это диалоговое окно. Ваше замещение образует цепь с функцией базового класса для действительного выполнения закрытия:

class mydialog : public CDialog
{
// ...
private:
virtual OnOk( void );
};

/* виртуальный */ mydialog::OnOk( void );
{
if( data_is_valid() )
CDialog::OnOk(); // Послать сообщение базовому классу
else
beep(); // Обычно содержательное сообщение
// Windows об ошибке
}

Функция OnOk() является закрытой в производном классе, потому что никто не будет посылать сообщение OnOk() объекту mydialog. OnOk() базового класса не может быть закрытой, потому что вам нужно образовать цепь с ней из замещения производного класса. Вы не желаете, чтобы CDialog::OnOk() была открытой, потому что снова никто не должен посылать сообщение OnOk() объекту CDialog. Поэтому вы делаете ее защищенной. Теперь замещение из производного класса может образовать цепочку с OnOk(), но эта функция не доступна извне.

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

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

143. Опасайтесь приведения типов (спорные вопросы С++).

Приведение типов в С рассмотрено ранее, но и в С++ приведение вызывает проблемы. В С++ у вас также существует проблема нисходящего приведения - приведения указателя или ссылки на базовый класс к производному классу. Эта проблема обычно появляется при замещениях виртуальных функций, потому что сигнатуры функций производного класса должны точно совпадать с сигнатурами базового класса. Рассмотрим этот код:

class base
{
public:
virtual int operator==( const base *&r ) = 0;
};

class derived
{
char *key;

public:
virtual int operator==( const base &r )
{
return strcmp(key, ((const derived &)r).key ) == 0;
}
};

К несчастью, здесь нет гарантии, что передаваемый аргумент r действительно ссылается на объект производного класса. Он не может ссылаться на объект базового класса из-за того, что функция чисто виртуальная: вы не можете создать экземпляр объекта base. Тем не менее, r мог бы быть ссылкой на объект некоего другого класса, унаследованного от base, но не являющегося классом derived. С учетом предыдущего определения следующий код не работает:

class other_derived : public base
{
int key;
// ...
};

f()
{
derived dobj;
other_derived other;

if( derived == other_derived )
id_be_shocked();
}

Комитет ISO/ANSI по С++ рекомендовал механизм преобразования типов во время выполнения, который решает эту проблему, но на момент написания этой книги многие компиляторы его не поддерживают. Предложенный синтаксис выглядит подобным образом:

class derived : public base
{
char *key;

public:
virtual int operator==( const base &r )
{
derived *p = dynamic_cast<derived *>( &r );

return !p ? 0 : strcmp(key, ((const derived &)r).key )==0 ;
}
};

Шаблон функции dynamic_cast<t> возвращает 0, если операнд не может быть безопасно преобразован в тип t, иначе он выполняет преобразование.

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

144. Не вызывайте конструкторов из операции operator=( ).

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

class some_class
{
public:

virtual
~some_class( void );
some_class( void );
some_class( const some_class &r );

const some_class &operator=( const some_class &r );
};

const some_class &operator=( const some_class &r )
{
if( this != &r )
{
this->~some_class();
new(this) some_class(r);
}
return *this;
}

Этот вариант оператора new инициализирует указываемый this объект как объект some_class, в данном случае из-за аргумента r используя конструктор копии. 12

Есть серьезные причины не делать показанное выше. Во-первых, это не будет работать после наследования. Если вы определяете:

class derived : public some_class
{
public:
~derived();

// Предположим, что генерированная компилятором операция operator=()
// выполнится за операцией operator=() базового класса.
}

Вследствие того, что деструктор базового класса определен (правильно) как виртуальный, обращение предыдущего базового класса к:

this->~some_class()

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

this->some_class::~some_class();

Явное упоминание имени класса - some_class:: в этом примере - подавляет механизм виртуальной функции. Функция вызывается, как если бы она не была виртуальной.

Деструктор не является единственной проблемой. Рассмотрим простое присваивание объектов производного класса:

derived d1, d2;
d1 = d2;

Операция производного класса operator=() (вне зависимости от того, генерируется она компилятором или нет) образует цепочку с operator=() базового класса, который в настоящем случае использует оператор new() для явного вызова конструктора базового класса. Конструктор, тем не менее, делает значительно больше, чем вы можете видеть в определении. В частности, он инициализирует указатель таблицы виртуальных функций так, чтобы он указывал на таблицу его класса. В текущем примере перед присваиванием указатель vtable указывает на таблицу производного класса. После присваивания указатель vtable указывает на таблицу базового класса; он был переинициализирован неявным вызовом конструктора при вызове new в перегруженной операции operator=().

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

Лучшим способом устранения дублирования кода в операции присваивания operator=() является использование простой вспомогательной функции:

class some_class
{
void create ( void );
void create ( const some_class &r );
void destroy( void );
public:

virtual
~some_class( void ) { destroy(); }
some_class( void ) { create(); }

const some_class &operator=( const some_class &r );
};
inline const some_class &some_class::operator=( const some_class &r )
{
destroy();
create( r );
}
inline some_class::some_class( void )
{
create();
}
~some_class::some_class( void )
{
destroy();
}
 








Инструменты Internet Explorer 8 Beta 2 для разработчиков.

Вебмастеру

В марте этого года мы уже писали об инструментах для разработчика в IE8 Beta 1, но IE8 Beta2 позволяет более полно использовать инструменты за счет значительных изменений в имеющихся функциях, а также новых возможностей. В принципе инструменты для разработчика должны обладать следующими свойствами: Быть интегрированными и простыми в использовании; Иметь визуальный интерфейсC их помощью можно быстро протестировать сайт.


Подробнее... | Рубрика: Вебмастеру | Добавлено: 05.09.2008

Google Developer Day 2008 в Москве.

Мероприятия

Дата проведения: 28 октября 2008 г.; Место проведения: Амбер Плаза, Москва, Россия. Конференция для веб-разработчиков и разработчиков мобильных приложений в Москве. Узнайте, как наилучшим образом использовать инструменты разработки и API от Google, чтобы создавать социальные, мобильные и картографические приложения, как использовать AJAX/JavaScript инструменты и библиотеки от Google и многое другое из первых уст.


Подробнее... | Рубрика: Мероприятия | Добавлено: 05.09.2008

ТОП 10 самых раздражающих факторов для программиста.

Разное

Совсем недавно наткнулся в интернете на забавный "хит-парад" наиболее раздражающих вещей для программиста. Поскольку он был на английском — решил перевести текст и несколько адаптировать к нашим реалиям…


Подробнее... | Рубрика: Разное | Добавлено: 03.09.2008

Остальные статьи:

Windows Server 7, 8 и 9
jQuery для JavaScript-программистов
Инновационный веб-броузер Google Chrome стартует уже сегодня
Windows 7: подход к производительности системы
Trac + Subversion @ Ubuntu: Revisited
[g]Vim в режиме Python: Рекомпиляция в Windows
Java + JSON. Пути к дружбе
Драйвер SQL Server 2005 для PHP
Типы данных в MySQL (сжатый справочник для PHP программиста)
PHP класс для работы с Яндекс.XML
Ошибки начинающих PHP разработчиков
Наследование шаблонов в Smarty
Особенности хранения сессий PHP в memcached
Internet Explorer 8 beta 2
9 правил для начинающего Ajax-разработчика
ExtJS 2.2 - полная поддержка Firefox 3, новые виджеты и другие нововведения


Цитата дня (все,добавить):



    Рубрикатор

Программирование

C/С++
Обучение
Windows API
XAML
Моделирование
Паттерны
Visual Basic 7 .NET
WxWidgets
Функции WinApi
Функции С++
Разработка под Mac OS
Eiffel
Visual Studio 2008
UI дизайн
Алгоритмы
Конкурсные статьи
Turbo Pascal
Visual Studio
CASE-средства
Visual Studio 2005
Без VCL
Delphi
Тех. документация
Тестирование
Software Testing
ООП
TCP/IP
Google Android
Windows Installer
.NET Framework
Драйвера
C# C Sharp
Справка
Проектирование
Информ. системы
Visual Basic
Assembler
Оптимизация кода
Gtk+
Компоненты
Реинжиниринг
Управление проектами
Extreeme programming
Lotus Notes
Алгебраическое проектирование

Интернет технологии

PHP
Perl
ASP
WAP
Cookies
SSI
CGI
Web Servers
VB Script
DNS
CSS
XML
Html
Java Script
Java2ME
Firewall
Flash
.htaccess
Apache
VRML
Протоколы
Поисковые системы
Технология JAVA
Учебник по PHP
Учебник по JavaScript
Учебник по XML
Java Q&A
AJAX
DHTML
XHTML
Dreamweaver
Web 2.0
Python
Вебмастеру
Cisco
Ruby on Rails
Silverlight

Базы данных

Access
InterBase
MySQL
Oracle
ADO .NET
Основы SQL
Учебник по Access 2002
MS
Microsoft FoxPro
Доступ к данным
XML в MS SQL Server 2000
ODBC и MyODBC
Обучение
Caché
DB2
PostgresSQL
Sybase
Теория
Хранилища данных
Безопасность
Реляционные данные
MySQL и mSQL

Остальное:

Разное
Обзоры книг
Безопасность
Графика и дизайн
Юмор
Linux
Фракталы
Microsoft Axapta
Многоядерность
Сети
Microsoft Office
Работа
MS-DOS
Криптография
Графика и игроделание
Новости SDK
Системы защиты
Учебник по AutoCad
CVS
Windows XP
Windows Server 2003
Windows Vista
Windows 7
Мероприятия

    Кто на сайте
Вы не зарегистрированы.
Имя:

Пароль:

Запомнить

Регистрация позволит Вам пользоваться дополнительными сервисами.
Сейчас на сайте:
Гостей: 78
Пользователей: 0