Яких пасток на C ++ слід уникати? [зачинено]


74

Я пам’ятаю, що вперше дізнався про вектори в STL, а через деякий час я хотів використати вектор bools для одного зі своїх проектів. Побачивши якусь дивну поведінку та провівши деякі дослідження, я дізнався, що вектор булів насправді не є булом .

Чи є в C ++ інші загальні підводні камені?


42
Я думав, що C ++ - це помилка, якої слід уникати.
Весело

Забавно читати відповіді у світлі професійного досвіду на вбудованих системах. (Навіть коли зазначені вбудовані системи мають багато процесорів і тонну пам'яті.)
dash-tom-bang

Це риторичне запитання, яке використовується для ініціювання дискусії, яке відповідає критеріям близької причини "не справжнє питання" і, схоже, більше підходить для вашого блогу чи сайту, присвяченого дискусії .

10
Е .. всі вони ..?
bobobobo

Відповіді:


76

Короткий список може бути:

  • Уникайте витоків пам'яті за допомогою спільних покажчиків для управління розподілом та очищенням пам'яті
  • Використовуйте ідіому « Придбання ресурсів - це ініціалізація» (RAII) для управління очищенням ресурсів - особливо за наявності винятків
  • Уникайте виклику віртуальних функцій у конструкторах
  • Застосовуйте мінімалістські прийоми кодування, де це можливо - наприклад, оголошення змінних лише за потреби, змінні масштабу та попереднє оформлення, де це можливо.
  • По-справжньому розумійте обробку винятків у коді - як щодо винятків, які ви створюєте, так і тих, які створюються класами, які ви можете використовувати опосередковано. Це особливо важливо при наявності шаблонів.

RAII, загальні вказівники та мінімалістичне кодування, звичайно, не характерні для C ++, але вони допомагають уникнути проблем, які часто виникають при розробці на мові.

Деякі чудові книги на цю тему:

  • Ефективний C ++ - Скотт Майерс
  • Більш ефективний C ++ - Скотт Майерс
  • Стандарти кодування С ++ - Саттер і Александреску
  • Поширені запитання щодо C ++ - Cline

Читання цих книг допомогло мені більше за все інше, щоб уникнути таких підводних каменів, про які ви питаєте.


Ви вказали правильні та найкращі книги, які я шукав. :)
Намратха Патіл

6
"Уникати виклику віртуальних функцій у конструкторах" <- Я б оновив цю функцію з "Уникати" до "Ніколи". +1, однак. (А саме тому, що це невизначена поведінка)
Billy ONeal

Можливо, включити віртуальні деструктори та як правильно ловити (і переробити) винятки?
Asgeir S. Nilsen

4
@BillyONeal я, мабуть, залишив би це, щоб "уникати". Але в будь-якому випадку поведінка чітко визначена для віртуальних викликів у конструкторах. Такий виклик не є невизначеною поведінкою, за винятком випадків, коли виклик відбувається з чистою віртуальною функцією з конструктора чистого віртуального класу (і аналогічно деструкторам)
Йоханнес Шауб - litb

Ще одна чудова книга Мейєрса - це, звичайно, «Ефективний НТЛ»! Можна також додати, оскільки зараз він вийшов, Effective Modern C ++
aho

52

Підводні камені у порядку зменшення їх важливості

Перш за все, вам слід відвідати нагородженого запитаннями про 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 .

Рішення : Віртуальний базовий клас - це база, яка виникає лише один раз, навіть якщо вона неодноразово успадковується різними класами в дереві успадкування. Виконання вищезазначеного не дозволяється стандартом. Для цього використовуйте 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 ++ .


1
a [i] = ++ i; // читання змінної двічі, яка модифікується, призводить до невизначеної поведінки ... Ви можете додати це також за бажанням
yesraaj

2
+1, багато хороших балів. Те, що стосується змішування typedef та delete [], було для мене абсолютно новим! Ще одна
наріжна

2
"Присвоїти 0 кожному видаленому покажчику." <- Вибачте, але неправильно. Єдине рішення - не писати про помилку спочатку. Цілком можливо, що хтось створив копію цього вказівника, на яку ви не вплинете, якщо ви встановите його на нуль.
Billy ONeal

@BillyONeal, ти не можеш визначити, чи видаляв ти вже вказівник, якщо після його видалення не встановиш нуль. Необов’язково помилку видаляти двічі, якщо після цього ви просто встановите для неї нуль, отже, запропоноване мною рішення.
Йоханнес Шауб - літ

@Johannes Schaub - litb: Правда, але я хочу сказати, що це не є надійним. Якщо хтось має копію вказівника і намагається її видалити, у вас все ще є подвійні проблеми.
Billy ONeal


12

У Брайана є чудовий список: я б додав "Завжди позначати конструктори одиничних аргументів явними (за винятком тих рідкісних випадків, коли потрібно автоматичне приведення)".


8

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

Не винаходити колесо заново. Ознайомтеся зі STL та Boost та використовуйте їх засоби, коли це можливо, прокачуючи власні. Зокрема, використовуйте рядки та колекції STL, якщо у вас немає дуже, дуже вагомих причин цього не робити. Дуже добре ознайомтеся з auto_ptr та бібліотекою інтелектуальних покажчиків Boost, зрозумійте, за яких обставин передбачено використання кожного типу розумних покажчиків, а потім використовуйте розумні вказівники скрізь, де ви могли б використовувати необроблені вказівники. Ваш код буде настільки ж ефективним і набагато менш схильний до витоків пам'яті.

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



6

Дві помилки, про які я хотів би навчитися не так важко:

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

(2) Будьте обережні з ініціалізацією - (a) уникайте екземплярів класів як глобальні / статичні; та (b) спробуйте ініціалізувати всі ваші змінні члени до якогось безпечного значення в ctor, навіть якщо це тривіальне значення, таке як NULL для покажчиків.

Обгрунтування: впорядкування глобальної ініціалізації об'єкта не гарантується (глобал включає статичні змінні), тому у вас може вийти код, який, здається, не детермінований, оскільки це залежить від об'єкта X, який ініціалізується перед об'єктом Y. Якщо ви явно не ініціалізуєте примітивна змінна типу, така як член bool або enum класу, у дивовижних ситуаціях ви отримаєте різні значення - знову ж таки, поведінка може здатися дуже недетермінованою.


рішення не в налагодженні відбитків
Дастін Гец

Іноді це єдиний варіант ... наприклад, збої налагодження, які трапляються лише у коді випуску та / або на цільовій архітектурі / платформі, відмінній від тієї, на якій ви розробляєте.
xan,

3
Безумовно, існують більш складні способи налагодження. Але використання відбитків є випробуваним і справжнім, і працює в набагато більших місцях, ніж у вас може бути доступ до гарного налагоджувача. Я не єдиний, хто так думає - див. Книгу «Практика програмування Пайка і Кернігана», наприклад.
Тайлер

+1 за відзначення недетермінованої ініціалізації глобальних об’єктів. (Є деякі правила, але вони не настільки інтуїтивні чи повні, як хотілося б.)
j_random_hacker

printf (і std :: cout) часто буферується лише рядком, тому, поки ви відносно впевнені, що не запускаєте printf і не натискаєте новий рядок, ви повинні бути в порядку. Розглянемо також помилки компілятора, які перешкоджають генерації символів налагодження <бурчання бурчання>
dash-tom-bang

6

Я вже згадував про це кілька разів, але книги Скотта Мейєрса " Ефективний C ++" та " Ефективний STL" справді на вагу золота за допомогу з C ++.

Якщо добре подумати, С ++ Готча Стівена Дьюхерста також є чудовим ресурсом "з траншей". Його стаття про просування власних винятків та те, як їх слід будувати, справді допомогла мені в одному проекті.


6

Використання 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.
}

я не впевнений, чи варто це додавати. але, можливо, нам слід зробити його непридатним для копіювання / непризначення?
Йоханнес Шауб - літб


4

Ось кілька ям, у які мені випало нещастя. У всіх цих є вагомі причини, які я зрозумів лише після того, як мене вкусила поведінка, яка мене здивувала.

  • 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`
    

1
Цей останній хитрий! Ой!
j_random_hacker

C # робить те саме (як віртуальне / значення за замовчуванням), тепер, коли C # 4 має значення за замовчуванням.
BlueRaja - Danny Pflughoeft

3

Найважливіші підводні камені для початківців розробників - уникнути плутанини між C та C ++. C ++ ніколи не слід розглядати як просто кращий C або C з класами, оскільки це підсилює його силу і може зробити його навіть небезпечним (особливо при використанні пам'яті, як у C).


3

Перевірте boost.org . Він надає багато додаткових функціональних можливостей, особливо їх реалізацію розумних покажчиків.



3
  1. Не читання C ++ FAQ Lite . Це пояснює багато поганих (і хороших!) Практик.
  2. Не використовується Boost . Ви врятуєте собі багато розчарувань, скориставшись Boost, де це можливо.

2

Будьте обережні, використовуючи розумні вказівники та класи контейнерів.


Питання для відповіді: Що поганого у використанні розумних покажчиків з класами контейнерів? приклад: вектор <shared_ptr <int>>. Чи можете ви детальніше сказати?
Аарон,

2
він має на увазі контейнери auto_ptr, що заборонено, але іноді компілюється
Дастін Гец

@Aaron: Зокрема, оператор присвоєння auto_ptr знищує свій операнд-джерело, тобто його не можна використовувати зі стандартними контейнерами, які покладаються на те, що цього не відбувається. Однак shared_ptr добре.
j_random_hacker


2

Забувши визначити віртуальний деструктор базового класу. Це означає, що виклик deleteBase * не призведе до руйнування похідної частини.


1

Тримайте простори імен прямими (включаючи структуру, клас, простір імен та використання). Це моє перше розчарування, коли програма просто не компілюється.


1

Щоб зіпсувати, багато використовуйте прямі вказівники. Натомість використовуйте RAII майже для будь-чого, переконавшись, звичайно, що ви використовуєте правильні розумні вказівники. Якщо ви пишете "видалити" де-небудь поза дескриптором або класом типу покажчика, ви, швидше за все, робите це неправильно.



1
  • Близпаста. Це величезна, яку я багато бачу ...

  • Неініціалізовані змінні - це величезна помилка, яку роблять мої студенти. Багато людей Java забувають, що просто сказати "int counter" не встановлює лічильник 0. Оскільки вам потрібно визначити змінні у файлі h (та ініціалізувати їх у конструкторі / налаштуванні об'єкта), це легко забути.

  • Поодинокі помилки при forдоступі до циклів / масиву.

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


1
  • static_cast вниз на віртуальному базовому класі

Не зовсім ... Тепер про моє помилкове уявлення: я думав, що Aдалі був віртуальний базовий клас, а насправді це не так; це, згідно з 10.3.1, поліморфний клас . Використовувати static_castтут, здається, добре.

struct B { virtual ~B() {} };

struct D : B { };

Підводячи підсумок, так, це небезпечний підводний камінь.


див. моє розширене запитання вище
Йоганнес Шауб - літб,

0

Завжди перевіряйте вказівник перед тим, як його розмежувати. У 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 є більш поширеним, ніж інші види недійсних покажчиків. Можливо, це тому, що я звикла NULLing моїх покажчиків після їх видалення.
Марк Ренсом,

Це частина вашої стратегії обробки помилок. Я б сказав, уникайте перевірки покажчика NULL в основному коді (скоріше твердіть), але гарантуйте, що ви не передасте неправильні значення (дизайн за контрактом).
xtofl

0

Забувши & і тим самим створення копії замість посилання.

Це траплялося зі мною двічі по-різному:

  • Один екземпляр був у списку аргументів, що призвело до того, що великий об’єкт потрапив у стек в результаті переповнення стека та аварійного завершення роботи вбудованої системи.

  • Я забув &змінну на екземплярі, що призвело до того, що об’єкт був скопійований. Зареєструвавшись як слухач копії, я здивувався, чому я ніколи не отримував зворотних дзвінків від оригінального об'єкта.

І ті, і інші досить важко помітити, оскільки різниця невелика і важко помітна, а в іншому - об’єкти та посилання використовуються синтаксично однаково.


0

Намір (x == 10):

if (x = 10) {
    //Do something
}

Я думав, що ніколи сам не допустив би цієї помилки, але насправді це зробив нещодавно.


3
Практично будь-який компілятор сьогодні випустить попередження щодо цього
Адам Розенфілд,

виконання константи == до змінної допоможе виявити ці помилки, скажімо, якщо (10 = x), компілятор
помилиться

Це не допомагає, якщо ви задумалиif (x == y)
dan04


0

Я багато років займався розробкою C ++. Я написав короткий зміст проблем, які виникли у мене багато років тому. Компілятори, що відповідають стандартам, насправді вже не є проблемою, але я підозрюю, що інші зазначені підводні камені все ще діють.


-1
#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);
}

3
Неможливість використовувати boost :: shared_ptr - це навряд чи підводний камінь мови.
0xC0DEFACE

+1. Незважаючи на те, що в документах shared_ptr зазначено, що це використання не підтримується (і передбачено обхідне рішення, enable_shared_from_this), це поширений випадок використання, і не відразу очевидно, що наведений вище код не вдасться. Здається, це відтворюється за правилом "негайно обернути будь-який сирий вказівник у shared_ptr". Справжня ловушка ІМХО.
j_random_hacker
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.