CHAP8_3 (1018811), страница 2
Текст из файла (страница 2)
string::string(const char *left, const char *right )
{
buf = new char[ strlen(left) + strlen(right) + 1 ];
strcpy( buf, left );
strcat( buf, right );
}
//--------------------------------------------------------------
inline const string::operator+( const string &r ) const
{
return string(buf, r.buf);
}
Более эффективные компиляторы здесь на самом деле рассматривают следующее:
string s1, s2;
s1 + s2;
как если бы вы сказали следующее (вы не можете сделать этого сами, потому что buf является закрытым):
string(s1.buf, s2.buf)
Полезный результат заключается в устранении неявного вызова конструктора копии в операторе return в первом варианте реализации.
150. Не сходите с ума с операторами преобразования типов.
151. Если можно, то делайте все преобразования типов с конструкторами.
Распространенной ошибкой среди начинающих программистов на С++ является сумасбродство с преобразованием типов. Вы чувствуете, что должны обеспечить преобразование каждого системного типа в ваш новый класс и обратно. Это может привести к подобному коду:
class riches // богачи
{
public:
riches( const rags &r );
};
class rags // оборванцы
{
public:
operator riches( void );
};
Проблема заключается в том, что обе функции определяют преобразование из rags в riches. Следующий код генерирует "постоянную ошибку" (которая прерывает компиляцию), потому что компилятор не знает, использовать ли ему для преобразования rags в riches конструктор в классе riches, или перегруженную операцию в классе rags; конструктор и перегруженная операция утверждают, что выполнят эту работу:
rags horatio_alger; // Гораций Алгер
riches bill_gates = (riches) horatio_alger; // Бил Гейтс
Эта проблема обычно не так очевидна. Например, если вы определите слишком много преобразований:
class some_class
{
public:
operator int (void);
operator const char * (void);
};
то простой оператор, подобный:
some_class x;
cout << x;
не сработает. Проблема в том, что класс stream определяет те же два преобразования:
ostream &ostream::operator<<( int x );
ostream &ostream::operator<<( const char *s );
Так как имеется два варианта преобразований, то компилятор не знает, какой из них вызывать.
Лучше выполнять все преобразования типов при помощи конструкторов и определять минимально необходимый их набор. Например, если у вас есть преобразование из типа doble, то вам не нужны int, long и так далее, потому что нормальные правила преобразования типов С применяются компилятором при вызове вашего конструктора.
Глава 8.Ж(G). Управление памятью
152. Используйте new/delete вместо malloc()/free().
Нет гарантии, что оператор new() вызывает malloc() при запросе памяти для себя. Он может реализовывать свою собственную функцию управления памятью. Следовательно, возникает трудно обнаруживаемая ошибка при передаче функцией free() памяти, полученной при помощи new (и наоборот).
Избегайте неприятностей, используя всегда при работе с С++ new и delete. Наряду с прочим это означает, что вы не должны пользоваться strdup() или любой другой функцией, скрывающей вызов malloc().
153. Вся память, выделенная в конструкторе, должна быть освобождена в деструкторе.
Не выполнение этого обычно приводит к ошибке, но я видел программу, где это делалось намеренно. Упомянутая программа на самом деле нарушала другое правило: Не позволяй открытого доступа к закрытому классу. Функция-член не только возвращала внутренний указатель на память, выделенную new, но класс ожидал, что вызывающая функция передает этот указатель delete. Это плохая идея со всех сторон: получить при этом утечку памяти - значит легко отделаться.
С точки зрения поиска ошибок помогает близкое физическое расположение конструктора и деструктора рядом друг с другом в файле .cpp, чтобы сделать их заметнее при отладке.
154. Локальные перегрузки операторов new и delete опасны.
Здесь основной проблемой является то, что операторы new и delete, определенные в виде членов класса, следуют другим правилам, чем перегруженные на глобальном уровне. Локальная перегрузка используется лишь тогда, когда вы размещаете единственный объект. Глобальная перегрузка используется вами всегда при размещении массива. Следовательно, этот код скорее всего не будет работать:
some_class *p = new some_class[1]; // вызывает глобальный оператор new()
//...
delete p; // вызывает some_class::operator delete()
Помните, что эти две строки могут быть в различных файлах.
Глава 8.З(H). Шаблоны
Многие проблемы с шаблонами в действительности вызваны учебниками, которые обычно настолько упрощенно рассматривают шаблоны, что вы заканчиваете чтение, не получив и намека на то, как они должны использоваться. Этот раздел посвящен распространенным затруднениям, связанным с шаблонами.
155. Используйте встроенные шаблоны функций вместо параметризированных макросов.
Приведенный ранее пример:
#define SQUARE(x) ((x) * (x))
где:
SQUARE(++x)
расширяется до:
((++x)*(++x))
инкрементируя x дважды. Вы не можете решить эту проблему в С, а в С++ можете. Простая встроенная функция работает вполне удовлетворительно, в таком виде:
inline int square( int x ){ return x * x; }
не давая побочного эффекта. Тем не менее, она допускает лишь целочисленные аргументы. Шаблон функции, который расширяется во множество перегруженных встроенных функций, является более общим решением:
template <class type>
inline type square( type x ){ return x * x; }
К несчастью, это срабатывает только в простых ситуациях. Следующий шаблон не может обработать вызов max(10, 10L), потому что не совпадают типы аргументов:
template <class type>
inline type max( type x, type y ){ return (x > y) ? x : y; }
Для обработки max(10, 10L) вы должны использовать прототип, чтобы принудить к расширению по тому варианту max(), который может выполнить данную работу:
long max( long, long );
Прототип вызывает расширение шаблона. Компилятор с легкостью преобразует аргумент типа int в long, даже если ему не нужно делать это преобразование для расширения шаблона.
Заметьте, что я здесь рекомендую использование шаблонов только потому, что square является встроенной функцией. Если бы этого не было, то для того, чтобы такой механизм был жизнеспособным, пришлось бы генерировать слишком много кода.
156. Всегда знайте размер шаблона после его расширения.
Большинство книг демонстрирует шаблоны типа простого контейнера массива, подобного показанному на листинге 13. Вы не можете использовать здесь наследование (скажем, с базовым классом array, от которого наследуется int_array). Проблема заключается в перегрузке операции operator[](). Вы бы хотели, чтобы она была виртуальной функцией в базовом классе, замещенная затем в производном классе, но сигнатура версии производного класса должна отличаться от сигнатуры базового класса, чтобы все это заработало. Здесь определения функций должны отличаться лишь возвращаемыми типами: int_array::operator[]() должна возвращать ссылку на тип int, а long_array::operator[]() должна возвращать ссылку на тип long, и так далее. Так как время возврата не рассматривается как часть сигнатуры при выборе перегруженной функции, то реализация на основе наследования не жизнеспособна. Единственным решением является шаблон.
Листинг 13. Простой контейнер массива.
-
template <class type, int size >
-
class array
-
{
-
type array[size];
-
public:
-
class out_of_bounds {}; // возбуждается исключение, если вы используете
-
// индекс за пределами массива
-
type &operator[](int index);
-
};
-
template <class type, int size >
-
inline type &array<type, size>::operator[](int index)
-
{
-
if( 0 <= index && index < size )
-
return array[ index ]
-
throw out_of_bounds;
-
}
Единственная причина осуществимости этого определения заключается в том, что функция-член является встроенной. Если бы этого не было, то вы могли бы получить значительное количество повторяющегося кода. Запомните, что везде далее происходит полное расширение шаблона, включая все функции-члены. Вследствие того, что каждое из следующих определений на самом деле создает разный тип, то вы должны расширить этот шаблон четыре раза, генерируя четыре идентичные функции operator[](), по одной для каждого расширения шаблона:
array<int,10> ten_element_array;
array<int,11> eleven_element_array;
array<int,12> twelve_element_array;
array<int,13> thirteen_element_array;
( то есть array<int,10>::operator[](),array<int,11>::operator[]() и так далее).
Вопрос состоит в том, как сократить до минимума дублирование кода. Что, если мы уберем размер за пределы шаблона как на листинге 14? Предыдущие объявления теперь выглядят так:
array<int> ten_element_array (10);
array<int> eleven_element_array (11);
array<int> twelve_element_array (12);
array<int> thirteen_element_array (13);
Теперь у нас есть только одно определение класса (и один вариант operator[]()) с четырьмя объектами этого класса.
Листинг 14. Шаблон массива (второй проход).
-
template <class type>
-
class array
-
{
-
type *array;
-
int size;
-
public:
-
virtual ~array( void );
-
array( int size = 128 );
-
class out_of_bounds {}; // возбуждается исключение, если вы используете
-
// индекс за пределами массива
-
type &operator[](int index);
-
};
-
template <class type>
-
array<type>::array( int sz /*= 128*/ ): size(sz)
-
, array( new type[ sz ] )
-
{}
-
template <class type>
-
array<type>::~array( void )
-
{
-
delete [] array;
-
}
-
template <class type>
-
inline type &array<type>::operator[](int index)
-
{
-
if( 0 <= index && index < size )
-
return array[ index ]
-
throw out_of_bounds;
-
}
Главным недостатком этой второй реализации является то, что вы не можете объявить двухмерный массив. Определение на листинге 13 разрешает следующее:
array< array<int, 10>, 20> ar;
(20-элементный массив из 10-элементных массивов). Определение на листинге 14 устанавливает размер массива, используя конструктор, поэтому лучшее, что вы можете получить, это:
array< array<int> > ar2(20);
Внутренний array<int> создан с использованием конструктора по умолчанию, поэтому это 128-элементный массив; мы объявили 20-элементный массив из 128-элементных массивов.
Вы можете решить эту последнюю проблему при помощи наследования. Рассмотрим следующее определение производного класса:
template< class type, int size >
class sized_array : public array<type>
{
public:
sized_array() : array<type>(size) {}
};
Здесь ничего нет, кроме единственной встроенной функции, поэтому это определение очень маленького класса. Оно совсем не будет увеличивать размер программы, вне зависимости от того, сколько раз будет расширен шаблон. Вы теперь можете записать:
sized_array< sized_array<int,10>, 20> ar3;
для того, чтобы получить 20-элементный массив из 10-элементных массивов.
157. Шаблоны классов должны обычно определять производные классы.
158. Шаблоны не заменяют наследование; они его автоматизируют.
Главное, что нужно запомнить о шаблонах классов, - это то, что они порождают много определений классов. Как и всякий раз, когда у вас есть множество сходных определений классов, идентичные функции должны быть соединены в общий базовый класс.
Во-первых, давайте взглянем на то, что не нужно делать. Класс storable, уже использованный мной, снова представляется хорошим примером. Сначала создадим объект collection для управления сохраняемыми объектами:
class collection














