Каких ошибок C++ следует избегать?[закрыто]

StackOverflow https://stackoverflow.com/questions/30373

  •  09-06-2019
  •  | 
  •  

Вопрос

Я помню, как впервые узнал о векторах в STL, и через некоторое время мне захотелось использовать вектор логических значений для одного из моих проектов.Увидев странное поведение и проведя небольшое исследование, я узнал, что вектор логических значений на самом деле не является вектором логических значений.

Есть ли еще какие-либо распространенные ошибки, которых следует избегать в C++?

Это было полезно?

Решение

Краткий список может быть таким:

  • Избегайте утечек памяти за счет использования общих указателей для управления распределением и очисткой памяти.
  • Использовать Получение ресурсов — это инициализация (RAII) идиома для управления очисткой ресурсов, особенно при наличии исключений
  • Избегайте вызова виртуальных функций в конструкторах
  • По возможности используйте минималистские методы кодирования — например, объявляйте переменные только при необходимости, определяйте область видимости переменных и, где это возможно, проектируйте на ранней стадии.
  • По-настоящему поймите обработку исключений в вашем коде — как в отношении исключений, которые вы создаете, так и в отношении исключений, создаваемых классами, которые вы можете использовать косвенно.Особенно это важно при наличии шаблонов.

RAII, общие указатели и минималистское кодирование, конечно, не являются специфичными для C++, но помогают избежать проблем, которые часто возникают при разработке на этом языке.

Вот несколько замечательных книг по этой теме:

  • Эффективный C++ — Скотт Мейерс
  • Более эффективный C++ — Скотт Мейерс
  • Стандарты кодирования C++ — Саттер и Александреску
  • Часто задаваемые вопросы по C++ — Клайн

Чтение этих книг помогло мне больше, чем что-либо еще, избежать ловушек, о которых вы спрашиваете.

Другие советы

Подводные камни в порядке убывания их важности.

Прежде всего, вам следует посетить отмеченный наградами Часто задаваемые вопросы по C++.В нем есть много хороших ответов на подводные камни.Если у вас есть дополнительные вопросы, посетите ##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); если base является указателем на виртуальный базовый класс Derived.

Решение:Виртуальный базовый класс — это базовый класс, который встречается только один раз, даже если он наследуется более одного раза разными классами косвенно в дереве наследования.Выполнение вышеперечисленного не допускается Стандартом.Для этого используйте динамический_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» объясняет вектор проблемы с логическими значениями :)

У Брайана есть отличный список:Я бы добавил: «Всегда отмечайте явные конструкторы с одним аргументом (за исключением тех редких случаев, когда вам нужно автоматическое приведение)».

Это не конкретный совет, а общее правило:проверьте свои источники.C++ — старый язык, и за прошедшие годы он сильно изменился.Лучшие практики изменились, но, к сожалению, все еще существует много старой информации.Здесь было несколько очень хороших рекомендаций по книгам - я могу купить все книги Скотта Мейерса по C++.Познакомьтесь с Boost и стилями кодирования, используемыми в Boost — люди, участвующие в этом проекте, находятся на переднем крае проектирования C++.

Не изобретайте велосипед.Познакомьтесь с STL и Boost и по возможности используйте их возможности, создавая свои собственные.В частности, используйте строки и коллекции STL, если у вас нет очень и очень веской причины не делать этого.Познакомьтесь с auto_ptr и библиотекой интеллектуальных указателей Boost, поймите, при каких обстоятельствах предназначен для использования каждый тип интеллектуальных указателей, а затем используйте интеллектуальные указатели везде, где в противном случае вы могли бы использовать необработанные указатели.Ваш код будет таким же эффективным и гораздо менее подвержен утечкам памяти.

Используйте static_cast, Dynamic_cast, const_cast и reinterpret_cast вместо приведения в стиле C.В отличие от приведения в стиле C, они сообщат вам, действительно ли вы запрашиваете приведение другого типа, чем вы думаете.И они выделяются визуально, предупреждая читателя о том, что идет кастинг.

Веб-страница Подводные камни C++ Скотт Уилер описывает некоторые основные ловушки C++.

Две ошибки, которые мне хотелось бы узнать на собственном горьком опыте:

(1) Большая часть вывода (например, printf) по умолчанию буферизуется.Если вы отлаживаете код, вызывающий сбой, и используете буферизованные операторы отладки, последний вывод, который вы видите, может нет действительно будет последним оператором печати, встречающимся в коде.Решение состоит в том, чтобы очищать буфер после каждой отладочной печати (или вообще отключать буферизацию).

(2) Будьте осторожны с инициализацией - (а) избегайте экземпляров классов как глобальных/статических;и (б) попытаться инициализировать все переменные-члены некоторым безопасным значением в векторе, даже если это тривиальное значение, такое как NULL для указателей.

Аргументация:порядок инициализации глобальных объектов не гарантируется (глобальные переменные включают в себя статические переменные), поэтому вы можете получить код, который, по-видимому, недетерминированно завершится сбоем, поскольку он зависит от того, что объект X инициализируется до объекта Y.Если вы явно не инициализируете переменную примитивного типа, такую ​​как член bool или перечисление класса, в неожиданных ситуациях вы получите разные значения — опять же, поведение может показаться очень недетерминированным.

Я уже упоминал об этом несколько раз, но книги Скотта Мейерса Эффективный С++ и Эффективный STL действительно на вес золота за помощь с C++.

Если подумать, Стивен Дьюхерст С++ ошибки также является отличным ресурсом «из окопов».Его статья о создании собственных исключений и о том, как их следует создавать, очень помогла мне в одном проекте.

Использование C++, такого как C.Наличие цикла создания и выпуска в коде.

В C++ это не является безопасным для исключений, и поэтому выпуск может не быть выполнен.В C++ мы используем РАИИ Для решения этой проблемы.

Все ресурсы, которые создаются и выпускаются вручную, должны быть обернуты объектом, чтобы эти действия выполнялись в конструкторе/деструкторе.

// 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.
}

Книга С++ ошибки может оказаться полезным.

Вот несколько ям, в которые мне посчастливилось попасть.У всего этого есть веские причины, которые я понял только после того, как меня укусило поведение, которое меня удивило.

  • 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++ основан на книгах Скотта Мейерса, Бьярна Страустропа и Херба Саттера.Всю эту информацию он объединяет в одном документе.

  1. Не читая Часто задаваемые вопросы по C++ Lite.Это объясняет многие плохие (и хорошие!) практики.
  2. Не используется Способствовать росту.Вы избавите себя от разочарований, воспользовавшись преимуществами Boost там, где это возможно.

Будьте осторожны при использовании интеллектуальных указателей и классов-контейнеров.

Избегать псевдоклассы и квазиклассы...По сути, овердизайн.

Забыли определить виртуальный деструктор базового класса.Это означает, что вызов delete на базе* не приведет к разрушению производной части.

Сохраняйте пространства имен прямыми (включая структуру, класс, пространство имен и использование).Это мое разочарование номер один, когда программа просто не компилируется.

Чтобы запутаться, часто используйте прямые указатели.Вместо этого используйте RAII практически для всего, конечно, убедившись, что вы используете правильные интеллектуальные указатели.Если вы пишете «удалить» где-либо за пределами класса дескриптора или типа указателя, вы, скорее всего, делаете это неправильно.

  • Близпаста.Это огромный, я часто вижу...

  • Неинициализированные переменные — огромная ошибка, которую допускают мои студенты.Многие пользователи Java забывают, что простое произнесение «int counter» не приводит к установке счетчика в 0.Поскольку вам необходимо определить переменные в файле h (и инициализировать их в конструкторе/настройке объекта), об этом легко забыть.

  • Ошибки с отклонением на единицу включены for циклы/доступ к массиву.

  • Неправильная очистка объектного кода при запуске вуду.

  • static_cast понижение уровня виртуального базового класса

Не совсем...Теперь о моем заблуждении:я думал так A далее был виртуальный базовый класс, хотя на самом деле это не так;это, согласно 10.3.1, полиморфный класс.С использованием 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++.Я написал краткое резюме проблем, которые у меня были с ним много лет назад.Соответствующие стандартам компиляторы больше не являются проблемой, но я подозреваю, что другие описанные ловушки все еще актуальны.

#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);
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top