我记得刚学STL时,学到了vector,后来有个项目需要用到bool类型的vector。但是在使用中发现一些奇怪的行为,经过研究发现bool类型的vector并不是真正的bool类型的vector。
在C++中还有其他常见的需要避免的陷阱吗?
我记得刚学STL时,学到了vector,后来有个项目需要用到bool类型的vector。但是在使用中发现一些奇怪的行为,经过研究发现bool类型的vector并不是真正的bool类型的vector。
在C++中还有其他常见的需要避免的陷阱吗?
以下是一份简短的清单:
当然,RAII、共享指针和极简主义编码并不仅适用于C++,但它们有助于避免在该语言开发中经常出现的问题。
关于这个主题的一些优秀书籍包括:
阅读这些书籍对我避免了你提到的那种陷阱,是最有帮助的。
首先,您应该访问屡获殊荣的C++ FAQ。它包含了许多有关陷阱的好答案。如果您有进一步的问题,请在IRC的irc.freenode.org
上访问##c++
。我们非常乐意帮助您。请注意,以下所有陷阱均由原作者撰写。它们不是从随机来源复制而来。
new[]
使用delete[]
释放内存,new
使用delete
释放内存
解决方法: 上述操作会导致未定义的行为:任何事情都可能发生。理解您的代码以及它的作用,并始终delete[]
您所new[]
的东西,并删除您所new
的东西,那么这种情况就不会发生。
例外情况:
typedef T type[N]; T * pT = new type; delete[] pT;
即使你使用new
创建了一个数组,你仍然需要使用delete[]
删除它。所以如果你在使用typedef
,需要特别小心。
在构造函数或析构函数中调用虚函数
解决方案: 调用虚函数不会调用派生类中的覆盖函数。在构造函数或析构函数中调用一个纯虚函数是未定义行为。
对已经删除的指针进行
delete
或delete[]
解决方案: 将要删除的每个指针赋值为0。对空指针调用delete
或delete[]
不会产生任何效果。
当计算“数组”的元素数量时,对指针进行
sizeof
操作。
解决方案: 当需要将数组作为指针传递到函数中时,请同时传递指针的元素数量。如果您需要对一个实际上应该是数组的数组进行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
。在两个序列点之间更改对象是未定义的行为。序列点包括||
、&&
、逗号运算符
、分号
和进入函数
(不详尽的列表!)。将代码更改为以下内容可使其正确运行:i = i + 1;
在调用阻塞函数(如
sleep
)之前忘记刷新流。
解决方案: 通过流式传输std::endl
而不是\n
,或者调用stream.flush();
来刷新流。
声明一个函数而不是变量。
解决方案: 这个问题的产生是因为编译器将例如
Type t(other_type(value));
t
返回类型 Type
,并且有一个参数是类型为 other_type
的变量,名为 value
。将第一个参数括在括号内即可解决此问题。现在您可以得到一个类型为 Type
的变量 t
: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++ pitfalls。
以下是一些必备的 C++ 书籍,它们将帮助您避免常见的 C++ 陷阱:
《Effective C++》
《More Effective C++》
《Effective STL》
《Effective STL》一书解释了布尔向量问题 :)
Brian有一个很棒的清单:我会补充一点,即“始终将单参数构造函数标记为显式(除了你想要自动转换的那些稀有情况)。”
像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.
}
我已经多次提到过,Scott Meyers的书籍《Effective C++》和《Effective STL》对于帮助学习C++非常有价值。
想起来了,Steven Dewhurst的《C++陷阱》也是一本极好的“实战”资源。他关于自定义异常及其构造方式的章节在我的一个项目中帮助了我很多。
我希望我没有学习这两个问题:
(1) 许多输出(例如printf)默认情况下都是缓冲的。如果你正在调试崩溃的代码,并且使用了缓冲调试语句,那么你看到的最后一个输出可能 不是 代码中遇到的最后一个打印语句。解决方法是在每个调试打印语句后刷新缓冲区(或完全关闭缓冲)。
(2) 在初始化时要小心 - (a) 避免将类实例作为全局/静态变量;(b) 尽量在构造函数中将所有成员变量初始化为安全值,即使是指针的NULL等微不足道的值。
原因:全局对象初始化的顺序不能保证(全局包括静态变量),所以您可能会得到似乎随机失败的代码,因为它取决于在对象Y之前初始化对象X。如果您不显式初始化基本类型变量,例如类的成员bool或enum,您将在令人惊讶的情况下得到不同的值 - 再次,行为可能非常难以确定。
以下是我不幸遇到的一些坑。所有这些都有充分的理由,只有在被行为惊讶地咬了一口后,我才明白。
virtual
functions in constructors aren't.
Don't violate the ODR (One Definition Rule), that's what anonymous namespaces are for (among other things).
Order of initialization of members depends on the order in which they are declared.
class bar {
vector<int> vec_;
unsigned size_; // Note size_ declared *after* vec_
public:
bar(unsigned size)
: size_(size)
, vec_(size_) // size_ is uninitialized
{}
};
Default values and virtual
have different semantics.
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`