关于stl:应该避免哪些C ++陷阱?

关于stl:应该避免哪些C ++陷阱?

What C++ pitfalls should I avoid?

我记得第一次学习STL中的向量,一段时间后,我想为我的一个项目使用布尔向量。 在看到一些奇怪的行为并进行了一些研究之后,我了解到布尔向量实际上并不是布尔向量。

在C ++中还有其他需要避免的常见陷阱吗?


简短列表可能是:

  • 通过使用共享指针来管理内存分配和清理,避免内存泄漏
  • 使用资源获取即初始化(RAII)惯用法来管理资源清理-尤其是在存在异常的情况下
  • 避免在构造函数中调用虚函数
  • 在可能的情况下使用简约的编码技术-例如,仅在需要时声明变量,范围界定变量,并在可能的情况下进行早期设计。
  • 真正了解代码中的异常处理-涉及到您抛出的异常以及您可能间接使用的类所抛出的异常。在存在模板的情况下,这一点尤其重要。

RAII,共享指针和最低限度的编码当然不是特定于C ++的,但是它们有助于避免使用该语言进行开发时经常出现的问题。

关于此主题的一些优秀书籍包括:

  • 有效的C ++-Scott Meyers
  • 更有效的C ++-Scott Meyers
  • C ++编码标准-Sutter和Alexandrescu
  • C ++常见问题解答-Cline

读这些书对我有最大的帮助,避免了您要问的那种陷阱。


陷阱的重要性降序排列

首先,您应该访问屡获殊荣的C ++常见问题解答。对于陷阱,它有很多好的答案。如果您还有其他疑问,请访问IRC中irc.freenode.org上的##c++。如果可以的话,我们很乐意为您提供帮助。请注意,以下所有陷阱均是最初编写的。它们不只是从随机来源复制而来。

好。

delete[] on new, delete on new[]

Ok.

解决方案:对未定义的行为进行以上操作会导致所有事情发生。了解您的代码及其作用,并且始终delete[]new[]以及deletenew的内容,那么那将不会发生。

好。

例外:

好。

1
typedef T type[N]; T * pT = new type; delete[] pT;

即使您是new,也需要delete[],因为您新建了一个数组。因此,如果您使用typedef,请格外小心。

好。

Calling a virtual function in a constructor or destructor

Ok.

解决方案:调用虚拟函数将不会调用派生类中的重写函数。在构造函数或解码器中调用纯虚函数是未定义的行为。

好。

Calling delete or delete[] on an already deleted pointer

Ok.

解决方案:为删除的每个指针分配0。在空指针上调用deletedelete[]不会执行任何操作。

好。

Taking the sizeof of a pointer, when the number of elements of an 'array' is to be calculated.

Ok.

解决方案:需要将数组作为指针传递给函数时,将指针旁边的元素数量传递给指针。如果采用的sizeof应该是真正的数组,请使用此处建议的函数。

好。

Using an array as if it were a pointer. Thus, using T ** for a two dimentional array.

Ok.

解决方案:请参阅此处以了解它们为何不同以及如何处理它们。

好。

Writing to a string literal: char * c ="hello"; *c = 'B';

Ok.

解决方案:分配一个从字符串文字数据初始化的数组,然后可以写入该数组:

好。

1
char c[] ="hello"; *c = 'B';

写入字符串文字是未定义的行为。无论如何,以上从字符串文字到char *的转换已被弃用。因此,如果您提高警告级别,编译器可能会发出警告。

好。

Creating resources, then forgetting to free them when something throws.

Ok.

解决方案:使用其他答案指出的智能指针,如std::unique_ptrstd::shared_ptr

好。

修改对象两次,如下例所示:i = ++i;

好。

解决方案:应该将上述内容分配给i值。但是它没有做什么。代替递增i并分配结果,它也更改了右侧的i。在两个序列点之间更改对象是未定义的行为。顺序点包括||&&comma-operatorsemicolonentering a function(非穷举列表!)。将代码更改为以下内容以使其正常运行:i = i + 1;

好。

杂项问题

Forgetting to flush streams before calling a blocking function like sleep.

Ok.

解决方案:通过流传输std::endl而不是
或调用stream.flush();来刷新流。

好。

Declaring a function instead of a variable.

Ok.

解决方案:出现此问题是因为编译器解释了例如

好。

1
Type t(other_type(value));

作为函数t的函数声明,该函数返回Type并具有类型为other_type的参数,称为value。您可以通过在第一个参数前后加上括号来解决该问题。现在,您获得类型为Type的变量t

好。

1
Type t((other_type(value)));

Calling the function of a free object that is only declared in the current translation unit (.cpp file).

Ok.

解决方案:该标准未定义跨不同翻译单元定义的免费对象(在命名空间范围内)的创建顺序。在尚未构造的对象上调用成员函数是未定义的行为。您可以改为在对象的翻译单元中定义以下函数,然后从其他函数调用它:

好。

1
House & getTheHouse() { static House h; return h; }

这将按需创建对象,并在您调用其上的函数时为您提供完整的对象。

好。

Defining a template in a .cpp file, while it's used in a different .cpp file.

Ok.

解决方案:几乎总是会出现类似undefined reference to ...的错误。将所有模板定义放在标头中,以便编译器在使用它们时已经可以生成所需的代码。

好。

static_cast(base); if base is a pointer to a virtual base class of Derived.

Ok.

解决方案:虚拟基类是仅发生一次的基类,即使它被继承树中的不同类间接继承了不止一次。标准不允许这样做。使用dynamic_cast来做到这一点,并确保您的基类是多态的。

好。

dynamic_cast(ptr_to_base); if base is non-polymorphic

Ok.

解决方案:当传递的对象不是多态的时,该标准不允许向下转换指针或引用。它或其基类之一必须具有虚拟功能。

好。

使函数接受T const **

好。

解决方案:您可能认为这比使用T **更安全,但实际上,它会使想要通过T**的人感到头痛:标准不允许这样做。 它给出了一个为什么禁止使用它的简洁示例:

好。

1
2
3
4
5
6
7
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 ++的另一个(封闭的)陷阱线程,因此正在寻找它们的人们会发现它们,是Stack Overflow问题C ++陷阱。

好。

好。


有些必须拥有C ++书籍,可帮助您避免常见的C ++陷阱:

有效的C ++
更有效的C ++
有效的STL

有效的STL书解释了bools问题的向量:)


Brian有一个很棒的清单:我要添加"总是将单参数构造函数标记为显式的(除非在极少数情况下需要自动转换)。


Scott Wheeler的网页C ++陷阱涵盖了一些主要的C ++陷阱。


并不是真正的技巧,而是一般准则:请检查您的资源。 C ++是一门古老的语言,多年来已经发生了很大的变化。最佳做法已随之改变,但不幸的是,那里仍然有很多旧信息。这里有一些很好的书籍推荐-我可以第二次购买每一本Scott Meyers C ++书籍。熟悉Boost和Boost中使用的编码样式-与该项目有关的人员处于C ++设计的最前沿。

不要重新发明轮子。熟悉STL和Boost,并尽可能使用它们的功能。特别是,除非有非常非常好的理由,否则请使用STL字符串和集合。很好地了解auto_ptr和Boost智能指针库,了解在哪种情况下打算使用每种类型的智能指针,然后在其他可能使用原始指针的地方使用智能指针。您的代码将同样高效,并且不易发生内存泄漏。

使用static_cast,dynamic_cast,const_cast和reinterpret_cast而不是C样式强制转换。与C样式的强制转换不同,它们将让您知道您是否真正要求的类型与您认为的不同。他们在视觉上脱颖而出,警告读者演员阵容正在发生。


我已经提到过几次了,但是Scott Meyers的书《 Effective C ++》和《 Effective STL》在帮助C ++方面确实物有所值。

想到这一点,史蒂文·德赫斯特(Steven Dewhurst)的C ++ Gotchas也是出色的"从战the"资源。他的有关滚动自己的异常以及如何构造异常的项目确实在一个项目中对我有所帮助。


与C一样使用C ++。在代码中具有创建和发布周期。

在C ++中,这不是异常安全的,因此可能无法执行发行版。在C ++中,我们使用RAII解决此问题。

所有具有手动创建和释放的资源都应包装在一个对象中,以便在构造函数/析构函数中完成这些操作。

1
2
3
4
5
6
7
8
9
// C Code
void myFunc()
{
    Plop*   plop = createMyPlopResource();

    // Use the plop

    releaseMyPlopResource(plop);
}

在C ++中,这应该包装在一个对象中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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.
}

我希望我没有学到很难的两个陷阱:

(1)默认情况下,很多输出(例如printf)被缓冲。如果您正在调试崩溃的代码,并且正在使用缓冲的调试语句,那么您看到的最后一个输出可能实际上并不是代码中遇到的最后一个打印语句。解决方案是在每次调试打印后刷新缓冲区(或完全关闭缓冲区)。

(2)注意初始化-(a)避免将类实例作为全局变量/静态变量; (b)尝试将所有成员变量初始化为ctor中的某个安全值,即使该值是微不足道的值(例如NULL)也是如此。

推理:不能保证全局对象初始化的顺序(全局变量包含静态变量),因此您最终可能会导致代码不确定性地失败,因为它取决于对象X在对象Y之前被初始化。如果您未明确初始化a基本类型的变量,例如类的成员bool或枚举,在令人惊讶的情况下,您将最终获得不同的值-再次,该行为似乎非常不


C ++ Gotchas这本书可能很有用。


这是我不幸碰到的几个陷阱。所有这些都有充分的理由,只有在被令我惊讶的行为咬伤之后,我才明白。

  • 构造函数中的virtual函数不是。

  • 不要违反ODR(一个定义规则),这就是匿名名称空间的用途(除其他外)。

  • 成员的初始化顺序取决于声明它们的顺序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class bar {
        vector<int> vec_;
        unsigned size_; // Note size_ declared *after* vec_
    public:
        bar(unsigned size)
            : size_(size)
            , vec_(size_) // size_ is uninitialized
            {}
    };
  • 默认值和virtual具有不同的语义。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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 ++ FAQ Lite。它解释了许多不好(也很好!)的做法。
  • 不使用Boost。通过在可能的情况下利用Boost可以节省很多挫败感。

  • 查看boost.org。它提供了许多附加功能,尤其是它们的智能指针实现。


    对于初学者来说,最重要的陷阱是避免C和C ++之间的混淆。 C ++永远不应被视为带有类的更好的C或C,因为这会削减其功能,甚至使其变得危险(尤其是像在C中那样使用内存时)。


    PRQA基于Scott Meyers,Bjarne Stroustrop和Herb Sutter的著作,有一个出色的免费C ++编码标准。它将所有这些信息汇总到一个文档中。


    使用智能指针和容器类时要小心。


    避免使用伪类和类...基本上是过度设计。


    忘记定义虚拟的基类析构函数。这意味着在Base *上调用delete不会最终破坏派生部分。


    保持名称空间平整(包括结构,类,名称空间和使用)。当程序无法编译时,这就是我的第一挫败感。


    要弄糟,请经常使用直指针。而是将RAII用于几乎所有内容,当然要确保使用正确的智能指针。如果在句柄或指针类型类之外的任何地方编写"删除",则很可能会做错。


    • Blizpasta。我看到很多东西了……

    • 未初始化的变量是我的学生犯的一个巨大错误。许多Java人士都忘记了只说" int counter"并没有将counter设置为0。由于您必须在h文件中定义变量(并在对象的构造函数/设置中对其进行初始化),因此很容易忘记。

    • for循环/阵列访问中的一对一错误。

    • 伏都教启动时无法正确清除目标代码。


    • static_cast downcast on a virtual base class

    并非如此……现在是关于我的误解:我认为以下内容中的A是虚拟基类,而实际上不是。根据10.3.1,它是一个多态类。在这里使用static_cast似乎很好。

    1
    2
    3
    struct B { virtual ~B() {} };

    struct D : B { };

    总之,是的,这是一个危险的陷阱。


    阅读《 C ++陷阱:避免编码和设计中的常见问题》一书。


    忘记&,从而创建副本而不是引用。

    这两次以不同的方式发生在我身上:

    • 一个实例位于参数列表中,该实例导致将大对象放到堆栈上,从而导致堆栈溢出和嵌入式系统崩溃。

    • 我忘记了实例变量上的&,结果是对象被复制了。注册为副本的侦听器后,我想知道为什么我从未从原始对象获得回调。

    两者都很难发现,因为差异很小且很难看到,否则对象和引用在语法上的使用方式相同。


    取消引用之前,请务必先检查指针。在C语言中,通常可以指望在取消引用错误指针时发生崩溃。在C ++中,您可以创建一个无效的引用,该引用将在远离问题根源的位置崩溃。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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)

    1
    2
    3
    if (x = 10) {
        //Do something
    }

    我以为自己永远不会犯这个错误,但实际上我是最近才犯的。


    文章/文章指针,参考和值非常有用。讨论避免避免陷阱和良好做法。您也可以浏览整个站点,其中包含主要针对C ++的编程技巧。


    我花了很多年从事C ++开发。我写了几年前遇到的问题的简短摘要。符合标准的编译器不再是真正的问题,但我怀疑所概述的其他陷阱仍然有效。


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #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);
    }

    推荐阅读