我记得第一次学习 STL 中的向量,一段时间后,我想在我的一个项目中使用布尔向量。在看到一些奇怪的行为并做了一些研究之后,我了解到bools 的向量并不是真正的 bools 向量。
在 C++ 中是否还有其他常见的陷阱需要避免?
我记得第一次学习 STL 中的向量,一段时间后,我想在我的一个项目中使用布尔向量。在看到一些奇怪的行为并做了一些研究之后,我了解到bools 的向量并不是真正的 bools 向量。
在 C++ 中是否还有其他常见的陷阱需要避免?
一个简短的列表可能是:
RAII、共享指针和极简编码当然不是 C++ 特有的,但它们有助于避免在使用该语言进行开发时经常出现的问题。
关于这个主题的一些优秀书籍是:
阅读这些书对我避免你所问的那种陷阱最有帮助。
首先,您应该访问获奖的C++ 常见问题解答。它对陷阱有很多很好的答案。如果您还有其他问题,请##c++
访问IRC。如果可以的话,我们很乐意为您提供帮助。请注意,以下所有陷阱都是最初编写的。它们不仅仅是从随机来源复制的。irc.freenode.org
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
ordelete[]
什么都不做。
当要计算“数组”的元素数时,取指针的大小。
解决方案:当您需要将数组作为指针传递给函数时,将元素的数量与指针一起传递。如果您采用应该是真正数组的数组的 sizeof,请使用此处建议的函数。
像使用指针一样使用数组。因此,
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);
如果 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 question C++ pitfalls。
Brian 有一个很棒的清单:我会添加“始终将单参数构造函数标记为显式(除非在极少数情况下您需要自动转换)。”
Scott Wheeler的网页C++ Pitfalls涵盖了一些主要的 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也是一个极好的“来自战壕”的资源。他关于滚动你自己的异常以及应该如何构建它们的项目确实在一个项目中帮助了我。
我希望我没有以艰难的方式学习的两个陷阱:
(1) 很多输出(如printf)默认缓冲。如果您正在调试崩溃的代码,并且您正在使用缓冲的调试语句,那么您看到的最后一个输出可能并不是代码中遇到的最后一个打印语句。解决方案是在每次调试打印后刷新缓冲区(或完全关闭缓冲)。
(2) 小心初始化 - (a) 避免将类实例作为全局变量/静态变量;(b) 尝试将所有成员变量初始化为 ctor 中的某个安全值,即使它是一个微不足道的值,例如指针的 NULL。
推理:无法保证全局对象初始化的顺序(全局变量包括静态变量),因此您最终可能会得到似乎不确定地失败的代码,因为它取决于对象 X 在对象 Y 之前被初始化。如果您没有显式初始化原始类型变量,例如类的成员 bool 或枚举,在令人惊讶的情况下,您最终会得到不同的值——同样,行为看起来非常不确定。
像 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++ Gotchas可能证明是有用的。
以下是我不幸落入的几个坑。所有这些都有很好的理由,我只有在被令我惊讶的行为咬伤后才明白。
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。它提供了许多附加功能,尤其是它们的智能指针实现。
PRQA 有一个基于 Scott Meyers、Bjarne Stroustrop 和 Herb Sutter 书籍的优秀且免费的 C++ 编码标准。它将所有这些信息集中在一个文档中。
使用智能指针和容器类时要小心。
避免伪类和准类......基本上是过度设计。
忘记定义基类析构函数 virtual。这意味着调用delete
Base* 不会最终破坏派生部分。
保持名称空间直截了当(包括结构、类、命名空间和使用)。当程序无法编译时,这是我的头号挫折。
要搞砸,请大量使用直指针。相反,几乎任何事情都使用 RAII,当然要确保使用正确的智能指针。如果你在句柄或指针类型类之外的任何地方写“删除”,你很可能做错了。
暴风雪。这是我经常看到的一个巨大的...
未初始化的变量是我的学生犯的一个巨大错误。许多 Java 人忘记了仅仅说“int counter”并不会将 counter 设置为 0。由于您必须在 h 文件中定义变量(并在对象的构造函数/设置中初始化它们),因此很容易忘记。
for
循环/数组访问上的错误。
voodoo 启动时未正确清理目标代码。
static_cast
在虚拟基类上垂头丧气
不是真的......现在关于我的误解:我认为A
下面是一个虚拟基类,而实际上它不是;根据 10.3.1,它是一个多态类。在这里使用static_cast
似乎很好。
struct B { virtual ~B() {} };
struct D : B { };
总之,是的,这是一个危险的陷阱。
在取消引用之前始终检查指针。在 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
}
我以为我自己永远不会犯这个错误,但我最近真的犯了。
文章/文章指针、参考和价值观非常有用。它谈论避免避免陷阱和良好做法。您也可以浏览整个站点,其中包含主要针对 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);
}