什么时候应该使用原始指针而不是智能指针

When should I use raw pointers over smart pointers?

本文关键字:指针 智能 原始 什么时候      更新时间:2023-10-16

看完这个答案后,看起来尽可能多地使用智能指针是一个最佳实践,并将"正常"/raw指针的使用减少到最低限度。

是真的吗?

不,这不是真的。如果一个函数需要一个指针,而与所有权无关,那么我强烈认为应该传递一个常规指针,原因如下:

  • 没有所有权,因此你不知道传递什么样的智能指针
  • 如果你传递一个特定的指针,比如shared_ptr,那么你将无法传递,比如scoped_ptr
规则是这样的——如果你知道一个实体必须拥有对象的某种所有权,总是使用智能指针——给你你需要的那种所有权。如果没有所有权的概念,永远不要使用智能指针。

例二:

void PrintObject(shared_ptr<const Object> po) //bad
{
    if(po)
      po->Print();
    else
      log_error();
}
void PrintObject(const Object* po) //good
{
    if(po)
      po->Print();
    else
      log_error();
}

Example2:

Object* createObject() //bad
{
    return new Object;
}
some_smart_ptr<Object> createObject() //good
{
   return some_smart_ptr<Object>(new Object);
}

使用智能指针来管理所有权是正确的事情。相反,在所有权为而不是的地方使用原始指针是错误的,而不是

下面是一些完全合法的原始指针用法(记住,总是假设它们是非拥有的):

与引用

竞争
  • 参数传递;但引用不能为空,所以首选
  • 作为类成员来表示关联而不是组合;通常比引用更可取,因为赋值的语义更直接,此外,构造函数设置的不变量可以确保它们在对象的生命周期内不是0
  • 作为其他地方拥有的(可能是多态的)对象的句柄;引用不能为空,所以首选
  • std::bind使用了一个约定,传递的参数被复制到生成的函子中;然而std::bind(&T::some_member, this, ...)只复制指针,而std::bind(&T::some_member, *this, ...)复制对象;std::bind(&T::some_member, std::ref(*this), ...)是一个备选

与引用

竞争的地方
  • 迭代器!
  • 可选参数的
  • 参数传递;这里他们与boost::optional<T&>竞争
  • 作为其他地方拥有的(可能是多态的)对象的句柄,当它们不能在初始化位置声明时;再次与boost::optional<T&>
  • 竞争

作为一个提醒,它几乎总是错误的编写一个函数(不是一个构造函数,或一个函数成员,例如,获取所有权),接受一个智能指针,除非它反过来传递给一个构造函数(例如,它是正确的std::async,因为在语义上它接近于std::thread构造函数的调用)。如果是同步的,就不需要智能指针了。


回顾一下,下面是演示上述几种用法的代码片段。我们正在编写和使用一个类,它对std::vector<int>的每个元素应用函子,同时输出一些输出。

class apply_and_log {
public:
    // C++03 exception: it's acceptable to pass by pointer to const
    // to avoid apply_and_log(std::cout, std::vector<int>())
    // notice that our pointer would be left dangling after call to constructor
    // this still adds a requirement on the caller that v != 0 or that we throw on 0
    apply_and_log(std::ostream& os, std::vector<int> const* v)
        : log(&os)
        , data(v)
    {}
    // C++0x alternative
    // also usable for C++03 with requirement on v
    apply_and_log(std::ostream& os, std::vector<int> const& v)
        : log(&os)
        , data(&v)
    {}
    // now apply_and_log(std::cout, std::vector<int> {}) is invalid in C++0x
    // && is also acceptable instead of const&&
    apply_and_log(std::ostream& os, std::vector<int> const&&) = delete;
    // Notice that without effort copy (also move), assignment and destruction
    // are correct.
    // Class invariants: member pointers are never 0.
    // Requirements on construction: the passed stream and vector must outlive *this
    typedef std::function<void(std::vector<int> const&)> callback_type;
    // optional callback
    // alternative: boost::optional<callback_type&>
    void
    do_work(callback_type* callback)
    {
        // for convenience
        auto& v = *data;
        // using raw pointers as iterators
        int* begin = &v[0];
        int* end = begin + v.size();
        // ...
        if(callback) {
            callback(v);
        }
    }
private:
    // association: we use a pointer
    // notice that the type is polymorphic and non-copyable,
    // so composition is not a reasonable option
    std::ostream* log;
    // association: we use a pointer to const
    // contrived example for the constructors
    std::vector<int> const* data;
};

引用计数(特别是shared_ptr使用)将崩溃的一个实例是当您创建指针的循环(例如a指向B, B指向a,或a ->B->C-> a,等等)。在这种情况下,任何对象都不会被自动释放,因为它们都保持彼此的引用计数大于零。

因此,每当我创建具有父子关系的对象(例如对象树)时,我将在父对象中使用shared_ptrs来保存它们的子对象,但如果子对象需要指向父对象的指针,我将使用普通的C/c++指针。

始终建议使用智能指针,因为它们清楚地记录了所有权。

然而,我们真正错过的是一个"空白"智能指针,它不意味着任何所有权的概念。

template <typename T>
class ptr // thanks to Martinho for the name suggestion :)
{
public:
  ptr(T* p): _p(p) {}
  template <typename U> ptr(U* p): _p(p) {}
  template <typename SP> ptr(SP const& sp): _p(sp.get()) {}
  T& operator*() const { assert(_p); return *_p; }
  T* operator->() const { assert(_p); return _p; }
private:
  T* _p;
}; // class ptr<T>

这实际上是可能存在的智能指针的最简单版本:一种记录它不拥有它所指向的资源的类型。

在少数情况下,您可能需要使用指针:

  • 函数指针(显然没有智能指针)
  • 定义自己的智能指针或容器
  • 处理低级编程,其中原始指针至关重要
  • 从原始数组衰减

我认为这里给出了更彻底的答案:我何时使用哪种指针?

摘自那个链接:"使用哑指针(原始指针)或引用对资源的非所有权引用,并且当您知道资源将比引用对象/作用域活得更长时。"(粗体从原文保留)

问题是,如果你写的代码是通用的,它并不总是容易绝对确定对象将比原始指针的寿命长。考虑这个例子:

struct employee_t {
    employee_t(const std::string& first_name, const std::string& last_name) : m_first_name(first_name), m_last_name(last_name) {}
    std::string m_first_name;
    std::string m_last_name;
};
void replace_current_employees_with(const employee_t* p_new_employee, std::list<employee_t>& employee_list) {
    employee_list.clear();
    employee_list.push_back(*p_new_employee);
}
void main(int argc, char* argv[]) {
    std::list<employee_t> current_employee_list;
    current_employee_list.push_back(employee_t("John", "Smith"));
    current_employee_list.push_back(employee_t("Julie", "Jones"));
    employee_t* p_person_who_convinces_boss_to_rehire_him = &(current_employee_list.front());
    replace_current_employees_with(p_person_who_convinces_boss_to_rehire_him, current_employee_list);
}

出乎意料的是,replace_current_employees_with()函数可能会在使用完它之前无意中导致它的一个参数被释放。

因此,尽管replace_current_employees_with()函数乍一看似乎不需要对其参数拥有所有权,但它需要某种防御措施,防止其参数在使用完之前被不知不觉地释放的可能性。最简单的解决方案是实际获得参数的(临时共享)所有权,大概是通过shared_ptr

但是如果你真的不想拥有所有权,现在有一个安全的选择——这就是答案中无耻的插入部分——"注册指针"。"注册指针"是智能指针,其行为类似于原始指针,除了它们在目标对象被销毁时(自动)设置为null_ptr,并且在默认情况下,如果您试图访问已被删除的对象,将抛出异常。

还要注意,注册的指针可以用编译时指令"禁用"(自动替换为原始指针),允许它们仅在调试/测试/beta模式下使用(并产生开销)。所以你应该很少使用原始指针。

这是真的。我看不出原始指针比智能指针有什么好处,尤其是在复杂的项目中。

对于临时和轻量级的使用,原始指针是可以的。