我应该避免哪些C ++陷阱?

时间:2020-03-05 18:43:55  来源:igfitidea点击:

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

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

解决方案

回答

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

回答

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

回答

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

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

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

回答

简短列表可能是:

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

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

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

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

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

回答

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

回答

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

回答

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

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

回答

我希望我没有学到的两个难题:

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

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

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

回答

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

回答

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

  • 构造函数中的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
        {}
};
  • 默认值和"虚拟"具有不同的语义。
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`

回答

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

回答

Brian有一个很棒的清单:我要添加"始终将单参数构造函数标记为显式(在极少数情况下,我们希望自动强制转换除外)"。

回答

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

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

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

回答

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

回答

  • 不阅读C ++ FAQ Lite。它解释了许多不好(也很好!)的做法。
  • 不使用Boost。尽可能利用Boost可以节省很多挫败感。

回答

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

回答

与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语言中,通常可以指望在取消引用错误指针时发生崩溃。在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
}

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

回答

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

回答

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

回答

  • Blizpasta。我看到很多东西了……
  • 未初始化的变量是我的学生犯的一个巨大错误。许多Java人士都忘记了,只说" int counter"并没有将counter设置为0。由于我们必须在h文件中定义变量(并在对象的构造函数/设置中对其进行初始化),因此很容易忘记。
  • for循环/数组访问中的一对一错误。
  • 伏都教启动时无法正确清除目标代码。

回答

static_cast downcast on a virtual base class

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

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

struct D : B { };

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

回答

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

回答

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

回答

陷阱按重要性从高到低的顺序排列

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

delete[] on new, delete on new[]

解决方案:对未定义的行为执行上述操作:可能发生的一切。了解代码及其作用,并始终"删除[]""新[]",然后"删除""新",那么这不会发生。

例外:

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

即使我们是new,也需要delete [],因为我们新建了一个数组。因此,如果我们使用的是typedef,请特别注意。

Calling a virtual function in a constructor or destructor

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

Calling delete or delete[] on an already deleted pointer

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

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

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

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

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

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

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

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

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

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

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

Modifying an object twice like in this example: i = ++i;

解决方案:上面应该为i分配i + 1的值。但是它没有做什么。而不是增加" i"并分配结果,它也在右侧更改了" i"。在两个序列点之间更改对象是未定义的行为。顺序点包括" ||"," &&","逗号运算符","分号"和"输入功能"(非穷举列表!)。将代码更改为以下内容,以使其正常运行:i = i + 1;

杂项问题

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

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

Declaring a function instead of a variable.

解决方案:之所以出现此问题,是因为编译器会解释例如

Type t(other_type(value));

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

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

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

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

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

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

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

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

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

dynamic_cast<Derived*>(ptr_to_base); if base is non-polymorphic

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

Making your function accept 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 ++开发。我写了几年前遇到的问题的简短摘要。符合标准的编译器不再是真正的问题,但我怀疑所概述的其他陷阱仍然有效。

回答

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

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

  • 一个实例位于参数列表中,该实例导致将大对象放到堆栈上,结果是堆栈溢出和嵌入式系统崩溃。
  • 我忘记了实例变量上的,结果是对象被复制了。注册为副本的侦听器后,我想知道为什么我从未从原始对象获得回调。

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