![](/user_photo/1334_ivfwg.png)
Языки программирования С, С++
.pdf![](/html/1334/253/html_OO5wltpros.BbpE/htmlconvd-lLglPR531x1.jpg)
Тонкости и хитрости в вопросах и ответах
Как встроенные функции могут влиять на соотношение безопасности и скорости?
В обычном С вы можете получить «инкапсулированные структуры», помещая в них указатель на 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
![](/html/1334/253/html_OO5wltpros.BbpE/htmlconvd-lLglPR532x1.jpg)
Тонкости и хитрости в вопросах и ответах
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
![](/html/1334/253/html_OO5wltpros.BbpE/htmlconvd-lLglPR533x1.jpg)
Тонкости и хитрости в вопросах и ответах
Проблема возникает только тогда, когда у вашего статического или глобального объекта есть конструктор.
Как предотвратить ошибку в порядке статической инициализации?
Используйте «создание при первом использовании», то есть, поместите ваш статический объект в функцию.
Представьте себе, что у нас есть два класса 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
![](/html/1334/253/html_OO5wltpros.BbpE/htmlconvd-lLglPR534x1.jpg)
Тонкости и хитрости в вопросах и ответах
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
![](/html/1334/253/html_OO5wltpros.BbpE/htmlconvd-lLglPR535x1.jpg)
Тонкости и хитрости в вопросах и ответах
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
![](/html/1334/253/html_OO5wltpros.BbpE/htmlconvd-lLglPR536x1.jpg)
Тонкости и хитрости в вопросах и ответах
После чего вы просто меняете все 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
![](/html/1334/253/html_OO5wltpros.BbpE/htmlconvd-lLglPR537x1.jpg)
Тонкости и хитрости в вопросах и ответах
В следующем примере деструктор для объекта 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
![](/html/1334/253/html_OO5wltpros.BbpE/htmlconvd-lLglPR538x1.jpg)
Тонкости и хитрости в вопросах и ответах
А что если я хочу, чтобы локальная переменная «умерла» раньше закрывающей фигурной скобки? Могу ли я при крайней необходимости вызвать деструктор для локальной переменной?
Нет!
Предположим, что (желаемый) побочный эффект от вызова деструктора для локального объекта File заключается в закрытии файла. И предположим, что у нас есть экземпляр f класса File и мы хотим, чтобы файл f был закрыт раньше конца своей области видимости (т.е., раньше }):
void someCode()
{
File f;
//... [Этот код выполняется при открытом f] ...
//< Нам нужен эффект деструктора f здесь
// ... [Этот код выполняется после закрытия f] ...
}
Для этой проблемы есть простое решение. Но пока запомните только следующее: нельзя явно вызывать деструктор.
Хорошо, я не буду явно вызывать деструктор. Но как мне справиться с этой проблемой?
Просто поместите вашу локальную переменную в отдельный блок {...}, соответствующий необходимому времени жизни этой переменной:
void someCode()
{
{
File f;
// ... [В этом месте f еще открыт] ...
}
// ^ деструктор f будет автоматически вызван здесь! // ... [В этом месте f уже будет закрыт] ...
}
Ачто делать, если я не могу поместить переменную в отдельный блок?
Вбольшинстве случаев вы можете воспользоваться дополнительным блоком {...} для ограничения времени жизни вашей переменной. Но если по какой то причине вы не можете добавить блок, добавьте функцию член, которая будет выполнять те же действия, что и деструктор. Но помните: вы не можете сами вызывать деструктор!
534
![](/html/1334/253/html_OO5wltpros.BbpE/htmlconvd-lLglPR539x1.jpg)
Тонкости и хитрости в вопросах и ответах
Например, в случае с классом 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
![](/html/1334/253/html_OO5wltpros.BbpE/htmlconvd-lLglPR540x1.jpg)
Тонкости и хитрости в вопросах и ответах
За исключением того случая, когда вы использовали синтаксис размещения для оператора 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