Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Сабуров С.В. - Язык программирования C и C++ - 2006

.pdf
Скачиваний:
312
Добавлен:
13.08.2013
Размер:
1.42 Mб
Скачать

Тонкости и хитрости в вопросах и ответах

Как встроенные функции могут влиять на соотношение безопасности и скорости?

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

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

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

Зачем мне использовать встроенные функции? Почему не использовать просто #define макросы?

Поскольку #define макросы опасны.

В отличие от #define макросов, встроенные (inline) функции не подвержены известным ошибкам двойного вычисления, поскольку каждый аргумент встроенной функции вычисляется только один раз. Другими словами, вызов встроенной функции — это то же самое что и вызов обычной функции, только быстрее:

// Макрос, возвращающий модуль (абсолютное значение) i #define unsafe(i) \

( (i) >= 0 ? (i) : (i) )

// Встроенная функция, возвращающая абсолютное значение i inline

int safe(int i)

{

return i >= 0 ? i : i;

}

527

Тонкости и хитрости в вопросах и ответах

int

f();

void

userCode(int x)

{

 

int

ans;

ans = unsafe(x++); // Ошибка! x инкрементируется дважды ans = unsafe(f()); // Опасно! f() вызывается дважды ans = safe(x++); // Верно! x инкрементируется один раз ans = safe(f()); // Верно! f() вызывается один раз

}

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

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

Что такое ошибка в порядке статической инициализации («static initialization order fiasco»)?

Незаметный и коварный способ убить ваш проект.

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

Представьте себе, что у вас есть два статических объекта x и y, которые находятся в двух разных исходных файлах, скажем x.cpp и y.cpp. И путь конструктор объекта y вызывает какой либо метод объекта x.

Вот и все. Так просто.

Проблема в том, что у вас ровно пятидесятипроцентная возможность катастрофы. Если случится, что единица трансляции с x.cpp будет проинициализирована первой, то все в порядке. Если же первой будет проинициализирована единица трансляции файла y.cpp, тогда конструктор объекта y будет запущен до конструктора x, и вам крышка. Т.е., конструктор y вызовет метод объекта x, когда сам x еще не создан.

Примечание: ошибки статической инициализации не распространяются на базовые/встроенные типы, такие как int или char*. Например, если вы создаете статическую переменную типа float, у вас не будет проблем с порядком инициализации.

528

Тонкости и хитрости в вопросах и ответах

Проблема возникает только тогда, когда у вашего статического или глобального объекта есть конструктор.

Как предотвратить ошибку в порядке статической инициализации?

Используйте «создание при первом использовании», то есть, поместите ваш статический объект в функцию.

Представьте себе, что у нас есть два класса Fred и Barney. Есть глобальный объект типа Fred, с именем x, и глобальный объект типа Barney, с именем y. Конструктор Barney вызывает метод goBowling() объекта x. Файл x.cpp содержит определение объекта x:

// File x.cpp

#include "Fred.hpp" Fred x;

Файл y.cpp содержит определение объекта y:

// File y.cpp

#include "Barney.hpp" Barney y;

Для полноты представим, что конструктор Barney::Barney() выглядит следующим образом:

// File Barney.cpp #include "Barney.hpp" Barney::Barney()

{

//...

x.goBowling();

//...

}

Проблема случается, если y создается раньше, чем x, что происходит в 50% случаев, поскольку x и y находятся в разных исходных файлах.

Есть много решений для этой проблемы, но одно очень простое и переносимое — заменить глобальный объект Fred x, глобальной функцией x(), которая возвращает объект типа Fred по ссылке.

// File x.cpp #include "Fred.hpp"

Fred& x()

{

529

Тонкости и хитрости в вопросах и ответах

static Fred* ans = new Fred(); return *ans;

}

Поскольку локальные статические объекты создаются в момент, когда программа в процессе работы в первый раз проходит через точку их объявления, инструкция new Fred() в примере выше будет выполнена только один раз: во время первого вызова функции x(). Каждый последующий вызов возвратит тот же самый объект Fred (тот, на который указывает ans). И далее все случаи использования объекта x замените на вызовы функции x():

// File Barney.cpp #include "Barney.hpp" Barney::Barney()

{

// ...

x().goBowling(); // ...

}

Это и называется «создание при первом использовании», глобальный объект Fred создается при первом обращении к нему.

Отрицательным моментом этой техники является тот факт, что объект Fred нигде не уничтожается.

Примечание: ошибки статической инициализации не распространяются на базовые/встроенные типы, такие как int или char*. Например, если вы создаете статическую переменную типа float, у вас не будет проблем с порядком инициализации. Проблема возникает только тогда, когда у вашего статического или глобального объекта есть конструктор.

Как бороться с ошибками порядка статической инициализации объектов — членов класса?

Предположим, у вас есть класс X, в котором есть статический объект Fred:

// File X.hpp class X {

public: // ...

private:

530

Тонкости и хитрости в вопросах и ответах

static Fred x_;

};

Естественно, этот статический член инициализируется отдельно:

// File X.cpp #include "X.hpp" Fred X::x_;

Опять же естественно, объект Fred будет использован в одном или нескольких методах класса X:

void X::someMethod()

{

x_.goBowling();

}

Проблема проявится, если кто то где то каким либо образом вызовет этот метод, до того как объект Fred будет создан. Например, если кто то создает статический объект X и вызывает его someMethod() во время статической инициализации, то ваша судьба всецело находится в руках компилятора, который либо создаст X::x_, до того как будет вызван someMethod(), либо же только после.

В любом случае, всегда можно сохранить переносимость (и это абсолютно безопасный метод), заменив статический член X::x_ на статическую функцию член:

// File X.hpp class X {

public: // ...

private:

static Fred& x();

};

Естественно, этот статический член инициализируется отдельно:

// File X.cpp #include "X.hpp"

Fred& X::x()

{

static Fred* ans = new Fred(); return *ans;

}

531

Тонкости и хитрости в вопросах и ответах

После чего вы просто меняете все x_ на x(): void X::someMethod()

{

x().goBowling();

}

Если для вас крайне важна скорость работы программы и вас беспокоит необходимость дополнительного вызова функции для каждого вызова X::someMethod(), то вы можете сделать static Fred&. Как вы помните, статические локальные переменные инициализируются только один раз (при первом прохождении программы через их объявление), так что X::x() теперь будет вызвана только один раз: во время первого вызова

X::someMethod():

void X::someMethod()

{

static Fred& x = X::x(); x.goBowling();

}

Примечание: ошибки статической инициализации не распространяются на базовые/встроенные типы, такие как int или char*. Например, если вы создаете статическую переменную типа float, у вас не будет проблем с порядком инициализации. Проблема возникает только тогда, когда у вашего статического или глобального объекта есть конструктор.

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

Сгенерируйте исключение.

Что такое деструктор?

Деструктор — это исполнение последней воли объекта.

Деструкторы используются для высвобождения занятых объектом ресурсов. Например, класс Lock может заблокировать ресурс для эксклюзивного использования, а его деструктор этот ресурс освободить. Но самый частый случай — это когда в конструкторе используется new, а в деструкторе — delete.

Деструктор это функция «готовься к смерти». Часто слово деструктор сокращается до dtor.

Вкаком порядке вызываются деструкторы для локальных объектов?

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

532

Тонкости и хитрости в вопросах и ответах

В следующем примере деструктор для объекта b будет вызван первым, а только затем деструктор для объекта a:

void userCode()

{

Fred a; Fred b; // ...

}

Вкаком порядке вызываются деструкторы для массивов объектов?

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

Вследующем примере порядок вызова деструкторов будет таким: a[9], a[8], ..., a[1], a[0]:

void userCode()

{

Fred a[10]; // ...

}

Могу ли я перегрузить деструктор для своего класса?

Нет.

У каждого класса может быть только один деструктор. Для класса Fred он всегда будет называться Fred::~Fred(). В деструктор никогда не передаётся никаких параметров, и сам деструктор никогда ничего не возвращает.

Всё равно вы не смогли бы указать параметры для деструктора, потому что вы никогда на вызываете деструктор напрямую (точнее, почти никогда).

Могу ли я явно вызвать деструктор для локальной переменной?

Нет!

Деструктор всё равно будет вызван еще раз при достижении закрывающей фигурной скобки } конца блока, в котором была создана локальная переменная. Этот вызов гарантируется языком, и он происходит автоматически; нет способа этот вызов предотвратить. Но последствия повторного вызова деструктора для одного и того же объекта могут быть плачевными. Бах! И вы покойник...

533

Тонкости и хитрости в вопросах и ответах

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

Нет!

Предположим, что (желаемый) побочный эффект от вызова деструктора для локального объекта File заключается в закрытии файла. И предположим, что у нас есть экземпляр f класса File и мы хотим, чтобы файл f был закрыт раньше конца своей области видимости (т.е., раньше }):

void someCode()

{

File f;

//... [Этот код выполняется при открытом f] ...

//< Нам нужен эффект деструктора f здесь

// ... [Этот код выполняется после закрытия f] ...

}

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

Хорошо, я не буду явно вызывать деструктор. Но как мне справиться с этой проблемой?

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

void someCode()

{

{

File f;

// ... [В этом месте f еще открыт] ...

}

// ^ деструктор f будет автоматически вызван здесь! // ... [В этом месте f уже будет закрыт] ...

}

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

Вбольшинстве случаев вы можете воспользоваться дополнительным блоком {...} для ограничения времени жизни вашей переменной. Но если по какой то причине вы не можете добавить блок, добавьте функцию член, которая будет выполнять те же действия, что и деструктор. Но помните: вы не можете сами вызывать деструктор!

534

Тонкости и хитрости в вопросах и ответах

Например, в случае с классом File, вы можете добавить метод close(). Обычный деструктор будет вызывать close(). Обратите внимание, что метод close() должен будет как то отмечать объект File, с тем чтобы последующие вызовы не пытались закрыть уже закрытый файл. Например, можно устанавливать переменную член fileHandle_ в какое нибудь неиспользуемое значение, типа 1, и проверять вначале, не содержит ли fileHandle_ значение 1.

class File { public:

void close(); ~File();

// ...

private:

int fileHandle_;

// fileHandle_ >= 0 если/только если файл открыт };

File::~File()

{

close();

}

void File::close()

{

if (fileHandle_ >= 0) {

// ... [Вызвать системную функцию для закрытия файла]

...

fileHandle_ = 1;

}

}

Обратите внимание, что другим методам класса File тоже может понадобиться проверять, не установлен ли fileHandle_ в 1 (т.е., не закрыт ли файл).

Также обратите внимание, что все конструкторы, которые не открывают файл, должны устанавливать fileHandle_ в 1.

А могу ли я явно вызывать деструктор для объекта, созданного при помощи new?

Скорее всего, нет.

535

Тонкости и хитрости в вопросах и ответах

За исключением того случая, когда вы использовали синтаксис размещения для оператора new, вам следует просто удалять объекты при помощи delete, а не вызывать явно деструктор. Предположим, что вы создали объект при помощи обычного new:

Fred* p = new Fred();

В таком случае деструктор Fred::~Fred() будет автоматически вызван, когда вы удаляете объект:

delete p; // Вызывает p >~Fred()

Вам не следует явно вызывать деструктор, поскольку этим вы не освобождаете память, выделенную для объекта Fred. Помните: delete p делает сразу две вещи: вызывает деструктор и освобождает память.

Что такое «синтаксис размещения» new («placement new») и зачем он нужен?

Есть много случаев для использования синтаксиса размещения для new. Самое простое — вы можете использовать синтаксис размещения для помещения объекта в определенное место в памяти. Для этого вы указываете место, передавая указатель на него в оператор new:

#include

<new>

// Необходимо для использования

синтаксиса размещения

 

 

 

#include

"Fred.h"

// Определение

класса Fred

void

someCode()

 

 

 

{

 

 

 

 

 

char

memory[sizeof(Fred)];

//

#1

void*

place =

memory;

//

#2

Fred*

f = new(place) Fred();

//

#3

//Указатели f и place будут равны

//...

}

В строчке #1 создаётся массив из sizeof(Fred) байт, размер которого достаточен для хранения объекта Fred. В строчке #2 создаётся указатель place, который указывает на первый байт массива (опытные программисты на С наверняка заметят, что можно было и не создавать этот указатель; мы это сделали лишь чтобы код был более понятным). В строчке #3 фактически происходит только вызов конструктора Fred::Fred(). Указатель this

536

Соседние файлы в предмете Программирование на C++