我应该避免哪些C ++陷阱?[关闭]


74

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

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


42
我认为C ++您应该避免的陷阱。
令人愉快的

根据嵌入式系统的专业经验来阅读答案很有趣。(即使所说的嵌入式系统有很多处理器和大量内存。)
dash-tom-bang 2010年

这是一个用于发起讨论的修辞性问题,其依据是“不是真正的问题”接近原因,并且似乎更适合您的博客或致力于讨论的网站。

10
嗯..所有人..?
bobobobo 2010年

Answers:


76

简短列表可能是:

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

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

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

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

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


您已经指定了我正在寻找的正确和最好的书。:)
Namratha Patil

6
“避免在构造函数中调用虚函数” <-我会将那个从“避免”升级为“从不”。+1。(即因为它是未定义的行为)
Billy ONeal 2010年

也许包括虚拟析构函数,以及如何正确捕获(并重新抛出)异常?
Asgeir S. Nilsen

4
@BillyONeal我可能会将其保留为“避免”。但是无论如何,对于构造函数中的虚拟调用,行为已得到很好的定义。这种调用不是未定义的行为,除非调用发生在纯虚拟类的构造函数(与析构函数类似)中的纯虚拟函数发生时
Johannes Schaub-litb 2010年

迈耶斯的另一本好书,当然是有效的STL!还不如加,因为它是现在的,有效的现代C ++
阿霍

52

重要性降序排列的陷阱

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


delete[]newdeletenew[]

解决方案:对未定义的行为进行上述操作会导致一切皆有可能。了解您的代码及其作用,始终delete[]了解您的内容new[]以及delete您的内容new,那么那将不会发生。

例外情况

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

delete[]即使您已经需要了new,因为您新建了一个数组。因此,如果您使用typedef,请特别小心。


在构造函数或析构函数中调用虚拟函数

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


调用deletedelete[]使用已删除的指针

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


当要计算“数组”的元素数时,采用指针的sizeof。

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


使用数组就像指针一样。因此,T **用于二维阵列。

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


写入字符串文字: char * c = "hello"; *c = 'B';

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

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

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


创建资源,然后忘记在出现异常时释放它们。

解决方法:使用智能指针像std::unique_ptrstd::shared_ptr通过其他的答案中指出。


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

解决方案:应该将i上述值分配给的值i+1。但是它没有定义。除了增加i和分配结果外,它还i在右侧更改。在两个序列点之间更改对象是未定义的行为。序列点包括||&&comma-operatorsemicolonentering 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 ++的另一个(封闭的)陷阱陷阱是Stack Overflow问题C ++陷阱,因此寻找它们的人会发现它们。


1
a [i] = ++ i; //读取两次被修改的变量会导致未定义的行为...如果您愿意,您也可以添加此代码
yesraaj

2
+1,很多优点。关于混合typedef和delete []的内容对我来说是全新的!还需要记住的另一个
极端

2
“为删除的每个指针分配0。” <-抱歉,但是错了。唯一的解决方案是首先不要编写错误。完全有可能有人复制了该指针,而不受您将其设置为零的影响。
Billy ONeal

@BillyONeal,如果删除指针后未将其设置为null,则无法检测是否已删除指针。如果您随后将其设置为null,则删除两次不一定是一个错误,因此是我建议的解决方案。
Johannes Schaub-litb 2010年

@Johannes Schaub-litb:是的,但是我的观点并不是万无一失。如果有人拥有该指针的副本并尝试删除它,您仍然会遇到双重释放问题。
Billy ONeal 2010年


12

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


8

并不是真正的技巧,而是一般准则:请检查您的资源。C ++是一门古老的语言,多年来已经发生了很大的变化。最佳做法已随之改变,但不幸的是,那里仍然有很多旧信息。这里有一些很好的书籍推荐-我可以第二次购买每一本Scott Meyers 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之前被初始化。如果您未明确初始化a基本类型的变量,例如类的成员bool或枚举,在令人惊讶的情况下,您将最终获得不同的值-同样,该行为似乎非常不确定。


解决方案是不使用打印调试
Dustin Getz

有时,这是唯一的选择……例如,调试崩溃仅发生在Release代码中和/或在与您所开发的目标Architecutre /平台不同的目标上发生。
xan

3
肯定有更复杂的调试方法。但是使用打印品是经过实践检验的事实,并且可以在比可以使用一个好的调试器更多的地方使用。我不是唯一一个这么认为的人-参见派克和克尼根的《编程实践》一书。
泰勒

+1用于表示全局对象的不确定性初始化。(有一些规则,但是没有我们想要的直观或完整。)
j_random_hacker 2009年

printf(和std :: cout)通常仅是行缓冲的,因此,只要相对确定在启动printf和单击换行之间不会崩溃,就可以了。考虑也编译器的错误,以防止调试符号<叽叽>的产生
虚线-汤姆爆炸

6

我已经提到过几次了,但是Scott Meyers的书《Effective C ++》和《Effective STL》确实很有价值,因为它们可以帮助C ++。

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


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

我不确定我们是否应该添加它。但也许我们应该使它不可复制/不可分配?
Johannes Schaub-litb


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 2009年

现在,C#4具有默认值,因此C#的功能相同(与虚拟/默认值一)。
BlueRaja-Danny Pflughoeft 2010年

3

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




3
  1. 不阅读C ++ FAQ Lite。它解释了许多不好(也很好!)的做法。
  2. 不使用Boost。通过在可能的情况下利用Boost可以节省很多挫败感。

2

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


问题的答案:对容器类使用智能指针有什么问题?例如:vector <shared_ptr <int>>。你能详细说明吗?
亚伦

2
他指的是禁止auto_ptr的容器,但有时会编译
Dustin Getz

@Aaron:具体来说,auto_ptr的赋值运算符会破坏其源操作数,这意味着它不能与依赖于此的标准容器一起使用。shared_ptr很好。
j_random_hacker 2009年

2

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


我目前正在与(n)个准类一起从事这样的项目。我们需要更多了解这种反模式!
DarenW

2

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


1

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


1

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



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

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

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

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


1
  • static_cast 在虚拟基类上贬低

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

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

struct D : B { };

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


请参阅上方的我的增强问题
Johannes Schaub-litb

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并没有太大帮助。指针可能具有非空值,并且仍指向已删除或无效的对象。
Nemanja Trifunovic

是的,但是以我的经验,NULL指针比其他类型的无效指针更常见。也许是因为我习惯于在删除指针后将其空为NULL。
Mark Ransom

这是您的错误处理策略的一部分。我会说,避免在核心代码中使用NULL指针检查(而是声明),但要确保您不要传入无效值(按合同设计)。
xtofl

0

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

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

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

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

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


0

目的是(x == 10)

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

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


3
这些天几乎所有编译器都会对此发出警告
Adam Rosenfield

对变量执行常量==会帮助发现这些错误,例如if(10 = x),编译器将因此出错
PiNoYBoY82

如果您有意if (x == y)
无济于事

0

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


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,2009年

+1。尽管shared_ptr文档指出不支持此用法(并提供了一种变通方法,enable_shared_from_this),但这是一个常见的用例,并且上述代码不会立即失败。它甚至似乎按照“立即将任何原始指针包装在shared_ptr中”的规则进行播放。真正的陷阱恕我直言。
j_random_hacker 2009年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.