Что такое грабли?
Код C++, который:
- компилируется,
- линкуется,
- запускается,
- но делает совсем не то, что ожидается.
Маленький пример:
if (-0.5 <= x <= 0.5) return 0;Грабли:
if (-0.5 <= x <= 0.5) return 0;Это выражение не проверяет математическое выражение вида:
-2.4 <= x <= 2.6
Вместо этого сначала вычисляется выражение -2.4 <= x, которое может принимать значение 0 либо 1 (в зависимости от x), а затем полученный результат сравнивается с числом 2.6.
Мораль: хоть C++ имеет встроенный тип bool, булевские переменные все еще спокойно конвертируются в тип int.
До тех пор, пока такое возможно, компилятор не сможет определить обоснованность записанного выражения. Единственное, на что он способен — это предупредить о небезопасном использовании типа bool.
Грабли, связанные с конструктором
Пример:
int main()
{
string a("Hello");
string b();
string c = string("World");
// ...
return 0;
}Грабли:
string b();
Это выражение не создает объект b типа string. В всего лишь определяет прототип функции b без аргументов и с возвращаемым типом string.
Мораль: помните, что необходимо опускать скобки при вызове конструктора по-умолчанию.
Объявление функции в стиле С в локальном пространстве имен не несет полезной нагрузки. Лучше всего помещать его в файл заголовков, чтобы вышеописанная ловушка не сработала.
Пример:
template<typename T>
class Array
{
public:
Array(int size);
T& operator[](int);
Array<T>& operator=(const Array<T>&);
//....
};
int main()
{
Array<double> a(10);
a[0] = 0; a[1] = 1; a[2] = 4;
a[3] = 9; a[4] = 16;
a[5] = 25; a = 36; a[7] = 49;
a[8] = 64; a[9] = 81;
// ...
return 0;
} Грабли:
a = 36;К удивлению, но, в общем-то, выполняется следующий код:
a = Array<double>(36);где, объект a заменяется новым массиов в 36 элементов.
Мораль: Конструкторы с одним аргументом выполняют побочную работу — приведение типов.
Избегайте конструкторы с одним аргументом (особенно с типом int). Либо используйте ключевое слово explicit, чтобы избежать такую ситуацию.
Пример:
template<typename T>
class Array
{
public:
explicit Array(int size);
// ...
private:
T* _data;
int _size;
};
template<typename T>
Array<T>::Array(int size)
: _size(size),
_data(new T(size))
{
}
int main()
{
Array<double> a(10);
a[1] = 64; // нарушение целостности памяти
// ...
} Грабли:
template<typename T>
Array<T>::Array(int size)
: _size(size),
_data(new T(size)) // должно быть new T[size]
{
}
В чем проблема?
new T(size)возвращает указатель T* на единичный объект T, инициализированный значением size.
new T[size]возвращает указатель T* на массив объектов типа T размером size, созданный конструктором по-умолчанию.
Мораль:
Массив и указатель имеют одинаковую природу, и поэтому несут в себе опасность.
Пример:
template<typename T>
class Array
{
public:
explicit Array(int size);
// ...
private:
T* _data;
int _capacity;
int _size;
};
template<typename T>
Array<T>::Array(int size)
: _size(size),
_capacity(_size + 10),
_data(new T[_capacity])
{}
int main()
{
Array<int> a(100);
//....
} Грабли:
Array<T>::Array(int size)
: _size(size),
_capacity(size + 10),
_data(new T[_capacity]) // <---
{} Инициализация членов выполняется в порядке объявления в классе, а не по порядку появления в строке инициализации конструктора.
Совет: не используйте члены-данные в выражениях инициализации.
Array<T>::Array(int size)
: _data(new T[size + 10])
_capacity(size + 10),
_size(size)Пример:
class Point
{
public:
Point(double x = 0, double y = 0);
// ...
private:
double _x, _y;
};
int main()
{
double a, r, x, y;
// ...
Point p = (x + r * cos(a), y + r * sin(a));
// ...
return 0;
}Грабли:
Point p = (x + r * cos(a), y + r * sin(a));Корректным будет выполнить следующий код
Point p(x + r * cos(a), y + r * sin(a))либо
Point p = Point(x + r * cos(a), y + r * sin(a));Выражение
(x + r * cos(a), y + r * sin(a))имеет искаженное понимание. Оператор запятая не учитывает x + r * cos(a) и выполняет y + r * sin(a). В данном случае вызывается конструктор со следующими параметрами:
Point(y + r * sin(a), 0)Мораль: аргументы по-умолчанию могут привести к непреднамеренным вызовам. В нашем случае, конструктор Point(double) некорректен, зато Point() - вполне приемлимо. Использовать аргументы по-умолчанию необходимо лишь в том случае, когда все способы вызова такого конструктора осмысленны.
Грабли, связанные с деструктором
Пример:
class Employee
{
public:
Employee(string name);
virtual void print() const;
private:
string _name;
};
class Manager : public Employee
{
public:
Manager(string name, string dept);
virtual void print() const;
private:
string _dept;
};
int main()
{
Employee* staff[10];
staff[0] = new Employee("Harry Hacker");
staff[1] = new Manager("Joe Smith", "Sales");
// ...
for (int i = 0; i < 10; i++)
staff[i]->print();
for (int i = 0; i < 10; i++)
delete staff[i];
return 0;
}Где здесь утечка памяти?
Грабли:
delete staff[i];удаляет все объекты деструктором ~Employee(). Строка _dept объекта Manager никогда не удалится.
Мораль: Наследуемый класс должен иметь виртуальный деструктор.
Пример:
class Employee
{
public:
Employee(string name);
virtual void print() const;
virtual ~Employee();
private:
string _name;
};
class Manager:public Employee
{
public:
Manager(string name, string sname);
~Manager();
private:
Employee* _secretary;
}
Manager::Manager(string name, string sname)
: Employee(name),
_secretary(new Employee(sname))
{}
Manager::~Manager() { delete _secretary; }Где затаилась проблема?
Грабли:
int main()
{
Manager m1 = Manager("Sally Smith","Joe Barnes");
Manager m2 = m1;
// ...
}Деструкторы обоих объектов m1 и m2 удалят один и тот же объект класс Employee.
Мораль: необходимо позаботиться о копирующем конструкторе
Manager::Manager(const Manager&)и об операторе присваивания
Manager& Manager::operator=(const Manager&)в случае, если класс содержит сложные типы данных (в том числе указатели)
Продолжение следует... Чтобы не упустить продолжение, достаточно подписаться на RSS.


5 коммент.:
class Employee
{
public:
Employee(string name);
virtual void print() const;
virtual ~Employee();
private:
string _name;
};
class Employee
{
public:
Employee(string name);
private:
string _name;
};
class Manager
{
public:
Manager(string name, string sname);
~Manager();
private:
Employee* _secretary;
}
Manager::Manager(string name, string sname)
: Employee(name),
_secretary(new Employee(sname))
{}
Manager::~Manager() { delete _secretary; }
2 разных класса Employee, и Manager не наследуется от Employee в данном случае (забыл прописать : public Employee).
Мораль: класс с деструктором должен иметь копирующий конструктор
ИМХО, мораль не в этом. А в том, что если у вас есть указатели в классе - то вам нужен деструктор и копирующий конструктор. Но не так как ты написал.
А вообще статья очень хорошая. Некоторых моментов я не знал. Спасибо.
А теперь длиннющий PS:
Ну нахрена ты вынес комментарии в отдельное окно - пипец как неудобно пользоваться.
Далее - реклама. Я, конечно, понимаю, что это деньги. Но много ли ты получаешь денег с рекламы. Думаю, нет. НАХРЕНА тебе в конце каждого поста по рекламному блоку?... Вынеси её куда-нибудь в сайдбар и пусть она не мешает читать твои интересные статьи.
А то просто ужас - интересный блог, который не хочется открывать потому, что им нереально пользоваться.
Спасибо, что поправили мой пост :) Очень устал, пока его набирал. Поэтому неудивительно, что имеются такие ошибки.
По поводу PS. Я с уважением отношусь к конструктивной критике. Поэтому пожелания были приняты к сведению. Некоторые из них включались в мою повестку дня (вернее недели - занят очень) по реорганизации этого блога. Спасибо за советы.
ЗЫ. Эта реклама ничего мне не приносит, хотя должна служить для притока посетителей. Статсы показывают, что она крайне неэффективна (feedmates.ru). Удаляю ее к черту.
Инициализация членов выполняется в порядке объявления в классе, а не по порядку появления в строке инициализации конструктора.
Совет: не используйте члены-данные в выражениях инициализации.
Можно и использовать, только нужно что-бы порядок инициализации в конструкторе совпадал с порядком объявления в классе. GCC даже предупреждение выдает если это не так.
зы
Статья интересная, сам на все эти грабли наступал =)
Можно и использовать, только нужно что-бы порядок инициализации в конструкторе совпадал с порядком объявления в классе.
Лучше все-таки обходить эту ситуацию стороной, а то мало ли что ;) Не зря это признанные грабли ))
Это ведь просто перевод этой статьи: http://www.horstmann.com/cpp/pitfalls.html
Причём, соблюдена та же синтаксическая ошибка с : public Employee. Жаль, что не приводится ссылка на оригинал.