Каких ошибок в 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 также является отличным ресурсом "из окопов". Его статья о создании собственных исключений и о том, как они должны быть сконструированы, действительно помогла мне в одном проекте.

Книга 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`
    
  1. Не читая C++ FAQ Lite. Это объясняет многие плохие (и хорошие!) Практики.
  2. Не использует 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);
}
Другие вопросы по тегам