我应该避免哪些C ++陷阱?
我记得第一次学习STL中的vector,经过一段时间后,我想为我的一个项目使用一个boolsvector。 在看到一些奇怪的行为并做了一些研究之后,我了解到一个bools向量并不是真正的bools向量 。
在C ++中是否还有其他常见的陷阱?
一个简短的列表可能是:
- 通过使用共享指针来pipe理内存分配和清理,避免内存泄漏
- 使用资源获取初始化 (RAII)惯用法来pipe理资源清理 – 特别是在存在exception的情况下
- 避免在构造函数中调用虚函数
- 在可能的情况下采用极简主义的编码技术 – 例如,仅在需要时声明variables,确定variables范围,尽可能提前devise。
- 真正理解代码中的exception处理 – 无论是抛出的exception,还是间接使用的类抛出的exception。 这在存在模板时尤其重要。
RAII,共享指针和简约编码当然不是C ++特有的,但是它们有助于避免在开发语言时经常出现的问题。
关于这个问题的一些优秀的书籍是:
- 有效的C ++ – Scott Meyers
- 更有效的C ++ – Scott Meyers
- C ++编码标准 – Sutter&Alexandrescu
- C ++常见问题 – Cline
读这些书已经帮了我更多的东西来避免你所问的那种陷阱。
陷阱的重要性在下降
首先,你应该访问屡获殊荣的C ++ FAQ 。 它有许多很好的答案,陷阱。 如果您还有其他问题,请irc.freenode.org
IRC的 irc.freenode.org
上的##c++
。 如果可以,我们很乐意帮助您。 注意以下所有的陷阱都是最初编写的。 他们不只是从随机来源复制。
删除
new[]
,delete
new[]
解决scheme :这样做会导致未定义的行为:一切都可能发生。 理解你的代码和它的作用,并且总是delete[]
你new[]
,并delete
你new
,那么这是不会发生的。
例外 :
typedef T type[N]; T * pT = new type; delete[] pT;
你需要delete[]
即使你是new
,因为你new'ed一个数组。 所以如果你正在使用typedef
,请特别注意。
在构造函数或析构函数中调用虚函数
解决scheme :调用一个虚函数将不会调用派生类中的重载函数。 在构造函数或desctructor中调用纯虚函数是未定义的行为。
调用已删除的指针上的
delete
或delete[]
解决scheme :将0分配给您删除的每个指针。 在空指针上调用delete
或delete[]
什么也不做。
取一个指针的大小,当一个“数组”的元素的数量要计算。
解决scheme :当需要将数组作为指针传递给函数时,传递指针旁边的元素数目。 使用这里提出的函数,如果你把一个数组的sizeof应该是一个真正的数组。
像使用指针一样使用数组。 因此,使用
T **
作为二维数组。
解决scheme :请看这里为什么他们是不同的,你如何处理他们。
写入string文字:
char * c = "hello"; *c = 'B';
char * c = "hello"; *c = 'B';
解决scheme :分配一个从string数据初始化的数组,然后可以写入:
char c[] = "hello"; *c = 'B';
写入string文字是未定义的行为。 无论如何,从string文字到char *
的上述转换已被废弃。 所以编译器可能会警告如果你提高警告级别。
创build资源,然后忘记释放一些东西时抛出。
解决scheme :使用智能指针如std::unique_ptr
或std::shared_ptr
正如其他答案指出的那样。
像这个例子中一样修改对象两次:
i = ++i;
解决scheme :上面应该给i
i+1
的值。 但是它没有定义。 而不是增加i
和分配的结果,它也改变i
在右侧。 在两个序列点之间更改对象是未定义的行为。 序列点包括||
, &&
, comma-operator
, semicolon
和entering a function
(非穷举列表!)。 将代码更改为以下,以使其行为正确: i = i + 1;
杂项问题
在调用像
sleep
这样的阻塞函数之前,忘记刷新stream。
解决scheme :通过stream式std::endl
而不是\n
或通过调用stream.flush();
来刷新streamstream.flush();
。
声明一个函数而不是一个variables。
解决scheme :问题出现是因为编译器解释了例如
Type t(other_type(value));
作为返回Type
的函数的一个函数声明,并且有一个名为value
other_type
的参数。 你通过在第一个参数旁边加括号来解决它。 现在你得到一个types为Type
的variablest
:
Type t((other_type(value)));
调用只在当前翻译单元(
.cpp
文件)中声明的自由对象的function。
解决scheme :标准没有定义跨不同翻译单元定义的自由对象(在名称空间范围内)的创build顺序。 在尚未构造的对象上调用成员函数是未定义的行为。 您可以在对象的翻译单元中定义以下函数,并从其他函数中调用它:
House & getTheHouse() { static House h; return h; }
这将按需创build对象,并在您调用函数时为您提供完整构造的对象。
在
.cpp
文件中定义模板,而在另一个.cpp
文件中使用。
解决scheme :几乎总是会遇到类似undefined reference to ...
错误undefined reference to ...
把所有的模板定义放在一个头文件中,这样编译器在使用它们的时候,已经可以产生所需的代码了。
static_cast<Derived*>(base);
如果base是指向Derived
的虚拟基类的指针。
解决scheme :虚拟基类是仅发生一次的基础,即使它在inheritance树中被不同的类间接地多次inheritance。 本标准不允许进行上述操作。 使用dynamic_cast来做到这一点,并确保你的基类是多态的。
dynamic_cast<Derived*>(ptr_to_base);
如果base是非多态的
解决scheme :标准不允许在传递的对象不是多态时丢弃指针或引用。 它或者它的一个基类必须有一个虚函数。
让你的函数接受
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 ++
更有效的C ++
有效的STL
有效的STL书籍解释了bools问题的向量:)
布赖恩有一个很好的列表:我会添加“总是标记单个参数构造函数显式(除非你希望自动投射的罕见情况除外)”。
不是一个特定的提示,而是一个总的指导方针:检查你的来源。 C ++是一种古老的语言,多年来变化很大。 最佳实践已经改变,但不幸的是,仍然有很多旧的信息。 在这里有一些非常好的书籍推荐 – 我可以第二次购买斯科特·迈耶斯C ++书籍的每一个。 熟悉Boost和Boost中使用的编码风格 – 与该项目有关的人员处于C ++devise的前沿。
不要重新发明轮子。 熟悉STL和Boost,并尽可能使用他们的设施。 特别是,使用STLstring和集合,除非你有一个非常非常好的理由不要。 熟悉auto_ptr和Boost智能指针库,了解哪种情况下每种types的智能指针都可以使用,然后在任何地方使用智能指针,否则可能会使用原始指针。 你的代码将会同样高效,并且不太容易发生内存泄漏。
使用static_cast,dynamic_cast,const_cast和reinterpret_cast代替C风格转换。 不像C风格的演员,他们会让你知道,如果你真的要求一个不同types的演员比你想的要求。 他们在视觉上脱颖而出,提醒读者正在发生演员阵容。
Scott Wheeler的网页C ++陷阱涵盖了一些主要的C ++陷阱。
我希望的两个陷阱我没有学到艰难的道路:
(1)大量输出(如printf)默认缓冲。 如果您正在debugging崩溃的代码,并且正在使用缓冲的debugging语句,则您看到的最后一个输出可能不是真正的代码中遇到的最后一个打印语句。 解决方法是在每次debugging打印后刷新缓冲区(或者完全closures缓冲区)。
(2)注意初始化 – (a)避免将类实例作为全局variables/静态variables; (b)尝试将所有成员variables初始化为ctor中的某个安全值,即使它是一个微不足道的值,例如指针的NULL。
推理:全局对象初始化的顺序是不能保证的(全局variables包含静态variables),所以你可能最终得到的代码看起来是非确定性的,因为它依赖于在对象Y之前被初始化的对象X.如果你没有明确地初始化原始types的variables,比如成员bool或类的枚举,在令人惊讶的情况下,最终会得到不同的值 – 这种行为看起来也是非常不确定的。
我已经提到了几次,但是Scott Meyers的书籍Effective C ++和Effective STL真的值得他们用C ++来帮助他们。
想想吧,Steven Dewhurst的C ++ Gotchas也是一个很好的“来自战壕”的资源。 他关于自己的例外情况的项目以及如何构build真正帮助我完成一个项目。
像C一样使用C ++。在代码中创build和释放循环。
在C ++中,这不是exception安全的,因此可能不会执行发布。 在C ++中,我们使用RAII来解决这个问题。
所有具有手动创build和释放的资源都应该包装在一个对象中,以便在构造器/析构器中完成这些操作。
// 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 ++陷阱可能会certificate是有用的。
这里有几个坑,我有不幸落入。 所有这些都有很好的理由,我只有被令人吃惊的行为所困扰才明白。
-
构造函数中的
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 。 它提供了很多额外的function,尤其是它们的智能指针实现。
PRQA拥有基于Scott Meyers,Bjarne Stroustrop和Herb Sutter的书籍的优秀免费C ++编码标准 。 它将所有这些信息汇集在一个文件中。
- 没有阅读C ++ FAQ Lite 。 它解释了许多不好的(和好的)做法。
- 不使用Boost 。 在可能的情况下,您可以利用Boost来节省很多挫折。
使用智能指针和容器类时要小心。
避免伪类和准类 …基本上过度devise。
忘了定义一个基类析构函数虚拟。 这意味着在一个Base *上调用delete
将不会最终破坏派生的部分。
保持名称空间直(包括结构,类,名称空间和使用)。 这是我的头号挫折,当程序不编译。
搞砸了,用了很多直接的指针。 相反,使用RAII几乎任何东西,确保你使用正确的智能指针。 如果你在句柄或指针类的外部任何地方写“delete”,你很可能做错了。
阅读“ C ++技巧:避免编码和devise中的常见问题 ”一书。
-
Blizpasta。 这是一个很大的我看到很多…
-
未初始化的variables是我的学生所犯的一个巨大的错误。 许多Java人忘记了只是说“int counter”不会将counter设置为0.因为你必须在h文件中定义variables(并在对象的构造函数/设置中初始化它们),所以很容易忘记。
-
for
循环/数组访问的逐个错误。 -
voodoo启动时没有正确清理目标代码。
static_cast
在虚拟基类上downcast
不是真的……现在我的误解是:我认为下面的A
是一个虚拟的基类,事实上它不是。 根据10.3.1,是一个多态类 。 在这里使用static_cast
似乎很好。
struct B { virtual ~B() {} }; struct D : B { };
总而言之,是的,这是一个危险的陷阱。
在取消引用之前总是检查一个指针。 在C语言中,通常可以指望在引用错误指针的地方崩溃; 在C ++中,你可以创build一个无效的引用,它会在远离问题源的地方崩溃。
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 }
遗忘&
,从而创build一个副本,而不是一个参考。
这发生在我身上两次不同的方式:
-
一个实例出现在参数列表中,导致一个大对象被堆栈溢出,导致embedded式系统崩溃。
-
我忘了
&
的一个实例variables,大意是该对象被复制。 在注册为副本的监听器之后,我想知道为什么我从来没有从原始对象获得callback。
这两个地方都很难find,因为它们之间的差别很小,很难看清楚,否则,对象和引用在语法上以相同的方式使用。
意图是(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); }