Я помню, как впервые узнал о векторах в STL, и через некоторое время я захотел использовать вектор bools для одного из моих проектов. Увидев странное поведение и проведя небольшое исследование, я узнал, что вектор bools на самом деле не является вектором bools.
Есть ли другие распространенные ошибки, которых следует избегать в C++?
Интересно читать ответы в свете профессионального опыта работы со встроенными системами. (Даже если указанные встроенные системы имеют много процессоров и тонну памяти.)
Это риторический вопрос, используемый для инициирования обсуждения, который соответствует критериям причины «не настоящий вопрос» и кажется более подходящим для вашего блога или сайта посвященный обсуждению.
Эээ .. все они ..?





Книга Проблемы с C++ может оказаться полезной.
Веб-страница Подводные камни C++ Скотта Уиллера описывает некоторые из основных ошибок C++.
У некоторых должны быть книги по C++, которые помогут избежать распространенных ошибок C++:
Эффективный C++
Более эффективный C++
Эффективный STL
Книга Effective STL объясняет вектор проблемы bools :)
Краткий список может быть:
RAII, общие указатели и минималистичное кодирование, конечно, не специфичны для C++, но они помогают избежать проблем, которые часто возникают при разработке на этом языке.
Вот несколько отличных книг по этой теме:
Чтение этих книг помогло мне больше, чем что-либо другое, избежать тех ловушек, о которых вы спрашиваете.
вы указали правильные и лучшие книги, которые я искал. :)
«Избегайте вызова виртуальных функций в конструкторах»
Может быть, включить виртуальные деструкторы и как правильно улавливать (и повторно генерировать) исключения?
@BillyONeal, я бы, наверное, оставил это «избегать». Но в любом случае поведение для виртуальных вызовов в конструкторах четко определено. Такой вызов не является неопределенным поведением, за исключением случаев, когда вызов происходит с чистой виртуальной функцией из конструктора чистого виртуального класса (и аналогично для деструкторов)
Еще одна замечательная книга Мейерса - это, конечно же, «Эффективный STL»! Можно также добавить, поскольку он сейчас отсутствует, Effective Modern C++
Самая важная ошибка для начинающих разработчиков - избегать путаницы между C и C++. C++ никогда не следует рассматривать как просто лучший C или C с классами, потому что это снижает его мощность и может сделать его даже опасным (особенно при использовании памяти, как в C).
Проверьте boost.org. Он предоставляет множество дополнительных функций, особенно их реализации интеллектуального указателя.
Я уже упоминал об этом несколько раз, но книги Скотта Мейерса Эффективный C++ и Эффективный STL действительно на вес золота за помощь в C++.
Если подумать, Проблемы с C++ Стивена Дьюхерста также является отличным ресурсом «из окопов». Его пункт о создании собственных исключений и о том, как они должны быть сконструированы, действительно помог мне в одном проекте.
Две ошибки, которые я хотел бы не усвоить на собственном горьком опыте:
(1) Большая часть вывода (например, printf) по умолчанию буферизуется. Если вы отлаживаете аварийный код и используете буферизованные операторы отладки, последний вывод, который вы видите, может действительно быть последним оператором печати, встреченным в коде. Решение состоит в том, чтобы очищать буфер после каждой отладочной печати (или вообще отключать буферизацию).
(2) Будьте осторожны с инициализацией - (a) избегайте экземпляров классов как глобальных объектов / статик; и (б) попытаться инициализировать все ваши переменные-члены некоторым безопасным значением в ctor, даже если это тривиальное значение, такое как NULL для указателей.
Обоснование: порядок инициализации глобального объекта не гарантируется (глобальные переменные включают статические переменные), поэтому вы можете получить код, который кажется недетерминированным, поскольку он зависит от инициализации объекта X до объекта Y. Если вы явно не инициализируете объект переменная примитивного типа, такая как член bool или enum класса, вы получите разные значения в неожиданных ситуациях - опять же, поведение может показаться очень недетерминированным.
решение не в том, чтобы отлаживать отпечатки
Иногда это единственный вариант ... например, сбои при отладке, которые происходят только в коде выпуска и / или на целевой архитектуре / платформе, отличной от той, на которой вы разрабатываете.
Определенно есть более изощренные способы отладки. Но использование распечаток проверено и верно, и работает в гораздо большем количестве мест, чем у вас может быть доступ к хорошему отладчику. Я не единственный, кто так думает - см., Например, книгу «Практика программирования» Пайка и Керниган.
+1 за недетерминированную инициализацию глобальных объектов. (Есть некоторые правила, но они не такие интуитивно понятные или полные, как хотелось бы.)
printf (и std :: cout) часто буферизируются только по строке, поэтому, если вы относительно уверены, что не происходит сбой между запуском printf и переходом на новую строку, все должно быть в порядке. Также обратите внимание на ошибки компилятора, которые не позволяют генерировать символы отладки <grumble grumble>
PRQA имеет отличный и бесплатный стандарт кодирования C++, основанный на книгах Скотта Мейерса, Бьярна Страустропа и Херба Саттера. Он объединяет всю эту информацию в одном документе.
Вот несколько ям, в которые мне посчастливилось попасть. У всего этого есть веские причины, которые я понял только после того, как меня укусило поведение, которое меня удивило.
virtual функционирует в конструкторах не.
Не нарушайте ODR (правило одного определения), для этого и нужны анонимные пространства имен (среди прочего).
Порядок инициализации членов зависит от порядка, в котором они объявлены.
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# делает то же самое (как виртуальное значение / значение по умолчанию), теперь, когда C# 4 имеет значения по умолчанию.
Будьте осторожны при использовании интеллектуальных указателей и контейнерных классов.
Вопрос к ответу: что плохого в использовании интеллектуальных указателей с классами контейнеров? например: vector
он имеет в виду контейнеры auto_ptr, что запрещено, но иногда компилируется
@Aaron: в частности, оператор присваивания auto_ptr уничтожает его исходный операнд, что означает, что он не может использоваться со стандартными контейнерами, которые полагаются на то, что этого не происходит. Однако shared_ptr в порядке.
У Брайана отличный список: я бы добавил: «Всегда явно отмечать конструкторы с одним аргументом (кроме тех редких случаев, когда требуется автоматическое приведение)».
Не совсем конкретный совет, а общее руководство: проверьте свои источники. C++ - старый язык, и с годами он сильно изменился. Лучшие практики изменились вместе с ним, но, к сожалению, есть еще много старой информации. Здесь было несколько очень хороших рекомендаций по книгам - я могу купить каждую книгу Скотта Мейерса по C++. Ознакомьтесь с Boost и стилями кодирования, используемыми в Boost - люди, участвующие в этом проекте, находятся на переднем крае дизайна C++.
Не изобретайте велосипед. Ознакомьтесь с STL и Boost и используйте их возможности, когда это возможно, катаясь самостоятельно. В частности, используйте строки и коллекции STL, если у вас нет очень и очень веской причины не делать этого. Познакомьтесь с auto_ptr и библиотекой интеллектуальных указателей Boost очень хорошо, поймите, при каких обстоятельствах предполагается использовать каждый тип интеллектуального указателя, а затем используйте интеллектуальные указатели везде, где в противном случае вы могли бы использовать необработанные указатели. Ваш код будет столь же эффективным и гораздо менее подвержен утечкам памяти.
Используйте static_cast, dynamic_cast, const_cast и reinterpret_cast вместо приведений в стиле C. В отличие от приведений в стиле C, они сообщат вам, действительно ли вы запрашиваете другой тип приведения, чем вы думаете. И они визуально выделяются, предупреждая читателя о том, что проводится гипсовая повязка.
Избегайте псевдоклассы и квазиклассы ... Перепроектировать в основном.
В настоящее время я работаю над таким проектом с (n) квазиклассами. Нам нужно больше осознавать этот антипаттерн!
Использование 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.
}
я не уверен, стоит ли нам его добавлять. но, может быть, нам стоит сделать его не копируемым / непередаваемым?
Всегда проверяйте указатель, прежде чем разыменовать его. В 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
}
Проверка на NULL не очень помогает. Указатель может иметь ненулевое значение и по-прежнему указывать на удаленный или иным образом недопустимый объект.
Верно, но по моему опыту, указатель NULL встречается чаще, чем другие типы недопустимых указателей. Может быть, это потому, что у меня есть привычка обнулять свои указатели после их удаления.
Это часть вашей стратегии обработки ошибок. Я бы сказал, избегайте проверки указателя NULL в основном коде (скорее, assert), но гарантируйте, что вы не передадите недопустимые значения (дизайн по контракту).
Намерение (x == 10):
if (x = 10) {
//Do something
}
Я думал, что сам никогда не совершу эту ошибку, но на самом деле я сделал это недавно.
Практически любой компилятор в наши дни выдаст предупреждение об этом
выполнение константы == для переменной поможет обнаружить эти ошибки, скажем, if (10 = x), компилятор выдаст ошибку на этом
Это не поможет, если вы намеревались использовать if (x == y)
Сохраняйте четкость пространств имен (включая структуру, класс, пространство имен и использование). Это мое разочарование номер один, когда программа просто не компилируется.
Чтобы напортачить, часто используйте прямые указатели. Вместо этого используйте RAII практически для всего, конечно, убедитесь, что вы используете правильные интеллектуальные указатели. Если вы пишете «удалить» где-нибудь за пределами дескриптора или класса указателя, скорее всего, вы делаете это неправильно.
Близпаста. Я очень часто вижу это ...
Неинициализированные переменные - огромная ошибка, которую совершают мои ученики. Многие люди, занимающиеся Java, забывают, что простая фраза "int counter" не устанавливает счетчик в 0. Поскольку вам нужно определить переменные в h-файле (и инициализировать их в конструкторе / настройке объекта), об этом легко забыть.
Поочередные ошибки при доступе к петлям / массиву for.
Некорректная очистка объектного кода при запуске voodoo.
static_castdowncast on a virtual base class
Не совсем ... Теперь о моем заблуждении: я думал, что A в дальнейшем был виртуальным базовым классом, хотя на самом деле это не так; это, согласно 10.3.1, полиморфный класс. Использование static_cast здесь вроде бы нормально.
struct B { virtual ~B() {} };
struct D : B { };
В общем, да, это опасная ловушка.
см. мой расширенный вопрос выше
#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);
}
Неспособность использовать boost :: shared_ptr вряд ли является ловушкой языка.
+1. Хотя в документах shared_ptr указано, что это использование не поддерживается (и предлагается обходной путь, enable_shared_from_this), это общий вариант использования, и не сразу очевидно, что приведенный выше код завершится ошибкой. Кажется, он даже действует по правилу «немедленно обернуть любой необработанный указатель в shared_ptr». Настоящая ловушка ИМХО.
Забыв определить деструктор базового класса virtual. Это означает, что вызов delete на базе * не приведет к разрушению производной части.
Прежде всего, вам следует посетить отмеченный наградами C++ FAQ. В нем есть много хороших ответов на подводные камни. Если у вас есть дополнительные вопросы, посетите ##c++ на irc.freenode.org в IRC. Мы рады помочь вам, если сможем. Обратите внимание, что все следующие подводные камни изначально написаны. Их не просто копируют из случайных источников.
delete[]onnew,deleteonnew[]
Решение: Выполнение вышеуказанного приводит к неопределенному поведению: все может случиться. Поймите свой код и то, что он делает, и всегда delete[] то, что вы new[], и delete, что вы new, тогда этого не произойдет.
Исключение:
typedef T type[N]; T * pT = new type; delete[] pT;
Вам нужен delete[], даже если вы new, поскольку вы недавно создали массив. Поэтому, если вы работаете с typedef, будьте особенно осторожны.
Calling a virtual function in a constructor or destructor
Решение: вызов виртуальной функции не вызовет замещающие функции в производных классах. Вызов чистая виртуальная функция в конструкторе или десктрукторе является неопределенным поведением.
Calling
deleteordelete[]on an already deleted pointer
Решение: присвоить 0 каждому удаляемому указателю. Вызов delete или delete[] по нулевому указателю ничего не делает.
Taking the sizeof of a pointer, when the number of elements of an 'array' is to be calculated.
Решение: передайте количество элементов рядом с указателем, когда вам нужно передать массив в качестве указателя в функцию. Используйте предложенную функцию здесь, если вы берете sizeof массива, который должен быть действительно массивом.
Using an array as if it were a pointer. Thus, using
T **for a two dimentional array.
Решение: См. здесь, почему они разные и как вы с ними справляетесь.
Writing to a string literal:
char * c = "hello"; *c = 'B';
Решение: выделите массив, который инициализируется данными строкового литерала, затем вы можете записать в него:
char c[] = "hello"; *c = 'B';
Запись в строковый литерал - это неопределенное поведение. В любом случае приведенное выше преобразование строкового литерала в char * не рекомендуется. Поэтому компиляторы, вероятно, предупредят, если вы увеличите уровень предупреждения.
Creating resources, then forgetting to free them when something throws.
Решение: используйте умные указатели, такие как std::unique_ptr или std::shared_ptr, как указано в других ответах.
Modifying an object twice like in this example:
i = ++i;
Решение: Вышеупомянутое должно было присвоить i значение i+1. Но что он делает, не определено. Вместо увеличения i и присвоения результата он также изменяет i с правой стороны. Изменение объекта между двумя точками следования - неопределенное поведение. Точки последовательности включают ||, &&, comma-operator, semicolon и entering a function (неполный список!). Измените код на следующий, чтобы он работал правильно: i = i + 1;
Forgetting to flush streams before calling a blocking function like
sleep.
Решение: очистить поток, передавая либо std::endl вместо \n, либо вызывая stream.flush();.
Declaring a function instead of a variable.
Решение: проблема возникает из-за того, что компилятор интерпретирует, например,
Type t(other_type(value));
как объявление функции функции t, возвращающей Type и имеющей параметр типа other_type, который называется value. Вы решаете это, заключая скобки вокруг первого аргумента. Теперь у вас есть переменная t типа Type:
Type t((other_type(value)));
Calling the function of a free object that is only declared in the current translation unit (
.cppfile).
Решение: Стандарт не определяет порядок создания свободных объектов (в области пространства имен), определенных для разных единиц перевода. Вызов функции-члена для еще не созданного объекта является неопределенным поведением. Вместо этого вы можете определить следующую функцию в единице трансляции объекта и вызывать ее из других:
House & getTheHouse() { static House h; return h; }
Это создало бы объект по запросу и оставило бы вас с полностью сконструированным объектом во время вызова функций на нем.
Defining a template in a
.cppfile, while it's used in a different.cppfile.
Решение: Почти всегда вы будете получать такие ошибки, как undefined reference to .... Поместите все определения шаблонов в заголовок, чтобы, когда компилятор их использует, он уже мог создать необходимый код.
static_cast<Derived*>(base);if base is a pointer to a virtual base class ofDerived.
Решение: виртуальный базовый класс - это базовый класс, который встречается только один раз, даже если он косвенно наследуется разными классами в дереве наследования более одного раза. Стандартом не разрешается делать вышеперечисленное. Для этого используйте dynamic_cast и убедитесь, что ваш базовый класс полиморфен.
dynamic_cast<Derived*>(ptr_to_base);if base is non-polymorphic
Решение: стандарт не допускает приведение указателя или ссылки вниз, если переданный объект не является полиморфным. Он или один из его базовых классов должен иметь виртуальную функцию.
Making your function accept
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++.
а [я] = ++ я; // чтение измененной переменной дважды приводит к неопределенному поведению ... вы также можете добавить это, если хотите
+1, много хороших моментов. Тот, о смешивании typedef и delete [], был для меня совершенно новым! Еще один угловой случай, который стоит запомнить ... :(
«Назначьте 0 каждому удаляемому указателю».
@BillyONeal, вы не можете определить, удалили ли вы указатель уже, если не установили для него значение null после его удаления. Это не обязательно ошибка, которую нужно удалить дважды, если вы просто установите для нее значение null после этого, поэтому мое предлагаемое решение.
@Johannes Schaub - litb: Верно, но я считаю, что это не надежно. Если у кого-то есть копия указателя и он пытается удалить ее, у вас все равно возникают проблемы с двойным освобождением.
@BillyONeal, верно. Вы не должны копировать указатель, а затем обнулять только оригинал. Это действительно еще одна ошибка, которую это не решит. Если вы не удаляете указатель дважды, это тоже не решит проблему, потому что даже если вы по-прежнему удаляете каждый указатель один раз - один раз копию и один раз оригинал, то, в конце концов, вы все равно дважды вызываете удаление для одного и того же объекта. Это еще одна ловушка, которую я не рассматривал. Я согласен с вами в этом, реальное решение для ошибки типа который - это найти и исправить эту проблему.
«Еще одна (закрытая) ветка о подводных камнях C++, так что люди, которые их ищут, найдут их, - это вопрос о переполнении стека Подводные камни C++». - на данный момент ссылка не работает.
Очерк / статья Указатели, ссылки и значения очень полезны. Он говорит, что избегает ловушек и хороших практик. Вы также можете просмотреть весь сайт, который содержит советы по программированию, в основном для C++.
Я много лет занимался разработкой на C++. Я написал краткое изложение о проблемах, которые у меня были с ним много лет назад. Совместимые со стандартами компиляторы больше не являются проблемой, но я подозреваю, что другие описанные подводные камни все еще актуальны.
Забыть & и тем самым создать копию вместо ссылки.
Это случилось со мной дважды по-разному:
Один экземпляр находился в списке аргументов, что привело к помещению большого объекта в стек, что привело к переполнению стека и отказу встроенной системы.
Я забыл & в переменной экземпляра, в результате чего объект был скопирован. После регистрации в качестве слушателя копии я задумался, почему я никогда не получал обратные вызовы от исходного объекта.
И то, и другое довольно сложно обнаружить, потому что разница небольшая и ее трудно увидеть, а в остальном объекты и ссылки синтаксически используются одинаково.
Я думал, что C++ является - ловушка, которую следует избегать.