Категории
Самые читаемые
RUSBOOK.SU » Компьютеры и Интернет » Программирование » Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Читать онлайн Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 12 13 14 15 16 17 18 19 20 ... 73
Перейти на страницу:

Есть разные варианты решения этой проблемы. Один из них – сделать функцию logTransaction невиртуальной в классе Transaction, затем потребовать, чтобы конструкторы производного класса передавали необходимую для записи в протокол информацию конструктору Transaction. Эта функция затем могла бы безопасно вызвать невиртуальную logTransaction. Примерно так:

class Transaction {

public:

explicit Transaction(const std::string& loginfo);

void logTransaction(const std::string& loginfo) const; // теперь –

// невиртуальная

// функция

...

};

Transaction::Transaction(const std::string& loginfo)

{

...

logTransaction(loginfo); // теперь –

// невиртуальный

// вызов

}

class BuyTransaction : public Transaction {

public:

BuyTransaction( parameters )

: Transaction(createLogString( parameters )) // передать информацию

{...} // для записи в протокол

... // конструктору базового

// класса

private:

static std::string createLogString( parameters );

}

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

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

Что следует помнить

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

Правило 10: Операторы присваивания должны возвращать ссылку на *this

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

int x,y,z;

x = y = z = 15; // цепочка присваиваний

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

x = (y = (z = 15));

Здесь переменной z присваивается значение 15, затем результат присваивания (новое значение z) присваивается переменной y, после чего результат (новое значение y) присваивается переменной x.

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

class Widget {

public:

...

Widget& operator=(const Widget& rhs) // возвращаемый тип – ссылка

{ // на текущий класс

...

return *this; // вернуть объект из левой части

} // выражения

...

};

Это соглашение касается всех операторов присваивания, а не только стандартной формы, показанной выше. Следовательно:

class Widget {

public:

...

Widget& operator+=(const Widget& rhs) // соглашение распространяется на

{ // +=, -=, *=, и т. д.

...

return *this;

}

Widget& operator=(int rhs) // это относится даже

{ // к параметрам разных типов

...

return *this;

}

...

};

Это всего лишь соглашение. Если программа его не придерживается, она тем не менее скомпилируется. Однако ему следуют все встроенные типы, как и все типы (см. правило 54) стандартной библиотеки (то есть string, vector, complex, tr1::shared_ptr и т. д.). Если у вас нет веской причины нарушать соглашение, не делайте этого.

Что следует помнить

• Пишите операторы присваивания так, чтобы они возвращали ссылку на *this.

Правило 11: В operator= осуществляйте проверку на присваивание самому себе

Присваивание самому себе возникает примерно в такой ситуации:

class Widget {...};

Widget w;

...

w = w; // присваивание себе

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

Кроме того, присваивание самому себе не всегда так легко узнаваемо. Например:

a[i] = a[j]; // потенциальное присваивание себе

это присваивание себе, если i и j равны одному и тому же значению, и

*px = *py; // потенциальное присваивание себе

тоже становится присваиванием самому себе, если окажется, что px и py указывают на одно и то же.

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

class Base {...};

class Derived: public Base {...};

void doSomething(const Base& rb, // rb и *pd могут быть одним и тем же

Derived * pd); // объектом

Если вы следуете правилам 13 и 14, то всегда пользуетесь объектами для управления ресурсами; следите за тем, чтобы управляющие объекты правильно вели себя при копировании. В таком случае операторы присваивания должны быть безопасны относительно присваивания самому себе. Если вы пытаетесь управлять ресурсами самостоятельно (а как же иначе, если вы пишете класс для управления ресурсами), то можете попасть в ловушку, нечаянно освободив ресурс до его использования. Например, предположим, что вы создали класс, который содержит указатель на динамически распределенный объект класса Bitmap:

class Bitmap {...};

class Widget {

...

private:

Bitmap *pb; // указатель на объект, размещенный в куче

};

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

Widget&

Widget::operator=(const Widget& rhs) // небезопасная реализация operator=

{

delete pb; // прекратить использование текущего

// объекта Bitmap

pb = new Bitmap(*rhs.pb); // начать использование копии объекта

// Bitmap, указанной в правой части

return *this; // см. правило 10

}

Проблема состоит в том, что внутри operator= *this (чему присваивается значение) и rhs (что присваивается) могут оказаться одним и тем же объектом. Если это случится, то delete уничтожит не только Bitmap, принадлежащий текущему объекту, но и Bitmap, принадлежащий объекту в правой части. По завершении работы этой функции Widget, который не должен был бы измениться в процессе присваивания самому себе, содержит указатель на удаленный объект!

Традиционный способ предотвратить эту ошибку состоит в том, что нужно выполнить проверку совпадения в начале operator=:

Widget&

Widget::operator=(const Widget& rhs) // небезопасная реализация operator=

{

if(this == &rhs) return *this; // проверка совпадения: если

// присваивание самому себе, то

// ничего не делать

delete pb;

pb = new Bitmap(*rhs.pb);

return *this;

}

Это решает проблему, но я уже упоминал, что предыдущая версия оператора присваивания была не только опасна в случае присваивания себе, но и небезопасна в смысле исключений, и последняя опасность остается актуальной во второй версии. В частности, если выражение «new Bitmap» вызовет исключение (либо по причине недостатка свободной памяти, либо исключение возбудит конструктор копирования Bitmap), то Widget также будет содержать указатель на несуществующий Bitmap. Такие указатели – источник неприятностей. Их нельзя безопасно удалить, их даже нельзя разыменовывать. А вот потратить массу времени на отладку, выясняя, откуда они взялись, – это можно.

К счастью, существует способ одновременно сделать operator= безопасным в смысле исключений и безопасным по части присваивания самому себе. Поэтому все чаще программисты не занимаются специально присваиванием самому себе, а сосредоточивают усилия на достижении безопасности в смысле исключений. В правиле 29 эта проблема рассмотрена детально, а сейчас достаточно упомянуть, что во многих случаях продуманная последовательность операторов присваивания может обеспечить безопасность в смысле исключений (а заодно безопасность присваивания самому себе) кода. Например, ниже мы просто не удаляем pb до тех пор, пока не скопируем то, на что он указывает:

1 ... 12 13 14 15 16 17 18 19 20 ... 73
Перейти на страницу:
На этой странице вы можете бесплатно скачать Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс торрент бесплатно.
Комментарии
Открыть боковую панель
Комментарии
Вася
Вася 24.11.2024 - 19:04
Прекрасное описание анального секса
Сергій
Сергій 25.01.2024 - 17:17
"Убийство миссис Спэнлоу" от Агаты Кристи – это великолепный детектив, который завораживает с первой страницы и держит в напряжении до последнего момента. Кристи, как всегда, мастерски строит