致 OP:您的 cmets 表示对 C++ 的工作方式有些困惑。有一些重要的事情你需要了解一些事情:
- 处理指针本质上是危险的。不正确地使用指针可能会引入未定义的行为,这不仅是您的程序中的错误,而且是编译器可能无法检测到的错误,甚至不会给您警告,并且在运行时不知道您的程序将如何表现:它可能会立即崩溃,但它也可能会继续运行,但会导致程序的另一部分(100% 正确)崩溃或只是表现得与应有的不同。没有自动方法可以确定地检测这些情况,因此良好的设计至关重要。
- 指向对象的指针与其指向的对象不同。附带说明一下,如果您来自 Java 之类的语言,那么在 C++ 中,“对象”一词也指内置类型的变量,例如 int、double 等。
- 指向 X 类型的指针是一个值类型,它可能指向也可能不指向 X 类型的对象。除了具有特殊值 0 (NULL) 之外,它可能只是指向内存中的随机位置不包含 X 类型的对象。这样的指针称为“悬空指针”。
- 访问由指针指向的对象的行为称为解除对指针的引用。取消引用悬空指针或空指针是未定义的行为。此外,虽然您的程序可以检查指针是否为空,但无法检查它是否为悬空指针。程序员有责任以这样一种方式构建程序,使悬空指针永远不会被取消引用。
- 在您的一个问题中,您询问了有关创建指针的问题。创建指针绝对不意味着您必须在其上调用
delete,因为创建指针与创建指针指向的对象不同。考虑以下代码,它创建了一个指针:SomeType* pointer;
虽然这确实创建了一个指针,但它是一个悬空指针,如果您尝试在其上调用 delete,您将获得未定义的行为。
- 内存分配的问题与指针本身无关,就像对象一样,内存被分配或取消分配。通常,您不会在 C++ 中显式分配或取消分配内存,而是创建和销毁对象,并为它们隐式分配内存。
-
new 运算符不创建指针,它创建动态对象,为它们分配所需的内存,并返回指向新创建对象的指针。动态分配的对象的生命周期将持续存在,并且它们将继续占用它们占用的内存,直到您在指针上调用 delete,这会破坏动态对象(而不是指针本身)并释放它占用的内存。如果您重复分配动态对象并且在停止使用它们后未能删除它们,则您的内存将被未使用的对象填满:这称为内存泄漏。
- 指针不仅可以指向动态分配的对象,还可以指向其他类型的对象:局部变量、结构或类成员、数组元素。正如无法判断指针是否悬空一样,也无法判断它是否指向动态对象。尝试在指向动态对象以外的任何对象的有效指针上调用
delete 会导致未定义行为。
- 可能有许多指针指向同一个对象。如果对象被销毁,无论是在动态对象的情况下调用 delete,还是以任何其他方式,所有指向它的指针都将成为悬空指针。
- 指针变量或类/结构成员本身就是对象,但它们的生命周期与它们指向的任何对象无关:就像在创建指针时没有创建指向的对象一样,指向的对象 (如果有)在指针被销毁时不会被销毁。这样就可以安全地销毁指向同一对象的多个指针,或销毁悬空指针。
使用new 创建动态对象并使用delete 销毁它们时的另一个问题是异常安全:在许多情况下,最初看起来像delete 的代码可能不会这样做,因为抛出了异常。例如:
{
SomeType *p = new Sometype
// some code
delete p;
}
在这种情况下,如果“某些代码”抛出异常,则永远无法到达 delete p 并且会发生内存泄漏。类似地:
class A {
SomeType *p;
// more members
public:
A(): p(new SomeType()) /* more intializers */ { /* body */ }
~A() { delete p; }
};
在这种情况下,如果 A 构造函数在 p 初始化后抛出异常,则析构函数将永远不会被调用,并且会发生内存泄漏。
鉴于这些陷阱,存在多种设计技术来解决这些问题。
除非必要,否则不要使用指针和/或动态对象:在许多情况下,您无需动态分配对象即可。在局部变量的情况下,变量的生命周期仅限于函数范围,您可以将对象定义为自动变量,而不是:
{
SomeType* p = new SomeType();
// do something with *p
delete p;
}
你会的:
{
SomeType var;
// do something with var
} // var destroyed here
在第二种情况下,var 将被销毁,并且当它超出范围时,它的内存将被释放 - 即使抛出异常也是如此。
同样,对于包含其他对象的对象,你可以直接将该对象作为成员包含进来,这样就可以代替:
class Containing {
Contained* member;
};
你可以的
class Containing {
Contained member;
};
在这种情况下,包含成员的内存在包含对象内部,并且在包含对象被释放时将被释放,此外,如果包含成员在包含对象的构造期间被初始化,但包含构造函数随后抛出异常,保证包含的成员将被正确销毁,包括调用它自己的析构函数,即使包含对象的析构函数没有被调用。
由于其中不涉及指针,因此您不必担心悬空指针或空指针,您还可以获得减少时间和内存开销的额外优势。
缺点是:数据结构中没有递归(因此对于列表、树等,您需要使用指针),没有多态性(指向 SomeType 的指针可能指向 SomeType 的对象或派生自 SomeType 的类型,自动SomeType 类型的变量只能是该特定类型),并且没有延迟初始化(这对于类数据成员尤其重要,其构造函数参数必须始终在包含的类初始值设定项列表中给出,而指针可以(重新)分配随时。
在必须使用动态对象的情况下,您需要依靠设计来确保删除动态对象。典型的模式是“所有者”的概念——负责删除动态对象的实体。在这个概念中,每个动态对象都有一个且只有一个“所有者”,它是当前正在执行的函数或另一个持有指向所拥有对象的指针的对象。因此,可能有许多指向一个对象的指针,但只有一个是拥有指针。这适用于许多情况:创建动态对象的函数仅在函数运行时才存在,它将拥有该对象,这意味着函数本身中必须有执行删除的代码。在任何类型的分层数据结构(如树)中,“父”对象都可以拥有其“子”。一些动态数据结构,如链表,可以包装在拥有其动态节点的对象中,并负责创建和删除它们,在此示例中,当一个元素被添加到列表中或从列表中删除时。
在更复杂的情况下,所有权可以在不同的代码段之间转移。例如,当使用insert 方法添加动态对象时,动态对象的集合可能会承担在其外部创建的对象的所有权,并使用具有get-and-remove 语义(pop、dequeue.. .)。创建动态对象并返回指向它们的指针的函数可以将所有权传递给调用者,等等。
对于保持动态对象所有权的对象,您将始终希望确保它们在析构函数中被删除,因为如果没有发生这种情况,当所有者被销毁时,所拥有的对象将保持没有所有者,这意味着它将永远不会被删除,实际上成为内存泄漏。拥有者也可以在自己销毁之前删除拥有的对象,这在很多情况下都很有用。一个警告:确保所有者没有留下一个悬空指针,它会认为它指向一个有效的对象并尝试再次销毁。在某些情况下,当您将拥有的对象替换为新的对象时,您会销毁它,在这种情况下,您不会遇到此问题,因为拥有指针现在将指向新的拥有对象,这也是有效的。在其他情况下,您需要将拥有指针设置为 0,以便对象知道它不再拥有该指针下的任何内容(在空指针上调用 delete 也是安全的)。
在函数体中使用手动deletes 或析构函数来维护所有权有几个问题:首先,这种所有权没有在代码中表示,您必须单独管理它,这很容易出错(因为编译器将并且不能在这里检测到任何错误)。我上面提到的异常问题使情况变得更加复杂:编写代码以便无论在哪里抛出异常都会调用 delete 是很困难的,而且容易出错。
其他答案中提到的 boost 和标准库中的智能指针有助于以异常安全的方式明确表达所有权语义。 STL 的 auto_ptr 和 boost scoped_ptr 具有上述严格的所有权语义,分别具有和不具有转移所有权的能力。如果您有幸使用当前的标准 C++,unique_ptr 是 auto_ptr 的替代品,它消除了它的一些问题。新标准 STL、boost 和 TR1 的 shared_ptr 具有“共享所有权”语义,其中一个对象有多个“所有者”,并在最后一个所有者放手时被销毁。使用这些智能指针类型中的每一种都有优点和注意事项,您应该在使用它们之前阅读它们。