Каких ошибок в C++ мне следует избегать?
Я помню, как впервые узнал о векторах в STL, и через некоторое время я захотел использовать вектор bools для одного из моих проектов. Увидев какое-то странное поведение и проведя некоторые исследования, я узнал, что вектор bools на самом деле не является вектором bools.
Есть ли другие распространенные ошибки, которых следует избегать в C++?
29 ответов
Краткий список может быть:
- Избегайте утечек памяти благодаря использованию общих указателей для управления распределением и очисткой памяти
- Используйте идиому " Приобретение ресурсов - инициализация" (RAII) для управления очисткой ресурсов - особенно при наличии исключений
- Избегайте вызова виртуальных функций в конструкторах
- По возможности используйте минималистские методы кодирования - например, объявление переменных только при необходимости, определение области видимости и раннее проектирование, где это возможно.
- Правильно понимать обработку исключений в вашем коде - как в отношении исключений, которые вы генерируете, так и в отношении тех, которые вызываются классами, которые вы можете использовать косвенно. Это особенно важно при наличии шаблонов.
RAII, общие указатели и минималистское кодирование, конечно, не являются специфическими для C++, но они помогают избежать проблем, которые часто возникают при разработке языка.
Некоторые отличные книги на эту тему:
- Эффективный C++ - Скотт Мейерс
- Более эффективный C++ - Скотт Мейерс
- Стандарты кодирования C++ - Саттер и Александреску
- C++ часто задаваемые вопросы - Cline
Чтение этих книг помогло мне больше всего избежать ошибок, о которых вы спрашиваете.
Подводные камни в порядке убывания их важности
Прежде всего, вы должны посетить отмеченный наградами C++ FAQ. У него много хороших ответов на подводные камни. Если у вас есть дополнительные вопросы, посетите ##c++
на irc.freenode.org
в IRC. Мы рады помочь вам, если сможем. Обратите внимание, что все следующие подводные камни изначально написаны. Они не просто скопированы из случайных источников.
delete[]
наnew
,delete
наnew[]
Решение: выполнение вышеизложенного приводит к неопределенному поведению: все может случиться. Понять свой код и то, что он делает, и всегда delete[]
что ты new[]
, а также delete
что ты new
тогда этого не произойдет.
Исключение:
typedef T type[N]; T * pT = new type; delete[] pT;
Вам нужно delete[]
хотя ты new
, поскольку вы создали новый массив. Так что если вы работаете с typedef
, будьте особенно внимательны.
Вызов виртуальной функции в конструкторе или деструкторе
Решение: вызов виртуальной функции не вызовет переопределяющие функции в производных классах. Вызов чисто виртуальной функции в конструкторе или дескрипторе - это неопределенное поведение.
призвание
delete
или жеdelete[]
на уже удаленном указателе
Решение: присвойте 0 каждому указателю, который вы удаляете. призвание delete
или же delete[]
на нулевой указатель ничего не делает.
Принимая размер указателя, когда нужно вычислить количество элементов в массиве.
Решение: Передайте количество элементов рядом с указателем, когда вам нужно передать массив в качестве указателя в функцию. Используйте предложенную здесь функцию, если вы возьмете размер массива, который должен быть действительно массивом.
Использование массива, как если бы это был указатель. Таким образом, используя
T **
для двумерного массива.
Решение: посмотрите здесь, почему они отличаются и как вы справляетесь с ними.
Запись в строковый литерал:
char * c = "hello"; *c = 'B';
Решение: Выделите массив, который инициализируется из данных строкового литерала, затем вы можете записать в него:
char c[] = "hello"; *c = 'B';
Запись в строковый литерал является неопределенным поведением. В любом случае, приведенное выше преобразование из строкового литерала в char *
устарела. Поэтому компиляторы, вероятно, будут предупреждать, если вы увеличите уровень предупреждения.
Создание ресурсов, а затем забыть освободить их, когда что-то выбрасывает.
Решение: используйте умные указатели, такие как std::unique_ptr
или же std::shared_ptr
как указано в других ответах.
Модификация объекта дважды, как в этом примере:
i = ++i;
Решение: вышесказанное предполагалось назначить i
значение i+1
, Но что это делает, не определено. Вместо увеличения i
и присваивая результат, он меняет i
на правой стороне, а также. Изменение объекта между двумя точками последовательности - неопределенное поведение. Очки последовательности включают ||
, &&
, comma-operator
, semicolon
а также entering a function
(не исчерпывающий список!). Измените код на следующий, чтобы он вел себя правильно: i = i + 1;
Разные вопросы
Забыв очищать потоки перед вызовом функции блокировки, такой как
sleep
,
Решение: очистить поток, используя поток std::endl
вместо \n
или по телефону stream.flush();
,
Объявление функции вместо переменной.
Решение: проблема возникает потому, что компилятор интерпретирует, например,
Type t(other_type(value));
как объявление функции функции t
возврате Type
и имеющий параметр типа other_type
который называется value
, Вы решаете это, помещая круглые скобки вокруг первого аргумента. Теперь вы получаете переменную t
типа Type
:
Type t((other_type(value)));
Вызов функции свободного объекта, который объявлен только в текущей единице перевода (
.cpp
файл).
Решение: Стандарт не определяет порядок создания свободных объектов (в области пространства имен), определенных в разных единицах перевода. Вызов функции-члена для еще не созданного объекта является неопределенным поведением. Вместо этого вы можете определить следующую функцию в модуле перевода объекта и вызвать ее из других:
House & getTheHouse() { static House h; return h; }
Это создало бы объект по требованию и оставило бы вас полностью сконструированным объектом во время вызова функций на нем.
Определение шаблона в
.cpp
файл, в то время как он используется в другом.cpp
файл.
Решение: почти всегда вы получите такие ошибки, как undefined reference to ...
, Поместите все определения шаблонов в заголовок, чтобы, когда их использовал компилятор, он уже мог генерировать необходимый код.
static_cast<Derived*>(base);
если база является указателем на виртуальный базовый классDerived
,
Решение. Виртуальный базовый класс - это базовый класс, который встречается только один раз, даже если он наследуется более одного раза различными классами косвенно в дереве наследования. Выполнение вышеизложенного не допускается Стандартом. Используйте dynamic_cast для этого и убедитесь, что ваш базовый класс полиморфен.
dynamic_cast<Derived*>(ptr_to_base);
если база не является полиморфной
Решение: Стандарт не допускает снижения указателя или ссылки, когда переданный объект не является полиморфным. Он или один из его базовых классов должен иметь виртуальную функцию.
Заставить вашу функцию принять
T const **
Решение: Вы можете подумать, что это безопаснее, чем использовать T **
, но на самом деле это вызовет головную боль людям, которые хотят пройти T**
Стандарт не позволяет этого. Это дает хороший пример того, почему это запрещено:
int main() {
char const c = ’c’;
char* pc;
char const** pcc = &pc; //1: not allowed
*pcc = &c;
*pc = ’C’; //2: modifies a const object
}
Всегда принимай T const* const*;
вместо.
Еще одна (закрытая) тема подводных камней о C++, так что люди, которые ищут их, найдут их, это вопрос переполнения стека C++.
У некоторых должны быть книги по C++, которые помогут вам избежать распространенных ошибок C++:
Эффективный C++
Более эффективный C++
Эффективный STL
В книге "Эффективный STL" объясняется вектор проблемы с bools:)
У Брайана отличный список: я бы добавил "Всегда помечать конструкторы с одним аргументом как явные (за исключением тех редких случаев, когда требуется автоматическое приведение)".
Не совсем конкретный совет, но общее руководство: проверьте свои источники. C++ - старый язык, и он сильно изменился за эти годы. Лучшие практики изменились с этим, но, к сожалению, есть еще много старой информации. Здесь были очень хорошие рекомендации по книгам - я могу купить каждую из книг Скотта Мейерса на С ++. Познакомьтесь с Boost и со стилями кодирования, используемыми в Boost, - люди, вовлеченные в этот проект, находятся на переднем крае дизайна C++.
Не изобретайте велосипед. Познакомьтесь с STL и Boost и по возможности пользуйтесь их возможностями. В частности, используйте строки и коллекции STL, если у вас нет очень веских причин не делать этого. Познакомьтесь с auto_ptr и библиотекой интеллектуальных указателей Boost, поймите, при каких обстоятельствах предполагается использовать каждый тип интеллектуальных указателей, и затем используйте интеллектуальные указатели везде, где вы могли бы использовать необработанные указатели. Ваш код будет столь же эффективным и намного менее подвержен утечкам памяти.
Используйте static_cast, dynamic_cast, const_cast и reinterpret_cast вместо приведений в стиле C. В отличие от бросков в стиле C, они сообщат вам, если вы действительно запрашиваете другой тип броска, который, по вашему мнению, вы запрашиваете. И они выделяются визуально, предупреждая читателя о том, что происходит кастинг.
Веб-страница С ++ Скотта Уилера " Подводные камни " охватывает некоторые основные подводные камни С ++.
Две ошибки, которые я хотел бы не узнать на собственном опыте:
(1) Большая часть вывода (например, printf) буферизируется по умолчанию. Если вы отлаживаете аварийный код и используете буферизованные операторы отладки, последний вывод, который вы видите, может не совпадать с последним оператором печати, встреченным в коде. Решение состоит в том, чтобы очищать буфер после каждой отладочной печати (или вообще отключать буферизацию).
(2) Будьте осторожны с инициализацией - (а) избегайте экземпляров классов как глобальных / статических; и (b) попытаться инициализировать все ваши переменные-члены некоторым безопасным значением в ctor, даже если это тривиальное значение, например NULL для указателей.
Причина: упорядочение инициализации глобального объекта не гарантируется (глобальные переменные включают статические переменные), поэтому вы можете в конечном итоге получить код, который, по-видимому, завершается с ошибкой недетерминированно, поскольку это зависит от инициализации объекта X перед объектом Y. Если вы не инициализируете явно Переменная типа примитива, такая как член bool или enum класса, в неожиданных ситуациях вы получите разные значения - опять же, поведение может показаться очень недетерминированным.
Использование C++ подобно C. Наличие в коде цикла создания и выпуска.
В C++ это не исключительная ситуация, и поэтому выпуск может быть не выполнен. В C++ мы используем RAII для решения этой проблемы.
Все ресурсы, которые создаются и выпускаются вручную, должны быть обернуты в объект, чтобы эти действия выполнялись в конструкторе / деструкторе.
// C Code
void myFunc()
{
Plop* plop = createMyPlopResource();
// Use the plop
releaseMyPlopResource(plop);
}
В C++ это должно быть заключено в объект:
// C++
class PlopResource
{
public:
PlopResource()
{
mPlop=createMyPlopResource();
// handle exceptions and errors.
}
~PlopResource()
{
releaseMyPlopResource(mPlop);
}
private:
Plop* mPlop;
};
void myFunc()
{
PlopResource plop;
// Use the plop
// Exception safe release on exit.
}
Я уже упоминал об этом несколько раз, но книги Скотта Мейерса " Effective C++" и " Effective STL" действительно стоят на вес золота, помогая с C++.
Если подумать, то Стивен Дьюхерст C++ Gotchas также является отличным ресурсом "из окопов". Его статья о создании собственных исключений и о том, как они должны быть сконструированы, действительно помогла мне в одном проекте.
Вот несколько ям, в которые я попал. Все это имеет веские причины, которые я понял только после того, как меня укусило поведение, которое меня удивило.
virtual
функции в конструкторах не являются.Не нарушайте ODR (One Definition Rule), для этого и нужны анонимные пространства имен (между прочим).
Порядок инициализации членов зависит от порядка, в котором они объявлены.
class bar { vector<int> vec_; unsigned size_; // Note size_ declared *after* vec_ public: bar(unsigned size) : size_(size) , vec_(size_) // size_ is uninitialized {} };
Значения по умолчанию и
virtual
имеют разную семантику.class base { public: virtual foo(int i = 42) { cout << "base " << i; } }; class derived : public base { public: virtual foo(int i = 12) { cout << "derived "<< i; } }; derived d; base& b = d; b.foo(); // Outputs `derived 42`
- Не читая C++ FAQ Lite. Это объясняет многие плохие (и хорошие!) Практики.
- Не использует Boost. Вы избавите себя от многих разочарований, используя возможности Boost, где это возможно.
Проверьте http://boost.org/. Он предоставляет множество дополнительных функций, особенно их интеллектуальные реализации указателей.
Самым важным подводным камнем для начинающих разработчиков является недопущение путаницы между C и C++. C++ никогда не следует рассматривать как просто лучший C или C с классами, потому что это сокращает его мощность и может сделать его даже опасным (особенно при использовании памяти, как в C).
PRQA имеет отличный и бесплатный стандарт кодирования на C++, основанный на книгах Скотта Мейерса, Бьярна Страустропа и Херба Саттера. Он объединяет всю эту информацию в одном документе.
Будьте осторожны при использовании умных указателей и контейнерных классов.
Избегайте псевдоклассов и квазиклассов... В общем, чрезмерный дизайн.
Забыв определить виртуальный деструктор базового класса. Это означает, что вызов delete
на базе * не приведет к разрушению производной части.
Чтобы испортить, часто используйте прямые указатели. Вместо этого используйте RAII для чего угодно, убедившись, что вы используете правильные умные указатели. Если вы пишете "удалить" где-либо за пределами класса дескриптора или указателя, вы, скорее всего, делаете это неправильно.
Blizpasta. Это огромный, я вижу много...
Неинициализированные переменные - огромная ошибка, которую допускают мои студенты. Многие Java-пользователи забывают, что простое выражение "int counter" не устанавливает счетчик в 0. Поскольку вам нужно определить переменные в файле h (и инициализировать их в конструкторе / настройке объекта), это легко забыть.
Одиночные ошибки на
for
петли / доступ к массиву.Неправильная очистка кода объекта при запуске voodoo.
static_cast
опуститься на виртуальный базовый класс
Не совсем... Теперь о моем заблуждении: я думал, что A
ниже был виртуальный базовый класс, хотя на самом деле это не так; согласно 10.3.1 это полиморфный класс. С помощью static_cast
здесь вроде бы все хорошо.
struct B { virtual ~B() {} };
struct D : B { };
В общем, да, это опасная ловушка.
Сохраняйте пространства имен прямыми (включая struct, class, namespace и using). Это мое разочарование номер один, когда программа просто не компилируется.
Всегда проверяйте указатель, прежде чем разыменовать его. В C вы обычно можете рассчитывать на сбой в том месте, где вы разыменовываете плохой указатель; в C++ вы можете создать недопустимую ссылку, которая потерпит крах в месте, удаленном от источника проблемы.
class SomeClass
{
...
void DoSomething()
{
++counter; // crash here!
}
int counter;
};
void Foo(SomeClass & ref)
{
...
ref.DoSomething(); // if DoSomething is virtual, you might crash here
...
}
void Bar(SomeClass * ptr)
{
Foo(*ptr); // if ptr is NULL, you have created an invalid reference
// which probably WILL NOT crash here
}
Эссе / статья Указатели, ссылки и ценности очень полезны. Это говорит о том, что нужно избегать ловушек и хороших практик. Вы также можете просмотреть весь сайт, который содержит советы по программированию, в основном для C++.
Намерение (x == 10)
:
if (x = 10) {
//Do something
}
Я думал, что никогда не сделаю эту ошибку сам, но на самом деле я сделал это недавно.
Я потратил много лет на разработку C++. Я написал краткое изложение проблем, с которыми я столкнулся много лет назад. Соответствующие стандартам компиляторы на самом деле больше не являются проблемой, но я подозреваю, что другие описанные подводные камни все еще актуальны.
Забывая &
и, таким образом, создавая копию вместо ссылки.
Это случилось со мной дважды по-разному:
Один экземпляр находился в списке аргументов, в результате чего большой стек помещался в стек, что приводило к переполнению стека и падению встроенной системы.
Я забыл
&
в переменной экземпляра с тем эффектом, что объект был скопирован. После регистрации в качестве слушателя копии я удивился, почему я так и не получил обратные вызовы от исходного объекта.
И то, и другое довольно сложно обнаружить, потому что разница невелика и трудно различима, а в остальном объекты и ссылки используются синтаксически одинаково.
#include <boost/shared_ptr.hpp>
class A {
public:
void nuke() {
boost::shared_ptr<A> (this);
}
};
int main(int argc, char** argv) {
A a;
a.nuke();
return(0);
}