【问题标题】:What are the benefits of using smart pointers over raw pointers for pointer class members here?在这里对指针类成员使用智能指针而不是原始指针有什么好处?
【发布时间】:2019-08-15 18:40:04
【问题描述】:

之前已经讨论过智能指针和原始指针之间的区别(例如When should I use raw pointers over smart pointers?),但我无法从我最近一天阅读的材料中得到这个问题的答案。

我有一个类A,它有一个指向某些数据的指针int* a。在我正在考虑的上下文中,a 指向的值可能会在程序中的其他地方使用,因此Aa 没有所有权,它只是引用它。例如,一个房子存在(例如int h)并且一个人(即班级)有一个对其房子的引用(例如int* my_h)。

我处理这个问题的第一种方法是不使用智能指针,但我很好奇在这个例子中使用智能指针的好处。我怀疑没有多少,因为所有权确实不是什么大问题,而且我没有打电话给newdelete

带有原始指针的示例:

#include<iostream>

class A{

public:
  A(int a_val){ 
    std::cout << "Creating A instance ";
    a = &a_val;
    std::cout << "with a = " << *a << std::endl;
  };

private:
  int* a;
};

int main()
{
  int x = 5; 
  std::cout << "x is " << x << std::endl;
  A a(x); 
  return 0;
}

这里,a 有原始指针int* a,它被分配给main() 中的&amp;x

智能指针示例(unique_ptr):

#include<memory>
#include<iostream>

class A{

public:
  A(std::unique_ptr<int> a_val){ 
    std::cout << "Creating A instance ";
    a = std::move(a_val);
    std::cout << "with a = " << *a << std::endl;
  };

private:
  std::unique_ptr<int> a;
};

int main()
{
  std::unique_ptr<int> x = std::make_unique<int> (5);//int x = 5;
  std::cout << "x is " << *x << std::endl;
  A a(std::move(x));
  return 0;
}

unique_ptr 的使用对我来说似乎有点矫枉过正,并且不利于可读性或性能。对吗?

编辑

正如 cmets 中指出的 (...),原始示例存在许多问题。原始指针示例应该是:

#include<iostream>

class A{

public:
  A(int* a_val){ 
    std::cout << "Creating A instance ";
    a = a_val;
    std::cout << "with a = " << *a << std::endl;
  };

private:
  int* a;
};

int main()
{
  int x = 5; 
  std::cout << "x is " << x << std::endl;
  A a(&x); 
  return 0;
}

智能指针示例可能应该使用shared_ptr

更具体地说,我对这种扩展到大量类实例的情况感兴趣,这些实例具有指向其他地方定义的数据结构的指针(或指针向量)。例如,在基于代理的模型中,代理有时需要“找到”另一个代理,因此(在我的理解中)可以使用指针向量来指代某个特定代理应该“了解”的其他代理。代理本身将在代理类之外创建。

【问题讨论】:

  • 一个区别是您的原始指针版本不起作用。您分配一个指向临时变量的指针,该临时变量在构造函数完成后立即死亡。这突出了原始指针和智能指针之间的一个非常重要的区别 - 智能指针可以防止大多数此类错误
  • 确实,YKsisarvinen 说的很对!您应该将构造函数声明为 A(int* a_val) 并在主体中有 a = a_val;用 A a(&x) 从 main 构造。
  • 只是稍微相关,我还将智能指针初始化移动到它所属的位置; ctor 的成员初始化列表。现在你不必要地使用默认构造和移动赋值。合身和形式,我猜。
  • @Yksisarvinen 这不是使用智能指针的理由。 OP所犯的错误与此无关。
  • 每个程序员在取消引用指针之前都必须问自己的一个问题是,“我怎么知道这个指针是有效的,即取消引用它不会调用未定义的行为?”使用原始指针,您可能必须检查整个程序的行为以说服自己它保证是有效的;使用智能指针,您只需要检查它是否为非空。

标签: c++ smart-pointers


【解决方案1】:

正如@YKsisarvinen 在评论中指出的那样,

一个区别是您的原始指针版本不起作用。您分配一个指向临时变量的指针,该临时变量在构造函数完成后立即死亡。这突出了原始指针和智能指针之间的一个非常重要的区别 - 智能指针可以防止大多数此类错误。

一种可能的方法是存储对其他人拥有的对象的引用。

struct A
{
  A(int& a_val) : a(a_val) {}
  int& a;
};

但是,这会阻止您分配对象。

int i = 10;
int j = 20;
A a1(i);
A a2(j);
a1 = a2;        // This will be an error.

您可以使用std::reference_wrapper 来克服这个障碍。

struct A
{
  A(int& a_val) : a(a_val) {}
  std::refernce_wrapper<int> a;
};

【讨论】:

  • 请注意std::reference_wrapper 只是一个指针的包装器。
【解决方案2】:

我很好奇在这个例子中使用智能指针的好处

取决于您正在谈论的智能指针:

  • unique_ptr:你不能在你的用例中使用这个,因为这意味着你拥有这个对象。
  • shared_ptr:这可行,但您需要在任何地方使用shared_ptr,并且您会将其生命周期与您的班级联系起来。
  • weak_ptr: 同上,但这不会绑定生命周期(同时可以被其他人销毁)。
  • 其他一些非标准智能指针:取决于。

一般来说:如果您只需要保留对对象的引用,请使用引用或指针。当然,您必须确保引用/指针保持有效,这可能是微不足道的,也可能是一场噩梦,具体取决于您的程序设计。

【讨论】:

  • 关于所有权,两个版本的 OP 代码完全不同。在具有unique_ptr 的类中,A 可以说拥有该对象,它接受一些输入值并从中创建一个新实例并负责管理其生命周期。带有原始指针的示例太糟糕了,甚至无法谈论所有权
  • @foreknownas_463035818 我知道,但是当代码和问题不一致时,您必须查看问题,而不是代码。在这种情况下,问题清楚地解释了 OP 希望引用某个对象而不绑定生命周期。
  • 恕我直言,代码被破坏得太厉害了,答案至少应该提到它是一个悬空指针
  • @foreknownas_463035818 我不同意。那应该是对问题的评论或编辑。否则,一旦有人纠正了错误,答案就会变得不同步和/或无效。
  • 好的点。我编辑了问题并向 OP 留下了评论,以防他们对编辑不满意
【解决方案3】:

主要问题是:谁拥有指针。在这个例子中:

int main()
{
  std::unique_ptr<int> x = std::make_unique<int> (5);//int x = 5;
  std::cout << "x is " << *x << std::endl;
  A a(std::move(x));
  return 0;
}

就是这样。

但如果你这样做:

A funct(int n)
{
  std::unique_ptr<int> x = std::make_unique<int> (n);
  std::cout << "x is " << *x << std::endl;
  A a(x.get());
  return a;
}

那么您需要转移所有权,因为一旦您离开functx 就会超出范围并被销毁。您现在返回的 A 对象指向垃圾。所以你会这样做:

A funct(int n)
{
  std::unique_ptr<int> x = std::make_unique<int> (n);
  std::cout << "x is " << *x << std::endl;
  return A(std::move(x));
}

所以,返回的A 现在拥有指针,并在自身被销毁时将其销毁。

【讨论】:

    【解决方案4】:

    在深入了解智能指针的好处之前,我建议您了解为什么需要它们。首先也是最重要的,智能指针从来没有打算取代所有原始指针的使用。它们的设计目的非常明确:内存管理,即清理/释放动态分配的内存。

    现在假设我们有一个简单的函数如下:

     1. void func()
     2. {
     3.    SomeResource* raw_sr = new SomeResource();  //--->Memory allocated here.
     4.    .
     5.    .
     6.    if(someCondition==false)
     7.       return;
     8.
     9.    delete raw_sr;
    10.       return;
    11.}
    

    上述函数可以通过两种方式终止: 1.从第10行返回。-->正常终止 2. 从第 7 行返回。-->在某些错误条件下终止

    在第一种情况下,我们清理了内存。 但是对于情况 2,我们有潜在的内存泄漏,因为我们没有释放 raw_sr。

    所以很明显,只有一个地方没有删除内存。 我们可以在那里添加一个显式代码来释放 raw_sr 内存。

    但关键是,代码中可能存在多种情况,可能会发生这种情况而忘记释放内存。也可以在处理异常时。

    但是,如果在所有终止条件下我们都可以保证内存将被释放呢?

    在所有这些情况下只有一个函数被调用:静态对象的析构函数。(不是动态的 - 非常重要。)

    因此,我们的想法是赋予持有指向特定类的指针的责任,该类的工作是在调​​用其析构函数时释放指针持有的内存。 此类称为智能指针。

    所以现在你的代码应该是这样的:

     1. void func()
     2. {
     3.    SmartPointer<SomeResource> smart_sr(new SomeResource());  //--->Memory allocated here
     4.    .                                                          // but passed onto the smart_sr obj.
     5.    .
     6.    if(someCondition==false)
     7.       return;
     8.
     9.    return;
    10.}
    

    现在在任何情况下,无论是在第 7 行还是第 10 行,都将调用 smart_sr 的析构函数,这将释放为 SomeResource 保留的内存。

    这是对智能指针的基本理解。

    因此,智能指针的使用取决于其使用上下文。

    现在基于您的示例,A 类表示持有 int* my_house 的人。 现在让我们说你不是唯一一个住在这所房子里的人。你和你的妻子住在一起。

    从逻辑角度来看,您的对象和您妻子的对象应该指向同一个 House 指针。 因此,仅您的对象不能成为该房屋指针的所有者。因为那样你妻子的对象将无法引用它。 所以 std::unique_ptr 不能用来保存房子指针。

    你们共享这个家..对吗? 因此,std::shared_ptr 将有意义。 现在让我们说,你的妻子离开了家(出于某种原因),所以她放弃了指针。房子会归还给它的主人吗?不,你还待在那里。随后,过几天你也搬出去,那么你也会放弃这个指针。而且因为你是最后一个留在里面的人,所以这个房子的记忆最终会被释放。

    希望这可以消除您的疑虑,并使您朝着正确的方向理解智能指针。

    【讨论】:

      猜你喜欢
      • 2015-10-01
      • 1970-01-01
      • 2020-11-18
      • 1970-01-01
      • 2013-03-16
      • 1970-01-01
      • 2016-08-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多