什么时候我需要删除一个指针?

24
什么时候我必须使用delete? 例如,假设我正在分配两个对象:
Fraction* f1 = new Fraction(user_input1, user_input2);
Fraction* f2 = new Fraction(user_input3, user_input4);

下次我想使用new操作符创建一个新对象时,我必须先delete吗?我感到困惑,因为我习惯了Java中的垃圾回收器负责对象及其删除。在再次使用new之前,我必须先delete吗?
if (f1) delete f1;
if (f2) delete f2;

f1 = /* ... */;
f2 = /* ... */;

1
只是提醒一下,同样值得问的一个引人注目的问题是,你是否应该首先使用 new。将其添加到需要考虑的事项列表中。 - WhozCraig
1
这个 if (!f1) delete f1; 不会删除对象,因为它只会在指针为 null0 时才删除。无论如何,在删除之前不应该测试指针,因为如果你删除一个空指针它什么也不会做。所以直接执行 delete f1; 即可。 - Galik
1
这不是实际的代码,只是简单代码的示例(片段)。它可以运行,我只是想确保在必要时进行删除。感谢大家的意见,你们是完全正确的。 - qnob
-1 不是真正的代码。 - Cheers and hth. - Alf
认为“删除指针”是一个概念错误。delete操作符并不删除指针本身,而是删除指针当前指向的对象。 - undefined
显示剩余2条评论
4个回答

33
不是告诉您何时使用delete,而是尝试解释为什么要使用指针。这样您就可以决定何时使用动态对象,如何使用它们以及何时调用delete(并且不要)。

经验法则:

  • 尽可能使用静态对象,然后在需要时创建对该实例的指针。不需要调用delete
  • 如果您创建了指向动态对象的指针,请创建清理代码。因此,当您编写new时,也请在适当位置编写delete(并确保已调用)。
  • 每个new关键字都需要有一个delete关键字。否则,您将占用机器的所有资源,导致应用程序崩溃或停止。这还会使系统变慢。

静态创建对象:

Fraction f1;
  • 不需要删除任何东西,这是在离开其创建的搜罗时处理的。

对象的动态创建:

Fraction* f1;

现在你拥有了一个指向堆内存块的地址。但由于你尚未为它分配任何值,因此它是无效的。根据情况,最好将其赋值为 NULL (Windows) 或 0 (跨平台)。

Fraction* f1 = 0;

何时使用delete

一旦创建动态对象,即调用new运算符,就需要在某个地方调用delete

int main()
{

    Fraction* f1 = 0;    // Good practise to avoid invalid pointers
                         // An invalid pointer - if( f1 ){ Access violation }

    f1 = new Fraction(); // Could have done this at the previous line

    /* do whatever you need */

    if( f1 )
    {

        delete f1; 
        f1 = 0;          // not needed since we are leaving the application
    
    }

    return 0;

}

在某些情况下,拥有一个Fraction数组或指向它的指针可能会很有用。这里为了简单起见使用int,跳过错误处理:
int arr[ 10 ];
int cur = -1;
int* Add( int fraction )
{
    arr[++cur] = fraction;
    return &arr[cur];
}

// Usage:
Add( 1 );
Add( 4 );

这里发生的一件事情是,没有通过动态对象分配任何内存的赋值。它们会自动释放。函数返回的指针是指向静态内存块的指针。

当将arr定义为指向int的指针时:

int* arr[ 10 ];
int cur = -1;
int* Add( int* fraction )
{
    arr[++cur] = fraction;
    return arr[cur];
}

// Usage:
int* test;

test = Add( new int( 1 ) );
test = Add( new int( 4 ) );

现在你有两个内存块泄露了,因为你没有清理代码。

当你在每个Add(...)之后调用delete test时,你清理了内存,但是你失去了存储在int* arr[ 10 ]中的值,因为它们指向持有该值的内存。

你可以创建另一个函数,在使用这些值后调用它:

void CleanUp()
{
    for( int a = 0; a < 10; ++a )
        delete arr[ a ];
}

一个小的使用示例:

int* test;
int  test2;

test  = Add( new int( 1 ) );
test2 = *Add( new int( 4 ) ); // dereference the returned value

/* do whatever you need */

CleanUp();

为什么我们要使用指针:

int Add( int val )
{
    return val; // indeed very lame
}

当您调用需要参数(类型)的函数时,您传递的不是实例本身,而是它的副本。在上面的函数中,您返回了该副本的一个副本。这将导致大量的内存复制和重复,使您的应用程序变得非常缓慢。
考虑以下内容:
class Test
{
    int  t;
    char str[ 256 ];
}

如果一个函数需要类型为Test的数据,你可能会复制int和256个字符。因此,将函数设计成只需要指向Test的指针即可。这样,指针所指向的内存就会被使用,而不需要进行复制。
int Add( int val )
{
    val++;
    return val;
}

在这个例子中,我们将 1 添加到 val 的副本中,然后返回该副本的副本。

int i = Add( 1 );

结果:i = 2;

void Add( int* val )
{
    // mind the return type
    *val++;
}

在这个例子中,你将地址传递给一个值,然后在取消引用后将一个值加一。
int i = 1;
Add( &i );

结果: i = 2;

现在你已经传入i的地址,而不是复制它。在函数内部,你直接将1添加到该内存块中的值。由于你改变了内存本身,所以没有返回任何内容。


空指针/测试有效指针

有时你会遇到如下例子:

if( p != 0 ) // or if( p )
{
    /* do something with p */
}

这只是为了检查指针p是否有效。但是,无效的地址 - 未指向您已保留的内存(访问冲突) - 也将通过。对于您的代码,无效指针是一个有效地址。

因此,要使用这样的检查,您必须将指针NULL(或0)。

Fraction* f1 = 0;

f1 == 0时,它不指向任何东西,否则它会指向它指向的任何东西。

当您在“main”类中有一个指针而该指针已创建或尚未创建时,这将非常有用。

class Fraction
{
    public:
    int* basicFeature;
    int* ExtendedFeature = 0; // NULL this pointer since we don't know if it
                              // will be used
    Fraction( int fraction )
    {
        // Create a pointer owned by this class
        basicFeature = new int( fraction );
    }
    Fraction( int fraction, int extended ) // mind the static
    : Fraction( fraction )
    {
        // Create a pointer owned by this class
        ExtendedFeature = new int( extended );
    }
    ~Fraction()
    {
        delete basicFeature;
        if( ExtendedFeature )
            // It is assigned, so delete it
            delete ExtendedFeature;
    }
}

在构造函数中,我们创建了两个指针,因此在析构函数中,我们清理这些指针。仅检查ExtendedFeature,因为它可能已经创建或未创建。 basicFeature始终被创建。

您可以通过调用一个新函数removeExtendedFeature()来替换包括其范围在内的析构函数中的if语句,该函数的实现如下:

Fraction::removeExtendedFeature()
{
    if( ExtendedFeature )
    {
        // It is assigned, so delete it
        delete ExtendedFeature;
        // Now it is important to NULL the pointer again since you would
        // get an access violation on the clean up of the instance of 
        // the class Fraction
        ExtendedFeature = 0;
    }
}

还有新的析构函数:

Fraction::~Fraction()
{
    delete basicFeature;
    removeExtendedFeature();
}

另一个空值的功能可能是:

int Fraction::getValue()
{
    int result = *basicFeature;
    if( ExtendedFeature )
        result += *ExtendedFeature;
    return result;
}

非常抱歉,我的Fraction类非常糟糕,而且扩展功能也更加糟糕。但是作为一个例子,它可以起到作用。


有些人建议删除的顺序与使用new运算符相反:就像Fraction的最后一个例子中,我在dtor中做错了。int one = new int(); int two = new int(); delete two; delete one;不知道为什么除了“可达性”时间线之外还应该是什么。@任何人:请帮我填补这个空缺。 - brainoverflow

13
有两种创建 C++ 选项的主要方式。一种是在堆栈上(即 Fraction f1;),当该堆栈帧弹出时,内存会自动释放。第二种是在堆上(即 Fraction* f1 = new Fraction();)。关键是使用 new 关键字。
基本总结如下:您的 newdelete 必须匹配。每次您 new 某些内容时,必须在完成后 delete 它。"完成后" 由您来确定。但是,如果您重用变量(请参见下文),则需要先 delete,否则您将无法将原始对象返回到 delete
Fraction* f1 = new Fraction(); // create memory on heap, will need to free
f1 = new Fraction(); // this is a memory leak because I didn't first free the
                     // original f1 object, which I can no longer access

这是最容易理解的答案。 - user13229973

8

原则上,每个new都必须有对应的delete

在C++中,手动使用newdelete并不常见。当你初始化对象时,如果没有使用newdelete,它会在下一个}处被自动处理。这是基于假设每个人都在遵循RAII原则来处理对象。

Fraction f1(...);

实例化一个名为f1的分数对象。在作用域结束时,其析构函数将被调用。

“现代”方法是像上面那样处理事情。在极少数情况下,这种方法无法工作,您应该使用智能指针。


3
你需要删除指针所指向的对象。
然后你可以用new创建另一个对象,但不要忘记稍后再delete它。 :)
在删除之前真的需要检查吗?请确保阅读这篇回答,实际上是说不需要。此外,一个好的做法是将指针(在delete之后)设置为NULL,以备将来使用。
那么,如果在同一个指针上两次使用new会发生什么?
记住,new将分配我们要求的内存大小。然后,我们需要知道这块内存的位置。为此,new返回指向该内存的指针。
所以,假设你这样做:
f1 = new Fraction(user_input1, user_input2);
f1 = new Fraction(user_input1, user_input2);
delete(f1);

可以吗?不行

  1. new关键字分配一段内存,让指针f1指向这段内存。
  2. 第二个new操作会覆盖指针的值,使得f1指向我们分配的新内存块。
  3. 然后我们使用delete释放了这段内存……等等,是哪一段内存呢?是第二个new分配给我们的那段。
  4. 那么第一个new分配的内存怎么办?我们无法访问它,因为没有保留这段内存的指针。
  5. 这意味着我们有了内存泄漏,在c++中是常见的错误。

记住:

你应该根据使用的new关键字进行相同数量的delete操作!


正如WhozCraig所说,你应该明智地选择对象的动态创建(需要newdelete)和静态创建,后者会自动完成。

关于这个主题,这里有好的答案这里


@qnob 确保检查我的更新中的链接,特别是因为您计划在未来使用指针。 :) - gsamaras
就像链接上写的那样@Galik。(但我会让它更明显) :) - gsamaras
请问给我点踩的人可以解释一下原因吗? :) 这样我就可以改进我的回答和未来的回答了。 :) - gsamaras
我的回答中链接所说的就是这个,@dreamlax。此外,我已经更新了答案,说明链接表明这不是必需的。你同意吗? - gsamaras
谢谢@dreamlax,不是因为点赞,而是因为改进我的回答。重点在于帮助OP和未来的用户,而不是声望,我觉得你已经明白了。 - gsamaras
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接