关于c ++:在哪种情况下删除指针


In Which Situations To Delete A Pointer

我接下来的问题是关于内存管理。例如,我有一个int变量没有在类中动态分配,比如invar1。我将这个int的内存地址传递给另一个类构造函数。那个班是这样做的:

1
2
3
4
5
6
7
8
class ex1{
    ex1(int* p_intvar1)
    {
       ptoint = p_intvar1;
    }

    int* ptoint;
};

我应该删除ptoint吗?因为它有一个非动态分配的int的地址,我想我不需要删除它。

我再次用new操作符向类声明一个对象:

1
objtoclass = new ex1();

我把这个传给另一个班:

1
2
3
4
5
6
7
8
class ex2{
    ex2(ex1* p_obj)
    {
       obj = p_obj;
    }

    ex1* obj;
};

当我已经删除objToClass时,我应该删除obj吗?

谢谢!


Because it has the address of an undynamically allocated int I thought I don't need to delete it.

对的。

Should I delete obj when I'm already deleting objtoclass?

不。

回想一下,您实际上并没有删除指针,而是使用指针删除它们指向的内容。因此,如果您同时编写delete objdelete objtoclass,因为两个指针指向同一个对象,您将删除该对象两次。

我要提醒您,对于您的ex2类,这是一个很容易犯的错误,在这个类中,指向对象的所有权语义并不完全清楚。您可以考虑使用智能指针实现来消除风险。


只是其他答案的附录

在智能指针(shared_ptrunique_ptr的帮助下,您可以摆脱原始指针,忘记内存管理。

智能指针负责在内存超出范围时释放内存。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>
#include <memory>

class ex1{
public:
    ex1(std::shared_ptr<int> p_intvar1)
    {
        ptoint = p_intvar1;
        std::cout << __func__ << std::endl;
    }

    ~ex1()
    {
        std::cout << __func__ << std::endl;
    }
private:
    std::shared_ptr<int> ptoint;
};

int main()
{
    std::shared_ptr<int> pi(new int(42));
    std::shared_ptr<ex1> objtoclass(new ex1(pi));

    /*
     * when the main function returns, these smart pointers will go
     * go out of scope and delete the dynamically allocated memory
     */


    return 0;
}

输出:

1
2
ex1
~ex1


Should I delete obj when I'm already deleting objtoclass?

好吧,你可以但是要注意,删除同一个对象两次是未定义的行为,应该避免。例如,如果您有两个指针(例如指向同一对象),并且使用一个指针删除原始对象,则也不应使用另一个指针删除该内存。在您的情况下,您可能会得到指向同一对象的两个指针。

一般来说,构建一个内部管理内存的类(就像你表面上所做的那样),并不是一件小事,你必须考虑诸如规则三等等的事情。

关于这一点,您应该删除动态分配的内存。如果没有动态分配内存,则不应删除它。

另外,为了避免上述并发症,你可以使用智能指针。


Because it has the address of an undynamically allocated int, I
thought I don't need to delete it.

没错。只是不要删除它。

问题的第二部分是关于动态分配的内存。在这里,你必须多想想,做些决定。

假设名为ex1的类在其构造函数中接收一个原始指针,该指针用于在类外部动态分配的内存。

作为类的设计者,您必须决定这个构造函数是否"拥有"这个指针。如果是这样,那么ex1负责删除它的内存,您可能应该在类析构函数上这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ex1 {
public:
    /**
     * Warning: This constructor takes the ownership of p_intvar1,
     * which means you must not delete it somewhere else.
     */

    ex1(int* p_intvar1)
    {
        ptoint = p_intvar1;
    }

    ~ex1()
    {
        delete ptoint;
    }

    int* ptoint;
};

然而,这通常是一个糟糕的设计决策。您必须为这个类的用户从根目录中读取构造函数的注释,并且记住不要删除在类ex1之外分配的内存。

接收指针并取得其所有权的方法(或构造函数)称为"接收器"。

有人会像这样使用这个类:

1
2
3
int* myInteger = new int(1);
ex1 obj(myInteger); // sink: obj takes the ownership of myInteger
// never delete myInteger outside ex1

另一种方法是说类ex1不具有所有权,为该指针分配内存的人负责删除它。类ex1不能删除其析构函数上的任何内容,它的用法如下:

1
2
3
4
int* myInteger = new int(1);
ex1 obj(myInteger);
// use obj here
delete myInteger; // remeber to delete myInteger

同样,您的类的用户必须阅读一些文档,以便知道他负责删除这些内容。

如果你不使用现代C++,你必须在这两个设计决策之间做出选择。

在现代C++(C++ 11和14)中,您可以在代码中明确地说明事物(也就是说,不必只依赖代码文档)。

首先,在现代C++中,避免使用原始指针。您必须在两种"智能指针"之间进行选择:唯一指针或共享指针。它们之间的区别在于所有权。

正如他们的名字所说,唯一的指针只能由一个人拥有,而共享的指针可以由一个或多个拥有(所有权是共享的)。

无法复制唯一指针(std::unique_ptr),只能从一个位置"移动"到另一个位置。如果一个类有一个唯一的指针作为属性,那么很明显这个类拥有该指针的所有权。如果一个方法收到一个唯一的指针作为副本,那么它显然是一个"接收器"方法(获得指针的所有权)。

您的类ex1可以这样写:

1
2
3
4
5
6
7
8
9
class ex1 {
public:
    ex1(std::unique_ptr<int> p_intvar1)
    {
        ptoint = std::move(p_intvar1);
    }

    std::unique_ptr<int> ptoint;
};

这个类的用户应该像这样使用它:

1
2
3
auto myInteger = std::make_unique<int>(1);
ex1 obj(std::move(myInteger)); // sink
// here, myInteger is nullptr (it was moved to ex1 constructor)

如果您忘记在上面的代码中执行"std::move",编译器将生成一个错误,告诉您唯一指针不可复制。

还要注意,您永远不必显式地删除内存。智能指针为您处理这个问题。


您当前不删除此int,也不显示它的分配位置。如果两个对象都不应该拥有它的参数,我会写

1
2
3
4
5
6
7
8
9
10
11
12
struct ex1 {
    ex1(int &i_) : i(i_) {}
    int &i;               // reference implies no ownership
};
struct ex2 {
    ex2(ex1 &e_) : e(e_) {}
    ex1 &e;               // reference implies no ownership
};

int i = 42;
ex1 a(i);
ex2 b(a);

如果任何一个参数都应该属于新对象,则将其作为unique_ptr传递。如果任何一个论点应该是共享的,则使用shared_ptr。我通常更喜欢这些(引用或智能指针)中的任何一个,而不是原始指针,因为它们提供了关于您意图的更多信息。

一般来说,为了做出这些决定,

Should I delete ptoint?

是个错误的问题。首先从一个稍微高一点的层次考虑问题:

  • 这个int在程序中代表什么?
  • 谁,如果有人,拥有它?
  • 与这些使用它的类相比,它应该生存多长时间?
  • 然后看看这些例子的答案是如何自然得出的:

    • 这个int是一个I/O映射的控制寄存器。

      在这种情况下,它不是用new创建的(它存在于整个程序之外),因此您当然不应该删除它。它可能也应该标记为volatile,但这不会影响寿命。

      可能是类之外的某个东西映射了地址,也应该取消映射,这与(取消)分配地址大致类似,或者它只是一个众所周知的地址。

    • 这个int是一个全局日志级别。

      在这种情况下,它可能具有静态生存期,在这种情况下,没有人拥有它,它没有显式地分配,因此不应该显式地取消分配。

      或者,它属于logger对象/singleton/mock/whatever,并且该对象负责在必要时解除分配它。

    • 这个int被显式地赋予对象以拥有

      在这种情况下,把这一点说得很明显是很好的做法,例如。

      1
      ex1::ex1(std::unique_ptr<int> &&p) : m_p(std::move(p)) {}

      请注意,使您的本地数据成员成为unique_ptr或类似的成员,也可以自动处理生命周期,而不需要您付出任何努力。

    • 这个in t被提供给您的对象使用,但是其他对象也可能正在使用它,并且不清楚它们将以哪个顺序结束。

      shared_ptr代替unique_ptr来描述这种关系。同样,智能指针将为您管理生命周期。

    一般来说,如果您可以对类型中的所有权和生存期信息进行编码,则不需要记住在哪里手动分配和解除分配内容。这更清楚更安全。

    如果您不能在类型中编码这些信息,那么您至少可以清楚地了解您的意图:您询问有关解除分配而不提及生存期或所有权的事实表明您的抽象级别是错误的。