Каких подводных камней C++ следует избегать? [закрытый]
Я помню, как впервые узнал о векторах в STL, и через некоторое время я хотел использовать вектор bools для одного из моих проектов. Увидев какое-то странное поведение и проведя некоторые исследования, я узнал, что вектор bools на самом деле не является вектором bools.
есть ли другие общие подводные камни, чтобы избежать в C++?
29 ответов:
короткий список может быть:
- избегайте утечек памяти с помощью общих указателей для управления выделением памяти и очистки
- использовать Приобретение Ресурсов Является Инициализацией (RAII) идиома для управления очисткой ресурсов-особенно при наличии исключений
- избегайте вызовов виртуальных функций в конструкторах
- используйте минималистские методы кодирования, где это возможно - например, объявляя переменные только при необходимости, переменные области и раннее проектирование, где это возможно.
- действительно понять обработку исключений в коде-как в отношении исключений, которые вы бросаете, а также те, которые бросают классы, которые вы можете использовать косвенно. Это особенно важно при наличии шаблонов.
RAII, общие указатели и минималистское кодирование, конечно, не специфичны для C++, но они помогают избежать проблем, которые часто возникают при разработке на языке.
некоторые отличные книги на эту тему:
- Эффективный C++ - Scott Meyers
- Более Эффективный C++ - Scott Meyers
- Стандарты Кодирования C++ - Sutter & Alexandrescu
- В C++ Часто Задаваемые Вопросы - Клайн
чтение этих книг помогло мне больше, чем что-либо еще, избежать тех ловушек, о которых вы спрашиваете.
подводные камни в порядке убывания их значимости
прежде всего, вы должны посетить наградами C++ FAQ. Он имеет много хороших ответов на подводные камни. Если у вас есть дополнительные вопросы, посетите
##c++
onirc.freenode.org
на IRC. Мы рады помочь вам, если сможем. Обратите внимание, что все следующие подводные камни изначально написаны. Они не просто копируются из случайных источников.
delete[]
onnew
,delete
наnew[]
решение: выполнение вышеизложенного приводит к неопределенному поведению: все может произойти. Поймите свой код и что он делает, и всегда
delete[]
что тыnew[]
иdelete
что тыnew
, то этого не произойдет.исключение:
typedef T type[N]; T * pT = new type; delete[] pT;
вам нужно
delete[]
даже если выnew
, так как вы новый объед массив. Так что если вы работаете сtypedef
, принимать специальные уход.
вызов виртуальной функции в конструкторе или деструкторе
решение: вызов виртуальной функции не вызовет переопределяющие функции в производных классах. Вызов чисто виртуальные функции в конструкторе или декструкторе неопределенное поведение.
вызов
delete
илиdelete[]
на уже удален указательрешение: назначьте 0 каждому удаленному указателю. Звоню
delete
илиdelete[]
на нулевой указатель ничего не делает.
принимая sizeof указателя, когда число элементов "массива" должно быть вычислено.
решение: передайте количество элементов рядом с указателем, когда вам нужно передать массив в качестве указателя в функцию. Используйте предложенную функцию здесь если вы берете размер массива, который должен быть действительно массивом.
использовать массив, как если бы это был указатель. Таким образом, используя
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
file).решение: стандарт не определяет порядок создания свободных объектов (в области пространства имен), определенных в разных переводах единицы. Вызов функции-члена для еще не построенного объекта является неопределенным поведением. Вместо этого вы можете определить следующую функцию в блоке перевода объекта и вызвать ее из других:
House & getTheHouse() { static House h; return h; }
это создало бы объект по требованию и оставило бы вас с полностью построенным объектом в то время, когда вы вызываете функции на нем.
определение шаблона , в то время как он используется в другом
.cpp
файл.решение: почти всегда вы получите ошибки, как
undefined reference to ...
. Поместите все определения шаблонов в заголовок, чтобы при их использовании компилятор уже мог создать необходимый код.
static_cast<Derived*>(base);
если 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++
эффективный STLэффективная книга STL объясняет вектор проблемы bools:)
у Брайана есть отличный список: я бы добавил: "всегда отмечайте конструкторы одиночных аргументов явными (за исключением тех редких случаев, когда вы хотите автоматическое литье)."
не совсем конкретный совет, но общее руководство: Проверьте свои источники. C++ не старый язык, и он сильно изменился за эти годы. Лучшие практики изменились с ним, но, к сожалению, там все еще много старой информации. Здесь были очень хорошие рекомендации по книгам-я могу купить каждую из книг Scott Meyers C++. Познакомьтесь с Boost и со стилями кодирования, используемыми в Boost - люди, участвующие в этом проекте, находятся на передний край дизайна C++.
Не изобретайте велосипед. Познакомьтесь с STL и Boost, а также используйте их возможности, когда это возможно, прокатывая свои собственные. В частности, используйте строки и коллекции STL, если у вас нет очень, очень веской причины этого не делать. Познакомьтесь с auto_ptr и библиотекой интеллектуальных указателей Boost очень хорошо, поймите, при каких обстоятельствах каждый тип интеллектуального указателя предназначен для использования, а затем используйте интеллектуальные указатели везде, где вы могли бы использовать их в противном случае сырые указатели. Ваш код будет так же эффективен и гораздо менее подвержен утечкам памяти.
используйте static_cast, dynamic_cast, const_cast и reinterpret_cast вместо приведений в стиле C. В отличие от бросков C-стиля они дадут вам знать, если вы действительно просите другой тип броска, чем вы думаете, что просите. И они выделяются ярко, предупреждая читателя о том, что происходит бросок.
веб-страницы C++ Подводные Камни Скотт Уилер охватывает некоторые из основных ловушек C++.
два gotchas, что я хотел бы, чтобы я не узнал трудный путь:
(1) много вывода (например, printf) буферизуется по умолчанию. Если вы отлаживаете аварийный код и используете буферизованные отладочные операторы, последний вывод, который вы видите, может не действительно последний оператор печати, встречающийся в коде. Решение состоит в том, чтобы очистить буфер после каждой отладочной печати (или полностью отключить буферизацию).
(2) Будьте осторожны с инициализациями - (a) избегайте экземпляры класса как глобалы / статики; и (b) попробуйте инициализировать все ваши переменные-члены до некоторого безопасного значения в ctor, даже если это тривиальное значение, такое как NULL для указателей.
рассуждение: порядок инициализации глобального объекта не гарантируется (глобалы включают статические переменные), поэтому вы можете получить код, который, похоже, не работает недетерминированно, поскольку он зависит от объекта X, инициализируемого перед объектом Y. Если вы явно не инициализируете переменную примитивного типа, например, член bool или перечисление класса, вы получите разные значения в неожиданных ситуациях-опять же, поведение может показаться очень недетерминированным.
Я уже упоминал об этом несколько раз, но книги Скотта Мейерса Эффективный C++ и эффективный STL действительно стоит своего веса в золоте за помощь с C++.
подумай об этом, Стивен Дьюхерст C++ Gotchas также является отличным ресурсом "из окопов". Его пункт о прокатке ваших собственных исключений и о том, как они должны быть построены, действительно помог мне в одном проекте.
использование 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++ Gotchas может оказаться полезным.
вот несколько ям, в которые я имел несчастье упасть. Все это имеет веские причины, которые я понял только после того, как был укушен поведением, которое меня удивило.
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++. C++ никогда не следует рассматривать как просто лучший C или C с классами, потому что это сокращает его мощность и может сделать его даже опасным (особенно при использовании памяти, как в C).
проверить boost.org. он предоставляет множество дополнительных функций, особенно их интеллектуальные реализации указателя.
PRQA есть отличный и бесплатный стандарт кодирования C++ на основе книг Скотта Мейерса, Бьярне Стростропа и Херба Саттера. Он объединяет всю эту информацию в одном документе.
- Не читал C++ FAQ Lite. Это объясняет много плохого (и хорошего!) практики.
- не используя Boost. Вы сэкономите себе много разочарований, воспользовавшись Boost, где это возможно.
избежать псевдо классы и классы квази... Овердизайн в основном.
забыв определить виртуальный деструктор базового класса. Это означает, что вызов
delete
на базе* не приведет к разрушению производной части.
держите пространства имен прямыми (включая структуру, класс, пространство имен и использование). Это мое разочарование номер один, когда программа просто не компилируется.
чтобы испортить, используйте прямые указатели много. Вместо этого используйте RAII почти для всего, убедившись, конечно, что вы используете правильные умные указатели. Если вы пишете "удалить" в любом месте вне класса типа дескриптора или указателя, вы, скорее всего, делаете это неправильно.
Blizpasta. Это огромная вещь, которую я часто вижу...
неинициализированные переменные-это огромная ошибка, которую совершают мои студенты. Многие люди Java забывают, что просто говоря "int counter" не устанавливает счетчик в 0. Поскольку вам нужно определить переменные в файле h (и инициализировать их в конструкторе/настройке объекта), это легко забыть.
Off-by-one ошибки на
for
петли / массив доступ.не правильно очистки объектного кода при запуске вуду.
static_cast
понижение на виртуальном базовом классене совсем так... Теперь о моем заблуждении: я думал, что
A
в следующем был виртуальный базовый класс, когда на самом деле это не так; это, согласно 10.3.1, a полиморфный класс. Используяstatic_cast
здесь, кажется, все в порядке.struct B { virtual ~B() {} }; struct D : B { };
в общем, да, это опасная ловушка.
всегда проверяйте указатель, прежде чем разыменовать его. В 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 }
забыли
&
и тем самым создать копию вместо ссылки.Это случилось со мной дважды по-разному:
один экземпляр был в списке аргументов, что привело к тому, что большой объект был помещен в стек с результатом переполнения стека и сбоя встроенной системы.
забыл
&
на переменной экземпляра, с эффектом, что объект был скопирован. После регистрации в качестве слушателя в копии я задавался вопросом, почему я никогда не получал обратные вызовы от исходного объекта.и где довольно трудно обнаружить, потому что разница мала и трудно увидеть, а в противном случае объекты и ссылки используются синтаксически одинаково.
намерение
(x == 10)
:if (x = 10) { //Do something }
Я думал, что никогда не сделаю эту ошибку сам, но я на самом деле сделал это недавно.
эссе/статьи указатели, ссылки и значения очень полезно. Он говорит, чтобы избежать ловушек и хороших практик. Вы также можете просмотреть весь сайт, который содержит советы по программированию, в основном для C++.
Я потратил много лет на разработку C++. Я написал краткое описание из проблем, которые я имел с ним несколько лет назад. Совместимые со стандартами компиляторы больше не являются проблемой, но я подозреваю, что другие описанные подводные камни все еще действительны.